diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a1c0613 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_STORE +__pycache__ +.vscode +test.py \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cbe5ad1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,437 @@ +Attribution-NonCommercial-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International +Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-NonCommercial-ShareAlike 4.0 International Public License +("Public License"). To the extent this Public License may be +interpreted as a contract, You are granted the Licensed Rights in +consideration of Your acceptance of these terms and conditions, and the +Licensor grants You such rights in consideration of benefits the +Licensor receives from making the Licensed Material available under +these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-NC-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution, NonCommercial, and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. NonCommercial means not primarily intended for or directed towards + commercial advantage or monetary compensation. For purposes of + this Public License, the exchange of the Licensed Material for + other material subject to Copyright and Similar Rights by digital + file-sharing or similar means is NonCommercial provided there is + no payment of monetary compensation in connection with the + exchange. + + l. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + m. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + n. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part, for NonCommercial purposes only; and + + b. produce, reproduce, and Share Adapted Material for + NonCommercial purposes only. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties, including when + the Licensed Material is used other than for NonCommercial + purposes. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-NC-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database for NonCommercial purposes + only; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + including for purposes of Section 3(b); and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/codes/A2C/README.md b/codes/A2C/README.md new file mode 100644 index 0000000..5856b80 --- /dev/null +++ b/codes/A2C/README.md @@ -0,0 +1,5 @@ +## A2C + + + +https://towardsdatascience.com/understanding-actor-critic-methods-931b97b6df3f \ No newline at end of file diff --git a/codes/A2C/agent.py b/codes/A2C/agent.py new file mode 100644 index 0000000..997401b --- /dev/null +++ b/codes/A2C/agent.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: JiangJi +Email: johnjim0816@gmail.com +Date: 2021-05-03 22:16:08 +LastEditor: JiangJi +LastEditTime: 2021-05-03 22:23:48 +Discription: +Environment: +''' +import torch.optim as optim +import torch.nn as nn +import torch.nn.functional as F +from torch.distributions import Categorical + +class ActorCritic(nn.Module): + ''' A2C网络模型,包含一个Actor和Critic + ''' + def __init__(self, input_dim, output_dim, hidden_dim): + super(ActorCritic, self).__init__() + self.critic = nn.Sequential( + nn.Linear(input_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, 1) + ) + + self.actor = nn.Sequential( + nn.Linear(input_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, output_dim), + nn.Softmax(dim=1), + ) + + def forward(self, x): + value = self.critic(x) + probs = self.actor(x) + dist = Categorical(probs) + return dist, value +class A2C: + ''' A2C算法 + ''' + def __init__(self,state_dim,action_dim,cfg) -> None: + self.gamma = cfg.gamma + self.device = cfg.device + self.model = ActorCritic(state_dim, action_dim, cfg.hidden_size).to(self.device) + self.optimizer = optim.Adam(self.model.parameters()) + + def compute_returns(self,next_value, rewards, masks): + R = next_value + returns = [] + for step in reversed(range(len(rewards))): + R = rewards[step] + self.gamma * R * masks[step] + returns.insert(0, R) + return returns \ No newline at end of file diff --git a/codes/A2C/outputs/CartPole-v0/20210503-224814/results/train_ma_rewards.npy b/codes/A2C/outputs/CartPole-v0/20210503-224814/results/train_ma_rewards.npy new file mode 100644 index 0000000..57f4174 Binary files /dev/null and b/codes/A2C/outputs/CartPole-v0/20210503-224814/results/train_ma_rewards.npy differ diff --git a/codes/A2C/outputs/CartPole-v0/20210503-224814/results/train_rewards.npy b/codes/A2C/outputs/CartPole-v0/20210503-224814/results/train_rewards.npy new file mode 100644 index 0000000..bdb3fce Binary files /dev/null and b/codes/A2C/outputs/CartPole-v0/20210503-224814/results/train_rewards.npy differ diff --git a/codes/A2C/outputs/CartPole-v0/20210503-224814/results/train_rewards_curve.png b/codes/A2C/outputs/CartPole-v0/20210503-224814/results/train_rewards_curve.png new file mode 100644 index 0000000..5f1cf9a Binary files /dev/null and b/codes/A2C/outputs/CartPole-v0/20210503-224814/results/train_rewards_curve.png differ diff --git a/codes/A2C/outputs/CartPole-v0/20211221-165620/results/train_ma_rewards.npy b/codes/A2C/outputs/CartPole-v0/20211221-165620/results/train_ma_rewards.npy new file mode 100644 index 0000000..6537afd Binary files /dev/null and b/codes/A2C/outputs/CartPole-v0/20211221-165620/results/train_ma_rewards.npy differ diff --git a/codes/A2C/outputs/CartPole-v0/20211221-165620/results/train_rewards.npy b/codes/A2C/outputs/CartPole-v0/20211221-165620/results/train_rewards.npy new file mode 100644 index 0000000..56f779b Binary files /dev/null and b/codes/A2C/outputs/CartPole-v0/20211221-165620/results/train_rewards.npy differ diff --git a/codes/A2C/task0.ipynb b/codes/A2C/task0.ipynb new file mode 100644 index 0000000..aa9b772 --- /dev/null +++ b/codes/A2C/task0.ipynb @@ -0,0 +1,265 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from pathlib import Path\n", + "curr_path = str(Path().absolute()) # 当前路径\n", + "parent_path = str(Path().absolute().parent) # 父路径\n", + "sys.path.append(parent_path) # 添加路径到系统路径\n", + "import math\n", + "import random\n", + "\n", + "import gym\n", + "import numpy as np\n", + "\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "import torch.nn.functional as F\n", + "from torch.distributions import Categorical\n", + "\n", + "from IPython.display import clear_output\n", + "import matplotlib.pyplot as plt\n", + "%matplotlib inline" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "use_cuda = torch.cuda.is_available()\n", + "device = torch.device(\"cuda\" if use_cuda else \"cpu\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from common.multiprocessing_env import SubprocVecEnv\n", + "\n", + "num_envs = 16\n", + "env_name = \"CartPole-v0\"\n", + "\n", + "def make_env():\n", + " def _thunk():\n", + " env = gym.make(env_name)\n", + " return env\n", + "\n", + " return _thunk\n", + "\n", + "envs = [make_env() for i in range(num_envs)]\n", + "envs = SubprocVecEnv(envs)\n", + "\n", + "env = gym.make(env_name)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "class ActorCritic(nn.Module):\n", + " def __init__(self, num_inputs, num_outputs, hidden_size, std=0.0):\n", + " super(ActorCritic, self).__init__()\n", + " \n", + " self.critic = nn.Sequential(\n", + " nn.Linear(num_inputs, hidden_size),\n", + " nn.ReLU(),\n", + " nn.Linear(hidden_size, 1)\n", + " )\n", + " \n", + " self.actor = nn.Sequential(\n", + " nn.Linear(num_inputs, hidden_size),\n", + " nn.ReLU(),\n", + " nn.Linear(hidden_size, num_outputs),\n", + " nn.Softmax(dim=1),\n", + " )\n", + " \n", + " def forward(self, x):\n", + " value = self.critic(x)\n", + " probs = self.actor(x)\n", + " dist = Categorical(probs)\n", + " return dist, value" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "def plot(frame_idx, rewards):\n", + " clear_output(True)\n", + " plt.figure(figsize=(20,5))\n", + " plt.subplot(131)\n", + " plt.title('frame %s. reward: %s' % (frame_idx, rewards[-1]))\n", + " plt.plot(rewards)\n", + " plt.show()\n", + " \n", + "def test_env(vis=False):\n", + " state = env.reset()\n", + " if vis: env.render()\n", + " done = False\n", + " total_reward = 0\n", + " while not done:\n", + " state = torch.FloatTensor(state).unsqueeze(0).to(device)\n", + " dist, _ = model(state)\n", + " next_state, reward, done, _ = env.step(dist.sample().cpu().numpy()[0])\n", + " state = next_state\n", + " if vis: env.render()\n", + " total_reward += reward\n", + " return total_reward" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def compute_returns(next_value, rewards, masks, gamma=0.99):\n", + " R = next_value\n", + " returns = []\n", + " for step in reversed(range(len(rewards))):\n", + " R = rewards[step] + gamma * R * masks[step]\n", + " returns.insert(0, R)\n", + " return returns" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "num_inputs = envs.observation_space.shape[0]\n", + "num_outputs = envs.action_space.n\n", + "\n", + "#Hyper params:\n", + "hidden_size = 256\n", + "lr = 3e-4\n", + "num_steps = 5\n", + "\n", + "model = ActorCritic(num_inputs, num_outputs, hidden_size).to(device)\n", + "optimizer = optim.Adam(model.parameters())" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "max_frames = 20000\n", + "frame_idx = 0\n", + "test_rewards = []" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "state = envs.reset()\n", + "\n", + "while frame_idx < max_frames:\n", + "\n", + " log_probs = []\n", + " values = []\n", + " rewards = []\n", + " masks = []\n", + " entropy = 0\n", + "\n", + " for _ in range(num_steps):\n", + " state = torch.FloatTensor(state).to(device)\n", + " dist, value = model(state)\n", + "\n", + " action = dist.sample()\n", + " next_state, reward, done, _ = envs.step(action.cpu().numpy())\n", + "\n", + " log_prob = dist.log_prob(action)\n", + " entropy += dist.entropy().mean()\n", + " \n", + " log_probs.append(log_prob)\n", + " values.append(value)\n", + " rewards.append(torch.FloatTensor(reward).unsqueeze(1).to(device))\n", + " masks.append(torch.FloatTensor(1 - done).unsqueeze(1).to(device))\n", + " \n", + " state = next_state\n", + " frame_idx += 1\n", + " \n", + " if frame_idx % 1000 == 0:\n", + " test_rewards.append(np.mean([test_env() for _ in range(10)]))\n", + " plot(frame_idx, test_rewards)\n", + " \n", + " next_state = torch.FloatTensor(next_state).to(device)\n", + " _, next_value = model(next_state)\n", + " returns = compute_returns(next_value, rewards, masks)\n", + " \n", + " log_probs = torch.cat(log_probs)\n", + " returns = torch.cat(returns).detach()\n", + " values = torch.cat(values)\n", + "\n", + " advantage = returns - values\n", + "\n", + " actor_loss = -(log_probs * advantage.detach()).mean()\n", + " critic_loss = advantage.pow(2).mean()\n", + "\n", + " loss = actor_loss + 0.5 * critic_loss - 0.001 * entropy\n", + "\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "fe38df673a99c62a9fea33a7aceda74c9b65b12ee9d076c5851d98b692a4989a" + }, + "kernelspec": { + "display_name": "Python 3.7.9 64-bit ('py37': conda)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.9" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/codes/A2C/task0.py b/codes/A2C/task0.py new file mode 100644 index 0000000..fd54d87 --- /dev/null +++ b/codes/A2C/task0.py @@ -0,0 +1,138 @@ +import sys +import os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import gym +import numpy as np +import torch +import torch.optim as optim +import datetime +from common.multiprocessing_env import SubprocVecEnv +from A2C.agent import ActorCritic +from common.utils import save_results, make_dir +from common.utils import plot_rewards + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 +algo_name = 'A2C' # 算法名称 +env_name = 'CartPole-v0' # 环境名称 + +class A2CConfig: + def __init__(self) -> None: + self.algo_name = algo_name# 算法名称 + self.env_name = env_name # 环境名称 + self.n_envs = 8 # 异步的环境数目 + self.gamma = 0.99 # 强化学习中的折扣因子 + self.hidden_dim = 256 + self.lr = 1e-3 # learning rate + self.max_frames = 30000 + self.n_steps = 5 + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") +class PlotConfig: + def __init__(self) -> None: + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.result_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/results/' # 保存结果的路径 + self.model_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + + +def make_envs(env_name): + def _thunk(): + env = gym.make(env_name) + env.seed(2) + return env + return _thunk +def test_env(env,model,vis=False): + state = env.reset() + if vis: env.render() + done = False + total_reward = 0 + while not done: + state = torch.FloatTensor(state).unsqueeze(0).to(cfg.device) + dist, _ = model(state) + next_state, reward, done, _ = env.step(dist.sample().cpu().numpy()[0]) + state = next_state + if vis: env.render() + total_reward += reward + return total_reward +def compute_returns(next_value, rewards, masks, gamma=0.99): + R = next_value + returns = [] + for step in reversed(range(len(rewards))): + R = rewards[step] + gamma * R * masks[step] + returns.insert(0, R) + return returns + + +def train(cfg,envs): + print('开始训练!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo}, 设备:{cfg.device}') + env = gym.make(cfg.env_name) # a single env + env.seed(10) + state_dim = envs.observation_space.shape[0] + action_dim = envs.action_space.n + model = ActorCritic(state_dim, action_dim, cfg.hidden_dim).to(cfg.device) + optimizer = optim.Adam(model.parameters()) + frame_idx = 0 + test_rewards = [] + test_ma_rewards = [] + state = envs.reset() + while frame_idx < cfg.max_frames: + log_probs = [] + values = [] + rewards = [] + masks = [] + entropy = 0 + # rollout trajectory + for _ in range(cfg.n_steps): + state = torch.FloatTensor(state).to(cfg.device) + dist, value = model(state) + action = dist.sample() + next_state, reward, done, _ = envs.step(action.cpu().numpy()) + log_prob = dist.log_prob(action) + entropy += dist.entropy().mean() + log_probs.append(log_prob) + values.append(value) + rewards.append(torch.FloatTensor(reward).unsqueeze(1).to(cfg.device)) + masks.append(torch.FloatTensor(1 - done).unsqueeze(1).to(cfg.device)) + state = next_state + frame_idx += 1 + if frame_idx % 100 == 0: + test_reward = np.mean([test_env(env,model) for _ in range(10)]) + print(f"frame_idx:{frame_idx}, test_reward:{test_reward}") + test_rewards.append(test_reward) + if test_ma_rewards: + test_ma_rewards.append(0.9*test_ma_rewards[-1]+0.1*test_reward) + else: + test_ma_rewards.append(test_reward) + # plot(frame_idx, test_rewards) + next_state = torch.FloatTensor(next_state).to(cfg.device) + _, next_value = model(next_state) + returns = compute_returns(next_value, rewards, masks) + log_probs = torch.cat(log_probs) + returns = torch.cat(returns).detach() + values = torch.cat(values) + advantage = returns - values + actor_loss = -(log_probs * advantage.detach()).mean() + critic_loss = advantage.pow(2).mean() + loss = actor_loss + 0.5 * critic_loss - 0.001 * entropy + optimizer.zero_grad() + loss.backward() + optimizer.step() + print('完成训练!') + return test_rewards, test_ma_rewards +if __name__ == "__main__": + cfg = A2CConfig() + plot_cfg = PlotConfig() + envs = [make_envs(cfg.env_name) for i in range(cfg.n_envs)] + envs = SubprocVecEnv(envs) + # 训练 + rewards,ma_rewards = train(cfg,envs) + make_dir(plot_cfg.result_path,plot_cfg.model_path) + save_results(rewards, ma_rewards, tag='train', path=plot_cfg.result_path) # 保存结果 + plot_rewards(rewards, ma_rewards, plot_cfg, tag="train") # 画出结果 diff --git a/codes/DDPG/README.md b/codes/DDPG/README.md new file mode 100644 index 0000000..bbcedcc --- /dev/null +++ b/codes/DDPG/README.md @@ -0,0 +1,7 @@ +# DDPG + +#TODO + +## 伪代码 + +![image-20210320151900695](assets/image-20210320151900695.png) \ No newline at end of file diff --git a/codes/DDPG/agent.py b/codes/DDPG/agent.py new file mode 100644 index 0000000..6ec2eef --- /dev/null +++ b/codes/DDPG/agent.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +@Author: John +@Email: johnjim0816@gmail.com +@Date: 2020-06-09 20:25:52 +@LastEditor: John +LastEditTime: 2021-09-16 00:55:30 +@Discription: +@Environment: python 3.7.7 +''' +import random +import numpy as np +import torch +import torch.nn as nn +import torch.optim as optim +import torch.nn.functional as F +class ReplayBuffer: + def __init__(self, capacity): + self.capacity = capacity # 经验回放的容量 + self.buffer = [] # 缓冲区 + self.position = 0 + + def push(self, state, action, reward, next_state, done): + ''' 缓冲区是一个队列,容量超出时去掉开始存入的转移(transition) + ''' + if len(self.buffer) < self.capacity: + self.buffer.append(None) + self.buffer[self.position] = (state, action, reward, next_state, done) + self.position = (self.position + 1) % self.capacity + + def sample(self, batch_size): + batch = random.sample(self.buffer, batch_size) # 随机采出小批量转移 + state, action, reward, next_state, done = zip(*batch) # 解压成状态,动作等 + return state, action, reward, next_state, done + + def __len__(self): + ''' 返回当前存储的量 + ''' + return len(self.buffer) +class Actor(nn.Module): + def __init__(self, state_dim, action_dim, hidden_dim, init_w=3e-3): + super(Actor, self).__init__() + self.linear1 = nn.Linear(state_dim, hidden_dim) + self.linear2 = nn.Linear(hidden_dim, hidden_dim) + self.linear3 = nn.Linear(hidden_dim, action_dim) + + self.linear3.weight.data.uniform_(-init_w, init_w) + self.linear3.bias.data.uniform_(-init_w, init_w) + + def forward(self, x): + x = F.relu(self.linear1(x)) + x = F.relu(self.linear2(x)) + x = torch.tanh(self.linear3(x)) + return x +class Critic(nn.Module): + def __init__(self, state_dim, action_dim, hidden_dim, init_w=3e-3): + super(Critic, self).__init__() + + self.linear1 = nn.Linear(state_dim + action_dim, hidden_dim) + self.linear2 = nn.Linear(hidden_dim, hidden_dim) + self.linear3 = nn.Linear(hidden_dim, 1) + # 随机初始化为较小的值 + self.linear3.weight.data.uniform_(-init_w, init_w) + self.linear3.bias.data.uniform_(-init_w, init_w) + + def forward(self, state, action): + # 按维数1拼接 + x = torch.cat([state, action], 1) + x = F.relu(self.linear1(x)) + x = F.relu(self.linear2(x)) + x = self.linear3(x) + return x +class DDPG: + def __init__(self, state_dim, action_dim, cfg): + self.device = cfg.device + self.critic = Critic(state_dim, action_dim, cfg.hidden_dim).to(cfg.device) + self.actor = Actor(state_dim, action_dim, cfg.hidden_dim).to(cfg.device) + self.target_critic = Critic(state_dim, action_dim, cfg.hidden_dim).to(cfg.device) + self.target_actor = Actor(state_dim, action_dim, cfg.hidden_dim).to(cfg.device) + + # 复制参数到目标网络 + for target_param, param in zip(self.target_critic.parameters(), self.critic.parameters()): + target_param.data.copy_(param.data) + for target_param, param in zip(self.target_actor.parameters(), self.actor.parameters()): + target_param.data.copy_(param.data) + + self.critic_optimizer = optim.Adam( + self.critic.parameters(), lr=cfg.critic_lr) + self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=cfg.actor_lr) + self.memory = ReplayBuffer(cfg.memory_capacity) + self.batch_size = cfg.batch_size + self.soft_tau = cfg.soft_tau # 软更新参数 + self.gamma = cfg.gamma + + def choose_action(self, state): + state = torch.FloatTensor(state).unsqueeze(0).to(self.device) + action = self.actor(state) + return action.detach().cpu().numpy()[0, 0] + + def update(self): + if len(self.memory) < self.batch_size: # 当 memory 中不满足一个批量时,不更新策略 + return + # 从经验回放中(replay memory)中随机采样一个批量的转移(transition) + state, action, reward, next_state, done = self.memory.sample(self.batch_size) + # 转变为张量 + state = torch.FloatTensor(state).to(self.device) + next_state = torch.FloatTensor(next_state).to(self.device) + action = torch.FloatTensor(action).to(self.device) + reward = torch.FloatTensor(reward).unsqueeze(1).to(self.device) + done = torch.FloatTensor(np.float32(done)).unsqueeze(1).to(self.device) + + policy_loss = self.critic(state, self.actor(state)) + policy_loss = -policy_loss.mean() + next_action = self.target_actor(next_state) + target_value = self.target_critic(next_state, next_action.detach()) + expected_value = reward + (1.0 - done) * self.gamma * target_value + expected_value = torch.clamp(expected_value, -np.inf, np.inf) + + value = self.critic(state, action) + value_loss = nn.MSELoss()(value, expected_value.detach()) + + self.actor_optimizer.zero_grad() + policy_loss.backward() + self.actor_optimizer.step() + self.critic_optimizer.zero_grad() + value_loss.backward() + self.critic_optimizer.step() + # 软更新 + for target_param, param in zip(self.target_critic.parameters(), self.critic.parameters()): + target_param.data.copy_( + target_param.data * (1.0 - self.soft_tau) + + param.data * self.soft_tau + ) + for target_param, param in zip(self.target_actor.parameters(), self.actor.parameters()): + target_param.data.copy_( + target_param.data * (1.0 - self.soft_tau) + + param.data * self.soft_tau + ) + def save(self,path): + torch.save(self.actor.state_dict(), path+'checkpoint.pt') + + def load(self,path): + self.actor.load_state_dict(torch.load(path+'checkpoint.pt')) \ No newline at end of file diff --git a/codes/DDPG/assets/image-20210320151900695.png b/codes/DDPG/assets/image-20210320151900695.png new file mode 100644 index 0000000..fd41201 Binary files /dev/null and b/codes/DDPG/assets/image-20210320151900695.png differ diff --git a/codes/DDPG/env.py b/codes/DDPG/env.py new file mode 100644 index 0000000..92fe482 --- /dev/null +++ b/codes/DDPG/env.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +@Author: John +@Email: johnjim0816@gmail.com +@Date: 2020-06-10 15:28:30 +@LastEditor: John +LastEditTime: 2021-09-16 00:52:30 +@Discription: +@Environment: python 3.7.7 +''' +import gym +import numpy as np + +class NormalizedActions(gym.ActionWrapper): + ''' 将action范围重定在[0.1]之间 + ''' + def action(self, action): + low_bound = self.action_space.low + upper_bound = self.action_space.high + action = low_bound + (action + 1.0) * 0.5 * (upper_bound - low_bound) + action = np.clip(action, low_bound, upper_bound) + return action + + def reverse_action(self, action): + low_bound = self.action_space.low + upper_bound = self.action_space.high + action = 2 * (action - low_bound) / (upper_bound - low_bound) - 1 + action = np.clip(action, low_bound, upper_bound) + return action + +class OUNoise(object): + '''Ornstein–Uhlenbeck噪声 + ''' + def __init__(self, action_space, mu=0.0, theta=0.15, max_sigma=0.3, min_sigma=0.3, decay_period=100000): + self.mu = mu # OU噪声的参数 + self.theta = theta # OU噪声的参数 + self.sigma = max_sigma # OU噪声的参数 + self.max_sigma = max_sigma + self.min_sigma = min_sigma + self.decay_period = decay_period + self.action_dim = action_space.shape[0] + self.low = action_space.low + self.high = action_space.high + self.reset() + def reset(self): + self.obs = np.ones(self.action_dim) * self.mu + def evolve_obs(self): + x = self.obs + dx = self.theta * (self.mu - x) + self.sigma * np.random.randn(self.action_dim) + self.obs = x + dx + return self.obs + def get_action(self, action, t=0): + ou_obs = self.evolve_obs() + self.sigma = self.max_sigma - (self.max_sigma - self.min_sigma) * min(1.0, t / self.decay_period) # sigma会逐渐衰减 + return np.clip(action + ou_obs, self.low, self.high) # 动作加上噪声后进行剪切 \ No newline at end of file diff --git a/codes/DDPG/outputs/Pendulum-v0/20210916-013138/models/checkpoint.pt b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/models/checkpoint.pt new file mode 100644 index 0000000..2051294 Binary files /dev/null and b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/models/checkpoint.pt differ diff --git a/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/eval_ma_rewards.npy b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/eval_ma_rewards.npy new file mode 100644 index 0000000..936884c Binary files /dev/null and b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/eval_ma_rewards.npy differ diff --git a/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/eval_rewards.npy b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/eval_rewards.npy new file mode 100644 index 0000000..4d497f4 Binary files /dev/null and b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/eval_rewards.npy differ diff --git a/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/eval_rewards_curve_cn.png b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/eval_rewards_curve_cn.png new file mode 100644 index 0000000..a442aac Binary files /dev/null and b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/eval_rewards_curve_cn.png differ diff --git a/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/train_ma_rewards.npy b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/train_ma_rewards.npy new file mode 100644 index 0000000..ab923ee Binary files /dev/null and b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/train_ma_rewards.npy differ diff --git a/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/train_rewards.npy b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/train_rewards.npy new file mode 100644 index 0000000..0374e2e Binary files /dev/null and b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/train_rewards.npy differ diff --git a/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/train_rewards_curve_cn.png b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/train_rewards_curve_cn.png new file mode 100644 index 0000000..06f3dc8 Binary files /dev/null and b/codes/DDPG/outputs/Pendulum-v0/20210916-013138/results/train_rewards_curve_cn.png differ diff --git a/codes/DDPG/task0.py b/codes/DDPG/task0.py new file mode 100644 index 0000000..81fa9a6 --- /dev/null +++ b/codes/DDPG/task0.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +@Author: John +@Email: johnjim0816@gmail.com +@Date: 2020-06-11 20:58:21 +@LastEditor: John +LastEditTime: 2021-09-16 01:31:33 +@Discription: +@Environment: python 3.7.7 +''' +import sys,os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径sys.path + +import datetime +import gym +import torch + +from DDPG.env import NormalizedActions +from DDPG.agent import DDPG +from DDPG.train import train,test +from common.utils import save_results,make_dir +from common.utils import plot_rewards + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 +algo_name = 'DDPG' # 算法名称 +env_name = 'Pendulum-v1' # 环境名称,gym新版本(约0.21.0之后)中Pendulum-v0改为Pendulum-v1 + +class DDPGConfig: + def __init__(self): + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.train_eps = 300 # 训练的回合数 + self.test_eps = 50 # 测试的回合数 + self.gamma = 0.99 # 折扣因子 + self.critic_lr = 1e-3 # 评论家网络的学习率 + self.actor_lr = 1e-4 # 演员网络的学习率 + self.memory_capacity = 8000 # 经验回放的容量 + self.batch_size = 128 # mini-batch SGD中的批量大小 + self.target_update = 2 # 目标网络的更新频率 + self.hidden_dim = 256 # 网络隐藏层维度 + self.soft_tau = 1e-2 # 软更新参数 + +class PlotConfig: + def __init__(self) -> None: + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.result_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/results/' # 保存结果的路径 + self.model_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + +def env_agent_config(cfg,seed=1): + env = NormalizedActions(gym.make(cfg.env_name)) # 装饰action噪声 + env.seed(seed) # 随机种子 + state_dim = env.observation_space.shape[0] + action_dim = env.action_space.shape[0] + agent = DDPG(state_dim,action_dim,cfg) + return env,agent + +cfg = DDPGConfig() +plot_cfg = PlotConfig() +# 训练 +env,agent = env_agent_config(cfg,seed=1) +rewards, ma_rewards = train(cfg, env, agent) +make_dir(plot_cfg.result_path, plot_cfg.model_path) +agent.save(path=plot_cfg.model_path) +save_results(rewards, ma_rewards, tag='train', path=plot_cfg.result_path) +plot_rewards(rewards, ma_rewards, plot_cfg, tag="train") # 画出结果 +# 测试 +env,agent = env_agent_config(cfg,seed=10) +agent.load(path=plot_cfg.model_path) +rewards,ma_rewards = test(plot_cfg,env,agent) +save_results(rewards,ma_rewards,tag = 'test',path = cfg.result_path) +plot_rewards(rewards, ma_rewards, plot_cfg, tag="test") # 画出结果 + diff --git a/codes/DDPG/train.py b/codes/DDPG/train.py new file mode 100644 index 0000000..4cdfa9d --- /dev/null +++ b/codes/DDPG/train.py @@ -0,0 +1,64 @@ +import sys +import os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +from DDPG.env import OUNoise + +def train(cfg, env, agent): + print('开始训练!') + print(f'环境:{cfg.env_name},算法:{cfg.algo},设备:{cfg.device}') + ou_noise = OUNoise(env.action_space) # 动作噪声 + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.train_eps): + state = env.reset() + ou_noise.reset() + done = False + ep_reward = 0 + i_step = 0 + while not done: + i_step += 1 + action = agent.choose_action(state) + action = ou_noise.get_action(action, i_step) + next_state, reward, done, _ = env.step(action) + ep_reward += reward + agent.memory.push(state, action, reward, next_state, done) + agent.update() + state = next_state + if (i_ep+1)%10 == 0: + print('回合:{}/{},奖励:{:.2f}'.format(i_ep+1, cfg.train_eps, ep_reward)) + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + print('完成训练!') + return rewards, ma_rewards + +def test(cfg, env, agent): + print('开始测试!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.test_eps): + state = env.reset() + done = False + ep_reward = 0 + i_step = 0 + while not done: + i_step += 1 + action = agent.choose_action(state) + next_state, reward, done, _ = env.step(action) + ep_reward += reward + state = next_state + print('回合:{}/{}, 奖励:{}'.format(i_ep+1, cfg.train_eps, ep_reward)) + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + print(f"回合:{i_ep+1}/{cfg.test_eps},奖励:{ep_reward:.1f}") + print('完成测试!') + return rewards, ma_rewards \ No newline at end of file diff --git a/codes/DQN/README.md b/codes/DQN/README.md new file mode 100644 index 0000000..fc82fe6 --- /dev/null +++ b/codes/DQN/README.md @@ -0,0 +1,218 @@ +# DQN + +## 原理简介 + +DQN是Q-leanning算法的优化和延伸,Q-leaning中使用有限的Q表存储值的信息,而DQN中则用神经网络替代Q表存储信息,这样更适用于高维的情况,相关知识基础可参考[datawhale李宏毅笔记-Q学习](https://datawhalechina.github.io/easy-rl/#/chapter6/chapter6)。 + +论文方面主要可以参考两篇,一篇就是2013年谷歌DeepMind团队的[Playing Atari with Deep Reinforcement Learning](https://www.cs.toronto.edu/~vmnih/docs/dqn.pdf),一篇是也是他们团队后来在Nature杂志上发表的[Human-level control through deep reinforcement learning](https://web.stanford.edu/class/psych209/Readings/MnihEtAlHassibis15NatureControlDeepRL.pdf)。后者在算法层面增加target q-net,也可以叫做Nature DQN。 + +Nature DQN使用了两个Q网络,一个当前Q网络𝑄用来选择动作,更新模型参数,另一个目标Q网络𝑄′用于计算目标Q值。目标Q网络的网络参数不需要迭代更新,而是每隔一段时间从当前Q网络𝑄复制过来,即延时更新,这样可以减少目标Q值和当前的Q值相关性。 + +要注意的是,两个Q网络的结构是一模一样的。这样才可以复制网络参数。Nature DQN和[Playing Atari with Deep Reinforcement Learning](https://www.cs.toronto.edu/~vmnih/docs/dqn.pdf)相比,除了用一个新的相同结构的目标Q网络来计算目标Q值以外,其余部分基本是完全相同的。细节也可参考[强化学习(九)Deep Q-Learning进阶之Nature DQN](https://www.cnblogs.com/pinard/p/9756075.html)。 + +https://blog.csdn.net/JohnJim0/article/details/109557173) + +## 伪代码 + +img + +## 代码实现 + +### RL接口 + +首先是强化学习训练的基本接口,即通用的训练模式: +```python +for i_episode in range(MAX_EPISODES): + state = env.reset() # reset环境状态 + for i_step in range(MAX_STEPS): + action = agent.choose_action(state) # 根据当前环境state选择action + next_state, reward, done, _ = env.step(action) # 更新环境参数 + agent.memory.push(state, action, reward, next_state, done) # 将state等这些transition存入memory + agent.update() # 每步更新网络 + state = next_state # 跳转到下一个状态 + if done: + break +``` +每个episode加一个MAX_STEPS,也可以使用while not done, 加这个max_steps有时是因为比如gym环境训练目标就是在200个step下达到200的reward,或者是当完成一个episode的步数较多时也可以设置,基本流程跟所有伪代码一致,如下: +1. agent选择动作 +2. 环境根据agent的动作反馈出next_state和reward +3. agent进行更新,如有memory就会将transition(包含state,reward,action等)存入memory中 +4. 跳转到下一个状态 +5. 如果done了,就跳出循环,进行下一个episode的训练。 + +想要实现完整的算法还需要创建Qnet,Replaybuffer等类 + +### 两个Q网络 + +上文讲了Nature DQN中有两个Q网络,一个是policy_net,一个是延时更新的target_net,两个网络的结构是一模一样的,如下(见```model.py```),注意DQN使用的Qnet就是全连接网络即FCH: +```python +import torch.nn as nn +import torch.nn.functional as F + +class FCN(nn.Module): + def __init__(self, state_dim=4, action_dim=18): + """ 初始化q网络,为全连接网络 + state_dim: 输入的feature即环境的state数目 + action_dim: 输出的action总个数 + """ + super(FCN, self).__init__() + self.fc1 = nn.Linear(state_dim, 128) # 输入层 + self.fc2 = nn.Linear(128, 128) # 隐藏层 + self.fc3 = nn.Linear(128, action_dim) # 输出层 + + def forward(self, x): + # 各层对应的激活函数 + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) +``` +输入为state_dim,输出为action_dim,包含一个128维度的隐藏层,这里根据需要可增加隐藏层维度和数量,然后一般使用relu激活函数,这里跟深度学习的网路设置是一样的。 + +### Replay Buffer + +然后就是Replay Memory了,其作用主要是是克服经验数据的相关性(correlated data)和非平稳分布(non-stationary distribution)问题,实现如下(见```memory.py```): + +```python +import random +import numpy as np + +class ReplayBuffer: + + def __init__(self, capacity): + self.capacity = capacity + self.buffer = [] + self.position = 0 + + def push(self, state, action, reward, next_state, done): + if len(self.buffer) < self.capacity: + self.buffer.append(None) + self.buffer[self.position] = (state, action, reward, next_state, done) + self.position = (self.position + 1) % self.capacity + + def sample(self, batch_size): + batch = random.sample(self.buffer, batch_size) + state, action, reward, next_state, done = zip(*batch) + return state, action, reward, next_state, done + + def __len__(self): + return len(self.buffer) +``` + +参数capacity表示buffer的容量,主要包括push和sample两个步骤,push是将transitions放到memory中,sample是从memory随机抽取一些transition。 + +### Agent类 + +在```agent.py```中我们定义强化学习算法类,包括```choose_action```(选择动作,使用e-greedy策略时会多一个```predict```函数,下面会将到)和```update```(更新)等函数。 + +在类中建立两个网络,以及optimizer和memory, + +```python +self.policy_net = MLP(state_dim, action_dim,hidden_dim=cfg.hidden_dim).to(self.device) +self.target_net = MLP(state_dim, action_dim,hidden_dim=cfg.hidden_dim).to(self.device) +for target_param, param in zip(self.target_net.parameters(),self.policy_net.parameters()): # copy params from policy net + target_param.data.copy_(param.data) +self.optimizer = optim.Adam(self.policy_net.parameters(), lr=cfg.lr) +self.memory = ReplayBuffer(cfg.memory_capacity) +``` +然后是选择action: + +```python +def choose_action(self, state): + '''选择动作 + ''' + self.frame_idx += 1 + if random.random() > self.epsilon(self.frame_idx): + action = self.predict(state) + else: + action = random.randrange(self.action_dim) + return action +``` + +这里使用e-greedy策略,即设置一个参数epsilon,如果生成的随机数大于epsilon,就根据网络预测的选择action,否则还是随机选择action,这个epsilon是会逐渐减小的,可以使用线性或者指数减小的方式,但不会减小到零,这样在训练稳定时还能保持一定的探索,这部分可以学习探索与利用(exploration and exploition)相关知识。 + +上面讲到的预测函数其实就是根据state选取q值最大的action,如下: + +```python +def predict(self,state): + with torch.no_grad(): + state = torch.tensor([state], device=self.device, dtype=torch.float32) + q_values = self.policy_net(state) + action = q_values.max(1)[1].item() +``` + +然后是更新函数了: + +```python +def update(self): + + if len(self.memory) < self.batch_size: + return + # 从memory中随机采样transition + state_batch, action_batch, reward_batch, next_state_batch, done_batch = self.memory.sample( + self.batch_size) + '''转为张量 + 例如tensor([[-4.5543e-02, -2.3910e-01, 1.8344e-02, 2.3158e-01],...,[-1.8615e-02, -2.3921e-01, -1.1791e-02, 2.3400e-01]])''' + state_batch = torch.tensor( + state_batch, device=self.device, dtype=torch.float) + action_batch = torch.tensor(action_batch, device=self.device).unsqueeze( + 1) # 例如tensor([[1],...,[0]]) + reward_batch = torch.tensor( + reward_batch, device=self.device, dtype=torch.float) # tensor([1., 1.,...,1]) + next_state_batch = torch.tensor( + next_state_batch, device=self.device, dtype=torch.float) + done_batch = torch.tensor(np.float32( + done_batch), device=self.device) + + '''计算当前(s_t,a)对应的Q(s_t, a)''' + '''torch.gather:对于a=torch.Tensor([[1,2],[3,4]]),那么a.gather(1,torch.Tensor([[0],[1]]))=torch.Tensor([[1],[3]])''' + q_values = self.policy_net(state_batch).gather( + dim=1, index=action_batch) # 等价于self.forward + # 计算所有next states的V(s_{t+1}),即通过target_net中选取reward最大的对应states + next_q_values = self.target_net(next_state_batch).max( + 1)[0].detach() # 比如tensor([ 0.0060, -0.0171,...,]) + # 计算 expected_q_value + # 对于终止状态,此时done_batch[0]=1, 对应的expected_q_value等于reward + expected_q_values = reward_batch + \ + self.gamma * next_q_values * (1-done_batch) + # self.loss = F.smooth_l1_loss(q_values,expected_q_values.unsqueeze(1)) # 计算 Huber loss + loss = nn.MSELoss()(q_values, expected_q_values.unsqueeze(1)) # 计算 均方误差loss + # 优化模型 + self.optimizer.zero_grad() # zero_grad清除上一步所有旧的gradients from the last step + # loss.backward()使用backpropagation计算loss相对于所有parameters(需要gradients)的微分 + loss.backward() + # for param in self.policy_net.parameters(): # clip防止梯度爆炸 + # param.grad.data.clamp_(-1, 1) + self.optimizer.step() # 更新模型 +``` + +更新遵循伪代码的以下部分: + +image-20210507162813393 + +首先从replay buffer中选取一个batch的数据,计算loss,然后进行minibatch SGD。 + +然后是保存与加载模型的部分,如下: + +```python +def save(self, path): + torch.save(self.target_net.state_dict(), path+'dqn_checkpoint.pth') +def load(self, path): + self.target_net.load_state_dict(torch.load(path+'dqn_checkpoint.pth')) + for target_param, param in zip(self.target_net.parameters(), self.policy_net.parameters()): + param.data.copy_(target_param.data) +``` + + + +### 实验结果 + +训练结果如下: + +train_rewards_curve + +eval_rewards_curve + +## 参考 + +[with torch.no_grad()](https://www.jianshu.com/p/1cea017f5d11) + diff --git a/codes/DQN/assets/eval_rewards_curve.png b/codes/DQN/assets/eval_rewards_curve.png new file mode 100644 index 0000000..0327b47 Binary files /dev/null and b/codes/DQN/assets/eval_rewards_curve.png differ diff --git a/codes/DQN/assets/image-20210507162813393.png b/codes/DQN/assets/image-20210507162813393.png new file mode 100644 index 0000000..3c4ae33 Binary files /dev/null and b/codes/DQN/assets/image-20210507162813393.png differ diff --git a/codes/DQN/assets/rewards_curve_train.png b/codes/DQN/assets/rewards_curve_train.png new file mode 100644 index 0000000..a077d9d Binary files /dev/null and b/codes/DQN/assets/rewards_curve_train.png differ diff --git a/codes/DQN/assets/train_rewards_curve.png b/codes/DQN/assets/train_rewards_curve.png new file mode 100644 index 0000000..b9667f1 Binary files /dev/null and b/codes/DQN/assets/train_rewards_curve.png differ diff --git a/codes/DQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70.png b/codes/DQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70.png new file mode 100644 index 0000000..c55ac87 Binary files /dev/null and b/codes/DQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70.png differ diff --git a/codes/DQN/dqn.py b/codes/DQN/dqn.py new file mode 100644 index 0000000..4a4dfc4 --- /dev/null +++ b/codes/DQN/dqn.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +@Author: John +@Email: johnjim0816@gmail.com +@Date: 2020-06-12 00:50:49 +@LastEditor: John +LastEditTime: 2021-12-22 14:01:37 +@Discription: +@Environment: python 3.7.7 +''' +'''off-policy +''' + +import torch +import torch.nn as nn +import torch.nn.functional as F +import torch.optim as optim +import random +import math +import numpy as np + +class MLP(nn.Module): + def __init__(self, state_dim,action_dim,hidden_dim=128): + """ 初始化q网络,为全连接网络 + state_dim: 输入的特征数即环境的状态维度 + action_dim: 输出的动作维度 + """ + super(MLP, self).__init__() + self.fc1 = nn.Linear(state_dim, hidden_dim) # 输入层 + self.fc2 = nn.Linear(hidden_dim,hidden_dim) # 隐藏层 + self.fc3 = nn.Linear(hidden_dim, action_dim) # 输出层 + + def forward(self, x): + # 各层对应的激活函数 + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + +class ReplayBuffer: + def __init__(self, capacity): + self.capacity = capacity # 经验回放的容量 + self.buffer = [] # 缓冲区 + self.position = 0 + + def push(self, state, action, reward, next_state, done): + ''' 缓冲区是一个队列,容量超出时去掉开始存入的转移(transition) + ''' + if len(self.buffer) < self.capacity: + self.buffer.append(None) + self.buffer[self.position] = (state, action, reward, next_state, done) + self.position = (self.position + 1) % self.capacity + + def sample(self, batch_size): + batch = random.sample(self.buffer, batch_size) # 随机采出小批量转移 + state, action, reward, next_state, done = zip(*batch) # 解压成状态,动作等 + return state, action, reward, next_state, done + + def __len__(self): + ''' 返回当前存储的量 + ''' + return len(self.buffer) + +class DQN: + def __init__(self, state_dim, action_dim, cfg): + + self.action_dim = action_dim # 总的动作个数 + self.device = cfg.device # 设备,cpu或gpu等 + self.gamma = cfg.gamma # 奖励的折扣因子 + # e-greedy策略相关参数 + self.frame_idx = 0 # 用于epsilon的衰减计数 + self.epsilon = lambda frame_idx: cfg.epsilon_end + \ + (cfg.epsilon_start - cfg.epsilon_end) * \ + math.exp(-1. * frame_idx / cfg.epsilon_decay) + self.batch_size = cfg.batch_size + self.policy_net = MLP(state_dim, action_dim,hidden_dim=cfg.hidden_dim).to(self.device) + self.target_net = MLP(state_dim, action_dim,hidden_dim=cfg.hidden_dim).to(self.device) + for target_param, param in zip(self.target_net.parameters(),self.policy_net.parameters()): # 复制参数到目标网路targe_net + target_param.data.copy_(param.data) + self.optimizer = optim.Adam(self.policy_net.parameters(), lr=cfg.lr) # 优化器 + self.memory = ReplayBuffer(cfg.memory_capacity) # 经验回放 + + def choose_action(self, state): + ''' 选择动作 + ''' + self.frame_idx += 1 + if random.random() > self.epsilon(self.frame_idx): + with torch.no_grad(): + state = torch.tensor([state], device=self.device, dtype=torch.float32) + q_values = self.policy_net(state) + action = q_values.max(1)[1].item() # 选择Q值最大的动作 + else: + action = random.randrange(self.action_dim) + return action + def update(self): + if len(self.memory) < self.batch_size: # 当memory中不满足一个批量时,不更新策略 + return + # 从经验回放中(replay memory)中随机采样一个批量的转移(transition) + state_batch, action_batch, reward_batch, next_state_batch, done_batch = self.memory.sample( + self.batch_size) + # 转为张量 + state_batch = torch.tensor(state_batch, device=self.device, dtype=torch.float) + action_batch = torch.tensor(action_batch, device=self.device).unsqueeze(1) + reward_batch = torch.tensor(reward_batch, device=self.device, dtype=torch.float) + next_state_batch = torch.tensor(next_state_batch, device=self.device, dtype=torch.float) + done_batch = torch.tensor(np.float32(done_batch), device=self.device) + q_values = self.policy_net(state_batch).gather(dim=1, index=action_batch) # 计算当前状态(s_t,a)对应的Q(s_t, a) + next_q_values = self.target_net(next_state_batch).max(1)[0].detach() # 计算下一时刻的状态(s_t_,a)对应的Q值 + # 计算期望的Q值,对于终止状态,此时done_batch[0]=1, 对应的expected_q_value等于reward + expected_q_values = reward_batch + self.gamma * next_q_values * (1-done_batch) + loss = nn.MSELoss()(q_values, expected_q_values.unsqueeze(1)) # 计算均方根损失 + # 优化更新模型 + self.optimizer.zero_grad() + loss.backward() + for param in self.policy_net.parameters(): # clip防止梯度爆炸 + param.grad.data.clamp_(-1, 1) + self.optimizer.step() + + def save(self, path): + torch.save(self.target_net.state_dict(), path+'dqn_checkpoint.pth') + + def load(self, path): + self.target_net.load_state_dict(torch.load(path+'dqn_checkpoint.pth')) + for target_param, param in zip(self.target_net.parameters(), self.policy_net.parameters()): + param.data.copy_(target_param.data) diff --git a/codes/DQN/dqn_cnn.py b/codes/DQN/dqn_cnn.py new file mode 100644 index 0000000..c14118f --- /dev/null +++ b/codes/DQN/dqn_cnn.py @@ -0,0 +1,133 @@ +import torch +import torch.nn as nn +import torch.optim as optim +import torch.autograd as autograd +import random +import math +class CNN(nn.Module): + def __init__(self, input_dim, output_dim): + super(CNN, self).__init__() + + self.input_dim = input_dim + self.output_dim = output_dim + + self.features = nn.Sequential( + nn.Conv2d(input_dim[0], 32, kernel_size=8, stride=4), + nn.ReLU(), + nn.Conv2d(32, 64, kernel_size=4, stride=2), + nn.ReLU(), + nn.Conv2d(64, 64, kernel_size=3, stride=1), + nn.ReLU() + ) + + self.fc = nn.Sequential( + nn.Linear(self.feature_size(), 512), + nn.ReLU(), + nn.Linear(512, self.output_dim) + ) + + def forward(self, x): + x = self.features(x) + x = x.view(x.size(0), -1) + x = self.fc(x) + return x + + def feature_size(self): + return self.features(autograd.Variable(torch.zeros(1, *self.input_dim))).view(1, -1).size(1) + + + def act(self, state, epsilon): + if random.random() > epsilon: + state = Variable(torch.FloatTensor(np.float32(state)).unsqueeze(0), volatile=True) + q_value = self.forward(state) + action = q_value.max(1)[1].data[0] + else: + action = random.randrange(env.action_space.n) + return action + +class ReplayBuffer: + def __init__(self, capacity): + self.capacity = capacity # 经验回放的容量 + self.buffer = [] # 缓冲区 + self.position = 0 + + def push(self, state, action, reward, next_state, done): + ''' 缓冲区是一个队列,容量超出时去掉开始存入的转移(transition) + ''' + if len(self.buffer) < self.capacity: + self.buffer.append(None) + self.buffer[self.position] = (state, action, reward, next_state, done) + self.position = (self.position + 1) % self.capacity + + def sample(self, batch_size): + batch = random.sample(self.buffer, batch_size) # 随机采出小批量转移 + state, action, reward, next_state, done = zip(*batch) # 解压成状态,动作等 + return state, action, reward, next_state, done + + def __len__(self): + ''' 返回当前存储的量 + ''' + return len(self.buffer) + +class DQN: + def __init__(self, state_dim, action_dim, cfg): + + self.action_dim = action_dim # 总的动作个数 + self.device = cfg.device # 设备,cpu或gpu等 + self.gamma = cfg.gamma # 奖励的折扣因子 + # e-greedy策略相关参数 + self.frame_idx = 0 # 用于epsilon的衰减计数 + self.epsilon = lambda frame_idx: cfg.epsilon_end + \ + (cfg.epsilon_start - cfg.epsilon_end) * \ + math.exp(-1. * frame_idx / cfg.epsilon_decay) + self.batch_size = cfg.batch_size + self.policy_net = CNN(state_dim, action_dim).to(self.device) + self.target_net = CNN(state_dim, action_dim).to(self.device) + for target_param, param in zip(self.target_net.parameters(),self.policy_net.parameters()): # 复制参数到目标网路targe_net + target_param.data.copy_(param.data) + self.optimizer = optim.Adam(self.policy_net.parameters(), lr=cfg.lr) # 优化器 + self.memory = ReplayBuffer(cfg.memory_capacity) # 经验回放 + + def choose_action(self, state): + ''' 选择动作 + ''' + self.frame_idx += 1 + if random.random() > self.epsilon(self.frame_idx): + with torch.no_grad(): + state = torch.tensor([state], device=self.device, dtype=torch.float32) + q_values = self.policy_net(state) + action = q_values.max(1)[1].item() # 选择Q值最大的动作 + else: + action = random.randrange(self.action_dim) + return action + def update(self): + if len(self.memory) < self.batch_size: # 当memory中不满足一个批量时,不更新策略 + return + # 从经验回放中(replay memory)中随机采样一个批量的转移(transition) + state_batch, action_batch, reward_batch, next_state_batch, done_batch = self.memory.sample( + self.batch_size) + # 转为张量 + state_batch = torch.tensor(state_batch, device=self.device, dtype=torch.float) + action_batch = torch.tensor(action_batch, device=self.device).unsqueeze(1) + reward_batch = torch.tensor(reward_batch, device=self.device, dtype=torch.float) + next_state_batch = torch.tensor(next_state_batch, device=self.device, dtype=torch.float) + done_batch = torch.tensor(np.float32(done_batch), device=self.device) + q_values = self.policy_net(state_batch).gather(dim=1, index=action_batch) # 计算当前状态(s_t,a)对应的Q(s_t, a) + next_q_values = self.target_net(next_state_batch).max(1)[0].detach() # 计算下一时刻的状态(s_t_,a)对应的Q值 + # 计算期望的Q值,对于终止状态,此时done_batch[0]=1, 对应的expected_q_value等于reward + expected_q_values = reward_batch + self.gamma * next_q_values * (1-done_batch) + loss = nn.MSELoss()(q_values, expected_q_values.unsqueeze(1)) # 计算均方根损失 + # 优化更新模型 + self.optimizer.zero_grad() + loss.backward() + for param in self.policy_net.parameters(): # clip防止梯度爆炸 + param.grad.data.clamp_(-1, 1) + self.optimizer.step() + + def save(self, path): + torch.save(self.target_net.state_dict(), path+'dqn_checkpoint.pth') + + def load(self, path): + self.target_net.load_state_dict(torch.load(path+'dqn_checkpoint.pth')) + for target_param, param in zip(self.target_net.parameters(), self.policy_net.parameters()): + param.data.copy_(target_param.data) \ No newline at end of file diff --git a/codes/DQN/outputs/CartPole-v0/20211229-144313/models/dqn_checkpoint.pth b/codes/DQN/outputs/CartPole-v0/20211229-144313/models/dqn_checkpoint.pth new file mode 100644 index 0000000..7fcf736 Binary files /dev/null and b/codes/DQN/outputs/CartPole-v0/20211229-144313/models/dqn_checkpoint.pth differ diff --git a/codes/DQN/outputs/CartPole-v0/20211229-144313/results/test_ma_rewards.npy b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/test_ma_rewards.npy new file mode 100644 index 0000000..343fcc6 Binary files /dev/null and b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/test_ma_rewards.npy differ diff --git a/codes/DQN/outputs/CartPole-v0/20211229-144313/results/test_rewards.npy b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/test_rewards.npy new file mode 100644 index 0000000..343fcc6 Binary files /dev/null and b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/test_rewards.npy differ diff --git a/codes/DQN/outputs/CartPole-v0/20211229-144313/results/test_rewards_curve.png b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/test_rewards_curve.png new file mode 100644 index 0000000..bc60080 Binary files /dev/null and b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/test_rewards_curve.png differ diff --git a/codes/DQN/outputs/CartPole-v0/20211229-144313/results/train_ma_rewards.npy b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/train_ma_rewards.npy new file mode 100644 index 0000000..d81acd2 Binary files /dev/null and b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/train_ma_rewards.npy differ diff --git a/codes/DQN/outputs/CartPole-v0/20211229-144313/results/train_rewards.npy b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/train_rewards.npy new file mode 100644 index 0000000..900914d Binary files /dev/null and b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/train_rewards.npy differ diff --git a/codes/DQN/outputs/CartPole-v0/20211229-144313/results/train_rewards_curve.png b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/train_rewards_curve.png new file mode 100644 index 0000000..9df7664 Binary files /dev/null and b/codes/DQN/outputs/CartPole-v0/20211229-144313/results/train_rewards_curve.png differ diff --git a/codes/DQN/task0.py b/codes/DQN/task0.py new file mode 100644 index 0000000..c7cd5da --- /dev/null +++ b/codes/DQN/task0.py @@ -0,0 +1,148 @@ +import sys +import os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import gym +import torch +import datetime +import numpy as np +from common.utils import save_results, make_dir +from common.utils import plot_rewards +from DQN.dqn import DQN + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 + + +class Config: + '''超参数 + ''' + + def __init__(self): + ################################## 环境超参数 ################################### + self.algo_name = 'DQN' # 算法名称 + self.env_name = 'CartPole-v0' # 环境名称 + self.device = torch.device( + "cuda" if torch.cuda.is_available() else "cpu") # 检测GPUgjgjlkhfsf风刀霜的撒发十 + self.seed = 10 # 随机种子,置0则不设置随机种子 + self.train_eps = 200 # 训练的回合数 + self.test_eps = 30 # 测试的回合数 + ################################################################################ + + ################################## 算法超参数 ################################### + self.gamma = 0.95 # 强化学习中的折扣因子 + self.epsilon_start = 0.90 # e-greedy策略中初始epsilon + self.epsilon_end = 0.01 # e-greedy策略中的终止epsilon + self.epsilon_decay = 500 # e-greedy策略中epsilon的衰减率 + self.lr = 0.0001 # 学习率 + self.memory_capacity = 100000 # 经验回放的容量 + self.batch_size = 64 # mini-batch SGD中的批量大小 + self.target_update = 4 # 目标网络的更新频率 + self.hidden_dim = 256 # 网络隐藏层 + ################################################################################ + + ################################# 保存结果相关参数 ############################## + self.result_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/results/' # 保存结果的路径 + self.model_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + ################################################################################ + + +def env_agent_config(cfg): + ''' 创建环境和智能体 + ''' + env = gym.make(cfg.env_name) # 创建环境 + state_dim = env.observation_space.shape[0] # 状态维度 + action_dim = env.action_space.n # 动作维度 + agent = DQN(state_dim, action_dim, cfg) # 创建智能体 + if cfg.seed !=0: # 设置随机种子 + torch.manual_seed(cfg.seed) + env.seed(cfg.seed) + np.random.seed(cfg.seed) + return env, agent + + +def train(cfg, env, agent): + ''' 训练 + ''' + print('开始训练!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.train_eps): + ep_reward = 0 # 记录一回合内的奖励 + state = env.reset() # 重置环境,返回初始状态 + while True: + action = agent.choose_action(state) # 选择动作 + next_state, reward, done, _ = env.step(action) # 更新环境,返回transition + agent.memory.push(state, action, reward, + next_state, done) # 保存transition + state = next_state # 更新下一个状态 + agent.update() # 更新智能体 + ep_reward += reward # 累加奖励 + if done: + break + if (i_ep + 1) % cfg.target_update == 0: # 智能体目标网络更新 + agent.target_net.load_state_dict(agent.policy_net.state_dict()) + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(0.9 * ma_rewards[-1] + 0.1 * ep_reward) + else: + ma_rewards.append(ep_reward) + if (i_ep + 1) % 10 == 0: + print('回合:{}/{}, 奖励:{}'.format(i_ep + 1, cfg.train_eps, ep_reward)) + print('完成训练!') + env.close() + return rewards, ma_rewards + + +def test(cfg, env, agent): + print('开始测试!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + ############# 由于测试不需要使用epsilon-greedy策略,所以相应的值设置为0 ############### + cfg.epsilon_start = 0.0 # e-greedy策略中初始epsilon + cfg.epsilon_end = 0.0 # e-greedy策略中的终止epsilon + ################################################################################ + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.test_eps): + ep_reward = 0 # 记录一回合内的奖励 + state = env.reset() # 重置环境,返回初始状态 + while True: + action = agent.choose_action(state) # 选择动作 + next_state, reward, done, _ = env.step(action) # 更新环境,返回transition + state = next_state # 更新下一个状态 + ep_reward += reward # 累加奖励 + if done: + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(ma_rewards[-1] * 0.9 + ep_reward * 0.1) + else: + ma_rewards.append(ep_reward) + print(f"回合:{i_ep+1}/{cfg.test_eps},奖励:{ep_reward:.1f}") + print('完成测试!') + env.close() + return rewards, ma_rewards + + +if __name__ == "__main__": + cfg = Config() + # 训练 + env, agent = env_agent_config(cfg) + rewards, ma_rewards = train(cfg, env, agent) + make_dir(cfg.result_path, cfg.model_path) # 创建保存结果和模型路径的文件夹 + agent.save(path=cfg.model_path) # 保存模型 + save_results(rewards, ma_rewards, tag='train', + path=cfg.result_path) # 保存结果 + plot_rewards(rewards, ma_rewards, cfg, tag="train") # 画出结果 + # 测试 + env, agent = env_agent_config(cfg) + agent.load(path=cfg.model_path) # 导入模型 + rewards, ma_rewards = test(cfg, env, agent) + save_results(rewards, ma_rewards, tag='test', + path=cfg.result_path) # 保存结果 + plot_rewards(rewards, ma_rewards, cfg, tag="test") # 画出结果 diff --git a/codes/DQN/task1.py b/codes/DQN/task1.py new file mode 100644 index 0000000..078aa4c --- /dev/null +++ b/codes/DQN/task1.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: JiangJi +Email: johnjim0816@gmail.com +Date: 2021-12-22 11:14:17 +LastEditor: JiangJi +LastEditTime: 2021-12-22 11:40:44 +Discription: 使用 Nature DQN 训练 CartPole-v1 +''' +import sys +import os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import gym +import torch +import datetime +from common.utils import save_results, make_dir +from common.utils import plot_rewards, plot_rewards_cn +from DQN.dqn import DQN + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 +algo_name = "DQN" # 算法名称 +env_name = 'CartPole-v1' # 环境名称 +class DQNConfig: + ''' 算法相关参数设置 + ''' + + def __init__(self): + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.device = torch.device( + "cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.train_eps = 200 # 训练的回合数 + self.test_eps = 30 # 测试的回合数 + # 超参数 + self.gamma = 0.95 # 强化学习中的折扣因子 + self.epsilon_start = 0.90 # e-greedy策略中初始epsilon + self.epsilon_end = 0.01 # e-greedy策略中的终止epsilon + self.epsilon_decay = 500 # e-greedy策略中epsilon的衰减率 + self.lr = 0.0001 # 学习率 + self.memory_capacity = 100000 # 经验回放的容量 + self.batch_size = 64 # mini-batch SGD中的批量大小 + self.target_update = 4 # 目标网络的更新频率 + self.hidden_dim = 256 # 网络隐藏层 +class PlotConfig: + ''' 绘图相关参数设置 + ''' + + def __init__(self) -> None: + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.device = torch.device( + "cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.result_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/results/' # 保存结果的路径 + self.model_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + + +def env_agent_config(cfg, seed=1): + ''' 创建环境和智能体 + ''' + env = gym.make(cfg.env_name) # 创建环境 + env.seed(seed) # 设置随机种子 + state_dim = env.observation_space.shape[0] # 状态维度 + action_dim = env.action_space.n # 动作维度 + agent = DQN(state_dim, action_dim, cfg) # 创建智能体 + return env, agent + +def train(cfg, env, agent): + ''' 训练 + ''' + print('开始训练!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.train_eps): + ep_reward = 0 # 记录一回合内的奖励 + state = env.reset() # 重置环境,返回初始状态 + while True: + action = agent.choose_action(state) # 选择动作 + next_state, reward, done, _ = env.step(action) # 更新环境,返回transition + agent.memory.push(state, action, reward, next_state, done) # 保存transition + state = next_state # 更新下一个状态 + agent.update() # 更新智能体 + ep_reward += reward # 累加奖励 + if done: + break + if (i_ep+1) % cfg.target_update == 0: # 智能体目标网络更新 + agent.target_net.load_state_dict(agent.policy_net.state_dict()) + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + if (i_ep+1)%10 == 0: + print('回合:{}/{}, 奖励:{}'.format(i_ep+1, cfg.train_eps, ep_reward)) + print('完成训练!') + return rewards, ma_rewards + +def test(cfg,env,agent): + print('开始测试!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + # 由于测试不需要使用epsilon-greedy策略,所以相应的值设置为0 + cfg.epsilon_start = 0.0 # e-greedy策略中初始epsilon + cfg.epsilon_end = 0.0 # e-greedy策略中的终止epsilon + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.test_eps): + ep_reward = 0 # 记录一回合内的奖励 + state = env.reset() # 重置环境,返回初始状态 + while True: + action = agent.choose_action(state) # 选择动作 + next_state, reward, done, _ = env.step(action) # 更新环境,返回transition + state = next_state # 更新下一个状态 + ep_reward += reward # 累加奖励 + if done: + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1) + else: + ma_rewards.append(ep_reward) + print(f"回合:{i_ep+1}/{cfg.test_eps},奖励:{ep_reward:.1f}") + print('完成测试!') + return rewards,ma_rewards +if __name__ == "__main__": + cfg = DQNConfig() + plot_cfg = PlotConfig() + # 训练 + env, agent = env_agent_config(cfg, seed=1) + rewards, ma_rewards = train(cfg, env, agent) + make_dir(plot_cfg.result_path, plot_cfg.model_path) # 创建保存结果和模型路径的文件夹 + agent.save(path=plot_cfg.model_path) # 保存模型 + save_results(rewards, ma_rewards, tag='train', + path=plot_cfg.result_path) # 保存结果 + plot_rewards_cn(rewards, ma_rewards, plot_cfg, tag="train") # 画出结果 + # 测试 + env, agent = env_agent_config(cfg, seed=10) + agent.load(path=plot_cfg.model_path) # 导入模型 + rewards, ma_rewards = test(cfg, env, agent) + save_results(rewards, ma_rewards, tag='test', + path=plot_cfg.result_path) # 保存结果 + plot_rewards_cn(rewards, ma_rewards, plot_cfg, tag="test") # 画出结果 diff --git a/codes/DQN/task2.py b/codes/DQN/task2.py new file mode 100644 index 0000000..16571b2 --- /dev/null +++ b/codes/DQN/task2.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: JiangJi +Email: johnjim0816@gmail.com +Date: 2021-12-22 11:14:17 +LastEditor: JiangJi +LastEditTime: 2021-12-22 15:27:48 +Discription: 使用 DQN-cnn 训练 PongNoFrameskip-v4 +''' +import sys +import os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import gym +import torch +import datetime +from common.utils import save_results, make_dir +from common.utils import plot_rewards, plot_rewards_cn +from common.atari_wrappers import make_atari, wrap_deepmind +from DQN.dqn import DQN + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 +algo_name = 'DQN-cnn' # 算法名称 +env_name = 'PongNoFrameskip-v4' # 环境名称 +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU +class DQNConfig: + ''' 算法相关参数设置 + ''' + + def __init__(self): + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.device = device # 检测GPU + self.train_eps = 500 # 训练的回合数 + self.test_eps = 30 # 测试的回合数 + # 超参数 + self.gamma = 0.95 # 强化学习中的折扣因子 + self.epsilon_start = 0.90 # e-greedy策略中初始epsilon + self.epsilon_end = 0.01 # e-greedy策略中的终止epsilon + self.epsilon_decay = 500 # e-greedy策略中epsilon的衰减率 + self.lr = 0.0001 # 学习率 + self.memory_capacity = 100000 # 经验回放的容量 + self.batch_size = 64 # mini-batch SGD中的批量大小 + self.target_update = 4 # 目标网络的更新频率 + self.hidden_dim = 256 # 网络隐藏层 +class PlotConfig: + ''' 绘图相关参数设置 + ''' + + def __init__(self) -> None: + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.device = device # 检测GPU + self.result_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/results/' # 保存结果的路径 + self.model_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + + +def env_agent_config(cfg, seed=1): + ''' 创建环境和智能体 + ''' + env = make_atari(cfg.env_name) # 创建环境 + # env = wrap_deepmind(env) + # env = wrap_pytorch(env) + env.seed(seed) # 设置随机种子 + state_dim = env.observation_space.shape[0] # 状态维度 + action_dim = env.action_space.n # 动作维度 + agent = DQN(state_dim, action_dim, cfg) # 创建智能体 + return env, agent + +def train(cfg, env, agent): + ''' 训练 + ''' + print('开始训练!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.train_eps): + ep_reward = 0 # 记录一回合内的奖励 + state = env.reset() # 重置环境,返回初始状态 + while True: + action = agent.choose_action(state) # 选择动作 + next_state, reward, done, _ = env.step(action) # 更新环境,返回transition + agent.memory.push(state, action, reward, next_state, done) # 保存transition + state = next_state # 更新下一个状态 + agent.update() # 更新智能体 + ep_reward += reward # 累加奖励 + if done: + break + if (i_ep+1) % cfg.target_update == 0: # 智能体目标网络更新 + agent.target_net.load_state_dict(agent.policy_net.state_dict()) + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + if (i_ep+1)%10 == 0: + print('回合:{}/{}, 奖励:{}'.format(i_ep+1, cfg.train_eps, ep_reward)) + print('完成训练!') + return rewards, ma_rewards + +def test(cfg,env,agent): + print('开始测试!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + # 由于测试不需要使用epsilon-greedy策略,所以相应的值设置为0 + cfg.epsilon_start = 0.0 # e-greedy策略中初始epsilon + cfg.epsilon_end = 0.0 # e-greedy策略中的终止epsilon + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.test_eps): + ep_reward = 0 # 记录一回合内的奖励 + state = env.reset() # 重置环境,返回初始状态 + while True: + action = agent.choose_action(state) # 选择动作 + next_state, reward, done, _ = env.step(action) # 更新环境,返回transition + state = next_state # 更新下一个状态 + ep_reward += reward # 累加奖励 + if done: + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1) + else: + ma_rewards.append(ep_reward) + print(f"回合:{i_ep+1}/{cfg.test_eps},奖励:{ep_reward:.1f}") + print('完成测试!') + return rewards,ma_rewards +if __name__ == "__main__": + cfg = DQNConfig() + plot_cfg = PlotConfig() + # 训练 + env, agent = env_agent_config(cfg, seed=1) + rewards, ma_rewards = train(cfg, env, agent) + make_dir(plot_cfg.result_path, plot_cfg.model_path) # 创建保存结果和模型路径的文件夹 + agent.save(path=plot_cfg.model_path) # 保存模型 + save_results(rewards, ma_rewards, tag='train', + path=plot_cfg.result_path) # 保存结果 + plot_rewards_cn(rewards, ma_rewards, plot_cfg, tag="train") # 画出结果 + # 测试 + env, agent = env_agent_config(cfg, seed=10) + agent.load(path=plot_cfg.model_path) # 导入模型 + rewards, ma_rewards = test(cfg, env, agent) + save_results(rewards, ma_rewards, tag='test', + path=plot_cfg.result_path) # 保存结果 + plot_rewards_cn(rewards, ma_rewards, plot_cfg, tag="test") # 画出结果 diff --git a/codes/Docs/assets/Qlearning_1.png b/codes/Docs/assets/Qlearning_1.png new file mode 100644 index 0000000..08e3bbc Binary files /dev/null and b/codes/Docs/assets/Qlearning_1.png differ diff --git a/codes/Docs/assets/cliffwalking_1.png b/codes/Docs/assets/cliffwalking_1.png new file mode 100644 index 0000000..ae5b0f8 Binary files /dev/null and b/codes/Docs/assets/cliffwalking_1.png differ diff --git a/codes/Docs/assets/eval_rewards_curve_cn-1689282.png b/codes/Docs/assets/eval_rewards_curve_cn-1689282.png new file mode 100644 index 0000000..1f55598 Binary files /dev/null and b/codes/Docs/assets/eval_rewards_curve_cn-1689282.png differ diff --git a/codes/Docs/assets/eval_rewards_curve_cn-1760950.png b/codes/Docs/assets/eval_rewards_curve_cn-1760950.png new file mode 100644 index 0000000..a442aac Binary files /dev/null and b/codes/Docs/assets/eval_rewards_curve_cn-1760950.png differ diff --git a/codes/Docs/assets/eval_rewards_curve_cn.png b/codes/Docs/assets/eval_rewards_curve_cn.png new file mode 100644 index 0000000..766251e Binary files /dev/null and b/codes/Docs/assets/eval_rewards_curve_cn.png differ diff --git a/codes/Docs/assets/image-20210915020027615.png b/codes/Docs/assets/image-20210915020027615.png new file mode 100644 index 0000000..f27c3e8 Binary files /dev/null and b/codes/Docs/assets/image-20210915020027615.png differ diff --git a/codes/Docs/assets/pendulum_1.png b/codes/Docs/assets/pendulum_1.png new file mode 100644 index 0000000..5eec82e Binary files /dev/null and b/codes/Docs/assets/pendulum_1.png differ diff --git a/codes/Docs/assets/poster.jpg b/codes/Docs/assets/poster.jpg new file mode 100644 index 0000000..139a48e Binary files /dev/null and b/codes/Docs/assets/poster.jpg differ diff --git a/codes/Docs/assets/train_rewards_curve_cn-1689150.png b/codes/Docs/assets/train_rewards_curve_cn-1689150.png new file mode 100644 index 0000000..617f693 Binary files /dev/null and b/codes/Docs/assets/train_rewards_curve_cn-1689150.png differ diff --git a/codes/Docs/assets/train_rewards_curve_cn-1760758.png b/codes/Docs/assets/train_rewards_curve_cn-1760758.png new file mode 100644 index 0000000..06f3dc8 Binary files /dev/null and b/codes/Docs/assets/train_rewards_curve_cn-1760758.png differ diff --git a/codes/Docs/assets/train_rewards_curve_cn.png b/codes/Docs/assets/train_rewards_curve_cn.png new file mode 100644 index 0000000..e9f4b83 Binary files /dev/null and b/codes/Docs/assets/train_rewards_curve_cn.png differ diff --git a/codes/Docs/使用DDPG解决倒立摆问题.md b/codes/Docs/使用DDPG解决倒立摆问题.md new file mode 100644 index 0000000..fd625f5 --- /dev/null +++ b/codes/Docs/使用DDPG解决倒立摆问题.md @@ -0,0 +1,175 @@ +前面项目讲的环境都是离散动作的,但实际中也有很多连续动作的环境,比如Open AI Gym中的[Pendulum-v0](https://github.com/openai/gym/wiki/Pendulum-v0)环境,它解决的是一个倒立摆问题,我们先对该环境做一个简要说明。 + +## Pendulum-v0简介 + +如果说 CartPole-v0 是一个离散动作的经典入门环境的话,那么对应 Pendulum-v0 就是连续动作的经典入门环境,如下图,我们通过施加力矩使其向上摆动并保持直立。 + +image-20210915161550713 + +该环境的状态维度有三个,设摆针竖直方向上的顺时针旋转角为$\theta$,$\theta$设在$[-\pi,\pi]$之间,则相应的状态为$[cos\theta,sin\theta,\dot{\theta}]$,即表示角度和角速度,我们的动作则是一个-2到2之间的力矩,它是一个连续量,因而该环境不能用离散动作的算法比如 DQN 来解决。关于奖励是根据相关的物理原理而计算出的等式,如下: +$$ +-\left(\theta^{2}+0.1 * \hat{\theta}^{2}+0.001 * \text { action }^{2}\right) +$$ +对于每一步,其最低奖励为$-\left(\pi^{2}+0.1 * 8^{2}+0.001 * 2^{2}\right)= -16.2736044$,最高奖励为0。同 CartPole-v0 环境一样,达到最优算法的情况下,每回合的步数是无限的,因此这里设定每回合最大步数为200以便于训练。 + +## DDPG 基本接口 + +我们依然使用接口的概念,通过伪代码分析并实现 DDPG 的训练模式,如下: + +> 初始化评论家网络$Q\left(s, a \mid \theta^{Q}\right)$和演员网络$\mu\left(s \mid \theta^{\mu}\right)$,其权重分别为$\theta^{Q}$和$\theta^{\mu}$ +> +> 初始化目标网络$Q'$和$\mu'$,并复制权重$\theta^{Q^{\prime}} \leftarrow \theta^{Q}, \theta^{\mu^{\prime}} \leftarrow \theta^{\mu}$ +> +> 初始化经验回放缓冲区$R$ +> +> 执行$M$个回合循环,对于每个回合: +> +> * 初始化动作探索的的随机过程即噪声$\mathcal{N}$ +> +> * 初始化状态$s_1$ +> +> 循环$T$个时间步长,对于每个时步$ +> +> * 根据当前策略和噪声选择动作$a_{t}=\mu\left(s_{t} \mid \theta^{\mu}\right)+\mathcal{N}_{t}$ +> * 执行动作$a_t$并得到反馈$r_t$和下一个状态$s_{t+1}$ +> * 存储转移$\left(s_{t}, a_{t}, r_{t}, s_{t+1}\right)$到经验缓冲$R$中 +> * (更新策略)从$D$随机采样一个小批量的转移 +> * (更新策略)计算实际的Q值$y_{i}=r_{i}+\gamma Q^{\prime}\left(s_{i+1}, \mu^{\prime}\left(s_{i+1} \mid \theta^{\mu^{\prime}}\right) \mid \theta^{Q^{\prime}}\right)$ +> * (更新策略)对损失函数$L=\frac{1}{N} \sum_{i}\left(y_{i}-Q\left(s_{i}, a_{i} \mid \theta^{Q}\right)\right)^{2}$关于参数$\theta$做梯度下降用于更新评论家网络 +> * (更新策略)使用采样梯度更新演员网络的策略:$\left.\left.\nabla_{\theta^{\mu}} J \approx \frac{1}{N} \sum_{i} \nabla_{a} Q\left(s, a \mid \theta^{Q}\right)\right|_{s=s_{i}, a=\mu\left(s_{i}\right)} \nabla_{\theta^{\mu}} \mu\left(s \mid \theta^{\mu}\right)\right|_{s_{i}}$ +> * (更新策略)更新目标网络:$\theta^{Q^{\prime}} \leftarrow \tau \theta^{Q}+(1-\tau) \theta^{Q^{\prime}}$,$\theta^{\mu^{\prime}} \leftarrow \tau \theta^{\mu}+(1-\tau) \theta^{\mu^{\prime}}$ + +代码如下: + +```python +ou_noise = OUNoise(env.action_space) # 动作噪声 +rewards = [] # 记录奖励 +ma_rewards = [] # 记录滑动平均奖励 +for i_ep in range(cfg.train_eps): + state = env.reset() + ou_noise.reset() + done = False + ep_reward = 0 + i_step = 0 + while not done: + i_step += 1 + action = agent.choose_action(state) + action = ou_noise.get_action(action, i_step) + next_state, reward, done, _ = env.step(action) + ep_reward += reward + agent.memory.push(state, action, reward, next_state, done) + agent.update() + state = next_state + if (i_ep+1)%10 == 0: + print('回合:{}/{},奖励:{}'.format(i_ep+1, cfg.train_eps, ep_reward)) + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) +``` + +相比于 DQN ,DDPG 主要多了两处修改,一个是给动作施加噪声,另外一个是软更新策略,即最后一步。 + +## Ornstein-Uhlenbeck噪声 + + OU 噪声适用于惯性系统,尤其是时间离散化粒度较小的情况。 OU 噪声是一种随机过程,下面略去证明,直接给出公式: +$$ +x(t+\Delta t)=x(t)-\theta(x(t)-\mu) \Delta t+\sigma W_t +$$ +其中 $W_t$ 属于正太分布,进而代码实现如下: + +```python +class OUNoise(object): + '''Ornstein–Uhlenbeck噪声 + ''' + def __init__(self, action_space, mu=0.0, theta=0.15, max_sigma=0.3, min_sigma=0.3, decay_period=100000): + self.mu = mu # OU噪声的参数 + self.theta = theta # OU噪声的参数 + self.sigma = max_sigma # OU噪声的参数 + self.max_sigma = max_sigma + self.min_sigma = min_sigma + self.decay_period = decay_period + self.action_dim = action_space.shape[0] + self.low = action_space.low + self.high = action_space.high + self.reset() + def reset(self): + self.obs = np.ones(self.action_dim) * self.mu + def evolve_obs(self): + x = self.obs + dx = self.theta * (self.mu - x) + self.sigma * np.random.randn(self.action_dim) + self.obs = x + dx + return self.obs + def get_action(self, action, t=0): + ou_obs = self.evolve_obs() + self.sigma = self.max_sigma - (self.max_sigma - self.min_sigma) * min(1.0, t / self.decay_period) # sigma会逐渐衰减 + return np.clip(action + ou_obs, self.low, self.high) # 动作加上噪声后进行剪切 +``` + +## DDPG算法 + +DDPG算法主要也包括两个功能,一个是选择动作,另外一个是更新策略,首先看选择动作: + +```python +def choose_action(self, state): + state = torch.FloatTensor(state).unsqueeze(0).to(self.device) + action = self.actor(state) + return action.detach().cpu().numpy()[0, 0] +``` + +由于DDPG是直接从演员网络取得动作,所以这里不用$\epsilon-greedy$策略。在更新策略函数中,也会跟DQN稍有不同,并且加入软更新: + +```python +def update(self): + if len(self.memory) < self.batch_size: # 当 memory 中不满足一个批量时,不更新策略 + return + # 从经验回放中(replay memory)中随机采样一个批量的转移(transition) + state, action, reward, next_state, done = self.memory.sample(self.batch_size) + # 转变为张量 + state = torch.FloatTensor(state).to(self.device) + next_state = torch.FloatTensor(next_state).to(self.device) + action = torch.FloatTensor(action).to(self.device) + reward = torch.FloatTensor(reward).unsqueeze(1).to(self.device) + done = torch.FloatTensor(np.float32(done)).unsqueeze(1).to(self.device) + + policy_loss = self.critic(state, self.actor(state)) + policy_loss = -policy_loss.mean() + next_action = self.target_actor(next_state) + target_value = self.target_critic(next_state, next_action.detach()) + expected_value = reward + (1.0 - done) * self.gamma * target_value + expected_value = torch.clamp(expected_value, -np.inf, np.inf) + + value = self.critic(state, action) + value_loss = nn.MSELoss()(value, expected_value.detach()) + + self.actor_optimizer.zero_grad() + policy_loss.backward() + self.actor_optimizer.step() + self.critic_optimizer.zero_grad() + value_loss.backward() + self.critic_optimizer.step() + # 软更新 + for target_param, param in zip(self.target_critic.parameters(), self.critic.parameters()): + target_param.data.copy_( + target_param.data * (1.0 - self.soft_tau) + + param.data * self.soft_tau + ) + for target_param, param in zip(self.target_actor.parameters(), self.actor.parameters()): + target_param.data.copy_( + target_param.data * (1.0 - self.soft_tau) + + param.data * self.soft_tau + ) +``` + +## 结果分析 + +实现算法之后,我们先看看训练效果: + +![train_rewards_curve_cn](../../easy_rl_book/res/ch12/assets/train_rewards_curve_cn-1760758.png) + +可以看到算法整体上是达到收敛了的,但是稳定状态下波动还比较大,依然有提升的空间,限于笔者的精力,这里只是帮助赌注实现一个基础的代码演示,想要使得算法调到最优感兴趣的读者可以多思考实现。我们再来看看测试的结果: + +![eval_rewards_curve_cn](../../easy_rl_book/res/ch12/assets/eval_rewards_curve_cn-1760950.png) + +从图中看出测试的平均奖励在-150左右,但其实训练的时候平均的稳态奖励在-300左右,这是因为测试的时候我们舍去了OU噪声的缘故。 \ No newline at end of file diff --git a/codes/Docs/使用DQN解决推车杆问题.md b/codes/Docs/使用DQN解决推车杆问题.md new file mode 100644 index 0000000..393c52d --- /dev/null +++ b/codes/Docs/使用DQN解决推车杆问题.md @@ -0,0 +1,208 @@ + + +在练习本项目之前,可以先回顾一下之前的项目实战,即使用Q学习解决悬崖寻路问题。本项目将具体实现DQN算法来解决推车杆问题,对应的模拟环境为Open AI Gym中的[CartPole-v0](https://datawhalechina.github.io/easy-rl/#/chapter7/project2?id=cartpole-v0),我们同样先对该环境做一个简要说明。 + +## CartPole-v0 简介 + +CartPole-v0是一个经典的入门环境,如下图,它通过向左(动作=0)或向右(动作=1)推动推车来实现竖直杆的平衡,每次实施一个动作后如果能够继续保持平衡就会得到一个+1的奖励,否则杆将无法保持平衡而导致游戏结束。 + +![Gym](assets/poster.jpg) + +我们来看看这个环境的一些参数,执行以下代码: + +```python +import gym +env = gym.make('CartPole-v0') # 建立环境 +env.seed(1) # 随机种子 +state_dim = env.observation_space.shape[0] # 状态维度 +action_dim = env.action_space.n # 动作维度 +state = env.reset() # 初始化环境 +print(f"状态维度:{state_dim},动作维度:{action_dim}") +print(f"初始状态:{state}") +``` + +可以得到结果: + +```bash +状态维度:4,动作维度:2 +初始状态:[ 0.03073904 0.00145001 -0.03088818 -0.03131252] +``` + +该环境状态维度是四个,分别为车的位置、车的速度、杆的角度以及杆顶部的速度,动作维度为两个,并且是离散的向左或者向右。理论上达到最优化算法的情况下,推车杆是一直能保持平衡的,也就是每回合的步数是无限,但是这不方便训练,所以环境内部设置了每回合的最大步数为200,也就是说理想情况下,只需要我们每回合的奖励达到200就算训练完成。 + +## DQN基本接口 + +介绍完环境之后,我们沿用接口的概念,通过分析伪代码来实现DQN的基本训练模式,以及一些要素比如建立什么网络需要什么模块等等。我们现在常用的DQN伪代码如下: + +> 初始化经验回放缓冲区(replay memory)$D$,容量(capacity)为$N$ +> +> 初始化状态-动作函数,即带有初始随机权重$\theta$的$Q$网络 +> +> 初始化目标状态-动作函数,即带有初始随机权重$\theta^-$的$\hat{Q}$网络,且$\theta^-=\theta$ +> +> 执行$M$个回合循环,对于每个回合 +> +> * 初始化环境,得到初始状态$s_1$ +> * 循环$T$个时间步长,对于每个时步$t$ +> * 使用$\epsilon-greedy$策略选择动作$a_t$ +> * 环境根据$a_t$反馈当前的奖励$r_t$和下一个状态$s_{t+1}$ +> * 更新状态$s_{t+1}=s_t$ +> * 存储转移(transition)即$(s_t,a_t,r-t,s_{t+1})$到经验回放$D$中 +> * (更新策略)从$D$随机采样一个小批量的转移 +> * (更新策略)计算实际的Q值$y_{j}=\left\{\begin{array}{cc}r_{j} & \text { 如果回合在时步 j+1终止 }\\ r_{j}+\gamma \max _{a^{\prime}} \hat{Q}\left(\phi_{j+1}, a^{\prime} ; \theta^{-}\right) & \text {否则 }\end{array}\right.$ +> * (更新策略)对损失函数$\left(y_{j}-Q\left(\phi_{j}, a_{j} ; \theta\right)\right)^{2}$关于参数$\theta$做梯度下降 +> * (更新策略)每$C$步重置$\hat{Q}=Q$ + +用代码来实现的话如下: + +```python +rewards = [] # 记录奖励 + ma_rewards = [] # 记录滑动平均奖励 + for i_ep in range(cfg.train_eps): + state = env.reset() + done = False + ep_reward = 0 + while True: + action = agent.choose_action(state) + next_state, reward, done, _ = env.step(action) + ep_reward += reward + agent.memory.push(state, action, reward, next_state, done) + state = next_state + agent.update() + if done: + break + if (i_ep+1) % cfg.target_update == 0: + agent.target_net.load_state_dict(agent.policy_net.state_dict()) + if (i_ep+1)%10 == 0: + print('回合:{}/{}, 奖励:{}'.format(i_ep+1, cfg.train_eps, ep_reward)) + rewards.append(ep_reward) + # save ma_rewards + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) +``` + + + +可以看到,DQN的训练模式其实和大多强化学习算法是一样的套路,但与传统的Q学习算法相比,DQN使用神经网络来代替之前的Q表格从而存储更多的信息,且由于使用了神经网络所以我们一般需要利用随机梯度下降来优化Q值的预测。此外多了经验回放缓冲区(replay memory),并且使用两个网络,即目标网络和当前网络。 + +## 经验回放缓冲区 + +从伪代码中可以看出来,经验回放缓冲区的功能有两个,一个是将每一步采集的转移(transition,包括状态,动作,奖励,下一时刻的状态)存储到缓冲区中,并且缓冲区具备一定的容量(capacity),另一个是在更新策略的时候需要随机采样小批量的转移进行优化。因此我们可以定义一个ReplayBuffer类,包括push和sample两个函数,用于存储和采样。 + +```python +import random +class ReplayBuffer: + def __init__(self, capacity): + self.capacity = capacity # 经验回放的容量 + self.buffer = [] # 缓冲区 + self.position = 0 + + def push(self, state, action, reward, next_state, done): + ''' 缓冲区是一个队列,容量超出时去掉开始存入的转移(transition) + ''' + if len(self.buffer) < self.capacity: + self.buffer.append(None) + self.buffer[self.position] = (state, action, reward, next_state, done) + self.position = (self.position + 1) % self.capacity + + def sample(self, batch_size): + batch = random.sample(self.buffer, batch_size) # 随机采出小批量转移 + state, action, reward, next_state, done = zip(*batch) # 解压成状态,动作等 + return state, action, reward, next_state, done + def __len__(self): + ''' 返回当前存储的量 + ''' + return len(self.buffer) +``` + +## Q网络 + +在DQN中我们使用神经网络替代原有的Q表,从而能够存储更多的Q值,实现更为高级的策略以便用于复杂的环境,这里我们用的是一个三层的感知机或者说全连接网络: + +```python +class MLP(nn.Module): + def __init__(self, input_dim,output_dim,hidden_dim=128): + """ 初始化q网络,为全连接网络 + input_dim: 输入的特征数即环境的状态维度 + output_dim: 输出的动作维度 + """ + super(MLP, self).__init__() + self.fc1 = nn.Linear(input_dim, hidden_dim) # 输入层 + self.fc2 = nn.Linear(hidden_dim,hidden_dim) # 隐藏层 + self.fc3 = nn.Linear(hidden_dim, output_dim) # 输出层 + + def forward(self, x): + # 各层对应的激活函数 + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) +``` + +学过深度学习的同学应该都对这个网络十分熟悉,在强化学习中,网络的输入一般是状态,输出则是一个动作,假如总共有两个动作,那么这里的动作维度就是2,可能的输出就是0或1,一般我们用ReLU作为激活函数。根据实际需要也可以改变神经网络的模型结构等等,比如若我们使用图像作为输入的话,这里可以使用卷积神经网络(CNN)。 + +## DQN算法 + +跟前面的项目实战一样,DQN算法一般也包括选择动作和更新策略两个函数,首先我们看选择动作: + +```python +def choose_action(self, state): + '''选择动作 + ''' + self.frame_idx += 1 + if random.random() > self.epsilon(self.frame_idx): + with torch.no_grad(): + state = torch.tensor([state], device=self.device, dtype=torch.float32) + q_values = self.policy_net(state) + action = q_values.max(1)[1].item() # 选择Q值最大的动作 + else: + action = random.randrange(self.action_dim) +``` + +可以看到跟Q学习算法其实是一样的,都是用的$\epsilon-greedy$策略,只是使用神经网络的话我们需要通过Torch或者Tensorflow工具来处理相应的数据。 + +而DQN更新策略的步骤稍微复杂一点,主要包括三个部分:随机采样,计算期望Q值和梯度下降,如下: + +```python +def update(self): + if len(self.memory) < self.batch_size: # 当memory中不满足一个批量时,不更新策略 + return + # 从经验回放中(replay memory)中随机采样一个批量的转移(transition) + state_batch, action_batch, reward_batch, next_state_batch, done_batch = self.memory.sample( + self.batch_size) + # 转为张量 + state_batch = torch.tensor( + state_batch, device=self.device, dtype=torch.float) + action_batch = torch.tensor(action_batch, device=self.device).unsqueeze( + 1) + reward_batch = torch.tensor( + reward_batch, device=self.device, dtype=torch.float) + next_state_batch = torch.tensor( + next_state_batch, device=self.device, dtype=torch.float) + done_batch = torch.tensor(np.float32( + done_batch), device=self.device) + q_values = self.policy_net(state_batch).gather(dim=1, index=action_batch) # 计算当前状态(s_t,a)对应的Q(s_t, a) + next_q_values = self.target_net(next_state_batch).max(1)[0].detach() # 计算下一时刻的状态(s_t_,a)对应的Q值 + # 计算期望的Q值,对于终止状态,此时done_batch[0]=1, 对应的expected_q_value等于reward + expected_q_values = reward_batch + self.gamma * next_q_values * (1-done_batch) + loss = nn.MSELoss()(q_values, expected_q_values.unsqueeze(1)) # 计算均方根损失 + # 优化更新模型 + self.optimizer.zero_grad() + loss.backward() + for param in self.policy_net.parameters(): # clip防止梯度爆炸 + param.grad.data.clamp_(-1, 1) + self.optimizer.step() +``` + +## 结果分析 + +完成代码之后,我们先来看看DQN算法的训练效果,曲线如下: + +![train_rewards_curve_cn](../../easy_rl_book/res/ch7/assets/train_rewards_curve_cn-1689150.png) + +从图中看出,算法其实已经在60回合左右达到收敛,最后一直维持在最佳奖励200左右,可能会有轻微的波动,这是因为我们在收敛的情况下依然保持了一定的探索率,即epsilon_end=0.01。现在我们可以载入模型看看测试的效果: + +![eval_rewards_curve_cn](../../easy_rl_book/res/ch7/assets/eval_rewards_curve_cn-1689282.png) + +我们测试了30个回合,每回合都保持在200左右,说明我们的模型学习得不错了! \ No newline at end of file diff --git a/codes/Docs/使用Q-learning解决悬崖寻路问题.md b/codes/Docs/使用Q-learning解决悬崖寻路问题.md new file mode 100644 index 0000000..44e5b6c --- /dev/null +++ b/codes/Docs/使用Q-learning解决悬崖寻路问题.md @@ -0,0 +1,165 @@ +# 使用Q学习解决悬崖寻路问题 + +强化学习在运动规划方面也有很大的应用前景,已有很多适用于强化学习的相关仿真环境,小到迷宫,大到贴近真实的自动驾驶环境[CARLA](http://carla.org/)。本次使用[OpenAI Gym](https://gym.openai.com/)开发的CliffWalking-v0环境,带大家入门Q学习算法的代码实战。 + +## CliffWalking-v0环境简介 + +我们首先简单介绍一下这个环境,该环境中文名叫悬崖寻路(CliffWalking),是一个迷宫类问题。如下图,在一个4 x 12的网格中,智能体以网格的左下角位置为起点,以网格的下角位置为终点,目标是移动智能体到达终点位置,智能体每次可以在上、下、左、右这4个方向中移动一步,每移动一步会得到-1单位的奖励。 + +
+cliffwalking_1 +
+起终点之间是一段悬崖,即编号为37~46的网格,智能体移动过程中会有如下的限制: + +* 智能体不能移出网格边界,如果智能体想执行某个动作移出网格,那么这一步智能体不会移动,但是这个操作依然会得到-1单位的奖励 +* 如果智能体“掉入悬崖” ,会立即回到起点位置,并得到-100单位的奖励 +* 当智能体移动到终点时,该回合结束,该回合总奖励为各步奖励之和 + +我们的目标是以最少的步数到达终点,容易看出最少需要13步智能体才能从起点到终点,因此最佳算法收敛的情况下,每回合的总奖励应该是-13,这样人工分析出期望的奖励也便于我们判断算法的收敛情况作出相应调整。 + +现在我们可以在代码中定义环境,如下: + +```python +import gym # 导入gym模块 +from envs.gridworld_env import CliffWalkingWapper # 导入自定义装饰器 + +env = gym.make('CliffWalking-v0') # 定义环境 +env = CliffWalkingWapper(env) # 装饰环境 +``` + +这里我们在程序中使用了一个装饰器重新定义环境,但不影响对环境的理解,感兴趣的同学具体看相关代码。可以由于gym环境封装得比较好,所以我们想要使用这个环境只需要使用gym.make命令输入函数名即可,然后我们可以查看环境的状态和动作维度目: + +```python +state_dim = env.observation_space.n # 状态维度 +action_dim = env.action_space.n # 动作维度 +print(f"状态维度:{state_dim},动作维度:{action_dim}") +``` + +打印出来的结果如下: + +```bash +状态维度:48,动作维度:4 +``` + +我们的状态维度是48个,这里我们设置的是智能体当前所在网格的编号,而动作维度是4,这表示有0,1,2,3对应着上下左右四个动作。另外我们也可以初始化环境并打印当前所在的状态: + +```python +state = env.reset() +print(state) +``` + +结果显示为: + +```bash +36 +``` + +也就是说当前智能体的状态即当前所在的网格编号是36,正好对应我们前面讲到的起点。 + +## 强化学习基本接口 + +这里所说的接口就是一般强化学习的训练模式,也是大多数算法伪代码遵循的套路,步骤如下: + +* 初始化环境和智能体 +* 对于每个回合,智能体选择动作 +* 环境接收动作反馈下一个状态和奖励 +* 智能体进行策略更新(学习) +* 多个回合算法收敛之后保存模型以及做后续的分析画图等 + +代码如下: + +```python +env = gym.make('CliffWalking-v0') # 定义环境 +env = CliffWalkingWapper(env) # 装饰环境 +env.seed(1) # 设置随机种子 +state_dim = env.observation_space.n # 状态维度 +action_dim = env.action_space.n # 动作维度 +agent = QLearning(state_dim,action_dim,cfg) # cfg存储算法相关参数 +for i_ep in range(cfg.train_eps): # cfg.train_eps表示最大训练的回合数 + ep_reward = 0 # 记录每个回合的奖励 + state = env.reset() # 重置环境 + while True: + action = agent.choose_action(state) # 算法选择一个动作 + next_state, reward, done, _ = env.step(action) # 环境根据动作反馈奖励和下一个状态 + agent.update(state, action, reward, next_state, done) # 算法更新 + state = next_state # 更新状态 + ep_reward += reward + if done: # 终止状态提前停止 + break +``` + +通常我们会记录并分析奖励的变化,所以在接口基础上加一些变量记录每回合的奖励,此外由于强化学习学习过程得到的奖励可能会产生振荡,因此我们也适用一个滑动平均的量来反映奖励变化的趋势,如下: + +```bash +rewards = [] +ma_rewards = [] # 滑动平均奖励 +for i_ep in range(cfg.train_eps): + ep_reward = 0 # 记录每个回合的奖励 + state = env.reset() # 重置环境, 重新开一局(即开始新的一个回合) + while True: + action = agent.choose_action(state) # 根据算法选择一个动作 + next_state, reward, done, _ = env.step(action) # 与环境进行一次动作交互 + agent.update(state, action, reward, next_state, done) # Q-learning算法更新 + state = next_state # 存储上一个观察值 + ep_reward += reward + if done: + break +rewards.append(ep_reward) +if ma_rewards: + ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1) + else: + ma_rewards.append(ep_reward) +``` + +## Q学习算法 + +了解了基本接口之后,现在我们看看Q学习算法具体是怎么实现的,前面讲到智能体其实在整个训练中就做两件事,一个是选择动作,一个是更新策略,所以我们可以定义一个Qlearning类,里面主要包含两个函数choose_action和update。 + +我们先看看choose_action函数是怎么定义的,如下: + +```python +def choose_action(self, state): + self.sample_count += 1 + self.epsilon = self.epsilon_end + (self.epsilon_start - self.epsilon_end) + math.exp(-1. * self.sample_count / self.epsilon_decay) # epsilon是会递减的,这里选择指数递减 + # e-greedy 策略 + if np.random.uniform(0, 1) > self.epsilon: + action = np.argmax(self.Q_table[str(state)]) # 选择Q(s,a)最大对应的动作 + else: + action = np.random.choice(self.action_dim) # 随机选择动作 + return action +``` + + + +一般我们使用$\varepsilon-greedy$策略选择动作,我们的输入就是当前的状态,随机选取一个值,当这个值大于我们设置的$\varepsilon$时,我们选取Q值最大对应的动作,否则随机选择动作,这样就能在训练中让智能体保持一定的探索率,这也是平衡探索与利用的技巧之一。 + +下面是我们要实现的策略更新函数: + +```python +def update(self, state, action, reward, next_state, done): + Q_predict = self.Q_table[str(state)][action] + if done: # 终止状态 + Q_target = reward + else: + Q_target = reward + self.gamma * np.max(self.Q_table[str(next_state)]) + self.Q_table[str(state)][action] += self.lr * (Q_target - Q_predict) +``` + +这里面实现的逻辑就是伪代码中的更新公式: + +image-20210911213241605 + +注意终止状态下,我们是获取不到下一个动作的,我们直接将Q值(Q_target)更新为对应的奖励即可。 + +## 结果分析 + +到现在我们就基本完成了Q学习的代码实现,具体可以查看github上的源码,运行代码结果如下: + +![train_rewards_curve_cn](assets/train_rewards_curve_cn.png) + +由于这个环境比较简单,可以看到算法很快达到收敛,然后我们再测试我们训练好的模型,一般测试模型只需要20到50左右的回合数即可: + +![eval_rewards_curve_cn](assets/eval_rewards_curve_cn.png) + +这里我们测试的回合数为30,可以看到每个回合智能体都达到了最优的奖励,说明我们的算法训练的效果很不错! diff --git a/codes/DoubleDQN/README.md b/codes/DoubleDQN/README.md new file mode 100644 index 0000000..714bd26 --- /dev/null +++ b/codes/DoubleDQN/README.md @@ -0,0 +1,39 @@ +食用本篇之前,需要有DQN算法的基础,参考[DQN算法实战](../DQN)。 + +## 原理简介 + +Double-DQN是2016年提出的算法,灵感源自2010年的Double-Qlearning,可参考论文[Deep Reinforcement Learning with Double Q-learning](https://arxiv.org/abs/1509.06461)。 +跟Nature DQN一样,Double-DQN也用了两个网络,一个当前网络(对应用$Q$表示),一个目标网络(对应一般用$Q'$表示,为方便区分,以下用$Q_{tar}$代替)。我们先回忆一下,对于非终止状态,目标$Q_{tar}$值计算如下 +![在这里插入图片描述](assets/20201222145725907.png) + +而在Double-DQN中,不再是直接从目标$Q_{tar}$网络中选择各个动作中的最大$Q_{tar}$值,而是先从当前$Q$网络选择$Q$值最大对应的动作,然后代入到目标网络中计算对应的值: +![在这里插入图片描述](assets/20201222150225327.png) +Double-DQN的好处是Nature DQN中使用max虽然可以快速让Q值向可能的优化目标靠拢,但是很容易过犹不及,导致过度估计(Over Estimation),所谓过度估计就是最终我们得到的算法模型有很大的偏差(bias)。为了解决这个问题, DDQN通过解耦目标Q值动作的选择和目标Q值的计算这两步,来达到消除过度估计的问题,感兴趣可以阅读原论文。 + +伪代码如下: +![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70.png) +当然也可以两个网络可以同时为当前网络和目标网络,如下: +![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837146.png) +或者这样更好理解如何同时为当前网络和目标网络: +![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837157.png) + +## 代码实战 +完整程序见[github](https://github.com/JohnJim0816/reinforcement-learning-tutorials/tree/master/DoubleDQN)。结合上面的原理,其实Double DQN改进来很简单,基本只需要在```update```中修改几行代码,如下: +```python +'''以下是Nature DQN的q_target计算方式 +next_q_state_value = self.target_net( +next_state_batch).max(1)[0].detach() # # 计算所有next states的Q'(s_{t+1})的最大值,Q'为目标网络的q函数,比如tensor([ 0.0060, -0.0171,...,]) +#计算 q_target +#对于终止状态,此时done_batch[0]=1, 对应的expected_q_value等于reward +q_target = reward_batch + self.gamma * next_q_state_value * (1-done_batch[0]) +''' +'''以下是Double DQNq_target计算方式,与NatureDQN稍有不同''' +next_target_values = self.target_net( +next_state_batch) +#选出Q(s_t‘, a)对应的action,代入到next_target_values获得target net对应的next_q_value,即Q’(s_t|a=argmax Q(s_t‘, a)) +next_target_q_value = next_target_values.gather(1, torch.max(next_q_values, 1)[1].unsqueeze(1)).squeeze(1) +q_target = reward_batch + self.gamma * next_target_q_value * (1-done_batch[0]) +``` +reward变化结果如下: +![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837128.png) +其中下边蓝色和红色分别表示Double DQN和Nature DQN在训练中的reward变化图,而上面蓝色和绿色则表示Double DQN和Nature DQN在测试中的reward变化图。 \ No newline at end of file diff --git a/codes/DoubleDQN/assets/20201222145725907.png b/codes/DoubleDQN/assets/20201222145725907.png new file mode 100644 index 0000000..d2cbb2d Binary files /dev/null and b/codes/DoubleDQN/assets/20201222145725907.png differ diff --git a/codes/DoubleDQN/assets/20201222150225327.png b/codes/DoubleDQN/assets/20201222150225327.png new file mode 100644 index 0000000..20b79be Binary files /dev/null and b/codes/DoubleDQN/assets/20201222150225327.png differ diff --git a/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837128.png b/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837128.png new file mode 100644 index 0000000..427a903 Binary files /dev/null and b/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837128.png differ diff --git a/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837146.png b/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837146.png new file mode 100644 index 0000000..d95f900 Binary files /dev/null and b/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837146.png differ diff --git a/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837157.png b/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837157.png new file mode 100644 index 0000000..ddeda96 Binary files /dev/null and b/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210328110837157.png differ diff --git a/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70.png b/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70.png new file mode 100644 index 0000000..dec19e5 Binary files /dev/null and b/codes/DoubleDQN/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70.png differ diff --git a/codes/DoubleDQN/double_dqn.py b/codes/DoubleDQN/double_dqn.py new file mode 100644 index 0000000..e712edb --- /dev/null +++ b/codes/DoubleDQN/double_dqn.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +@Author: John +@Email: johnjim0816@gmail.com +@Date: 2020-06-12 00:50:49 +@LastEditor: John +LastEditTime: 2021-11-19 18:07:09 +@Discription: +@Environment: python 3.7.7 +''' +'''off-policy +''' + + +import torch +import torch.nn as nn +import torch.optim as optim +import torch.nn.functional as F +import random +import math +import numpy as np + +class ReplayBuffer: + def __init__(self, capacity): + self.capacity = capacity # 经验回放的容量 + self.buffer = [] # 缓冲区 + self.position = 0 + + def push(self, state, action, reward, next_state, done): + ''' 缓冲区是一个队列,容量超出时去掉开始存入的转移(transition) + ''' + if len(self.buffer) < self.capacity: + self.buffer.append(None) + self.buffer[self.position] = (state, action, reward, next_state, done) + self.position = (self.position + 1) % self.capacity + + def sample(self, batch_size): + batch = random.sample(self.buffer, batch_size) # 随机采出小批量转移 + state, action, reward, next_state, done = zip(*batch) # 解压成状态,动作等 + return state, action, reward, next_state, done + + def __len__(self): + ''' 返回当前存储的量 + ''' + return len(self.buffer) + +class MLP(nn.Module): + def __init__(self, state_dim,action_dim,hidden_dim=128): + """ 初始化q网络,为全连接网络 + state_dim: 输入的特征数即环境的状态维度 + action_dim: 输出的动作维度 + """ + super(MLP, self).__init__() + self.fc1 = nn.Linear(state_dim, hidden_dim) # 输入层 + self.fc2 = nn.Linear(hidden_dim,hidden_dim) # 隐藏层 + self.fc3 = nn.Linear(hidden_dim, action_dim) # 输出层 + + def forward(self, x): + # 各层对应的激活函数 + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + +class DoubleDQN: + def __init__(self, state_dim, action_dim, cfg): + self.action_dim = action_dim # 总的动作个数 + self.device = cfg.device # 设备,cpu或gpu等 + self.gamma = cfg.gamma + # e-greedy策略相关参数 + self.actions_count = 0 + self.epsilon_start = cfg.epsilon_start + self.epsilon_end = cfg.epsilon_end + self.epsilon_decay = cfg.epsilon_decay + self.batch_size = cfg.batch_size + self.policy_net = MLP(state_dim, action_dim,hidden_dim=cfg.hidden_dim).to(self.device) + self.target_net = MLP(state_dim, action_dim,hidden_dim=cfg.hidden_dim).to(self.device) + # target_net copy from policy_net + for target_param, param in zip(self.target_net.parameters(), self.policy_net.parameters()): + target_param.data.copy_(param.data) + # self.target_net.eval() # 不启用 BatchNormalization 和 Dropout + # 可查parameters()与state_dict()的区别,前者require_grad=True + self.optimizer = optim.Adam(self.policy_net.parameters(), lr=cfg.lr) + self.loss = 0 + self.memory = ReplayBuffer(cfg.memory_capacity) + + def choose_action(self, state): + '''选择动作 + ''' + self.actions_count += 1 + self.epsilon = self.epsilon_end + (self.epsilon_start - self.epsilon_end) * \ + math.exp(-1. * self.actions_count / self.epsilon_decay) + if random.random() > self.epsilon: + with torch.no_grad(): + # 先转为张量便于丢给神经网络,state元素数据原本为float64 + # 注意state=torch.tensor(state).unsqueeze(0)跟state=torch.tensor([state])等价 + state = torch.tensor( + [state], device=self.device, dtype=torch.float32) + # 如tensor([[-0.0798, -0.0079]], grad_fn=) + q_value = self.policy_net(state) + # tensor.max(1)返回每行的最大值以及对应的下标, + # 如torch.return_types.max(values=tensor([10.3587]),indices=tensor([0])) + # 所以tensor.max(1)[1]返回最大值对应的下标,即action + action = q_value.max(1)[1].item() + else: + action = random.randrange(self.action_dim) + return action + def update(self): + + if len(self.memory) < self.batch_size: + return + # 从memory中随机采样transition + state_batch, action_batch, reward_batch, next_state_batch, done_batch = self.memory.sample( + self.batch_size) + # convert to tensor + state_batch = torch.tensor( + state_batch, device=self.device, dtype=torch.float) + action_batch = torch.tensor(action_batch, device=self.device).unsqueeze( + 1) # 例如tensor([[1],...,[0]]) + reward_batch = torch.tensor( + reward_batch, device=self.device, dtype=torch.float) # tensor([1., 1.,...,1]) + next_state_batch = torch.tensor( + next_state_batch, device=self.device, dtype=torch.float) + + done_batch = torch.tensor(np.float32( + done_batch), device=self.device) # 将bool转为float然后转为张量 + # 计算当前(s_t,a)对应的Q(s_t, a) + q_values = self.policy_net(state_batch) + next_q_values = self.policy_net(next_state_batch) + # 代入当前选择的action,得到Q(s_t|a=a_t) + q_value = q_values.gather(dim=1, index=action_batch) + '''以下是Nature DQN的q_target计算方式 + # 计算所有next states的Q'(s_{t+1})的最大值,Q'为目标网络的q函数 + next_q_state_value = self.target_net( + next_state_batch).max(1)[0].detach() # 比如tensor([ 0.0060, -0.0171,...,]) + # 计算 q_target + # 对于终止状态,此时done_batch[0]=1, 对应的expected_q_value等于reward + q_target = reward_batch + self.gamma * next_q_state_value * (1-done_batch[0]) + ''' + '''以下是Double DQN q_target计算方式,与NatureDQN稍有不同''' + next_target_values = self.target_net( + next_state_batch) + # 选出Q(s_t‘, a)对应的action,代入到next_target_values获得target net对应的next_q_value,即Q’(s_t|a=argmax Q(s_t‘, a)) + next_target_q_value = next_target_values.gather(1, torch.max(next_q_values, 1)[1].unsqueeze(1)).squeeze(1) + q_target = reward_batch + self.gamma * next_target_q_value * (1-done_batch) + self.loss = nn.MSELoss()(q_value, q_target.unsqueeze(1)) # 计算 均方误差loss + # 优化模型 + self.optimizer.zero_grad() # zero_grad清除上一步所有旧的gradients from the last step + # loss.backward()使用backpropagation计算loss相对于所有parameters(需要gradients)的微分 + self.loss.backward() + for param in self.policy_net.parameters(): # clip防止梯度爆炸 + param.grad.data.clamp_(-1, 1) + self.optimizer.step() # 更新模型 + + def save(self,path): + torch.save(self.target_net.state_dict(), path+'checkpoint.pth') + + def load(self,path): + self.target_net.load_state_dict(torch.load(path+'checkpoint.pth')) + for target_param, param in zip(self.target_net.parameters(), self.policy_net.parameters()): + param.data.copy_(target_param.data) diff --git a/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/models/checkpoint.pth b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/models/checkpoint.pth new file mode 100644 index 0000000..2ec6bfd Binary files /dev/null and b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/models/checkpoint.pth differ diff --git a/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/test_ma_rewards.npy b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/test_ma_rewards.npy new file mode 100644 index 0000000..81e0bba Binary files /dev/null and b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/test_ma_rewards.npy differ diff --git a/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/test_rewards.npy b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/test_rewards.npy new file mode 100644 index 0000000..e7b6307 Binary files /dev/null and b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/test_rewards.npy differ diff --git a/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/test_rewards_curve.png b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/test_rewards_curve.png new file mode 100644 index 0000000..4fbd77c Binary files /dev/null and b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/test_rewards_curve.png differ diff --git a/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/train_ma_rewards.npy b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/train_ma_rewards.npy new file mode 100644 index 0000000..a73bbde Binary files /dev/null and b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/train_ma_rewards.npy differ diff --git a/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/train_rewards.npy b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/train_rewards.npy new file mode 100644 index 0000000..3e707c5 Binary files /dev/null and b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/train_rewards.npy differ diff --git a/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/train_rewards_curve.png b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/train_rewards_curve.png new file mode 100644 index 0000000..cb9dbeb Binary files /dev/null and b/codes/DoubleDQN/outputs/CartPole-v0/20211229-145006/results/train_rewards_curve.png differ diff --git a/codes/DoubleDQN/task0.py b/codes/DoubleDQN/task0.py new file mode 100644 index 0000000..7657a88 --- /dev/null +++ b/codes/DoubleDQN/task0.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: JiangJi +Email: johnjim0816@gmail.com +Date: 2021-11-07 18:10:37 +LastEditor: JiangJi +LastEditTime: 2021-12-29 15:02:30 +Discription: +''' + +import sys,os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import gym +import torch +import datetime + +from common.utils import save_results, make_dir +from common.utils import plot_rewards +from DoubleDQN.double_dqn import DoubleDQN + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 + +class Config: + def __init__(self): + ################################## 环境超参数 ################################### + self.algo_name = 'DoubleDQN' # 算法名称 + self.env_name = 'CartPole-v0' # 环境名称 + self.device = torch.device( + "cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.train_eps = 200 # 训练的回合数 + self.test_eps = 30 # 测试的回合数 + ################################################################################ + + ################################## 算法超参数 ################################### + self.gamma = 0.95 # 强化学习中的折扣因子 + self.epsilon_start = 0.95 # e-greedy策略中初始epsilon + self.epsilon_end = 0.01 # e-greedy策略中的终止epsilon + self.epsilon_decay = 500 # e-greedy策略中epsilon的衰减率 + self.lr = 0.0001 # 学习率 + self.memory_capacity = 100000 # 经验回放的容量 + self.batch_size = 64 # mini-batch SGD中的批量大小 + self.target_update = 2 # 目标网络的更新频率 + self.hidden_dim = 256 # 网络隐藏层 + ################################################################################ + + ################################# 保存结果相关参数 ############################## + self.result_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/results/' # 保存结果的路径 + self.model_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + ################################################################################ + + +def env_agent_config(cfg,seed=1): + env = gym.make(cfg.env_name) + env.seed(seed) + state_dim = env.observation_space.shape[0] + action_dim = env.action_space.n + agent = DoubleDQN(state_dim,action_dim,cfg) + return env,agent + +def train(cfg,env,agent): + print('开始训练!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.train_eps): + ep_reward = 0 # 记录一回合内的奖励 + state = env.reset() # 重置环境,返回初始状态 + while True: + action = agent.choose_action(state) + next_state, reward, done, _ = env.step(action) + ep_reward += reward + agent.memory.push(state, action, reward, next_state, done) + state = next_state + agent.update() + if done: + break + if i_ep % cfg.target_update == 0: + agent.target_net.load_state_dict(agent.policy_net.state_dict()) + if (i_ep+1)%10 == 0: + print(f'回合:{i_ep+1}/{cfg.train_eps},奖励:{ep_reward}') + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append( + 0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + print('完成训练!') + env.close() + return rewards,ma_rewards + +def test(cfg,env,agent): + print('开始测试!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + ############# 由于测试不需要使用epsilon-greedy策略,所以相应的值设置为0 ############### + cfg.epsilon_start = 0.0 # e-greedy策略中初始epsilon + cfg.epsilon_end = 0.0 # e-greedy策略中的终止epsilon + ################################################################################ + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + + for i_ep in range(cfg.test_eps): + state = env.reset() + ep_reward = 0 + while True: + action = agent.choose_action(state) + next_state, reward, done, _ = env.step(action) + state = next_state + ep_reward += reward + if done: + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1) + else: + ma_rewards.append(ep_reward) + print(f"回合:{i_ep+1}/{cfg.test_eps},奖励:{ep_reward:.1f}") + print('完成测试!') + env.close() + return rewards,ma_rewards + +if __name__ == "__main__": + cfg = Config() + # 训练 + env, agent = env_agent_config(cfg) + rewards, ma_rewards = train(cfg, env, agent) + make_dir(cfg.result_path, cfg.model_path) # 创建保存结果和模型路径的文件夹 + agent.save(path=cfg.model_path) # 保存模型 + save_results(rewards, ma_rewards, tag='train', + path=cfg.result_path) # 保存结果 + plot_rewards(rewards, ma_rewards, cfg, tag="train") # 画出结果 + # 测试 + env, agent = env_agent_config(cfg) + agent.load(path=cfg.model_path) # 导入模型 + rewards, ma_rewards = test(cfg, env, agent) + save_results(rewards, ma_rewards, tag='test', + path=cfg.result_path) # 保存结果 + plot_rewards(rewards, ma_rewards, cfg, tag="test") # 画出结果 diff --git a/codes/DuelingDQN/assets/task0_train_20211112021954.png b/codes/DuelingDQN/assets/task0_train_20211112021954.png new file mode 100644 index 0000000..2529311 Binary files /dev/null and b/codes/DuelingDQN/assets/task0_train_20211112021954.png differ diff --git a/codes/DuelingDQN/task0_train.ipynb b/codes/DuelingDQN/task0_train.ipynb new file mode 100644 index 0000000..7e38218 --- /dev/null +++ b/codes/DuelingDQN/task0_train.ipynb @@ -0,0 +1,418 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import math, random\n", + "import gym\n", + "import numpy as np\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "import torch.autograd as autograd \n", + "import torch.nn.functional as F\n", + "from IPython.display import clear_output # 清空单元格输出区域\n", + "import matplotlib.pyplot as plt\n", + "# %matplotlib inline\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "USE_CUDA = torch.cuda.is_available()\n", + "Variable = lambda *args, **kwargs: autograd.Variable(*args, **kwargs).cuda() if USE_CUDA else autograd.Variable(*args, **kwargs)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from collections import deque\n", + "\n", + "class ReplayBuffer(object):\n", + " def __init__(self, capacity):\n", + " self.buffer = deque(maxlen=capacity)\n", + " \n", + " def push(self, state, action, reward, next_state, done):\n", + " state = np.expand_dims(state, 0)\n", + " next_state = np.expand_dims(next_state, 0)\n", + " \n", + " self.buffer.append((state, action, reward, next_state, done))\n", + " \n", + " def sample(self, batch_size):\n", + " state, action, reward, next_state, done = zip(*random.sample(self.buffer, batch_size))\n", + " return np.concatenate(state), action, reward, np.concatenate(next_state), done\n", + " \n", + " def __len__(self):\n", + " return len(self.buffer)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "env_name = \"CartPole-v0\"\n", + "env = gym.make(env_name)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "epsilon_start = 1.0\n", + "epsilon_final = 0.01\n", + "epsilon_decay = 500\n", + "\n", + "epsilon_by_frame = lambda frame_idx: epsilon_final + (epsilon_start - epsilon_final) * math.exp(-1. * frame_idx / epsilon_decay)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot([epsilon_by_frame(i) for i in range(10000)])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Dueling DQN 网络\n", + "\n", + "DQN等算法中使用的是一个简单的三层神经网络:一个输入层,一个隐藏层和一个输出层。如下左图:\n", + "\n", + "\"image-20211112022028670\"\n", + "\n", + "而在Dueling DQN中,我们在后面加了两个子网络结构,分别对应上面上到价格函数网络部分和优势函数网络部分。对应上面右图所示。最终Q网络的输出由价格函数网络的输出和优势函数网络的输出线性组合得到。\n", + "\n", + "我们可以直接使用上一节的价值函数的组合公式得到我们的动作价值,但是这个式子无法辨识最终输出里面$V(S, w, \\alpha)$和$A(S, A, w, \\beta)$各自的作用,为了可以体现这种可辨识性(identifiability),实际使用的组合公式如下:\n", + "\n", + "$$\n", + "Q(S, A, w, \\alpha, \\beta)=V(S, w, \\alpha)+\\left(A(S, A, w, \\beta)-\\frac{1}{\\mathcal{A}} \\sum_{a^{\\prime} \\in \\mathcal{A}} A\\left(S, a^{\\prime}, w, \\beta\\right)\\right)\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "class DuelingNet(nn.Module):\n", + " def __init__(self, state_dim, action_dim,hidden_size=128):\n", + " super(DuelingNet, self).__init__()\n", + " \n", + " # 隐藏层\n", + " self.hidden = nn.Sequential(\n", + " nn.Linear(state_dim, hidden_size),\n", + " nn.ReLU()\n", + " )\n", + " \n", + " # 优势函数\n", + " self.advantage = nn.Sequential(\n", + " nn.Linear(hidden_size, hidden_size),\n", + " nn.ReLU(),\n", + " nn.Linear(hidden_size, action_dim)\n", + " )\n", + " \n", + " # 价值函数\n", + " self.value = nn.Sequential(\n", + " nn.Linear(hidden_size, hidden_size),\n", + " nn.ReLU(),\n", + " nn.Linear(hidden_size, 1)\n", + " )\n", + " \n", + " def forward(self, x):\n", + " x = self.hidden(x)\n", + " advantage = self.advantage(x)\n", + " value = self.value(x)\n", + " return value + advantage - advantage.mean()\n", + " \n", + " def act(self, state, epsilon):\n", + " if random.random() > epsilon:\n", + " with torch.no_grad():\n", + " state = Variable(torch.FloatTensor(state).unsqueeze(0))\n", + " q_value = self.forward(state)\n", + " action = q_value.max(1)[1].item()\n", + " else:\n", + " action = random.randrange(env.action_space.n)\n", + " return action" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "ename": "SyntaxError", + "evalue": "unexpected EOF while parsing (, line 1)", + "output_type": "error", + "traceback": [ + "\u001b[0;36m File \u001b[0;32m\"\"\u001b[0;36m, line \u001b[0;32m1\u001b[0m\n\u001b[0;31m class DuelingDQN:\u001b[0m\n\u001b[0m ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m unexpected EOF while parsing\n" + ] + } + ], + "source": [ + "class DuelingDQN:\n", + " def __init__(self,state_dim,action_dim,cfg) -> None:\n", + " self.batch_size = cfg.batch_size\n", + " self.device = cfg.device\n", + " self.loss_history = [] # 记录loss的变化\n", + " self.frame_idx = 0 # 用于epsilon的衰减计数\n", + " self.epsilon = lambda frame_idx: cfg.epsilon_end + \\\n", + " (cfg.epsilon_start - cfg.epsilon_end) * \\\n", + " math.exp(-1. * frame_idx / cfg.epsilon_decay)\n", + " self.policy_net = DuelingNet(state_dim, action_dim,hidden_dim=cfg.hidden_dim).to(self.device)\n", + " self.target_net = DuelingNet(state_dim, action_dim,hidden_dim=cfg.hidden_dim).to(self.device)\n", + " for target_param, param in zip(self.target_net.parameters(),self.policy_net.parameters()): # 复制参数到目标网络targe_net\n", + " target_param.data.copy_(param.data)\n", + " self.optimizer = optim.Adam(self.policy_net.parameters(), lr=cfg.lr) # 优化器\n", + " self.memory = ReplayBuffer(cfg.memory_capacity) \n", + " def choose_action(self,state):\n", + " self.frame_idx += 1\n", + " if random.random() > self.epsilon(self.frame_idx):\n", + " with torch.no_grad():\n", + " state = torch.tensor([state], device=self.device, dtype=torch.float32)\n", + " q_values = self.policy_net(state)\n", + " action = q_values.max(1)[1].item() # 选择Q值最大的动作\n", + " else:\n", + " action = random.randrange(self.action_dim)\n", + " return action\n", + " def update(self):\n", + " if len(self.memory) < self.batch_size: # 当memory中不满足一个批量时,不更新策略\n", + " return\n", + " state, action, reward, next_state, done = self.memory.sample(batch_size)\n", + " state = torch.tensor(state, device=self.device, dtype=torch.float)\n", + " action = torch.tensor(action, device=self.device).unsqueeze(1) \n", + " reward = torch.tensor(reward, device=self.device, dtype=torch.float) \n", + " next_state = torch.tensor(next_state, device=self.device, dtype=torch.float)\n", + " done = torch.tensor(np.float32(done), device=self.device)\n", + " q_values = self.policy_net(state)\n", + " next_q_values = self.target_net(next_state)\n", + "\n", + " q_value = q_values.gather(1, action.unsqueeze(1)).squeeze(1)\n", + " next_q_value = next_q_values.max(1)[0]\n", + " expected_q_value = reward + gamma * next_q_value * (1 - done)\n", + " \n", + " loss = (q_value - expected_q_value.detach()).pow(2).mean()\n", + " self.loss_history.append(loss)\n", + " self.optimizer.zero_grad()\n", + " loss.backward()\n", + " self.optimizer.step()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "current_model = DuelingNet(env.observation_space.shape[0], env.action_space.n)\n", + "target_model = DuelingNet(env.observation_space.shape[0], env.action_space.n)\n", + "\n", + "if USE_CUDA:\n", + " current_model = current_model.cuda()\n", + " target_model = target_model.cuda()\n", + " \n", + "optimizer = optim.Adam(current_model.parameters())\n", + "\n", + "replay_buffer = ReplayBuffer(1000)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "def update_target(current_model, target_model):\n", + " target_model.load_state_dict(current_model.state_dict())" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "update_target(current_model, target_model)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "def compute_td_loss(batch_size):\n", + " state, action, reward, next_state, done = replay_buffer.sample(batch_size)\n", + "\n", + " state = Variable(torch.FloatTensor(np.float32(state)))\n", + " next_state = Variable(torch.FloatTensor(np.float32(next_state)))\n", + " action = Variable(torch.LongTensor(action))\n", + " reward = Variable(torch.FloatTensor(reward))\n", + " done = Variable(torch.FloatTensor(done))\n", + "\n", + " q_values = current_model(state)\n", + " next_q_values = target_model(next_state)\n", + "\n", + " q_value = q_values.gather(1, action.unsqueeze(1)).squeeze(1)\n", + " next_q_value = next_q_values.max(1)[0]\n", + " expected_q_value = reward + gamma * next_q_value * (1 - done)\n", + " \n", + " loss = (q_value - expected_q_value.detach()).pow(2).mean()\n", + " \n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + " \n", + " return loss" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def plot(frame_idx, rewards, losses):\n", + " clear_output(True) # 清空单元格输出区域,因为多次打印,每次需要清楚前面打印的图片\n", + " plt.figure(figsize=(20,5))\n", + " plt.subplot(131)\n", + " plt.title('frame %s. reward: %s' % (frame_idx, np.mean(rewards[-10:])))\n", + " plt.plot(rewards)\n", + " plt.subplot(132)\n", + " plt.title('loss')\n", + " plt.plot(losses)\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "num_frames = 10000\n", + "batch_size = 32\n", + "gamma = 0.99\n", + "\n", + "losses = []\n", + "all_rewards = []\n", + "ep_reward = 0\n", + "\n", + "state = env.reset()\n", + "for frame_idx in range(1, num_frames + 1):\n", + " epsilon = epsilon_by_frame(frame_idx)\n", + " action = current_model.act(state, epsilon)\n", + " next_state, reward, done, _ = env.step(action)\n", + " replay_buffer.push(state, action, reward, next_state, done)\n", + " \n", + " state = next_state\n", + " ep_reward += reward\n", + " \n", + " if done:\n", + " state = env.reset()\n", + " all_rewards.append(ep_reward)\n", + " ep_reward = 0\n", + " \n", + " if len(replay_buffer) > batch_size:\n", + " loss = compute_td_loss(batch_size)\n", + " losses.append(loss.item())\n", + " \n", + " if frame_idx % 200 == 0:\n", + " plot(frame_idx, all_rewards, losses)\n", + " \n", + " if frame_idx % 100 == 0:\n", + " update_target(current_model, target_model)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 参考\n", + "\n", + "[强化学习(十二) Dueling DQN](https://www.cnblogs.com/pinard/p/9923859.html)" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "fe38df673a99c62a9fea33a7aceda74c9b65b12ee9d076c5851d98b692a4989a" + }, + "kernelspec": { + "display_name": "Python 3.7.10 64-bit ('py37': conda)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/codes/GAE/task0_train.py b/codes/GAE/task0_train.py new file mode 100644 index 0000000..961816c --- /dev/null +++ b/codes/GAE/task0_train.py @@ -0,0 +1,167 @@ +import math +import random + +import gym +import numpy as np + +import torch +import torch.nn as nn +import torch.optim as optim +import torch.nn.functional as F +from torch.distributions import Normal +import matplotlib.pyplot as plt +import seaborn as sns +import sys,os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加父路径到系统路径sys.path + +use_cuda = torch.cuda.is_available() +device = torch.device("cuda" if use_cuda else "cpu") + +from common.multiprocessing_env import SubprocVecEnv + +num_envs = 16 +env_name = "Pendulum-v0" + +def make_env(): + def _thunk(): + env = gym.make(env_name) + return env + + return _thunk + +envs = [make_env() for i in range(num_envs)] +envs = SubprocVecEnv(envs) + +env = gym.make(env_name) + +def init_weights(m): + if isinstance(m, nn.Linear): + nn.init.normal_(m.weight, mean=0., std=0.1) + nn.init.constant_(m.bias, 0.1) + +class ActorCritic(nn.Module): + def __init__(self, num_inputs, num_outputs, hidden_size, std=0.0): + super(ActorCritic, self).__init__() + + self.critic = nn.Sequential( + nn.Linear(num_inputs, hidden_size), + nn.ReLU(), + nn.Linear(hidden_size, 1) + ) + + self.actor = nn.Sequential( + nn.Linear(num_inputs, hidden_size), + nn.ReLU(), + nn.Linear(hidden_size, num_outputs), + ) + self.log_std = nn.Parameter(torch.ones(1, num_outputs) * std) + + self.apply(init_weights) + + def forward(self, x): + value = self.critic(x) + mu = self.actor(x) + std = self.log_std.exp().expand_as(mu) + dist = Normal(mu, std) + return dist, value + + +def plot(frame_idx, rewards): + plt.figure(figsize=(20,5)) + plt.subplot(131) + plt.title('frame %s. reward: %s' % (frame_idx, rewards[-1])) + plt.plot(rewards) + plt.show() + +def test_env(vis=False): + state = env.reset() + if vis: env.render() + done = False + total_reward = 0 + while not done: + state = torch.FloatTensor(state).unsqueeze(0).to(device) + dist, _ = model(state) + next_state, reward, done, _ = env.step(dist.sample().cpu().numpy()[0]) + state = next_state + if vis: env.render() + total_reward += reward + return total_reward + +def compute_gae(next_value, rewards, masks, values, gamma=0.99, tau=0.95): + values = values + [next_value] + gae = 0 + returns = [] + for step in reversed(range(len(rewards))): + delta = rewards[step] + gamma * values[step + 1] * masks[step] - values[step] + gae = delta + gamma * tau * masks[step] * gae + returns.insert(0, gae + values[step]) + return returns + +num_inputs = envs.observation_space.shape[0] +num_outputs = envs.action_space.shape[0] + +#Hyper params: +hidden_size = 256 +lr = 3e-2 +num_steps = 20 + +model = ActorCritic(num_inputs, num_outputs, hidden_size).to(device) +optimizer = optim.Adam(model.parameters()) + +max_frames = 100000 +frame_idx = 0 +test_rewards = [] + +state = envs.reset() + +while frame_idx < max_frames: + + log_probs = [] + values = [] + rewards = [] + masks = [] + entropy = 0 + + for _ in range(num_steps): + state = torch.FloatTensor(state).to(device) + dist, value = model(state) + + action = dist.sample() + next_state, reward, done, _ = envs.step(action.cpu().numpy()) + + log_prob = dist.log_prob(action) + entropy += dist.entropy().mean() + + log_probs.append(log_prob) + values.append(value) + rewards.append(torch.FloatTensor(reward).unsqueeze(1).to(device)) + masks.append(torch.FloatTensor(1 - done).unsqueeze(1).to(device)) + + state = next_state + frame_idx += 1 + + if frame_idx % 1000 == 0: + test_rewards.append(np.mean([test_env() for _ in range(10)])) + print(test_rewards[-1]) + # plot(frame_idx, test_rewards) + + next_state = torch.FloatTensor(next_state).to(device) + _, next_value = model(next_state) + returns = compute_gae(next_value, rewards, masks, values) + + log_probs = torch.cat(log_probs) + returns = torch.cat(returns).detach() + values = torch.cat(values) + + advantage = returns - values + + actor_loss = -(log_probs * advantage.detach()).mean() + critic_loss = advantage.pow(2).mean() + + loss = actor_loss + 0.5 * critic_loss - 0.001 * entropy + + optimizer.zero_grad() + loss.backward() + optimizer.step() diff --git a/codes/HierarchicalDQN/README.md b/codes/HierarchicalDQN/README.md new file mode 100644 index 0000000..383cdd0 --- /dev/null +++ b/codes/HierarchicalDQN/README.md @@ -0,0 +1,13 @@ +# Hierarchical DQN + +## 原理简介 + +Hierarchical DQN是一种分层强化学习方法,与DQN相比增加了一个meta controller, + +![image-20210331153115575](assets/image-20210331153115575.png) + +即学习时,meta controller每次会生成一个goal,然后controller或者说下面的actor就会达到这个goal,直到done为止。这就相当于给agent增加了一个队长,队长擅长制定局部目标,指导agent前行,这样应对一些每回合步数较长或者稀疏奖励的问题会有所帮助。 + +## 伪代码 + +![image-20210331153542314](assets/image-20210331153542314.png) \ No newline at end of file diff --git a/codes/HierarchicalDQN/agent.py b/codes/HierarchicalDQN/agent.py new file mode 100644 index 0000000..ce0cd1f --- /dev/null +++ b/codes/HierarchicalDQN/agent.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-24 22:18:18 +LastEditor: John +LastEditTime: 2021-05-04 22:39:34 +Discription: +Environment: +''' +import torch +import torch.nn as nn +import torch.optim as optim +import torch.nn.functional as F +import numpy as np +import random,math + +class ReplayBuffer: + def __init__(self, capacity): + self.capacity = capacity # 经验回放的容量 + self.buffer = [] # 缓冲区 + self.position = 0 + + def push(self, state, action, reward, next_state, done): + ''' 缓冲区是一个队列,容量超出时去掉开始存入的转移(transition) + ''' + if len(self.buffer) < self.capacity: + self.buffer.append(None) + self.buffer[self.position] = (state, action, reward, next_state, done) + self.position = (self.position + 1) % self.capacity + + def sample(self, batch_size): + batch = random.sample(self.buffer, batch_size) # 随机采出小批量转移 + state, action, reward, next_state, done = zip(*batch) # 解压成状态,动作等 + return state, action, reward, next_state, done + + def __len__(self): + ''' 返回当前存储的量 + ''' + return len(self.buffer) +class MLP(nn.Module): + def __init__(self, input_dim,output_dim,hidden_dim=128): + """ 初始化q网络,为全连接网络 + input_dim: 输入的特征数即环境的状态维度 + output_dim: 输出的动作维度 + """ + super(MLP, self).__init__() + self.fc1 = nn.Linear(input_dim, hidden_dim) # 输入层 + self.fc2 = nn.Linear(hidden_dim,hidden_dim) # 隐藏层 + self.fc3 = nn.Linear(hidden_dim, output_dim) # 输出层 + + def forward(self, x): + # 各层对应的激活函数 + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + +class HierarchicalDQN: + def __init__(self,state_dim,action_dim,cfg): + self.state_dim = state_dim + self.action_dim = action_dim + self.gamma = cfg.gamma + self.device = cfg.device + self.batch_size = cfg.batch_size + self.frame_idx = 0 # 用于epsilon的衰减计数 + self.epsilon = lambda frame_idx: cfg.epsilon_end + (cfg.epsilon_start - cfg.epsilon_end ) * math.exp(-1. * frame_idx / cfg.epsilon_decay) + self.policy_net = MLP(2*state_dim, action_dim,cfg.hidden_dim).to(self.device) + self.meta_policy_net = MLP(state_dim, state_dim,cfg.hidden_dim).to(self.device) + self.optimizer = optim.Adam(self.policy_net.parameters(),lr=cfg.lr) + self.meta_optimizer = optim.Adam(self.meta_policy_net.parameters(),lr=cfg.lr) + self.memory = ReplayBuffer(cfg.memory_capacity) + self.meta_memory = ReplayBuffer(cfg.memory_capacity) + self.loss_numpy = 0 + self.meta_loss_numpy = 0 + self.losses = [] + self.meta_losses = [] + def to_onehot(self,x): + oh = np.zeros(self.state_dim) + oh[x - 1] = 1. + return oh + def set_goal(self,state): + if random.random() > self.epsilon(self.frame_idx): + with torch.no_grad(): + state = torch.tensor(state, device=self.device, dtype=torch.float32).unsqueeze(0) + goal = self.meta_policy_net(state).max(1)[1].item() + else: + goal = random.randrange(self.state_dim) + return goal + def choose_action(self,state): + self.frame_idx += 1 + if random.random() > self.epsilon(self.frame_idx): + with torch.no_grad(): + state = torch.tensor(state, device=self.device, dtype=torch.float32).unsqueeze(0) + q_value = self.policy_net(state) + action = q_value.max(1)[1].item() + else: + action = random.randrange(self.action_dim) + return action + def update(self): + self.update_policy() + self.update_meta() + def update_policy(self): + if self.batch_size > len(self.memory): + return + state_batch, action_batch, reward_batch, next_state_batch, done_batch = self.memory.sample(self.batch_size) + state_batch = torch.tensor(state_batch,device=self.device,dtype=torch.float) + action_batch = torch.tensor(action_batch,device=self.device,dtype=torch.int64).unsqueeze(1) + reward_batch = torch.tensor(reward_batch,device=self.device,dtype=torch.float) + next_state_batch = torch.tensor(next_state_batch,device=self.device, dtype=torch.float) + done_batch = torch.tensor(np.float32(done_batch),device=self.device) + q_values = self.policy_net(state_batch).gather(dim=1, index=action_batch).squeeze(1) + next_state_values = self.policy_net(next_state_batch).max(1)[0].detach() + expected_q_values = reward_batch + 0.99 * next_state_values * (1-done_batch) + loss = nn.MSELoss()(q_values, expected_q_values) + self.optimizer.zero_grad() + loss.backward() + for param in self.policy_net.parameters(): # clip防止梯度爆炸 + param.grad.data.clamp_(-1, 1) + self.optimizer.step() + self.loss_numpy = loss.detach().cpu().numpy() + self.losses.append(self.loss_numpy) + def update_meta(self): + if self.batch_size > len(self.meta_memory): + return + state_batch, action_batch, reward_batch, next_state_batch, done_batch = self.meta_memory.sample(self.batch_size) + state_batch = torch.tensor(state_batch,device=self.device,dtype=torch.float) + action_batch = torch.tensor(action_batch,device=self.device,dtype=torch.int64).unsqueeze(1) + reward_batch = torch.tensor(reward_batch,device=self.device,dtype=torch.float) + next_state_batch = torch.tensor(next_state_batch,device=self.device, dtype=torch.float) + done_batch = torch.tensor(np.float32(done_batch),device=self.device) + q_values = self.meta_policy_net(state_batch).gather(dim=1, index=action_batch).squeeze(1) + next_state_values = self.meta_policy_net(next_state_batch).max(1)[0].detach() + expected_q_values = reward_batch + 0.99 * next_state_values * (1-done_batch) + meta_loss = nn.MSELoss()(q_values, expected_q_values) + self.meta_optimizer.zero_grad() + meta_loss.backward() + for param in self.meta_policy_net.parameters(): # clip防止梯度爆炸 + param.grad.data.clamp_(-1, 1) + self.meta_optimizer.step() + self.meta_loss_numpy = meta_loss.detach().cpu().numpy() + self.meta_losses.append(self.meta_loss_numpy) + + def save(self, path): + torch.save(self.policy_net.state_dict(), path+'policy_checkpoint.pth') + torch.save(self.meta_policy_net.state_dict(), path+'meta_checkpoint.pth') + + def load(self, path): + self.policy_net.load_state_dict(torch.load(path+'policy_checkpoint.pth')) + self.meta_policy_net.load_state_dict(torch.load(path+'meta_checkpoint.pth')) + + + + \ No newline at end of file diff --git a/codes/HierarchicalDQN/assets/image-20210331153115575.png b/codes/HierarchicalDQN/assets/image-20210331153115575.png new file mode 100644 index 0000000..5bb9251 Binary files /dev/null and b/codes/HierarchicalDQN/assets/image-20210331153115575.png differ diff --git a/codes/HierarchicalDQN/assets/image-20210331153542314.png b/codes/HierarchicalDQN/assets/image-20210331153542314.png new file mode 100644 index 0000000..6db2d82 Binary files /dev/null and b/codes/HierarchicalDQN/assets/image-20210331153542314.png differ diff --git a/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/models/meta_checkpoint.pth b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/models/meta_checkpoint.pth new file mode 100644 index 0000000..02f3f7c Binary files /dev/null and b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/models/meta_checkpoint.pth differ diff --git a/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/models/policy_checkpoint.pth b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/models/policy_checkpoint.pth new file mode 100644 index 0000000..9d906ea Binary files /dev/null and b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/models/policy_checkpoint.pth differ diff --git a/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/test_ma_rewards.npy b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/test_ma_rewards.npy new file mode 100644 index 0000000..14dd955 Binary files /dev/null and b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/test_ma_rewards.npy differ diff --git a/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/test_rewards.npy b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/test_rewards.npy new file mode 100644 index 0000000..e815222 Binary files /dev/null and b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/test_rewards.npy differ diff --git a/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/test_rewards_curve.png b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/test_rewards_curve.png new file mode 100644 index 0000000..645b21a Binary files /dev/null and b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/test_rewards_curve.png differ diff --git a/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/train_ma_rewards.npy b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/train_ma_rewards.npy new file mode 100644 index 0000000..bf58391 Binary files /dev/null and b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/train_ma_rewards.npy differ diff --git a/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/train_rewards.npy b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/train_rewards.npy new file mode 100644 index 0000000..f4d20ff Binary files /dev/null and b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/train_rewards.npy differ diff --git a/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/train_rewards_curve.png b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/train_rewards_curve.png new file mode 100644 index 0000000..20ccbc5 Binary files /dev/null and b/codes/HierarchicalDQN/outputs/CartPole-v0/20211221-200119/results/train_rewards_curve.png differ diff --git a/codes/HierarchicalDQN/task0.py b/codes/HierarchicalDQN/task0.py new file mode 100644 index 0000000..3eceefd --- /dev/null +++ b/codes/HierarchicalDQN/task0.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-29 10:37:32 +LastEditor: John +LastEditTime: 2021-05-04 22:35:56 +Discription: +Environment: +''' +import sys +import os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import datetime +import numpy as np +import torch +import gym + +from common.utils import save_results,make_dir +from common.utils import plot_rewards +from HierarchicalDQN.agent import HierarchicalDQN +from HierarchicalDQN.train import train,test + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 +algo_name = "Hierarchical DQN" # 算法名称 +env_name = 'CartPole-v0' # 环境名称 +class HierarchicalDQNConfig: + def __init__(self): + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.device = torch.device( + "cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.train_eps = 300 # 训练的episode数目 + self.test_eps = 50 # 测试的episode数目 + self.gamma = 0.99 + self.epsilon_start = 1 # start epsilon of e-greedy policy + self.epsilon_end = 0.01 + self.epsilon_decay = 200 + self.lr = 0.0001 # learning rate + self.memory_capacity = 10000 # Replay Memory capacity + self.batch_size = 32 + self.target_update = 2 # 目标网络的更新频率 + self.hidden_dim = 256 # 网络隐藏层 +class PlotConfig: + ''' 绘图相关参数设置 + ''' + + def __init__(self) -> None: + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.device = torch.device( + "cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.result_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/results/' # 保存结果的路径 + self.model_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + +def env_agent_config(cfg,seed=1): + env = gym.make(cfg.env_name) + env.seed(seed) + state_dim = env.observation_space.shape[0] + action_dim = env.action_space.n + agent = HierarchicalDQN(state_dim,action_dim,cfg) + return env,agent + +if __name__ == "__main__": + cfg = HierarchicalDQNConfig() + plot_cfg = PlotConfig() + # 训练 + env, agent = env_agent_config(cfg, seed=1) + rewards, ma_rewards = train(cfg, env, agent) + make_dir(plot_cfg.result_path, plot_cfg.model_path) # 创建保存结果和模型路径的文件夹 + agent.save(path=plot_cfg.model_path) # 保存模型 + save_results(rewards, ma_rewards, tag='train', + path=plot_cfg.result_path) # 保存结果 + plot_rewards(rewards, ma_rewards, plot_cfg, tag="train") # 画出结果 + # 测试 + env, agent = env_agent_config(cfg, seed=10) + agent.load(path=plot_cfg.model_path) # 导入模型 + rewards, ma_rewards = test(cfg, env, agent) + save_results(rewards, ma_rewards, tag='test', path=plot_cfg.result_path) # 保存结果 + plot_rewards(rewards, ma_rewards, plot_cfg, tag="test") # 画出结果 + diff --git a/codes/HierarchicalDQN/train.py b/codes/HierarchicalDQN/train.py new file mode 100644 index 0000000..3dc8aa3 --- /dev/null +++ b/codes/HierarchicalDQN/train.py @@ -0,0 +1,77 @@ +import sys +import os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import numpy as np + +def train(cfg, env, agent): + print('开始训练!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.train_eps): + state = env.reset() + done = False + ep_reward = 0 + while not done: + goal = agent.set_goal(state) + onehot_goal = agent.to_onehot(goal) + meta_state = state + extrinsic_reward = 0 + while not done and goal != np.argmax(state): + goal_state = np.concatenate([state, onehot_goal]) + action = agent.choose_action(goal_state) + next_state, reward, done, _ = env.step(action) + ep_reward += reward + extrinsic_reward += reward + intrinsic_reward = 1.0 if goal == np.argmax( + next_state) else 0.0 + agent.memory.push(goal_state, action, intrinsic_reward, np.concatenate( + [next_state, onehot_goal]), done) + state = next_state + agent.update() + if (i_ep+1)%10 == 0: + print(f'回合:{i_ep+1}/{cfg.train_eps},奖励:{ep_reward},Loss:{agent.loss_numpy:.2f}, Meta_Loss:{agent.meta_loss_numpy:.2f}') + agent.meta_memory.push(meta_state, goal, extrinsic_reward, state, done) + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append( + 0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + print('完成训练!') + return rewards, ma_rewards + +def test(cfg, env, agent): + print('开始测试!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.train_eps): + state = env.reset() + done = False + ep_reward = 0 + while not done: + goal = agent.set_goal(state) + onehot_goal = agent.to_onehot(goal) + extrinsic_reward = 0 + while not done and goal != np.argmax(state): + goal_state = np.concatenate([state, onehot_goal]) + action = agent.choose_action(goal_state) + next_state, reward, done, _ = env.step(action) + ep_reward += reward + extrinsic_reward += reward + state = next_state + agent.update() + if (i_ep+1)%10 == 0: + print(f'回合:{i_ep+1}/{cfg.train_eps},奖励:{ep_reward},Loss:{agent.loss_numpy:.2f}, Meta_Loss:{agent.meta_loss_numpy:.2f}') + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append( + 0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + print('完成训练!') + return rewards, ma_rewards \ No newline at end of file diff --git a/codes/LICENSE b/codes/LICENSE new file mode 100644 index 0000000..673d927 --- /dev/null +++ b/codes/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 John Jim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/codes/Logs.md b/codes/Logs.md new file mode 100644 index 0000000..7dc6497 --- /dev/null +++ b/codes/Logs.md @@ -0,0 +1,7 @@ +## 记录笔者更新的日志 + +**2021.12.28-1**:将```task.py```中的两个Config类合并为一个,并加以注释便于阅读,从DQN算法开始更新 + +**2021.12.22-3**:将```agent.py```更改为对应的算法名称,便于区分如```dqn```与```dqn_cnn```的情况 +**2021.12.22-2**:简化了代码结构,将原来的```train.py```和```task.py```等合并到```task.py```中 +**2021.12.22-1**:简化了代码结构,将原来的```model.py```和```memory.py```等合并到```agent.py```中,```plot.py```的内容合并到```common.utils.py```中 \ No newline at end of file diff --git a/codes/MonteCarlo/README.md b/codes/MonteCarlo/README.md new file mode 100644 index 0000000..91ff767 --- /dev/null +++ b/codes/MonteCarlo/README.md @@ -0,0 +1,5 @@ +# *On-Policy First-Visit MC Control* + +### 伪代码 + +![mc_control_algo](assets/mc_control_algo.png) \ No newline at end of file diff --git a/codes/MonteCarlo/agent.py b/codes/MonteCarlo/agent.py new file mode 100644 index 0000000..44af71d --- /dev/null +++ b/codes/MonteCarlo/agent.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-12 16:14:34 +LastEditor: John +LastEditTime: 2021-05-05 16:58:39 +Discription: +Environment: +''' +import numpy as np +from collections import defaultdict +import torch +import dill + +class FisrtVisitMC: + ''' On-Policy First-Visit MC Control + ''' + def __init__(self,action_dim,cfg): + self.action_dim = action_dim + self.epsilon = cfg.epsilon + self.gamma = cfg.gamma + self.Q_table = defaultdict(lambda: np.zeros(action_dim)) + self.returns_sum = defaultdict(float) # sum of returns + self.returns_count = defaultdict(float) + + def choose_action(self,state): + ''' e-greed policy ''' + if state in self.Q_table.keys(): + best_action = np.argmax(self.Q_table[state]) + action_probs = np.ones(self.action_dim, dtype=float) * self.epsilon / self.action_dim + action_probs[best_action] += (1.0 - self.epsilon) + action = np.random.choice(np.arange(len(action_probs)), p=action_probs) + else: + action = np.random.randint(0,self.action_dim) + return action + def update(self,one_ep_transition): + # Find all (state, action) pairs we've visited in this one_ep_transition + # We convert each state to a tuple so that we can use it as a dict key + sa_in_episode = set([(tuple(x[0]), x[1]) for x in one_ep_transition]) + for state, action in sa_in_episode: + sa_pair = (state, action) + # Find the first occurence of the (state, action) pair in the one_ep_transition + first_occurence_idx = next(i for i,x in enumerate(one_ep_transition) + if x[0] == state and x[1] == action) + # Sum up all rewards since the first occurance + G = sum([x[2]*(self.gamma**i) for i,x in enumerate(one_ep_transition[first_occurence_idx:])]) + # Calculate average return for this state over all sampled episodes + self.returns_sum[sa_pair] += G + self.returns_count[sa_pair] += 1.0 + self.Q_table[state][action] = self.returns_sum[sa_pair] / self.returns_count[sa_pair] + def save(self,path): + '''把 Q表格 的数据保存到文件中 + ''' + torch.save( + obj=self.Q_table, + f=path+"Q_table", + pickle_module=dill + ) + + def load(self, path): + '''从文件中读取数据到 Q表格 + ''' + self.Q_table =torch.load(f=path+"Q_table",pickle_module=dill) \ No newline at end of file diff --git a/codes/MonteCarlo/assets/mc_control_algo.png b/codes/MonteCarlo/assets/mc_control_algo.png new file mode 100644 index 0000000..0b436fa Binary files /dev/null and b/codes/MonteCarlo/assets/mc_control_algo.png differ diff --git a/codes/MonteCarlo/outputs/Racetrack/20210505-165945/models/Q_table b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/models/Q_table new file mode 100644 index 0000000..6205ee5 Binary files /dev/null and b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/models/Q_table differ diff --git a/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/eval_ma_rewards.npy b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/eval_ma_rewards.npy new file mode 100644 index 0000000..5cc42f1 Binary files /dev/null and b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/eval_ma_rewards.npy differ diff --git a/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/eval_rewards.npy b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/eval_rewards.npy new file mode 100644 index 0000000..19bb2f1 Binary files /dev/null and b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/eval_rewards.npy differ diff --git a/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/eval_rewards_curve.png b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/eval_rewards_curve.png new file mode 100644 index 0000000..0738ac5 Binary files /dev/null and b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/eval_rewards_curve.png differ diff --git a/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/train_ma_rewards.npy b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/train_ma_rewards.npy new file mode 100644 index 0000000..f52b398 Binary files /dev/null and b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/train_ma_rewards.npy differ diff --git a/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/train_rewards.npy b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/train_rewards.npy new file mode 100644 index 0000000..fe83b3c Binary files /dev/null and b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/train_rewards.npy differ diff --git a/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/train_rewards_curve.png b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/train_rewards_curve.png new file mode 100644 index 0000000..b10b7ab Binary files /dev/null and b/codes/MonteCarlo/outputs/Racetrack/20210505-165945/results/train_rewards_curve.png differ diff --git a/codes/MonteCarlo/task0_train.py b/codes/MonteCarlo/task0_train.py new file mode 100644 index 0000000..dae0c95 --- /dev/null +++ b/codes/MonteCarlo/task0_train.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-11 14:26:44 +LastEditor: John +LastEditTime: 2021-05-05 17:27:50 +Discription: +Environment: +''' + +import sys,os +curr_path = os.path.dirname(__file__) +parent_path = os.path.dirname(curr_path) +sys.path.append(parent_path) # add current terminal path to sys.path + +import torch +import datetime + +from common.utils import save_results,make_dir +from common.plot import plot_rewards +from MonteCarlo.agent import FisrtVisitMC +from envs.racetrack_env import RacetrackEnv + +curr_time = datetime.datetime.now().strftime( + "%Y%m%d-%H%M%S") # obtain current time + +class MCConfig: + def __init__(self): + self.algo = "MC" # name of algo + self.env = 'Racetrack' + self.result_path = curr_path+"/outputs/" + self.env + \ + '/'+curr_time+'/results/' # path to save results + self.model_path = curr_path+"/outputs/" + self.env + \ + '/'+curr_time+'/models/' # path to save models + # epsilon: The probability to select a random action . + self.epsilon = 0.15 + self.gamma = 0.9 # gamma: Gamma discount factor. + self.train_eps = 200 + self.device = torch.device( + "cuda" if torch.cuda.is_available() else "cpu") # check gpu + +def env_agent_config(cfg,seed=1): + env = RacetrackEnv() + action_dim = 9 + agent = FisrtVisitMC(action_dim, cfg) + return env,agent + +def train(cfg, env, agent): + print('Start to eval !') + print(f'Env:{cfg.env}, Algorithm:{cfg.algo}, Device:{cfg.device}') + rewards = [] + ma_rewards = [] # moving average rewards + for i_ep in range(cfg.train_eps): + state = env.reset() + ep_reward = 0 + one_ep_transition = [] + while True: + action = agent.choose_action(state) + next_state, reward, done = env.step(action) + ep_reward += reward + one_ep_transition.append((state, action, reward)) + state = next_state + if done: + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1) + else: + ma_rewards.append(ep_reward) + agent.update(one_ep_transition) + if (i_ep+1) % 10 == 0: + print(f"Episode:{i_ep+1}/{cfg.train_eps}: Reward:{ep_reward}") + print('Complete training!') + return rewards, ma_rewards + +def eval(cfg, env, agent): + print('Start to eval !') + print(f'Env:{cfg.env}, Algorithm:{cfg.algo}, Device:{cfg.device}') + rewards = [] + ma_rewards = [] # moving average rewards + for i_ep in range(cfg.train_eps): + state = env.reset() + ep_reward = 0 + while True: + action = agent.choose_action(state) + next_state, reward, done = env.step(action) + ep_reward += reward + state = next_state + if done: + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1) + else: + ma_rewards.append(ep_reward) + if (i_ep+1) % 10 == 0: + print(f"Episode:{i_ep+1}/{cfg.train_eps}: Reward:{ep_reward}") + return rewards, ma_rewards + +if __name__ == "__main__": + cfg = MCConfig() + + # train + env,agent = env_agent_config(cfg,seed=1) + rewards, ma_rewards = train(cfg, env, agent) + make_dir(cfg.result_path, cfg.model_path) + agent.save(path=cfg.model_path) + save_results(rewards, ma_rewards, tag='train', path=cfg.result_path) + plot_rewards(rewards, ma_rewards, tag="train", + algo=cfg.algo, path=cfg.result_path) + # eval + env,agent = env_agent_config(cfg,seed=10) + agent.load(path=cfg.model_path) + rewards,ma_rewards = eval(cfg,env,agent) + save_results(rewards,ma_rewards,tag='eval',path=cfg.result_path) + plot_rewards(rewards,ma_rewards,tag="eval",env=cfg.env,algo = cfg.algo,path=cfg.result_path) diff --git a/codes/NoisyDQN/noisy_dqn.py b/codes/NoisyDQN/noisy_dqn.py new file mode 100644 index 0000000..45cc5d2 --- /dev/null +++ b/codes/NoisyDQN/noisy_dqn.py @@ -0,0 +1,52 @@ +import torch +import torch.nn as nn + +class NoisyLinear(nn.Module): + def __init__(self, input_dim, output_dim, std_init=0.4): + super(NoisyLinear, self).__init__() + + self.input_dim = input_dim + self.output_dim = output_dim + self.std_init = std_init + + self.weight_mu = nn.Parameter(torch.FloatTensor(output_dim, input_dim)) + self.weight_sigma = nn.Parameter(torch.FloatTensor(output_dim, input_dim)) + self.register_buffer('weight_epsilon', torch.FloatTensor(output_dim, input_dim)) + + self.bias_mu = nn.Parameter(torch.FloatTensor(output_dim)) + self.bias_sigma = nn.Parameter(torch.FloatTensor(output_dim)) + self.register_buffer('bias_epsilon', torch.FloatTensor(output_dim)) + + self.reset_parameters() + self.reset_noise() + + def forward(self, x): + if self.training: + weight = self.weight_mu + self.weight_sigma.mul( (self.weight_epsilon)) + bias = self.bias_mu + self.bias_sigma.mul(Variable(self.bias_epsilon)) + else: + weight = self.weight_mu + bias = self.bias_mu + + return F.linear(x, weight, bias) + + def reset_parameters(self): + mu_range = 1 / math.sqrt(self.weight_mu.size(1)) + + self.weight_mu.data.uniform_(-mu_range, mu_range) + self.weight_sigma.data.fill_(self.std_init / math.sqrt(self.weight_sigma.size(1))) + + self.bias_mu.data.uniform_(-mu_range, mu_range) + self.bias_sigma.data.fill_(self.std_init / math.sqrt(self.bias_sigma.size(0))) + + def reset_noise(self): + epsilon_in = self._scale_noise(self.input_dim) + epsilon_out = self._scale_noise(self.output_dim) + + self.weight_epsilon.copy_(epsilon_out.ger(epsilon_in)) + self.bias_epsilon.copy_(self._scale_noise(self.output_dim)) + + def _scale_noise(self, size): + x = torch.randn(size) + x = x.sign().mul(x.abs().sqrt()) + return x \ No newline at end of file diff --git a/codes/NoisyDQN/task0_train.ipynb b/codes/NoisyDQN/task0_train.ipynb new file mode 100644 index 0000000..ecd0092 --- /dev/null +++ b/codes/NoisyDQN/task0_train.ipynb @@ -0,0 +1,25 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from pathlib import Path\n", + "curr_path = str(Path().absolute()) # 当前路径\n", + "parent_path = str(Path().absolute().parent) # 父路径\n", + "sys.path.append(parent_path) # 添加路径到系统路径" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/codes/PPO/README.md b/codes/PPO/README.md new file mode 100644 index 0000000..66825c9 --- /dev/null +++ b/codes/PPO/README.md @@ -0,0 +1,142 @@ +## 原理简介 + +PPO是一种on-policy算法,具有较好的性能,其前身是TRPO算法,也是policy gradient算法的一种,它是现在 OpenAI 默认的强化学习算法,具体原理可参考[PPO算法讲解](https://datawhalechina.github.io/easy-rl/#/chapter5/chapter5)。PPO算法主要有两个变种,一个是结合KL penalty的,一个是用了clip方法,本文实现的是后者即```PPO-clip```。 +## 伪代码 +要实现必先了解伪代码,伪代码如下: +![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70.png) +这是谷歌找到的一张比较适合的图,本人比较懒就没有修改,上面的```k```就是第```k```个episode,第六步是用随机梯度下降的方法优化,这里的损失函数(即```argmax```后面的部分)可能有点难理解,可参考[PPO paper](https://arxiv.org/abs/1707.06347),如下: +![在这里插入图片描述](assets/20210323154236878.png) +第七步就是一个平方损失函数,即实际回报与期望回报的差平方。 +## 代码实战 +[点击查看完整代码](https://github.com/JohnJim0816/rl-tutorials/tree/master/PPO) +### PPOmemory +首先第三步需要搜集一条轨迹信息,我们可以定义一个```PPOmemory```来存储相关信息: +```python +class PPOMemory: + def __init__(self, batch_size): + self.states = [] + self.probs = [] + self.vals = [] + self.actions = [] + self.rewards = [] + self.dones = [] + self.batch_size = batch_size + def sample(self): + batch_step = np.arange(0, len(self.states), self.batch_size) + indices = np.arange(len(self.states), dtype=np.int64) + np.random.shuffle(indices) + batches = [indices[i:i+self.batch_size] for i in batch_step] + return np.array(self.states),\ + np.array(self.actions),\ + np.array(self.probs),\ + np.array(self.vals),\ + np.array(self.rewards),\ + np.array(self.dones),\ + batches + + def push(self, state, action, probs, vals, reward, done): + self.states.append(state) + self.actions.append(action) + self.probs.append(probs) + self.vals.append(vals) + self.rewards.append(reward) + self.dones.append(done) + + def clear(self): + self.states = [] + self.probs = [] + self.actions = [] + self.rewards = [] + self.dones = [] + self.vals = [] +``` +这里的push函数就是将得到的相关量放入memory中,sample就是随机采样出来,方便第六步的随机梯度下降。 +### PPO model +model就是actor和critic两个网络了: +```python +import torch.nn as nn +from torch.distributions.categorical import Categorical +class Actor(nn.Module): + def __init__(self,state_dim, action_dim, + hidden_dim=256): + super(Actor, self).__init__() + + self.actor = nn.Sequential( + nn.Linear(state_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, action_dim), + nn.Softmax(dim=-1) + ) + def forward(self, state): + dist = self.actor(state) + dist = Categorical(dist) + return dist + +class Critic(nn.Module): + def __init__(self, state_dim,hidden_dim=256): + super(Critic, self).__init__() + self.critic = nn.Sequential( + nn.Linear(state_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, 1) + ) + def forward(self, state): + value = self.critic(state) + return value +``` +这里Actor就是得到一个概率分布(Categorica,也可以是别的分布,可以搜索torch distributionsl),critc根据当前状态得到一个值,这里的输入维度可以是```state_dim+action_dim```,即将action信息也纳入critic网络中,这样会更好一些,感兴趣的小伙伴可以试试。 + +### PPO update +定义一个update函数主要实现伪代码中的第六步和第七步: +```python +def update(self): + for _ in range(self.n_epochs): + state_arr, action_arr, old_prob_arr, vals_arr,\ + reward_arr, dones_arr, batches = \ + self.memory.sample() + values = vals_arr + ### compute advantage ### + advantage = np.zeros(len(reward_arr), dtype=np.float32) + for t in range(len(reward_arr)-1): + discount = 1 + a_t = 0 + for k in range(t, len(reward_arr)-1): + a_t += discount*(reward_arr[k] + self.gamma*values[k+1]*\ + (1-int(dones_arr[k])) - values[k]) + discount *= self.gamma*self.gae_lambda + advantage[t] = a_t + advantage = torch.tensor(advantage).to(self.device) + ### SGD ### + values = torch.tensor(values).to(self.device) + for batch in batches: + states = torch.tensor(state_arr[batch], dtype=torch.float).to(self.device) + old_probs = torch.tensor(old_prob_arr[batch]).to(self.device) + actions = torch.tensor(action_arr[batch]).to(self.device) + dist = self.actor(states) + critic_value = self.critic(states) + critic_value = torch.squeeze(critic_value) + new_probs = dist.log_prob(actions) + prob_ratio = new_probs.exp() / old_probs.exp() + weighted_probs = advantage[batch] * prob_ratio + weighted_clipped_probs = torch.clamp(prob_ratio, 1-self.policy_clip, + 1+self.policy_clip)*advantage[batch] + actor_loss = -torch.min(weighted_probs, weighted_clipped_probs).mean() + returns = advantage[batch] + values[batch] + critic_loss = (returns-critic_value)**2 + critic_loss = critic_loss.mean() + total_loss = actor_loss + 0.5*critic_loss + self.actor_optimizer.zero_grad() + self.critic_optimizer.zero_grad() + total_loss.backward() + self.actor_optimizer.step() + self.critic_optimizer.step() + self.memory.clear() +``` +该部分首先从memory中提取搜集到的轨迹信息,然后计算gae,即advantage,接着使用随机梯度下降更新网络,最后清除memory以便搜集下一条轨迹信息。 + +最后实现效果如下: +![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210405110725113.png) \ No newline at end of file diff --git a/codes/PPO/agent.py b/codes/PPO/agent.py new file mode 100644 index 0000000..0a7edd9 --- /dev/null +++ b/codes/PPO/agent.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-23 15:17:42 +LastEditor: John +LastEditTime: 2021-09-26 22:02:00 +Discription: +Environment: +''' +import os +import numpy as np +import torch +import torch.optim as optim +from PPO.model import Actor,Critic +from PPO.memory import PPOMemory +class PPO: + def __init__(self, state_dim, action_dim,cfg): + self.gamma = cfg.gamma + self.continuous = cfg.continuous + self.policy_clip = cfg.policy_clip + self.n_epochs = cfg.n_epochs + self.gae_lambda = cfg.gae_lambda + self.device = cfg.device + self.actor = Actor(state_dim, action_dim,cfg.hidden_dim).to(self.device) + self.critic = Critic(state_dim,cfg.hidden_dim).to(self.device) + self.actor_optimizer = optim.Adam(self.actor.parameters(), lr=cfg.actor_lr) + self.critic_optimizer = optim.Adam(self.critic.parameters(), lr=cfg.critic_lr) + self.memory = PPOMemory(cfg.batch_size) + self.loss = 0 + + def choose_action(self, state): + state = torch.tensor([state], dtype=torch.float).to(self.device) + dist = self.actor(state) + value = self.critic(state) + action = dist.sample() + probs = torch.squeeze(dist.log_prob(action)).item() + if self.continuous: + action = torch.tanh(action) + else: + action = torch.squeeze(action).item() + value = torch.squeeze(value).item() + return action, probs, value + + def update(self): + for _ in range(self.n_epochs): + state_arr, action_arr, old_prob_arr, vals_arr,reward_arr, dones_arr, batches = self.memory.sample() + values = vals_arr[:] + ### compute advantage ### + advantage = np.zeros(len(reward_arr), dtype=np.float32) + for t in range(len(reward_arr)-1): + discount = 1 + a_t = 0 + for k in range(t, len(reward_arr)-1): + a_t += discount*(reward_arr[k] + self.gamma*values[k+1]*\ + (1-int(dones_arr[k])) - values[k]) + discount *= self.gamma*self.gae_lambda + advantage[t] = a_t + advantage = torch.tensor(advantage).to(self.device) + ### SGD ### + values = torch.tensor(values).to(self.device) + for batch in batches: + states = torch.tensor(state_arr[batch], dtype=torch.float).to(self.device) + old_probs = torch.tensor(old_prob_arr[batch]).to(self.device) + actions = torch.tensor(action_arr[batch]).to(self.device) + dist = self.actor(states) + critic_value = self.critic(states) + critic_value = torch.squeeze(critic_value) + new_probs = dist.log_prob(actions) + prob_ratio = new_probs.exp() / old_probs.exp() + weighted_probs = advantage[batch] * prob_ratio + weighted_clipped_probs = torch.clamp(prob_ratio, 1-self.policy_clip, + 1+self.policy_clip)*advantage[batch] + actor_loss = -torch.min(weighted_probs, weighted_clipped_probs).mean() + returns = advantage[batch] + values[batch] + critic_loss = (returns-critic_value)**2 + critic_loss = critic_loss.mean() + total_loss = actor_loss + 0.5*critic_loss + self.loss = total_loss + self.actor_optimizer.zero_grad() + self.critic_optimizer.zero_grad() + total_loss.backward() + self.actor_optimizer.step() + self.critic_optimizer.step() + self.memory.clear() + def save(self,path): + actor_checkpoint = os.path.join(path, 'ppo_actor.pt') + critic_checkpoint= os.path.join(path, 'ppo_critic.pt') + torch.save(self.actor.state_dict(), actor_checkpoint) + torch.save(self.critic.state_dict(), critic_checkpoint) + def load(self,path): + actor_checkpoint = os.path.join(path, 'ppo_actor.pt') + critic_checkpoint= os.path.join(path, 'ppo_critic.pt') + self.actor.load_state_dict(torch.load(actor_checkpoint)) + self.critic.load_state_dict(torch.load(critic_checkpoint)) + + diff --git a/codes/PPO/assets/20210323154236878.png b/codes/PPO/assets/20210323154236878.png new file mode 100644 index 0000000..0e8d796 Binary files /dev/null and b/codes/PPO/assets/20210323154236878.png differ diff --git a/codes/PPO/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210405110725113.png b/codes/PPO/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210405110725113.png new file mode 100644 index 0000000..e1b61f4 Binary files /dev/null and b/codes/PPO/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210405110725113.png differ diff --git a/codes/PPO/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70.png b/codes/PPO/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70.png new file mode 100644 index 0000000..944c7a6 Binary files /dev/null and b/codes/PPO/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70.png differ diff --git a/codes/PPO/memory.py b/codes/PPO/memory.py new file mode 100644 index 0000000..c47fbc8 --- /dev/null +++ b/codes/PPO/memory.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-23 15:30:46 +LastEditor: John +LastEditTime: 2021-09-26 22:00:07 +Discription: +Environment: +''' +import numpy as np +class PPOMemory: + def __init__(self, batch_size): + self.states = [] + self.probs = [] + self.vals = [] + self.actions = [] + self.rewards = [] + self.dones = [] + self.batch_size = batch_size + def sample(self): + batch_step = np.arange(0, len(self.states), self.batch_size) + indices = np.arange(len(self.states), dtype=np.int64) + np.random.shuffle(indices) + batches = [indices[i:i+self.batch_size] for i in batch_step] + return np.array(self.states),np.array(self.actions),np.array(self.probs),\ + np.array(self.vals),np.array(self.rewards),np.array(self.dones),batches + + def push(self, state, action, probs, vals, reward, done): + self.states.append(state) + self.actions.append(action) + self.probs.append(probs) + self.vals.append(vals) + self.rewards.append(reward) + self.dones.append(done) + + def clear(self): + self.states = [] + self.probs = [] + self.actions = [] + self.rewards = [] + self.dones = [] + self.vals = [] \ No newline at end of file diff --git a/codes/PPO/model.py b/codes/PPO/model.py new file mode 100644 index 0000000..fc182d5 --- /dev/null +++ b/codes/PPO/model.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-23 15:29:24 +LastEditor: John +LastEditTime: 2021-04-08 22:36:43 +Discription: +Environment: +''' +import torch.nn as nn +from torch.distributions.categorical import Categorical +class Actor(nn.Module): + def __init__(self,state_dim, action_dim, + hidden_dim): + super(Actor, self).__init__() + + self.actor = nn.Sequential( + nn.Linear(state_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, action_dim), + nn.Softmax(dim=-1) + ) + def forward(self, state): + dist = self.actor(state) + dist = Categorical(dist) + return dist + +class Critic(nn.Module): + def __init__(self, state_dim,hidden_dim): + super(Critic, self).__init__() + self.critic = nn.Sequential( + nn.Linear(state_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, 1) + ) + def forward(self, state): + value = self.critic(state) + return value \ No newline at end of file diff --git a/codes/PPO/outputs/CartPole-v0/20211117-184614/models/ppo_actor.pt b/codes/PPO/outputs/CartPole-v0/20211117-184614/models/ppo_actor.pt new file mode 100644 index 0000000..6d7edc6 Binary files /dev/null and b/codes/PPO/outputs/CartPole-v0/20211117-184614/models/ppo_actor.pt differ diff --git a/codes/PPO/outputs/CartPole-v0/20211117-184614/models/ppo_critic.pt b/codes/PPO/outputs/CartPole-v0/20211117-184614/models/ppo_critic.pt new file mode 100644 index 0000000..63c35a8 Binary files /dev/null and b/codes/PPO/outputs/CartPole-v0/20211117-184614/models/ppo_critic.pt differ diff --git a/codes/PPO/outputs/CartPole-v0/20211117-184614/results/eval_ma_rewards.npy b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/eval_ma_rewards.npy new file mode 100644 index 0000000..14bca8b Binary files /dev/null and b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/eval_ma_rewards.npy differ diff --git a/codes/PPO/outputs/CartPole-v0/20211117-184614/results/eval_rewards.npy b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/eval_rewards.npy new file mode 100644 index 0000000..14bca8b Binary files /dev/null and b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/eval_rewards.npy differ diff --git a/codes/PPO/outputs/CartPole-v0/20211117-184614/results/eval_rewards_curve.png b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/eval_rewards_curve.png new file mode 100644 index 0000000..59eb91a Binary files /dev/null and b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/eval_rewards_curve.png differ diff --git a/codes/PPO/outputs/CartPole-v0/20211117-184614/results/train_ma_rewards.npy b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/train_ma_rewards.npy new file mode 100644 index 0000000..9db0ffe Binary files /dev/null and b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/train_ma_rewards.npy differ diff --git a/codes/PPO/outputs/CartPole-v0/20211117-184614/results/train_rewards.npy b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/train_rewards.npy new file mode 100644 index 0000000..5800e79 Binary files /dev/null and b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/train_rewards.npy differ diff --git a/codes/PPO/outputs/CartPole-v0/20211117-184614/results/train_rewards_curve.png b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/train_rewards_curve.png new file mode 100644 index 0000000..b4a5cfe Binary files /dev/null and b/codes/PPO/outputs/CartPole-v0/20211117-184614/results/train_rewards_curve.png differ diff --git a/codes/PPO/task0.py b/codes/PPO/task0.py new file mode 100644 index 0000000..8e0d92a --- /dev/null +++ b/codes/PPO/task0.py @@ -0,0 +1,67 @@ +import sys,os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import gym +import torch +import datetime +from common.plot import plot_rewards +from common.utils import save_results,make_dir +from PPO.agent import PPO +from PPO.train import train + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 + +class PPOConfig: + def __init__(self) -> None: + self.algo = "DQN" # 算法名称 + self.env_name = 'CartPole-v0' # 环境名称 + self.continuous = False # 环境是否为连续动作 + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.train_eps = 200 # 训练的回合数 + self.test_eps = 20 # 测试的回合数 + self.batch_size = 5 + self.gamma=0.99 + self.n_epochs = 4 + self.actor_lr = 0.0003 + self.critic_lr = 0.0003 + self.gae_lambda=0.95 + self.policy_clip=0.2 + self.hidden_dim = 256 + self.update_fre = 20 # frequency of agent update + +class PlotConfig: + def __init__(self) -> None: + self.algo = "DQN" # 算法名称 + self.env_name = 'CartPole-v0' # 环境名称 + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.result_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/results/' # 保存结果的路径 + self.model_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + +def env_agent_config(cfg,seed=1): + env = gym.make(cfg.env_name) + env.seed(seed) + state_dim = env.observation_space.shape[0] + action_dim = env.action_space.n + agent = PPO(state_dim,action_dim,cfg) + return env,agent + +cfg = PPOConfig() +plot_cfg = PlotConfig() +# 训练 +env,agent = env_agent_config(cfg,seed=1) +rewards, ma_rewards = train(cfg, env, agent) +make_dir(plot_cfg.result_path, plot_cfg.model_path) # 创建保存结果和模型路径的文件夹 +agent.save(path=plot_cfg.model_path) +save_results(rewards, ma_rewards, tag='train', path=plot_cfg.result_path) +plot_rewards(rewards, ma_rewards, plot_cfg, tag="train") +# 测试 +env,agent = env_agent_config(cfg,seed=10) +agent.load(path=plot_cfg.model_path) +rewards,ma_rewards = eval(cfg,env,agent) +save_results(rewards,ma_rewards,tag='eval',path=plot_cfg.result_path) +plot_rewards(rewards,ma_rewards,plot_cfg,tag="eval") \ No newline at end of file diff --git a/codes/PPO/task1.py b/codes/PPO/task1.py new file mode 100644 index 0000000..38d9152 --- /dev/null +++ b/codes/PPO/task1.py @@ -0,0 +1,68 @@ +import sys,os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import gym +import torch +import datetime +from common.plot import plot_rewards +from common.utils import save_results,make_dir +from PPO.agent import PPO +from PPO.train import train + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 + +class PPOConfig: + def __init__(self) -> None: + self.algo = "PPO" # 算法名称 + self.env_name = 'Pendulum-v1' # 环境名称 + self.continuous = True # 环境是否为连续动作 + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.train_eps = 200 # 训练的回合数 + self.test_eps = 20 # 测试的回合数 + self.batch_size = 5 + self.gamma=0.99 + self.n_epochs = 4 + self.actor_lr = 0.0003 + self.critic_lr = 0.0003 + self.gae_lambda=0.95 + self.policy_clip=0.2 + self.hidden_dim = 256 + self.update_fre = 20 # frequency of agent update + +class PlotConfig: + def __init__(self) -> None: + self.algo = "PPO" # 算法名称 + self.env_name = 'Pendulum-v1' # 环境名称 + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.result_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/results/' # 保存结果的路径 + self.model_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + +def env_agent_config(cfg,seed=1): + env = gym.make(cfg.env_name) + env.seed(seed) + state_dim = env.observation_space.shape[0] + action_dim = env.action_space.shape[0] + agent = PPO(state_dim,action_dim,cfg) + return env,agent + + +cfg = PPOConfig() +plot_cfg = PlotConfig() +# 训练 +env,agent = env_agent_config(cfg,seed=1) +rewards, ma_rewards = train(cfg, env, agent) +make_dir(plot_cfg.result_path, plot_cfg.model_path) # 创建保存结果和模型路径的文件夹 +agent.save(path=plot_cfg.model_path) +save_results(rewards, ma_rewards, tag='train', path=plot_cfg.result_path) +plot_rewards(rewards, ma_rewards, plot_cfg, tag="train") +# 测试 +env,agent = env_agent_config(cfg,seed=10) +agent.load(path=plot_cfg.model_path) +rewards,ma_rewards = eval(cfg,env,agent) +save_results(rewards,ma_rewards,tag='eval',path=plot_cfg.result_path) +plot_rewards(rewards,ma_rewards,plot_cfg,tag="eval") \ No newline at end of file diff --git a/codes/PPO/train.ipynb b/codes/PPO/train.ipynb new file mode 100644 index 0000000..b2dc91a --- /dev/null +++ b/codes/PPO/train.ipynb @@ -0,0 +1,257 @@ +{ + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + }, + "orig_nbformat": 2, + "kernelspec": { + "name": "python3710jvsc74a57bd0366e1054dee9d4501b0eb8f87335afd3c67fc62db6ee611bbc7f8f5a1fefe232", + "display_name": "Python 3.7.10 64-bit ('py37': conda)" + }, + "metadata": { + "interpreter": { + "hash": "366e1054dee9d4501b0eb8f87335afd3c67fc62db6ee611bbc7f8f5a1fefe232" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from pathlib import Path\n", + "curr_path = str(Path().absolute())\n", + "parent_path = str(Path().absolute().parent)\n", + "sys.path.append(parent_path) # add current terminal path to sys.path" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import gym\n", + "import torch\n", + "import datetime\n", + "from PPO.agent import PPO\n", + "from common.plot import plot_rewards\n", + "from common.utils import save_results,make_dir\n", + "\n", + "curr_time = datetime.datetime.now().strftime(\"%Y%m%d-%H%M%S\") # obtain current time" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "class PPOConfig:\n", + " def __init__(self) -> None:\n", + " self.env = 'CartPole-v0'\n", + " self.algo = 'PPO'\n", + " self.result_path = curr_path+\"/results/\" +self.env+'/'+curr_time+'/results/' # path to save results\n", + " self.model_path = curr_path+\"/results/\" +self.env+'/'+curr_time+'/models/' # path to save models\n", + " self.train_eps = 200 # max training episodes\n", + " self.test_eps = 50\n", + " self.batch_size = 5\n", + " self.gamma=0.99\n", + " self.n_epochs = 4\n", + " self.actor_lr = 0.0003\n", + " self.critic_lr = 0.0003\n", + " self.gae_lambda=0.95\n", + " self.policy_clip=0.2\n", + " self.hidden_dim = 256\n", + " self.update_fre = 20 # frequency of agent update\n", + " self.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\") # check gpu" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def env_agent_config(cfg,seed=1):\n", + " env = gym.make(cfg.env) \n", + " env.seed(seed)\n", + " state_dim = env.observation_space.shape[0]\n", + " action_dim = env.action_space.n\n", + " agent = PPO(state_dim,action_dim,cfg)\n", + " return env,agent" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def train(cfg,env,agent):\n", + " print('Start to train !')\n", + " print(f'Env:{cfg.env}, Algorithm:{cfg.algo}, Device:{cfg.device}')\n", + " rewards= []\n", + " ma_rewards = [] # moving average rewards\n", + " running_steps = 0\n", + " for i_ep in range(cfg.train_eps):\n", + " state = env.reset()\n", + " done = False\n", + " ep_reward = 0\n", + " while not done:\n", + " action, prob, val = agent.choose_action(state)\n", + " state_, reward, done, _ = env.step(action)\n", + " running_steps += 1\n", + " ep_reward += reward\n", + " agent.memory.push(state, action, prob, val, reward, done)\n", + " if running_steps % cfg.update_fre == 0:\n", + " agent.update()\n", + " state = state_\n", + " rewards.append(ep_reward)\n", + " if ma_rewards:\n", + " ma_rewards.append(\n", + " 0.9*ma_rewards[-1]+0.1*ep_reward)\n", + " else:\n", + " ma_rewards.append(ep_reward)\n", + " if (i_ep+1)%10==0:\n", + " print(f\"Episode:{i_ep+1}/{cfg.train_eps}, Reward:{ep_reward:.3f}\")\n", + " print('Complete training!')\n", + " return rewards,ma_rewards" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def eval(cfg,env,agent):\n", + " print('Start to eval !')\n", + " print(f'Env:{cfg.env}, Algorithm:{cfg.algo}, Device:{cfg.device}')\n", + " rewards= []\n", + " ma_rewards = [] # moving average rewards\n", + " for i_ep in range(cfg.test_eps):\n", + " state = env.reset()\n", + " done = False\n", + " ep_reward = 0\n", + " while not done:\n", + " action, prob, val = agent.choose_action(state)\n", + " state_, reward, done, _ = env.step(action)\n", + " ep_reward += reward\n", + " state = state_\n", + " rewards.append(ep_reward)\n", + " if ma_rewards:\n", + " ma_rewards.append(\n", + " 0.9*ma_rewards[-1]+0.1*ep_reward)\n", + " else:\n", + " ma_rewards.append(ep_reward)\n", + " if (i_ep+1)%10==0:\n", + " print(f\"Episode:{i_ep+1}/{cfg.train_eps}, Reward:{ep_reward:.3f}\")\n", + " print('Complete evaling!')\n", + " return rewards,ma_rewards" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Start to train !\n", + "Env:CartPole-v0, Algorithm:PPO, Device:cuda\n", + "Episode:10/200, Reward:15.000\n", + "Episode:20/200, Reward:9.000\n", + "Episode:30/200, Reward:20.000\n", + "Episode:40/200, Reward:17.000\n", + "Episode:50/200, Reward:64.000\n", + "Episode:60/200, Reward:90.000\n", + "Episode:70/200, Reward:23.000\n", + "Episode:80/200, Reward:138.000\n", + "Episode:90/200, Reward:150.000\n", + "Episode:100/200, Reward:200.000\n", + "Episode:110/200, Reward:200.000\n", + "Episode:120/200, Reward:200.000\n", + "Episode:130/200, Reward:200.000\n", + "Episode:140/200, Reward:200.000\n", + "Episode:150/200, Reward:200.000\n", + "Episode:160/200, Reward:200.000\n", + "Episode:170/200, Reward:200.000\n", + "Episode:180/200, Reward:200.000\n", + "Episode:190/200, Reward:200.000\n", + "Episode:200/200, Reward:200.000\n", + "Complete training!\n", + "results saved!\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/svg+xml": "\n\n\n \n \n \n \n 2021-05-06T01:36:50.188726\n image/svg+xml\n \n \n Matplotlib v3.4.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXsAAAEcCAYAAAAmzxTpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Z1A+gAAAACXBIWXMAAAsTAAALEwEAmpwYAACCHklEQVR4nO2deZwU1bm/n6rqdfaVYVgERUEUhYEBUVQU911jVC5xTUxiFpcYTExiNEFNgjEm0UuuMSbm+tOrMXELbmjcd0EBBRUB2WeYfe/pparO74/qqu6e6Znp2aeZ83w+MN3VVeec2t5663ve8x5FCCGQSCQSyT6NOtwNkEgkEsngI429RCKRjAKksZdIJJJRgDT2EolEMgqQxl4ikUhGAdLYSyQSyShAGvtRwD333MPSpUuHpe4zzjiD999/f1jqHmkEg0Guuuoq5syZwzXXXDPczeHDDz/k5JNPpqysjP/85z/D3ZxBZ9q0aezYsWO4mzFsSGMvGVSeffZZjjjiiOFuxojghRdeoLa2lvfff5+777670+/33HMPhx56KGVlZZSXl7N48WLWrl0LwBNPPMH06dMpKytj9uzZnHPOObz66qvOts3Nzdxyyy0sWLCAmTNnctZZZ/H444932567776br33ta6xdu5YTTzxxQPbx448/5pvf/Cbl5eXMmzePr371qz22ozsuueQS/vnPfyYsmzZtGrNmzaKsrIxjjjmGX//61xiG0d+m95nPPvuMr3zlK8ycOZOvfOUrfPbZZ8PWlu6Qxn4Y0HV9uJswIOwL+zGU+1BRUcHkyZNxuVxdrnPaaaexdu1a3n33XWbPns3VV1+NPe5x1qxZrF27ljVr1vDVr36V6667jqamJsLhMJdffjkVFRU8+uijrFmzhhtuuIHf/e53PPDAA92256CDDurTviQ7bmvXruWyyy5j7ty5vPjii7z//vv84he/4I033uh1+UIITNPs8venn36atWvX8ve//51nnnmGxx57rNd1DAThcJjvfve7nH322axevZpzzz2X7373u4TD4WFpT3dIY9+B++67jxNPPJGysjJOP/10XnrpJcA6qeXl5XzxxRfOuvX19Rx++OHU1dUB8Oqrr3LOOec4Xtnnn3/urLto0SLuu+8+zjrrLGbNmoWu613WBWAYBr/5zW844ogjWLRoEQ899BDTpk1zbrKWlhZ++tOfcvTRR3PMMcfw+9//PmXvZt26dSxevJjy8nLOPvvsBJnl8ccf57TTTqOsrIwTTjiBRx991Pnt/fff59hjj+W+++5jwYIF/OQnP+Gee+7h2muv5Uc/+hFlZWWcccYZfPLJJwn7/c477wD0uO7GjRs599xzKSsr45prruG6667j97//fZf78dhjjzltPf3009m4cSPQ+XX9xhtvdMpJtg+nnXZagpes6zrz5893yuvueHVk69atXHLJJZSXl3PGGWfw8ssvA5YX/ac//Ynnn3+esrKyTt5qR9xuN+eddx41NTU0NDQk/KaqKueffz7BYJCdO3fy9NNPU1lZyR//+EcmTpyI2+3m2GOP5aabbuLuu++mtbW1U/knnngiu3bt4qqrrqKsrIxwOExVVRVXXXUV8+bN46STTkowoPfccw/XXHMNS5cuZfbs2Tz55JOdyrzjjjs499xz+da3vkVBQQGKojBjxgz++Mc/AtDU1MS3v/1t5s+fz9y5c/n2t7/N3r17ne0vueQSfv/737N48WJmzpzJDTfcwJo1a1i2bBllZWUsW7asU51Tpkxhzpw5bN68GbCuiZNOOol58+Zx1VVXUVVVlfT4hsNhli9fznHHHcdRRx3FzTffTDAYTLpud9fHBx98gK7rXHbZZXg8Hi699FKEELz33ntJyxpWhCSB5557Tuzdu1cYhiGeffZZMXPmTFFVVSWEEOLGG28Ud911l7PuQw89JL7+9a8LIYTYuHGjmD9/vli3bp3QdV088cQT4vjjjxehUEgIIcTxxx8vzj77bFFRUSHa29t7rOv//u//xGmnnSYqKytFY2OjuOyyy8TUqVNFJBIRQgjx3e9+V/z85z8XbW1tora2Vpx//vnikUceSbpPd999t/jhD38ohBBi7969Yt68eeK1114ThmGIt956S8ybN0/U1dUJIYR49dVXxY4dO4RpmuL9998Xhx9+uNiwYYMQQoj33ntPTJ8+Xdxxxx0iFAqJ9vZ2cffdd4sZM2aI1157Tei6Lu68805xwQUXOHUff/zx4u2333ba0dW6oVBIHHfcceLvf/+7CIfDYtWqVeLQQw9NON4dz9PRRx8t1q9fL0zTFNu3bxe7d+8WQggxdepUsX37dmfdH//4x045yfbhnnvuEddff72z/quvvipOPfXUlI5XPOFwWJx44onif/7nf0QoFBLv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r371q/ztb39jw4YNCCHYsWOHc6MffPDBPPPMMxiGwRtvvNFJKkjG6aefzttvv80jjzyScF67O14dOfzww/H5fNx///1EIhHef/99XnnlFU4//fQ+HJHUOeeccxg7dizXXnstu3fvJhKJ8Oabb3Lbbbfx/e9/v5PMkIzS0lLKysq46667CIVCfP755/zrX//i7LPPTrkdN9xwA08++ST333+/84b6+eef84Mf/ACwPF6v10tOTg6NjY0JElVXFBUVsWvXrpTqP/PMM3niiSf47LPPCIfD3HXXXRx++OEJXj1YMt4FF1zAr371Kye4oqqqijfffLPLsru6PubNm4emaTz44IOEw2Hnzb6rB95wIo19HAceeCBf//rXWbx4MUcddRRffPEFs2fPTlhn5syZ+P1+qqurOfbYY53lhx12GLfeeivLli1j7ty5nHzyyTzxxBN9ruvCCy9kwYIFnH322Zx77rksXLgQl8vlvO7ecccdRCIRTj/9dObOncs111yT9FW+I6WlpfzpT3/iz3/+M0ceeSQLFy7kr3/9K6ZpkpWVxU033cR1113H3LlzeeaZZ1i0aFFvD2Of8Hg83HPPPfzrX/9i7ty5/Pvf/+a4447D4/EkXf+0007jqquu4oc//CGzZ8/me9/7niMN/OxnP+PVV1+lvLyclStXphRDPmbMGCe0Md44d3e8ku3DvffeyxtvvMH8+fP55S9/yR133NGlrDFQeDweHnjgAUpLS7nwwguZM2cOv/nNb/jBD37AlVdemXI5d911F3v27OGYY47h+9//PldffTVHHXVUytvPnj2b//3f/+W9997jxBNPZN68efz85z9n4cKFAFx22WWEQiHmz5/PRRddxDHHHNNjmZdeeimrVq1i7ty5Pb5dHXXUUVx77bVcffXVHH300ezatavLaK4bbriBSZMmceGFFzJ79mwuv/xytm3b1mXZXV0fHo+HFStW8PTTT1NeXs7jjz/OihUrurxuhxNFCDl5STrw+uuv84tf/CJBO97XueCCC1i8eDHnn3/+cDdFIkl7pGc/QgkGg7z++uvouk5VVRUrVqwYsFGOI5UPPviAmpoadF3nySefZNOmTSl5fxKJpGdkB+0IRQjB3XffzXXXXYfP5+O4447j2muvHe5mDSrbtm3juuuuo729nQkTJnD33XczZsyY4W6WRLJPIGUciUQi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\n" + }, + "metadata": {} + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "Start to eval !\n", + "Env:CartPole-v0, Algorithm:PPO, Device:cuda\n", + "Episode:10/200, Reward:200.000\n", + "Episode:20/200, Reward:183.000\n", + "Episode:30/200, Reward:157.000\n", + "Episode:40/200, Reward:200.000\n", + "Episode:50/200, Reward:113.000\n", + "Complete evaling!\n", + "results saved!\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": "
", + "image/svg+xml": "\n\n\n \n \n \n \n 2021-05-06T01:36:55.923900\n image/svg+xml\n \n \n Matplotlib v3.4.1, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAXsAAAEcCAYAAAAmzxTpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/Z1A+gAAAACXBIWXMAAAsTAAALEwEAmpwYAAB9qUlEQVR4nO2dd5QUVfr3vxU7TJ5hEhkRcECFgSEoSlZAYUBdBVnDmll/BtbV1V1RMK6oq64svurq6rq6pjUCKhgQxICggJKRJGFynk6V7vtHT9X0zHSo7ume0H0/53AO01VddW9X9dNPfe8TGEIIAYVCoVDiGrazB0ChUCiU2EONPYVCoSQA1NhTKBRKAkCNPYVCoSQA1NhTKBRKAkCNPYVCoSQA1NgnAMuXL8ftt9/eKec+//zzsWnTpk45d1fD7XZj4cKFGDVqFG655ZbOHg5++OEHnHvuuSgsLMRnn33W2cOJOUOGDMGRI0c6exidBjX2lJiyevVqjB07trOH0SX45JNPUFlZiU2bNuHpp59us3358uUYNmwYCgsLUVRUhPnz52Pr1q0AgHfffRcFBQUoLCzEyJEjMWfOHKxbt854b319PZYsWYLx48dj+PDhmD17Nt55552g43n66afx29/+Flu3bsW0adOiMseffvoJ1113HYqKijBmzBj85je/CTmOYFx++eV4++23W7w2ZMgQjBgxAoWFhTj77LPx17/+FaqqtnfoEbN7925ceOGFGD58OC688ELs3r2708YSDGrsOwFFUTp7CFEhHubRkXM4ceIE+vfvD57nA+4zc+ZMbN26Fd9++y1GjhyJm2++GXre44gRI7B161Zs2bIFv/nNb7Bo0SLU1dVBkiT87ne/w4kTJ/DGG29gy5YtuOOOO/C3v/0NL730UtDxDBo0KKK5+Pvctm7diiuvvBKjR4/G2rVrsWnTJixduhQbNmwI+/iEEGiaFnD7Bx98gK1bt+Lll1/GqlWr8NZbb4V9jmggSRJuvPFGFBcXY/PmzZg7dy5uvPFGSJLUKeMJBjX2rXj++ecxbdo0FBYW4rzzzsOnn34KwHtRi4qKsG/fPmPf6upqnH766aiqqgIArFu3DnPmzDG8sj179hj7TpkyBc8//zxmz56NESNGQFGUgOcCAFVV8cgjj2Ds2LGYMmUKXn31VQwZMsT4kjU0NOAvf/kLzjrrLJx99tl48sknTXs327Ztw/z581FUVITi4uIWMss777yDmTNnorCwEFOnTsUbb7xhbNu0aRMmTJiA559/HuPHj8ef//xnLF++HLfeeiv+9Kc/obCwEOeffz5+/vnnFvP+5ptvACDkvjt37sTcuXNRWFiIW265BYsWLcKTTz4ZcB5vvfWWMdbzzjsPO3fuBND2cf2uu+4yjuNvDjNnzmzhJSuKgnHjxhnHC/Z5tebAgQO4/PLLUVRUhPPPPx+ff/45AK8X/cwzz+Djjz9GYWFhG2+1NYIg4IILLkBFRQVqampabGNZFhdddBHcbjd+/fVXfPDBBygpKcHf//539OnTB4IgYMKECVi8eDGefvppNDY2tjn+tGnTcPToUSxcuBCFhYWQJAllZWVYuHAhxowZg3POOaeFAV2+fDluueUW3H777Rg5ciTee++9Nsd89NFHMXfuXFx//fXIzMwEwzA49dRT8fe//x0AUFdXhxtuuAHjxo3D6NGjccMNN6C0tNR4/+WXX44nn3wS8+fPx/Dhw3HHHXdgy5YtuP/++1FYWIj777+/zTkHDhyIUaNGYf/+/QC898Q555yDMWPGYOHChSgrK/P7+UqShGXLlmHSpEk488wzce+998LtdvvdN9j98f3330NRFFx55ZUQRRFXXHEFCCH47rvv/B6rUyGUFnz00UektLSUqKpKVq9eTYYPH07KysoIIYTcdddd5IknnjD2ffXVV8nVV19NCCFk586dZNy4cWTbtm1EURTy7rvvksmTJxOPx0MIIWTy5MmkuLiYnDhxgrhcrpDn+u9//0tmzpxJSkpKSG1tLbnyyivJ4MGDiSzLhBBCbrzxRnLPPfcQh8NBKisryUUXXURef/11v3N6+umnyR//+EdCCCGlpaVkzJgx5MsvvySqqpKNGzeSMWPGkKqqKkIIIevWrSNHjhwhmqaRTZs2kdNPP53s2LGDEELId999RwoKCsijjz5KPB4Pcblc5Omnnyannnoq+fLLL4miKOTxxx8nF198sXHuyZMnk6+//toYR6B9PR4PmTRpEnn55ZeJJElkzZo1ZNiwYS0+79bX6ayzziLbt28nmqaRw4cPk2PHjhFCCBk8eDA5fPiwse+dd95pHMffHJYvX05uu+02Y/9169aRGTNmmPq8fJEkiUybNo38v//3/4jH4yHffPMNGTFiBDlw4ECb6xDqOnk8HvLII4+QiRMnEkIIeeedd8j8+fMJIYTIskxefvllMmLECFJfX08WLVpE/vSnP7U5nizLpKCggGzYsMHv+XyvDSGELFiwgCxZsoS43W6ya9cuMnbsWPLNN98YYxs6dCj59NNPiaqqxj2s43Q6ySmnnEK+/fbbgPOrrq4mn3zyCXE6naShoYHcfPPN5Pe//72x/bLLLiMTJ04k+/btI7IsE0mSyGWXXUbeeuutFsfxvb779+8nZ555JnnrrbfIN998Q8aMGUN27NhBPB4Puf/++8mCBQv8vu+hhx4iN9xwA6mpqSENDQ3khhtuII8//rjfcQe7P1566SVyzTXXtNj/+uuvJy+++GLAz6GzoJ59K2bOnInc3FywLIvzzjsP/fr1w08//QQAmD17NlavXm3su3LlSsyePRsA8Oabb2LevHkYPnw4OI7DBRdcAEEQsG3bNmP/yy+/HPn5+bBarSHP9fHHH+OKK65AXl4e0tLScP311xvHqaysxPr16/GXv/wFdrsdWVlZ+N3vftdibIH44IMPMGHCBEycOBEsy2L8+PE49dRTsX79egDApEmT0LdvXzAMgzFjxmD8+PHYsmWL8X6WZXHLLbdAFEVjHqNGjcLEiRPBcRzmzJnT4ommNYH23b59OxRFwRVXXAFBEHDuuefitNNOC3ic//3vf7j22mtx+umng2EY9OvXD7169Qo5f39zmD17Nr744gu4XC4A3ut6/vnnm/q8fNm+fTucTieuv/56iKKIM844A5MnTzZ1XXQ++eQTFBUVYeLEidi5cyf+8Y9/tDh+UVERxo8fj9WrV2PFihVISUlBTU0NsrOz2xyL53lkZGS0eTLwR0lJCX788UfcfvvtsFgsKCgowMUXX4wPPvjA2GfEiBGYNm0aWJY1rr1OfX09NE3zOw6djIwMTJ8+HTabDcnJyfj973+PzZs3t9jnggsuwKBBg8DzPARBCHisCy64AKNHj8bChQvxm9/8BhdddBFWrlyJiy66CMOGDYMoirjtttuwbds2HDt2rMV7CSF466238Je//AXp6elITk7GDTfcEPA6Bbs/HA4HUlJSWuyfnJwMh8MRcOydRWDxMEF5//338dJLL+H48eMAAKfTaXxZxo4dC7fbje3btyMrKwt79uwxFrZOnDiB999/H6+++qpxLFmWUV5ebvydn59v+lzl5eUt9s/L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N45s27aNKIpC3n33XTJ58mTi8XiCnp9CiRbUs6fEDT///DOqq6tx0003QRRF9OnTB5dccgk++ugjAMCwYcMwY8YMCIKAq666CpIkYfv27eA4DpIk4cCBA5BlGb1790bfvn3bHP+jjz7CxIkTMX78eAiCgGuuuQZutxtbt27F9u3bIcsyrrzySgiCgBkzZuC0004z3vvmm29i3rx5GD58ODiOwwUXXABBELBt2zbT56dQ2gNdoKXEDcePH0d5ebnRyQfwlv0tKipCz549kZeXZ7zOsixyc3ON/f/yl79g+fLl+OWXX3DWWWfhrrvuQm5ubovjl5eXt2geoZdXLisrA8dxyM3NBcMwxnbffU+cOIH3338fr776qvGaLMsoLy/HmDFjTJ2fQmkP1LOnxA35+fno3bs3tmzZYvzbunUr/vnPfwIASktLjX01TUNZWRlycnIAALNnz8brr7+OdevWgWEYPP74422On5OT06KeOCHE6BaVnZ2NsrIyEJ+6gr775ufnY+HChS3Gtn37dqOloJnzUyjtgRp7Stxw+umnIykpCc8//zzcbjdUVcW+ffvw008/AQB27tyJtWvXQlEU/Pvf/4Yoihg+fDgOHjyIb7/9FpIkQRRFWCwWsGzbr8bMmTOxfv16fPvtt5BlGf/6178giiIKCwsxYsQI8DyPV155BbIsY+3atfj555+N91588cV44403sH37dhBC4HQ68eWXX6KxsdH0+SmU9kBlHErcwHEcnn32WSxbtgxTp06FJEkYMGAAFi1aBACYOnUqPvroI9x5553o168fli9fDkEQIEkS/va3v+HAgQMQBAGFhYV+28CddNJJeOyxx/DAAw+grKwMBQUFePbZZ40mKsuXL8c999yDp556ChMnTsQ555xjvPe0007DAw88gPvvvx9HjhyB1WrFyJEjUVRUZPr8FEp7oPXsKQnB8uXLceTIESqPUBIW+qxIoVAoCQA19hQKhZIAUBmHQqFQEgDq2VMoFEoCQI09hUKhJADU2FMoFEoCQI09hUKhJADU2FMoFEoCQI09hUKhJAD/HyGIMZHomwikAAAAAElFTkSuQmCC\n" + }, + "metadata": {} + } + ], + "source": [ + "if __name__ == '__main__':\n", + " cfg = PPOConfig()\n", + " # train\n", + " env,agent = env_agent_config(cfg,seed=1)\n", + " rewards, ma_rewards = train(cfg, env, agent)\n", + " make_dir(cfg.result_path, cfg.model_path)\n", + " agent.save(path=cfg.model_path)\n", + " save_results(rewards, ma_rewards, tag='train', path=cfg.result_path)\n", + " plot_rewards(rewards, ma_rewards, tag=\"train\",\n", + " algo=cfg.algo, path=cfg.result_path)\n", + " # eval\n", + " env,agent = env_agent_config(cfg,seed=10)\n", + " agent.load(path=cfg.model_path)\n", + " rewards,ma_rewards = eval(cfg,env,agent)\n", + " save_results(rewards,ma_rewards,tag='eval',path=cfg.result_path)\n", + " plot_rewards(rewards,ma_rewards,tag=\"eval\",env=cfg.env,algo = cfg.algo,path=cfg.result_path)" + ] + } + ] +} \ No newline at end of file diff --git a/codes/PPO/train.py b/codes/PPO/train.py new file mode 100644 index 0000000..e642df0 --- /dev/null +++ b/codes/PPO/train.py @@ -0,0 +1,121 @@ +def train(cfg,env,agent): + print('开始训练!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + steps = 0 + for i_ep in range(cfg.train_eps): + state = env.reset() + done = False + ep_reward = 0 + while not done: + action, prob, val = agent.choose_action(state) + state_, reward, done, _ = env.step(action) + steps += 1 + ep_reward += reward + agent.memory.push(state, action, prob, val, reward, done) + if steps % cfg.update_fre == 0: + agent.update() + state = state_ + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + if (i_ep+1)%10 == 0: + print(f"回合:{i_ep+1}/{cfg.train_eps},奖励:{ep_reward:.2f}") + print('完成训练!') + return rewards,ma_rewards + +def eval(cfg,env,agent): + print('开始测试!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.test_eps): + state = env.reset() + done = False + ep_reward = 0 + while not done: + action, prob, val = agent.choose_action(state) + state_, reward, done, _ = env.step(action) + ep_reward += reward + state = state_ + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append( + 0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + print('回合:{}/{}, 奖励:{}'.format(i_ep+1, cfg.test_eps, ep_reward)) + print('完成训练!') + return rewards,ma_rewards + +if __name__ == '__main__': + import sys,os + curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 + parent_path = os.path.dirname(curr_path) # 父路径 + sys.path.append(parent_path) # 添加路径到系统路径 + + import gym + import torch + import datetime + from common.plot import plot_rewards + from common.utils import save_results,make_dir + from PPO.agent import PPO + from PPO.train import train + + curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 + + class PPOConfig: + def __init__(self) -> None: + self.algo = "DQN" # 算法名称 + self.env_name = 'CartPole-v0' # 环境名称 + self.continuous = False # 环境是否为连续动作 + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.train_eps = 200 # 训练的回合数 + self.test_eps = 20 # 测试的回合数 + self.batch_size = 5 + self.gamma=0.99 + self.n_epochs = 4 + self.actor_lr = 0.0003 + self.critic_lr = 0.0003 + self.gae_lambda=0.95 + self.policy_clip=0.2 + self.hidden_dim = 256 + self.update_fre = 20 # frequency of agent update + + class PlotConfig: + def __init__(self) -> None: + self.algo = "DQN" # 算法名称 + self.env_name = 'CartPole-v0' # 环境名称 + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.result_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/results/' # 保存结果的路径 + self.model_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + + def env_agent_config(cfg,seed=1): + env = gym.make(cfg.env_name) + env.seed(seed) + state_dim = env.observation_space.shape[0] + action_dim = env.action_space.n + agent = PPO(state_dim,action_dim,cfg) + return env,agent + + cfg = PPOConfig() + plot_cfg = PlotConfig() + # 训练 + env,agent = env_agent_config(cfg,seed=1) + rewards, ma_rewards = train(cfg, env, agent) + make_dir(plot_cfg.result_path, plot_cfg.model_path) # 创建保存结果和模型路径的文件夹 + agent.save(path=plot_cfg.model_path) + save_results(rewards, ma_rewards, tag='train', path=plot_cfg.result_path) + plot_rewards(rewards, ma_rewards, plot_cfg, tag="train") + # 测试 + env,agent = env_agent_config(cfg,seed=10) + agent.load(path=plot_cfg.model_path) + rewards,ma_rewards = eval(cfg,env,agent) + save_results(rewards,ma_rewards,tag='eval',path=plot_cfg.result_path) + plot_rewards(rewards,ma_rewards,plot_cfg,tag="eval") \ No newline at end of file diff --git a/codes/PolicyGradient/README.md b/codes/PolicyGradient/README.md new file mode 100644 index 0000000..956cdbf --- /dev/null +++ b/codes/PolicyGradient/README.md @@ -0,0 +1,27 @@ +# Policy Gradient + + +Policy-based方法是强化学习中与Value-based(比如Q-learning)相对的方法,其目的是对策略本身进行梯度下降,相关基础知识参考[Datawhale-Policy Gradient](https://datawhalechina.github.io/leedeeprl-notes/#/chapter4/chapter4)。 +其中REINFORCE是一个最基本的Policy Gradient方法,主要解决策略梯度无法直接计算的问题,具体原理参考[CSDN-REINFORCE和Reparameterization Trick](https://blog.csdn.net/JohnJim0/article/details/110230703) + +## 伪代码 + +结合REINFORCE原理,其伪代码如下: + +image-20211016004808604 + +https://pytorch.org/docs/stable/distributions.html + +加负号的原因是,在公式中应该是实现的梯度上升算法,而loss一般使用随机梯度下降的,所以加个负号保持一致性。 + +![img](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210428001336032.png) + +## 实现 + +## 参考 + +[REINFORCE和Reparameterization Trick](https://blog.csdn.net/JohnJim0/article/details/110230703) + +[Policy Gradient paper](https://papers.nips.cc/paper/1713-policy-gradient-methods-for-reinforcement-learning-with-function-approximation.pdf) + +[REINFORCE](https://towardsdatascience.com/policy-gradient-methods-104c783251e0) \ No newline at end of file diff --git a/codes/PolicyGradient/agent.py b/codes/PolicyGradient/agent.py new file mode 100644 index 0000000..8f349b5 --- /dev/null +++ b/codes/PolicyGradient/agent.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2020-11-22 23:27:44 +LastEditor: John +LastEditTime: 2021-10-16 00:43:52 +Discription: +Environment: +''' +import torch +from torch.distributions import Bernoulli +from torch.autograd import Variable +import numpy as np +from PolicyGradient.model import MLP + +class PolicyGradient: + + def __init__(self, state_dim,cfg): + self.gamma = cfg.gamma + self.policy_net = MLP(state_dim,hidden_dim=cfg.hidden_dim) + self.optimizer = torch.optim.RMSprop(self.policy_net.parameters(), lr=cfg.lr) + self.batch_size = cfg.batch_size + + def choose_action(self,state): + + state = torch.from_numpy(state).float() + state = Variable(state) + probs = self.policy_net(state) + m = Bernoulli(probs) # 伯努利分布 + action = m.sample() + action = action.data.numpy().astype(int)[0] # 转为标量 + return action + + def update(self,reward_pool,state_pool,action_pool): + # Discount reward + running_add = 0 + for i in reversed(range(len(reward_pool))): + if reward_pool[i] == 0: + running_add = 0 + else: + running_add = running_add * self.gamma + reward_pool[i] + reward_pool[i] = running_add + + # Normalize reward + reward_mean = np.mean(reward_pool) + reward_std = np.std(reward_pool) + for i in range(len(reward_pool)): + reward_pool[i] = (reward_pool[i] - reward_mean) / reward_std + + # Gradient Desent + self.optimizer.zero_grad() + + for i in range(len(reward_pool)): + state = state_pool[i] + action = Variable(torch.FloatTensor([action_pool[i]])) + reward = reward_pool[i] + state = Variable(torch.from_numpy(state).float()) + probs = self.policy_net(state) + m = Bernoulli(probs) + loss = -m.log_prob(action) * reward # Negtive score function x reward + # print(loss) + loss.backward() + self.optimizer.step() + def save(self,path): + torch.save(self.policy_net.state_dict(), path+'pg_checkpoint.pt') + def load(self,path): + self.policy_net.load_state_dict(torch.load(path+'pg_checkpoint.pt')) \ No newline at end of file diff --git a/codes/PolicyGradient/assets/image-20211016004808604.png b/codes/PolicyGradient/assets/image-20211016004808604.png new file mode 100644 index 0000000..b0a56b5 Binary files /dev/null and b/codes/PolicyGradient/assets/image-20211016004808604.png differ diff --git a/codes/PolicyGradient/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210428001336032.png b/codes/PolicyGradient/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210428001336032.png new file mode 100644 index 0000000..44c1874 Binary files /dev/null and b/codes/PolicyGradient/assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0pvaG5KaW0w,size_16,color_FFFFFF,t_70-20210428001336032.png differ diff --git a/codes/PolicyGradient/model.py b/codes/PolicyGradient/model.py new file mode 100644 index 0000000..6d9bc64 --- /dev/null +++ b/codes/PolicyGradient/model.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-23 16:35:58 +LastEditor: John +LastEditTime: 2021-12-21 23:21:26 +Discription: +Environment: +''' +import torch.nn as nn +import torch.nn.functional as F +class MLP(nn.Module): + + ''' 多层感知机 + 输入:state维度 + 输出:概率 + ''' + def __init__(self,input_dim,hidden_dim = 36): + super(MLP, self).__init__() + # 24和36为hidden layer的层数,可根据input_dim, action_dim的情况来改变 + self.fc1 = nn.Linear(input_dim, hidden_dim) + self.fc2 = nn.Linear(hidden_dim,hidden_dim) + self.fc3 = nn.Linear(hidden_dim, 1) # Prob of Left + + def forward(self, x): + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + x = F.sigmoid(self.fc3(x)) + return x \ No newline at end of file diff --git a/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/models/pg_checkpoint.pt b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/models/pg_checkpoint.pt new file mode 100644 index 0000000..2ea029d Binary files /dev/null and b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/models/pg_checkpoint.pt differ diff --git a/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/eval_ma_rewards.npy b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/eval_ma_rewards.npy new file mode 100644 index 0000000..a8a5243 Binary files /dev/null and b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/eval_ma_rewards.npy differ diff --git a/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/eval_rewards.npy b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/eval_rewards.npy new file mode 100644 index 0000000..a8a5243 Binary files /dev/null and b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/eval_rewards.npy differ diff --git a/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/eval_rewards_curve.png b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/eval_rewards_curve.png new file mode 100644 index 0000000..2c19fd2 Binary files /dev/null and b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/eval_rewards_curve.png differ diff --git a/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/train_ma_rewards.npy b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/train_ma_rewards.npy new file mode 100644 index 0000000..3238411 Binary files /dev/null and b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/train_ma_rewards.npy differ diff --git a/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/train_rewards.npy b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/train_rewards.npy new file mode 100644 index 0000000..3450bf8 Binary files /dev/null and b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/train_rewards.npy differ diff --git a/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/train_rewards_curve.png b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/train_rewards_curve.png new file mode 100644 index 0000000..5fee65a Binary files /dev/null and b/codes/PolicyGradient/outputs/CartPole-v0/20210505-173524/results/train_rewards_curve.png differ diff --git a/codes/PolicyGradient/task0_train.py b/codes/PolicyGradient/task0_train.py new file mode 100644 index 0000000..b6866f0 --- /dev/null +++ b/codes/PolicyGradient/task0_train.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2020-11-22 23:21:53 +LastEditor: John +LastEditTime: 2021-10-16 00:34:13 +Discription: +Environment: +''' +import sys,os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加父路径到系统路径sys.path + +import gym +import torch +import datetime +from itertools import count + +from PolicyGradient.agent import PolicyGradient +from common.plot import plot_rewards +from common.utils import save_results,make_dir + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 + +class PGConfig: + def __init__(self): + self.algo = "PolicyGradient" # 算法名称 + self.env = 'CartPole-v0' # 环境名称 + self.result_path = curr_path+"/outputs/" + self.env + \ + '/'+curr_time+'/results/' # 保存结果的路径 + self.model_path = curr_path+"/outputs/" + self.env + \ + '/'+curr_time+'/models/' # 保存模型的路径 + self.train_eps = 300 # 训练的回合数 + self.test_eps = 30 # 测试的回合数 + self.batch_size = 8 + self.lr = 0.01 # 学习率 + self.gamma = 0.99 + self.hidden_dim = 36 # dimmension of hidden layer + self.device = torch.device( + "cuda" if torch.cuda.is_available() else "cpu") # check gpu + + +def env_agent_config(cfg,seed=1): + env = gym.make(cfg.env) + env.seed(seed) + state_dim = env.observation_space.shape[0] + agent = PolicyGradient(state_dim,cfg) + return env,agent + +def train(cfg,env,agent): + print('Start to eval !') + print(f'Env:{cfg.env}, Algorithm:{cfg.algo}, Device:{cfg.device}') + state_pool = [] # 存放每batch_size个episode的state序列 + action_pool = [] + reward_pool = [] + rewards = [] + ma_rewards = [] + for i_ep in range(cfg.train_eps): + state = env.reset() + ep_reward = 0 + for _ in count(): + action = agent.choose_action(state) # 根据当前环境state选择action + next_state, reward, done, _ = env.step(action) + ep_reward += reward + if done: + reward = 0 + state_pool.append(state) + action_pool.append(float(action)) + reward_pool.append(reward) + state = next_state + if done: + print('Episode:', i_ep, ' Reward:', ep_reward) + break + if i_ep > 0 and i_ep % cfg.batch_size == 0: + agent.update(reward_pool,state_pool,action_pool) + state_pool = [] # 每个episode的state + action_pool = [] + reward_pool = [] + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append( + 0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + print('complete training!') + return rewards, ma_rewards + + +def eval(cfg,env,agent): + print('Start to eval !') + print(f'Env:{cfg.env}, Algorithm:{cfg.algo}, Device:{cfg.device}') + rewards = [] + ma_rewards = [] + for i_ep in range(cfg.test_eps): + state = env.reset() + ep_reward = 0 + for _ in count(): + action = agent.choose_action(state) # 根据当前环境state选择action + next_state, reward, done, _ = env.step(action) + ep_reward += reward + if done: + reward = 0 + state = next_state + if done: + print('Episode:', i_ep, ' Reward:', ep_reward) + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append( + 0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + print('complete evaling!') + return rewards, ma_rewards + +if __name__ == "__main__": + cfg = PGConfig() + + # train + env,agent = env_agent_config(cfg,seed=1) + rewards, ma_rewards = train(cfg, env, agent) + make_dir(cfg.result_path, cfg.model_path) + agent.save(path=cfg.model_path) + save_results(rewards, ma_rewards, tag='train', path=cfg.result_path) + plot_rewards(rewards, ma_rewards, tag="train", + algo=cfg.algo, path=cfg.result_path) + # eval + env,agent = env_agent_config(cfg,seed=10) + agent.load(path=cfg.model_path) + rewards,ma_rewards = eval(cfg,env,agent) + save_results(rewards,ma_rewards,tag='eval',path=cfg.result_path) + plot_rewards(rewards,ma_rewards,tag="eval",env=cfg.env,algo = cfg.algo,path=cfg.result_path) + diff --git a/codes/QLearning/agent.py b/codes/QLearning/agent.py new file mode 100644 index 0000000..b72de22 --- /dev/null +++ b/codes/QLearning/agent.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2020-09-11 23:03:00 +LastEditor: John +LastEditTime: 2021-12-22 10:54:57 +Discription: use defaultdict to define Q table +Environment: +''' +import numpy as np +import math +import torch +from collections import defaultdict + +class QLearning(object): + def __init__(self,state_dim, + action_dim,cfg): + self.action_dim = action_dim + self.lr = cfg.lr # 学习率 + self.gamma = cfg.gamma + self.epsilon = 0 + self.sample_count = 0 + self.epsilon_start = cfg.epsilon_start + self.epsilon_end = cfg.epsilon_end + self.epsilon_decay = cfg.epsilon_decay + self.Q_table = defaultdict(lambda: np.zeros(action_dim)) # 用嵌套字典存放状态->动作->状态-动作值(Q值)的映射,即Q表 + def choose_action(self, state): + self.sample_count += 1 + self.epsilon = self.epsilon_end + (self.epsilon_start - self.epsilon_end) * \ + math.exp(-1. * self.sample_count / self.epsilon_decay) # epsilon是会递减的,这里选择指数递减 + # e-greedy 策略 + if np.random.uniform(0, 1) > self.epsilon: + action = np.argmax(self.Q_table[str(state)]) # 选择Q(s,a)最大对应的动作 + else: + action = np.random.choice(self.action_dim) # 随机选择动作 + return action + def predict(self,state): + action = np.argmax(self.Q_table[str(state)]) + return action + def update(self, state, action, reward, next_state, done): + Q_predict = self.Q_table[str(state)][action] + if done: # 终止状态 + Q_target = reward + else: + Q_target = reward + self.gamma * np.max(self.Q_table[str(next_state)]) + self.Q_table[str(state)][action] += self.lr * (Q_target - Q_predict) + def save(self,path): + import dill + torch.save( + obj=self.Q_table, + f=path+"Qleaning_model.pkl", + pickle_module=dill + ) + print("保存模型成功!") + def load(self, path): + import dill + self.Q_table =torch.load(f=path+'Qleaning_model.pkl',pickle_module=dill) + print("加载模型成功!") \ No newline at end of file diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/models/Qleaning_model.pkl b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/models/Qleaning_model.pkl new file mode 100644 index 0000000..dc89386 Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/models/Qleaning_model.pkl differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/test_ma_rewards.npy b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/test_ma_rewards.npy new file mode 100644 index 0000000..a67d064 Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/test_ma_rewards.npy differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/test_rewards.npy b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/test_rewards.npy new file mode 100644 index 0000000..6de67e1 Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/test_rewards.npy differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/test_rewards_curve.png b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/test_rewards_curve.png new file mode 100644 index 0000000..d745634 Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/test_rewards_curve.png differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/train_ma_rewards.npy b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/train_ma_rewards.npy new file mode 100644 index 0000000..23e7c95 Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/train_ma_rewards.npy differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/train_rewards.npy b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/train_rewards.npy new file mode 100644 index 0000000..0ceb153 Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/train_rewards.npy differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/train_rewards_curve.png b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/train_rewards_curve.png new file mode 100644 index 0000000..a15bd2a Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-110223/results/train_rewards_curve.png differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/models/Qleaning_model.pkl b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/models/Qleaning_model.pkl new file mode 100644 index 0000000..c362dbd Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/models/Qleaning_model.pkl differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/test_ma_rewards.npy b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/test_ma_rewards.npy new file mode 100644 index 0000000..9bee5e4 Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/test_ma_rewards.npy differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/test_rewards.npy b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/test_rewards.npy new file mode 100644 index 0000000..8aeb5dd Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/test_rewards.npy differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/test_rewards_curve.png b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/test_rewards_curve.png new file mode 100644 index 0000000..5f3ffb5 Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/test_rewards_curve.png differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/train_ma_rewards.npy b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/train_ma_rewards.npy new file mode 100644 index 0000000..261a3d5 Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/train_ma_rewards.npy differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/train_rewards.npy b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/train_rewards.npy new file mode 100644 index 0000000..b1a0f23 Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/train_rewards.npy differ diff --git a/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/train_rewards_curve.png b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/train_rewards_curve.png new file mode 100644 index 0000000..9a9d6ad Binary files /dev/null and b/codes/QLearning/outputs/CliffWalking-v0/20211222-111747/results/train_rewards_curve.png differ diff --git a/codes/QLearning/task0.ipynb b/codes/QLearning/task0.ipynb new file mode 100644 index 0000000..dc447ce --- /dev/null +++ b/codes/QLearning/task0.ipynb @@ -0,0 +1,386 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from pathlib import Path\n", + "curr_path = str(Path().absolute())\n", + "parent_path = str(Path().absolute().parent)\n", + "sys.path.append(parent_path) # 添加路径到系统路径\n", + "\n", + "import gym\n", + "import torch\n", + "import math\n", + "import datetime\n", + "import numpy as np\n", + "from collections import defaultdict\n", + "from envs.gridworld_env import CliffWalkingWapper\n", + "from QLearning.agent import QLearning\n", + "from common.utils import plot_rewards\n", + "from common.utils import save_results,make_dir\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## QLearning算法" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "class QLearning(object):\n", + " def __init__(self,state_dim,\n", + " action_dim,cfg):\n", + " self.action_dim = action_dim \n", + " self.lr = cfg.lr # 学习率\n", + " self.gamma = cfg.gamma \n", + " self.epsilon = 0 \n", + " self.sample_count = 0 \n", + " self.epsilon_start = cfg.epsilon_start\n", + " self.epsilon_end = cfg.epsilon_end\n", + " self.epsilon_decay = cfg.epsilon_decay\n", + " self.Q_table = defaultdict(lambda: np.zeros(action_dim)) # 用嵌套字典存放状态->动作->状态-动作值(Q值)的映射,即Q表\n", + " def choose_action(self, state):\n", + " self.sample_count += 1\n", + " self.epsilon = self.epsilon_end + (self.epsilon_start - self.epsilon_end) * \\\n", + " math.exp(-1. * self.sample_count / self.epsilon_decay) # epsilon是会递减的,这里选择指数递减\n", + " # e-greedy 策略\n", + " if np.random.uniform(0, 1) > self.epsilon:\n", + " action = np.argmax(self.Q_table[str(state)]) # 选择Q(s,a)最大对应的动作\n", + " else:\n", + " action = np.random.choice(self.action_dim) # 随机选择动作\n", + " return action\n", + " def predict(self,state):\n", + " action = np.argmax(self.Q_table[str(state)])\n", + " return action\n", + " def update(self, state, action, reward, next_state, done):\n", + " Q_predict = self.Q_table[str(state)][action] \n", + " if done: # 终止状态\n", + " Q_target = reward \n", + " else:\n", + " Q_target = reward + self.gamma * np.max(self.Q_table[str(next_state)]) \n", + " self.Q_table[str(state)][action] += self.lr * (Q_target - Q_predict)\n", + " def save(self,path):\n", + " import dill\n", + " torch.save(\n", + " obj=self.Q_table,\n", + " f=path+\"Qleaning_model.pkl\",\n", + " pickle_module=dill\n", + " )\n", + " print(\"保存模型成功!\")\n", + " def load(self, path):\n", + " import dill\n", + " self.Q_table =torch.load(f=path+'Qleaning_model.pkl',pickle_module=dill)\n", + " print(\"加载模型成功!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 训练" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "def train(cfg,env,agent):\n", + " print('开始训练!')\n", + " print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}')\n", + " rewards = [] # 记录奖励\n", + " ma_rewards = [] # 记录滑动平均奖励\n", + " for i_ep in range(cfg.train_eps):\n", + " ep_reward = 0 # 记录每个episode的reward\n", + " state = env.reset() # 重置环境, 重新开一局(即开始新的一个episode)\n", + " while True:\n", + " action = agent.choose_action(state) # 根据算法选择一个动作\n", + " next_state, reward, done, _ = env.step(action) # 与环境进行一次动作交互\n", + " agent.update(state, action, reward, next_state, done) # Q-learning算法更新\n", + " state = next_state # 存储上一个观察值\n", + " ep_reward += reward\n", + " if done:\n", + " break\n", + " rewards.append(ep_reward)\n", + " if ma_rewards:\n", + " ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1)\n", + " else:\n", + " ma_rewards.append(ep_reward)\n", + " if (i_ep+1)%20 == 0: \n", + " print('回合:{}/{}, 奖励:{}'.format(i_ep+1, cfg.train_eps, ep_reward))\n", + " print('完成训练!')\n", + " return rewards,ma_rewards" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 测试" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "def test(cfg,env,agent):\n", + " # env = gym.make(\"FrozenLake-v0\", is_slippery=False) # 0 left, 1 down, 2 right, 3 up\n", + " # env = FrozenLakeWapper(env)\n", + " print('开始测试!')\n", + " print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}')\n", + " # 由于测试不需要使用epsilon-greedy策略,所以相应的值设置为0\n", + " cfg.epsilon_start = 0.0 # e-greedy策略中初始epsilon\n", + " cfg.epsilon_end = 0.0 # e-greedy策略中的终止epsilon\n", + " rewards = [] # 记录所有回合的奖励\n", + " ma_rewards = [] # 记录所有回合的滑动平均奖励\n", + " rewards = [] # 记录所有episode的reward\n", + " ma_rewards = [] # 滑动平均的reward\n", + " for i_ep in range(cfg.test_eps):\n", + " ep_reward = 0 # 记录每个episode的reward\n", + " state = env.reset() # 重置环境, 重新开一局(即开始新的一个episode)\n", + " while True:\n", + " action = agent.predict(state) # 根据算法选择一个动作\n", + " next_state, reward, done, _ = env.step(action) # 与环境进行一个交互\n", + " state = next_state # 存储上一个观察值\n", + " ep_reward += reward\n", + " if done:\n", + " break\n", + " rewards.append(ep_reward)\n", + " if ma_rewards:\n", + " ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1)\n", + " else:\n", + " ma_rewards.append(ep_reward)\n", + " print(f\"回合:{i_ep+1}/{cfg.test_eps},奖励:{ep_reward:.1f}\")\n", + " print('完成测试!')\n", + " return rewards,ma_rewards" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 设置参数" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "curr_time = datetime.datetime.now().strftime(\"%Y%m%d-%H%M%S\") # 获取当前时间\n", + "algo_name = 'Q-learning' # 算法名称\n", + "env_name = 'CliffWalking-v0' # 环境名称\n", + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\") # 检测GPU\n", + "class QlearningConfig:\n", + " '''训练相关参数'''\n", + " def __init__(self):\n", + " self.algo_name = algo_name # 算法名称\n", + " self.env_name = env_name # 环境名称\n", + " self.device = device # 检测GPU\n", + " self.train_eps = 400 # 训练的回合数\n", + " self.test_eps = 20 # 测试的回合数\n", + " self.gamma = 0.9 # reward的衰减率\n", + " self.epsilon_start = 0.95 # e-greedy策略中初始epsilon\n", + " self.epsilon_end = 0.01 # e-greedy策略中的终止epsilon\n", + " self.epsilon_decay = 300 # e-greedy策略中epsilon的衰减率\n", + " self.lr = 0.1 # 学习率 \n", + "class PlotConfig:\n", + " ''' 绘图相关参数设置\n", + " '''\n", + "\n", + " def __init__(self) -> None:\n", + " self.algo_name = algo_name # 算法名称\n", + " self.env_name = env_name # 环境名称\n", + " self.device = device # 检测GPU\n", + " self.result_path = curr_path + \"/outputs/\" + self.env_name + \\\n", + " '/' + curr_time + '/results/' # 保存结果的路径\n", + " self.model_path = curr_path + \"/outputs/\" + self.env_name + \\\n", + " '/' + curr_time + '/models/' # 保存模型的路径\n", + " self.save = True # 是否保存图片" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 创建环境和智能体" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "def env_agent_config(cfg,seed=1):\n", + " '''创建环境和智能体\n", + " Args:\n", + " cfg ([type]): [description]\n", + " seed (int, optional): 随机种子. Defaults to 1.\n", + " Returns:\n", + " env [type]: 环境\n", + " agent : 智能体\n", + " ''' \n", + " env = gym.make(cfg.env_name) \n", + " env = CliffWalkingWapper(env)\n", + " env.seed(seed) # 设置随机种子\n", + " state_dim = env.observation_space.n # 状态维度\n", + " action_dim = env.action_space.n # 动作维度\n", + " agent = QLearning(state_dim,action_dim,cfg)\n", + " return env,agent" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 执行训练并输出结果" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "开始训练!\n", + "环境:CliffWalking-v0, 算法:Q-learning, 设备:cuda\n", + "回合:20/400, 奖励:-82\n", + "回合:40/400, 奖励:-51\n", + "回合:60/400, 奖励:-50\n", + "回合:80/400, 奖励:-53\n", + "回合:100/400, 奖励:-21\n", + "回合:120/400, 奖励:-35\n", + "回合:140/400, 奖励:-44\n", + "回合:160/400, 奖励:-28\n", + "回合:180/400, 奖励:-28\n", + "回合:200/400, 奖励:-17\n", + "回合:220/400, 奖励:-18\n", + "回合:240/400, 奖励:-22\n", + "回合:260/400, 奖励:-19\n", + "回合:280/400, 奖励:-15\n", + "回合:300/400, 奖励:-14\n", + "回合:320/400, 奖励:-13\n", + "回合:340/400, 奖励:-13\n", + "回合:360/400, 奖励:-13\n", + "回合:380/400, 奖励:-13\n", + "回合:400/400, 奖励:-13\n", + "完成训练!\n", + "保存模型成功!\n", + "结果保存完毕!\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "加载模型成功!\n", + "开始测试!\n", + "环境:CliffWalking-v0, 算法:Q-learning, 设备:cuda\n", + "回合:1/20,奖励:-13.0\n", + "回合:2/20,奖励:-13.0\n", + "回合:3/20,奖励:-13.0\n", + "回合:4/20,奖励:-13.0\n", + "回合:5/20,奖励:-13.0\n", + "回合:6/20,奖励:-13.0\n", + "回合:7/20,奖励:-13.0\n", + "回合:8/20,奖励:-13.0\n", + "回合:9/20,奖励:-13.0\n", + "回合:10/20,奖励:-13.0\n", + "回合:11/20,奖励:-13.0\n", + "回合:12/20,奖励:-13.0\n", + "回合:13/20,奖励:-13.0\n", + "回合:14/20,奖励:-13.0\n", + "回合:15/20,奖励:-13.0\n", + "回合:16/20,奖励:-13.0\n", + "回合:17/20,奖励:-13.0\n", + "回合:18/20,奖励:-13.0\n", + "回合:19/20,奖励:-13.0\n", + "回合:20/20,奖励:-13.0\n", + "完成测试!\n", + "结果保存完毕!\n" + ] + }, + { + "data": { + "image/png": "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sD5pWJ80z2t9crkMDRpYFD/dlJ2npqYaHqelpSElJQWOjo5F2mVnZ2PMmDF4++23MWTIEFOWREREZST5JHVcXByWLl2K+/fvIyEhAdHR0Vi/fj0cHR0RFRWF1NRUmJmZQaFQYN68eWjRogUAYPny5bC1tcXw4cMRHR2N69evY8eOHdixYwcAYOTIkRg8eLDU8oiIqIJKPQfxd8JDTNWH9UnD+qRhfRVTrYeYiIjo74sBQUREohgQREQkigFBRESiGBBERCSKAUFERKIYEEREJIoBQUREohgQREQkigFBRESiGBBERCSKAUFERKIYEEREJIoBQUREohgQREQkigFBRESiGBBERCSKAUFERKIYEEREJIoBQUREohgQREQkigFBRESiJAdETEwM1Go1nJ2dsWXLFqNpq1evhlqtRkBAAPz9/bF///4Sl1VQUABfX18MGjRIallERCSRmdQFODk5ITIyEtHR0UWmBQUFYdKkSQCAjIwM+Pj4wMPDA1ZWVqLLioyMRPv27XH58mWpZRERkUSS9yBatmwJR0dHyOVFF2VpaWl4nJ+fD5lMBr1eL7qc06dP4/r16/D395daEhERVQLJexCl2bZtGzZt2oT09HQsWrQI1tbWRdrk5+dj0aJFWL16Na5fv17hvho0sJBQKWBjY1l6o2rE+qRhfdKwPmme9/rElBoQgYGB0Gg0otOSkpKgUChKnH/48OEYPnw4rly5gpCQEHTp0qVISCxduhRvvfUW7OzsJAVEVlYu9HqhQvPa2Fji1q2cCvdtaqxPGtYnDeuT5nmtTy6XlfjFutSA2L17d6UU0qpVK9ja2uLkyZPo37+/0bQzZ84gMTERq1atQkFBAe7duwe1Wo3Y2NhK6ZuIiMrPpIeYUlNT4ejoCABIS0tDSkqK4fnTng6CEydOICIiArt27TJlaUREVArJAREXF4elS5fi/v37SEhIQHR0NNavXw9HR0dERUUhNTUVZmZmUCgUmDdvHlq0aAEAWL58OWxtbTF8+HDJK0FERJVPJghCxQ7aP4d4DqL6sD5pWJ80rK9iSjsHwf9JTUREohgQREQkigFBRESiGBBERCSKAUFERKIYEEREJIoBQUREohgQREQkigFBRESiGBBERCSKAUFERKIYEEREJIoBQUREohgQREQkigFBRESiGBBERCSKAUFERKIYEEREJIoBQURE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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cfg = QlearningConfig()\n", + "plot_cfg = PlotConfig()\n", + "# 训练\n", + "env, agent = env_agent_config(cfg, seed=1)\n", + "rewards, ma_rewards = train(cfg, env, agent)\n", + "make_dir(plot_cfg.result_path, plot_cfg.model_path) # 创建保存结果和模型路径的文件夹\n", + "agent.save(path=plot_cfg.model_path) # 保存模型\n", + "save_results(rewards, ma_rewards, tag='train',\n", + " path=plot_cfg.result_path) # 保存结果\n", + "plot_rewards(rewards, ma_rewards, plot_cfg, tag=\"train\") # 画出结果\n", + "# 测试\n", + "env, agent = env_agent_config(cfg, seed=10)\n", + "agent.load(path=plot_cfg.model_path) # 导入模型\n", + "rewards, ma_rewards = test(cfg, env, agent)\n", + "save_results(rewards, ma_rewards, tag='test', path=plot_cfg.result_path) # 保存结果\n", + "plot_rewards(rewards, ma_rewards, plot_cfg, tag=\"test\") # 画出结果" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "fbea1422c2cf61ed9c0cfc03f38f71cc9083cc288606edc4170b5309b352ce27" + }, + "kernelspec": { + "display_name": "Python 3.7.11 64-bit ('py37': conda)", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + }, + "orig_nbformat": 2 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/codes/QLearning/task0.py b/codes/QLearning/task0.py new file mode 100644 index 0000000..59a1668 --- /dev/null +++ b/codes/QLearning/task0.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2020-09-11 23:03:00 +LastEditor: John +LastEditTime: 2021-12-22 11:13:23 +Discription: +Environment: +''' +import sys +import os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import gym +import torch +import datetime + +from envs.gridworld_env import CliffWalkingWapper +from QLearning.agent import QLearning +from QLearning.train import train,test +from common.utils import plot_rewards,plot_rewards_cn +from common.utils import save_results,make_dir + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 +algo_name = 'Q-learning' # 算法名称 +env_name = 'CliffWalking-v0' # 环境名称 +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU +class QlearningConfig: + '''训练相关参数''' + def __init__(self): + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.device = device # 检测GPU + self.train_eps = 400 # 训练的回合数 + self.test_eps = 30 # 测试的回合数 + self.gamma = 0.9 # reward的衰减率 + self.epsilon_start = 0.95 # e-greedy策略中初始epsilon + self.epsilon_end = 0.01 # e-greedy策略中的终止epsilon + self.epsilon_decay = 300 # e-greedy策略中epsilon的衰减率 + self.lr = 0.1 # 学习率 +class PlotConfig: + ''' 绘图相关参数设置 + ''' + + def __init__(self) -> None: + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.device = device # 检测GPU + self.result_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/results/' # 保存结果的路径 + self.model_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + +def env_agent_config(cfg,seed=1): + '''创建环境和智能体 + Args: + cfg ([type]): [description] + seed (int, optional): 随机种子. Defaults to 1. + Returns: + env [type]: 环境 + agent : 智能体 + ''' + env = gym.make(cfg.env_name) + env = CliffWalkingWapper(env) + env.seed(seed) # 设置随机种子 + state_dim = env.observation_space.n # 状态维度 + action_dim = env.action_space.n # 动作维度 + agent = QLearning(state_dim,action_dim,cfg) + return env,agent + +cfg = QlearningConfig() +plot_cfg = PlotConfig() +# 训练 +env, agent = env_agent_config(cfg, seed=1) +rewards, ma_rewards = train(cfg, env, agent) +make_dir(plot_cfg.result_path, plot_cfg.model_path) # 创建保存结果和模型路径的文件夹 +agent.save(path=plot_cfg.model_path) # 保存模型 +save_results(rewards, ma_rewards, tag='train', + path=plot_cfg.result_path) # 保存结果 +plot_rewards(rewards, ma_rewards, plot_cfg, tag="train") # 画出结果 +# 测试 +env, agent = env_agent_config(cfg, seed=10) +agent.load(path=plot_cfg.model_path) # 导入模型 +rewards, ma_rewards = test(cfg, env, agent) +save_results(rewards, ma_rewards, tag='test', path=plot_cfg.result_path) # 保存结果 +plot_rewards(rewards, ma_rewards, plot_cfg, tag="test") # 画出结果 + + diff --git a/codes/QLearning/train.py b/codes/QLearning/train.py new file mode 100644 index 0000000..2c4aa09 --- /dev/null +++ b/codes/QLearning/train.py @@ -0,0 +1,50 @@ +def train(cfg,env,agent): + print('开始训练!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + rewards = [] # 记录奖励 + ma_rewards = [] # 记录滑动平均奖励 + for i_ep in range(cfg.train_eps): + ep_reward = 0 # 记录每个回合的奖励 + state = env.reset() # 重置环境,即开始新的回合 + while True: + action = agent.choose_action(state) # 根据算法选择一个动作 + next_state, reward, done, _ = env.step(action) # 与环境进行一次动作交互 + agent.update(state, action, reward, next_state, done) # Q学习算法更新 + state = next_state # 更新状态 + ep_reward += reward + if done: + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1) + else: + ma_rewards.append(ep_reward) + print("回合数:{}/{},奖励{:.1f}".format(i_ep+1, cfg.train_eps,ep_reward)) + print('完成训练!') + return rewards,ma_rewards + +def test(cfg,env,agent): + print('开始测试!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + for item in agent.Q_table.items(): + print(item) + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 滑动平均的奖励 + for i_ep in range(cfg.test_eps): + ep_reward = 0 # 记录每个episode的reward + state = env.reset() # 重置环境, 重新开一局(即开始新的一个回合) + while True: + action = agent.predict(state) # 根据算法选择一个动作 + next_state, reward, done, _ = env.step(action) # 与环境进行一个交互 + state = next_state # 更新状态 + ep_reward += reward + if done: + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1) + else: + ma_rewards.append(ep_reward) + print(f"回合数:{i_ep+1}/{cfg.test_eps}, 奖励:{ep_reward:.1f}") + print('完成测试!') + return rewards,ma_rewards \ No newline at end of file diff --git a/codes/README.md b/codes/README.md new file mode 100644 index 0000000..3896fbb --- /dev/null +++ b/codes/README.md @@ -0,0 +1,51 @@ +## 写在前面 + +本项目用于学习RL基础算法,尽量做到: **注释详细**,**结构清晰**。 + +代码结构主要分为以下几个脚本: + +* ```model.py``` 强化学习算法的基本模型,比如神经网络,actor,critic等 +* ```memory.py``` 保存Replay Buffer,用于off-policy +* ```plot.py``` 利用matplotlib或seaborn绘制rewards图,包括滑动平均的reward,结果保存在result文件夹中 +* ```env.py``` 用于构建强化学习环境,也可以重新自定义环境,比如给action加noise +* ```agent.py``` RL核心算法,比如dqn等,主要包含update和choose_action两个方法, +* ```train.py``` 保存用于训练和测试的函数 + +其中```model.py```,```memory.py```,```plot.py``` 由于不同算法都会用到,所以放入```common```文件夹中。 + +**注意:新版本中将```model```,```memory```相关内容全部放到了```agent.py```里面,```plot```放到了```common.utils```中。** +## 运行环境 + +python 3.7、pytorch 1.6.0-1.8.1、gym 0.21.0 + +## 使用说明 + +直接运行带有```train```的py文件或ipynb文件会进行训练默认的任务; +也可以运行带有```task```的py文件训练不同的任务 + +## 内容导航 + +| 算法名称 | 相关论文材料 | 环境 | 备注 | +| :--------------------------------------: | :----------------------------------------------------------: | ----------------------------------------- | :--------------------------------: | +| [On-Policy First-Visit MC](./MonteCarlo) | [medium blog](https://medium.com/analytics-vidhya/monte-carlo-methods-in-reinforcement-learning-part-1-on-policy-methods-1f004d59686a) | [Racetrack](./envs/racetrack_env.md) | | +| [Q-Learning](./QLearning) | [towardsdatascience blog](https://towardsdatascience.com/simple-reinforcement-learning-q-learning-fcddc4b6fe56),[q learning paper](https://ieeexplore.ieee.org/document/8836506) | [CliffWalking-v0](./envs/gym_info.md) | | +| [Sarsa](./Sarsa) | [geeksforgeeks blog](https://www.geeksforgeeks.org/sarsa-reinforcement-learning/) | [Racetrack](./envs/racetrack_env.md) | | +| [DQN](./DQN) | [DQN Paper](https://www.cs.toronto.edu/~vmnih/docs/dqn.pdf),[Nature DQN Paper](https://www.nature.com/articles/nature14236) | [CartPole-v0](./envs/gym_info.md) | | +| [DQN-cnn](./DQN_cnn) | [DQN Paper](https://www.cs.toronto.edu/~vmnih/docs/dqn.pdf) | [CartPole-v0](./envs/gym_info.md) | 与DQN相比使用了CNN而不是全链接网络 | +| [DoubleDQN](./DoubleDQN) | [DoubleDQN Paper](https://arxiv.org/abs/1509.06461) | [CartPole-v0](./envs/gym_info.md) | | +| [Hierarchical DQN](HierarchicalDQN) | [H-DQN Paper](https://arxiv.org/abs/1604.06057) | [CartPole-v0](./envs/gym_info.md) | | +| [PolicyGradient](./PolicyGradient) | [Lil'log](https://lilianweng.github.io/lil-log/2018/04/08/policy-gradient-algorithms.html) | [CartPole-v0](./envs/gym_info.md) | | +| [A2C](./A2C) | [A3C Paper](https://arxiv.org/abs/1602.01783) | [CartPole-v0](./envs/gym_info.md) | | +| [SAC](./SoftActorCritic) | [SAC Paper](https://arxiv.org/abs/1801.01290) | [Pendulum-v0](./envs/gym_info.md) | | +| [PPO](./PPO) | [PPO paper](https://arxiv.org/abs/1707.06347) | [CartPole-v0](./envs/gym_info.md) | | +| [DDPG](./DDPG) | [DDPG Paper](https://arxiv.org/abs/1509.02971) | [Pendulum-v0](./envs/gym_info.md) | | +| [TD3](./TD3) | [TD3 Paper](https://arxiv.org/abs/1802.09477) | [HalfCheetah-v2]((./envs/mujoco_info.md)) | | + + +## Refs + +[RL-Adventure-2](https://github.com/higgsfield/RL-Adventure-2) + +[RL-Adventure](https://github.com/higgsfield/RL-Adventure) + +[Google 开源项目风格指南——中文版](https://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/python_style_rules/#comments) \ No newline at end of file diff --git a/codes/Sarsa/README.md b/codes/Sarsa/README.md new file mode 100644 index 0000000..5258664 --- /dev/null +++ b/codes/Sarsa/README.md @@ -0,0 +1,19 @@ +# Sarsa + +## 使用说明 + +运行```main.py```即可 + +## 环境说明 + +见[环境说明](https://github.com/JohnJim0816/reinforcement-learning-tutorials/blob/master/env_info.md)中的The Racetrack + +## 算法伪代码 + +![sarsa_algo](assets/sarsa_algo.png) + +## 其他说明 + +### 与Q-learning区别 + +算法上区别很小,只在更新公式上,但Q-learning是Off-policy,而Sarsa是On-policy,可参考[知乎:强化学习中sarsa算法是不是比q-learning算法收敛速度更慢?](https://www.zhihu.com/question/268461866) \ No newline at end of file diff --git a/codes/Sarsa/agent.py b/codes/Sarsa/agent.py new file mode 100644 index 0000000..020f6da --- /dev/null +++ b/codes/Sarsa/agent.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-12 16:58:16 +LastEditor: John +LastEditTime: 2021-03-13 11:02:50 +Discription: +Environment: +''' +import numpy as np +from collections import defaultdict +import torch +class Sarsa(object): + def __init__(self, + action_dim,sarsa_cfg,): + self.action_dim = action_dim # number of actions + self.lr = sarsa_cfg.lr # learning rate + self.gamma = sarsa_cfg.gamma + self.epsilon = sarsa_cfg.epsilon + self.Q = defaultdict(lambda: np.zeros(action_dim)) + # self.Q = np.zeros((state_dim, action_dim)) # Q表 + def choose_action(self, state): + best_action = np.argmax(self.Q[state]) + # action = best_action + action_probs = np.ones(self.action_dim, dtype=float) * self.epsilon / self.action_dim + action_probs[best_action] += (1.0 - self.epsilon) + action = np.random.choice(np.arange(len(action_probs)), p=action_probs) + return action + + def update(self, state, action, reward, next_state, next_action,done): + Q_predict = self.Q[state][action] + if done: + Q_target = reward # terminal state + else: + Q_target = reward + self.gamma * self.Q[next_state][next_action] + self.Q[state][action] += self.lr * (Q_target - Q_predict) + def save(self,path): + '''把 Q表格 的数据保存到文件中 + ''' + import dill + torch.save( + obj=self.Q, + f=path+"sarsa_model.pkl", + pickle_module=dill + ) + def load(self, path): + '''从文件中读取数据到 Q表格 + ''' + import dill + self.Q =torch.load(f=path+'sarsa_model.pkl',pickle_module=dill) \ No newline at end of file diff --git a/codes/Sarsa/assets/sarsa_algo.png b/codes/Sarsa/assets/sarsa_algo.png new file mode 100644 index 0000000..0abef7a Binary files /dev/null and b/codes/Sarsa/assets/sarsa_algo.png differ diff --git a/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/models/sarsa_model.pkl b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/models/sarsa_model.pkl new file mode 100644 index 0000000..ff25fd5 Binary files /dev/null and b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/models/sarsa_model.pkl differ diff --git a/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/eval_ma_rewards.npy b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/eval_ma_rewards.npy new file mode 100644 index 0000000..d7d62e3 Binary files /dev/null and b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/eval_ma_rewards.npy differ diff --git a/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/eval_rewards.npy b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/eval_rewards.npy new file mode 100644 index 0000000..de0a816 Binary files /dev/null and b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/eval_rewards.npy differ diff --git a/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/eval_rewards_curve.png b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/eval_rewards_curve.png new file mode 100644 index 0000000..3de2db7 Binary files /dev/null and b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/eval_rewards_curve.png differ diff --git a/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/train_ma_rewards.npy b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/train_ma_rewards.npy new file mode 100644 index 0000000..3f9bf83 Binary files /dev/null and b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/train_ma_rewards.npy differ diff --git a/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/train_rewards.npy b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/train_rewards.npy new file mode 100644 index 0000000..e0fd7e9 Binary files /dev/null and b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/train_rewards.npy differ diff --git a/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/train_rewards_curve.png b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/train_rewards_curve.png new file mode 100644 index 0000000..0a8cd37 Binary files /dev/null and b/codes/Sarsa/outputs/CliffWalking-v0/20210506-171245/results/train_rewards_curve.png differ diff --git a/codes/Sarsa/task0_train.py b/codes/Sarsa/task0_train.py new file mode 100644 index 0000000..e477afa --- /dev/null +++ b/codes/Sarsa/task0_train.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-11 17:59:16 +LastEditor: John +LastEditTime: 2021-05-06 17:12:37 +Discription: +Environment: +''' +import sys,os +curr_path = os.path.dirname(__file__) +parent_path = os.path.dirname(curr_path) +sys.path.append(parent_path) # add current terminal path to sys.path + +import datetime +from envs.racetrack_env import RacetrackEnv +from Sarsa.agent import Sarsa +from common.plot import plot_rewards +from common.utils import save_results,make_dir + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # obtain current time + +class SarsaConfig: + ''' parameters for Sarsa + ''' + def __init__(self): + self.algo = 'Qlearning' + self.env = 'CliffWalking-v0' # 0 up, 1 right, 2 down, 3 left + self.result_path = curr_path+"/outputs/" +self.env+'/'+curr_time+'/results/' # path to save results + self.model_path = curr_path+"/outputs/" +self.env+'/'+curr_time+'/models/' # path to save models + self.train_eps = 200 + self.test_eps = 50 + self.epsilon = 0.15 # epsilon: The probability to select a random action . + self.gamma = 0.9 # gamma: Gamma discount factor. + self.lr = 0.2 # learning rate: step size parameter + self.n_steps = 2000 + +def env_agent_config(cfg,seed=1): + env = RacetrackEnv() + action_dim=9 + agent = Sarsa(action_dim,cfg) + return env,agent + +def train(cfg,env,agent): + rewards = [] + ma_rewards = [] + for i_episode in range(cfg.train_eps): + # Print out which episode we're on, useful for debugging. + # Generate an episode. + # An episode is an array of (state, action, reward) tuples + state = env.reset() + ep_reward = 0 + while True: + # for t in range(cfg.n_steps): + action = agent.choose_action(state) + next_state, reward, done = env.step(action) + ep_reward+=reward + next_action = agent.choose_action(next_state) + agent.update(state, action, reward, next_state, next_action,done) + state = next_state + if done: + break + if ma_rewards: + ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1) + else: + ma_rewards.append(ep_reward) + rewards.append(ep_reward) + if (i_episode+1)%10==0: + print("Episode:{}/{}: Reward:{}".format(i_episode+1, cfg.train_eps,ep_reward)) + return rewards,ma_rewards + +def eval(cfg,env,agent): + rewards = [] + ma_rewards = [] + for i_episode in range(cfg.test_eps): + # Print out which episode we're on, useful for debugging. + # Generate an episode. + # An episode is an array of (state, action, reward) tuples + state = env.reset() + ep_reward = 0 + while True: + # for t in range(cfg.n_steps): + action = agent.choose_action(state) + next_state, reward, done = env.step(action) + ep_reward+=reward + state = next_state + if done: + break + if ma_rewards: + ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1) + else: + ma_rewards.append(ep_reward) + rewards.append(ep_reward) + if (i_episode+1)%10==0: + print("Episode:{}/{}: Reward:{}".format(i_episode+1, cfg.test_eps,ep_reward)) + print('Complete evaling!') + return rewards,ma_rewards + +if __name__ == "__main__": + cfg = SarsaConfig() + env,agent = env_agent_config(cfg,seed=1) + rewards,ma_rewards = train(cfg,env,agent) + make_dir(cfg.result_path,cfg.model_path) + agent.save(path=cfg.model_path) + save_results(rewards,ma_rewards,tag='train',path=cfg.result_path) + plot_rewards(rewards,ma_rewards,tag="train",env=cfg.env,algo = cfg.algo,path=cfg.result_path) + + env,agent = env_agent_config(cfg,seed=10) + agent.load(path=cfg.model_path) + rewards,ma_rewards = eval(cfg,env,agent) + save_results(rewards,ma_rewards,tag='eval',path=cfg.result_path) + plot_rewards(rewards,ma_rewards,tag="eval",env=cfg.env,algo = cfg.algo,path=cfg.result_path) + + + diff --git a/codes/SoftActorCritic/env_wrapper.py b/codes/SoftActorCritic/env_wrapper.py new file mode 100644 index 0000000..dfe1c4d --- /dev/null +++ b/codes/SoftActorCritic/env_wrapper.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: JiangJi +Email: johnjim0816@gmail.com +Date: 2021-04-29 12:52:11 +LastEditor: JiangJi +LastEditTime: 2021-12-22 15:36:36 +Discription: +Environment: +''' +import gym +import numpy as np + +class NormalizedActions(gym.ActionWrapper): + def action(self, action): + low = self.action_space.low + high = self.action_space.high + + action = low + (action + 1.0) * 0.5 * (high - low) + action = np.clip(action, low, high) + + return action + + def reverse_action(self, action): + low = self.action_space.low + high = self.action_space.high + action = 2 * (action - low) / (high - low) - 1 + action = np.clip(action, low, high) + return action \ No newline at end of file diff --git a/codes/SoftActorCritic/model.py b/codes/SoftActorCritic/model.py new file mode 100644 index 0000000..85bbfcd --- /dev/null +++ b/codes/SoftActorCritic/model.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: JiangJi +Email: johnjim0816@gmail.com +Date: 2021-04-29 12:53:58 +LastEditor: JiangJi +LastEditTime: 2021-11-19 18:04:19 +Discription: +Environment: +''' +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.distributions import Normal + +device=torch.device("cuda" if torch.cuda.is_available() else "cpu") + +class ValueNet(nn.Module): + def __init__(self, state_dim, hidden_dim, init_w=3e-3): + super(ValueNet, self).__init__() + + self.linear1 = nn.Linear(state_dim, hidden_dim) + self.linear2 = nn.Linear(hidden_dim, hidden_dim) + self.linear3 = nn.Linear(hidden_dim, 1) + + self.linear3.weight.data.uniform_(-init_w, init_w) + self.linear3.bias.data.uniform_(-init_w, init_w) + + def forward(self, state): + x = F.relu(self.linear1(state)) + x = F.relu(self.linear2(x)) + x = self.linear3(x) + return x + + +class SoftQNet(nn.Module): + def __init__(self, state_dim, action_dim, hidden_dim, init_w=3e-3): + super(SoftQNet, self).__init__() + + self.linear1 = nn.Linear(state_dim + action_dim, hidden_dim) + self.linear2 = nn.Linear(hidden_dim, hidden_dim) + self.linear3 = nn.Linear(hidden_dim, 1) + + self.linear3.weight.data.uniform_(-init_w, init_w) + self.linear3.bias.data.uniform_(-init_w, init_w) + + def forward(self, state, action): + x = torch.cat([state, action], 1) + x = F.relu(self.linear1(x)) + x = F.relu(self.linear2(x)) + x = self.linear3(x) + return x + + +class PolicyNet(nn.Module): + def __init__(self, state_dim, action_dim, hidden_dim, init_w=3e-3, log_std_min=-20, log_std_max=2): + super(PolicyNet, self).__init__() + + self.log_std_min = log_std_min + self.log_std_max = log_std_max + + self.linear1 = nn.Linear(state_dim, hidden_dim) + self.linear2 = nn.Linear(hidden_dim, hidden_dim) + + self.mean_linear = nn.Linear(hidden_dim, action_dim) + self.mean_linear.weight.data.uniform_(-init_w, init_w) + self.mean_linear.bias.data.uniform_(-init_w, init_w) + + self.log_std_linear = nn.Linear(hidden_dim, action_dim) + self.log_std_linear.weight.data.uniform_(-init_w, init_w) + self.log_std_linear.bias.data.uniform_(-init_w, init_w) + + def forward(self, state): + x = F.relu(self.linear1(state)) + x = F.relu(self.linear2(x)) + + mean = self.mean_linear(x) + log_std = self.log_std_linear(x) + log_std = torch.clamp(log_std, self.log_std_min, self.log_std_max) + + return mean, log_std + + def evaluate(self, state, epsilon=1e-6): + mean, log_std = self.forward(state) + std = log_std.exp() + + normal = Normal(mean, std) + z = normal.sample() + action = torch.tanh(z) + + log_prob = normal.log_prob(z) - torch.log(1 - action.pow(2) + epsilon) + log_prob = log_prob.sum(-1, keepdim=True) + + return action, log_prob, z, mean, log_std + + + def get_action(self, state): + state = torch.FloatTensor(state).unsqueeze(0).to(device) + mean, log_std = self.forward(state) + std = log_std.exp() + + normal = Normal(mean, std) + z = normal.sample() + action = torch.tanh(z) + + action = action.detach().cpu().numpy() + return action[0] \ No newline at end of file diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_policy b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_policy new file mode 100644 index 0000000..9ae4e7b Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_policy differ diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_policy_optimizer b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_policy_optimizer new file mode 100644 index 0000000..49c0d2a Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_policy_optimizer differ diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_soft_q b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_soft_q new file mode 100644 index 0000000..3ff692f Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_soft_q differ diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_soft_q_optimizer b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_soft_q_optimizer new file mode 100644 index 0000000..73be931 Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_soft_q_optimizer differ diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_value b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_value new file mode 100644 index 0000000..853ac6f Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_value differ diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_value_optimizer b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_value_optimizer new file mode 100644 index 0000000..79410e4 Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/models/sac_value_optimizer differ diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/test_ma_rewards.npy b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/test_ma_rewards.npy new file mode 100644 index 0000000..eca3369 Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/test_ma_rewards.npy differ diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/test_rewards.npy b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/test_rewards.npy new file mode 100644 index 0000000..09edb0e Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/test_rewards.npy differ diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/test_rewards_curve.png b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/test_rewards_curve.png new file mode 100644 index 0000000..5cc6e1d Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/test_rewards_curve.png differ diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/train_ma_rewards.npy b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/train_ma_rewards.npy new file mode 100644 index 0000000..3e1feac Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/train_ma_rewards.npy differ diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/train_rewards.npy b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/train_rewards.npy new file mode 100644 index 0000000..1c77a83 Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/train_rewards.npy differ diff --git a/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/train_rewards_curve.png b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/train_rewards_curve.png new file mode 100644 index 0000000..3e4c8aa Binary files /dev/null and b/codes/SoftActorCritic/outputs/Pendulum-v1/20211222-162722/results/train_rewards_curve.png differ diff --git a/codes/SoftActorCritic/sac.py b/codes/SoftActorCritic/sac.py new file mode 100644 index 0000000..d565db5 --- /dev/null +++ b/codes/SoftActorCritic/sac.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: JiangJi +Email: johnjim0816@gmail.com +Date: 2021-04-29 12:53:54 +LastEditor: JiangJi +LastEditTime: 2021-12-22 15:41:19 +Discription: +Environment: +''' +import copy +import torch +import torch.nn as nn +import torch.optim as optim +import torch.nn.functional as F +from torch.distributions import Normal +import numpy as np +import random +device=torch.device("cuda" if torch.cuda.is_available() else "cpu") +class ReplayBuffer: + def __init__(self, capacity): + self.capacity = capacity # 经验回放的容量 + self.buffer = [] # 缓冲区 + self.position = 0 + + def push(self, state, action, reward, next_state, done): + ''' 缓冲区是一个队列,容量超出时去掉开始存入的转移(transition) + ''' + if len(self.buffer) < self.capacity: + self.buffer.append(None) + self.buffer[self.position] = (state, action, reward, next_state, done) + self.position = (self.position + 1) % self.capacity + + def sample(self, batch_size): + batch = random.sample(self.buffer, batch_size) # 随机采出小批量转移 + state, action, reward, next_state, done = zip(*batch) # 解压成状态,动作等 + return state, action, reward, next_state, done + + def __len__(self): + ''' 返回当前存储的量 + ''' + return len(self.buffer) + +class ValueNet(nn.Module): + def __init__(self, state_dim, hidden_dim, init_w=3e-3): + super(ValueNet, self).__init__() + + self.linear1 = nn.Linear(state_dim, hidden_dim) + self.linear2 = nn.Linear(hidden_dim, hidden_dim) + self.linear3 = nn.Linear(hidden_dim, 1) + + self.linear3.weight.data.uniform_(-init_w, init_w) + self.linear3.bias.data.uniform_(-init_w, init_w) + + def forward(self, state): + x = F.relu(self.linear1(state)) + x = F.relu(self.linear2(x)) + x = self.linear3(x) + return x + + +class SoftQNet(nn.Module): + def __init__(self, state_dim, action_dim, hidden_dim, init_w=3e-3): + super(SoftQNet, self).__init__() + + self.linear1 = nn.Linear(state_dim + action_dim, hidden_dim) + self.linear2 = nn.Linear(hidden_dim, hidden_dim) + self.linear3 = nn.Linear(hidden_dim, 1) + + self.linear3.weight.data.uniform_(-init_w, init_w) + self.linear3.bias.data.uniform_(-init_w, init_w) + + def forward(self, state, action): + x = torch.cat([state, action], 1) + x = F.relu(self.linear1(x)) + x = F.relu(self.linear2(x)) + x = self.linear3(x) + return x + + +class PolicyNet(nn.Module): + def __init__(self, state_dim, action_dim, hidden_dim, init_w=3e-3, log_std_min=-20, log_std_max=2): + super(PolicyNet, self).__init__() + + self.log_std_min = log_std_min + self.log_std_max = log_std_max + + self.linear1 = nn.Linear(state_dim, hidden_dim) + self.linear2 = nn.Linear(hidden_dim, hidden_dim) + + self.mean_linear = nn.Linear(hidden_dim, action_dim) + self.mean_linear.weight.data.uniform_(-init_w, init_w) + self.mean_linear.bias.data.uniform_(-init_w, init_w) + + self.log_std_linear = nn.Linear(hidden_dim, action_dim) + self.log_std_linear.weight.data.uniform_(-init_w, init_w) + self.log_std_linear.bias.data.uniform_(-init_w, init_w) + + def forward(self, state): + x = F.relu(self.linear1(state)) + x = F.relu(self.linear2(x)) + + mean = self.mean_linear(x) + log_std = self.log_std_linear(x) + log_std = torch.clamp(log_std, self.log_std_min, self.log_std_max) + + return mean, log_std + + def evaluate(self, state, epsilon=1e-6): + mean, log_std = self.forward(state) + std = log_std.exp() + + normal = Normal(mean, std) + z = normal.sample() + action = torch.tanh(z) + + log_prob = normal.log_prob(z) - torch.log(1 - action.pow(2) + epsilon) + log_prob = log_prob.sum(-1, keepdim=True) + + return action, log_prob, z, mean, log_std + + + def get_action(self, state): + state = torch.FloatTensor(state).unsqueeze(0).to(device) + mean, log_std = self.forward(state) + std = log_std.exp() + + normal = Normal(mean, std) + z = normal.sample() + action = torch.tanh(z) + + action = action.detach().cpu().numpy() + return action[0] + +class SAC: + def __init__(self,state_dim,action_dim,cfg) -> None: + self.batch_size = cfg.batch_size + self.memory = ReplayBuffer(cfg.capacity) + self.device = cfg.device + self.value_net = ValueNet(state_dim, cfg.hidden_dim).to(self.device) + self.target_value_net = ValueNet(state_dim, cfg.hidden_dim).to(self.device) + self.soft_q_net = SoftQNet(state_dim, action_dim, cfg.hidden_dim).to(self.device) + self.policy_net = PolicyNet(state_dim, action_dim, cfg.hidden_dim).to(self.device) + self.value_optimizer = optim.Adam(self.value_net.parameters(), lr=cfg.value_lr) + self.soft_q_optimizer = optim.Adam(self.soft_q_net.parameters(), lr=cfg.soft_q_lr) + self.policy_optimizer = optim.Adam(self.policy_net.parameters(), lr=cfg.policy_lr) + for target_param, param in zip(self.target_value_net.parameters(), self.value_net.parameters()): + target_param.data.copy_(param.data) + self.value_criterion = nn.MSELoss() + self.soft_q_criterion = nn.MSELoss() + def update(self, gamma=0.99,mean_lambda=1e-3, + std_lambda=1e-3, + z_lambda=0.0, + soft_tau=1e-2, + ): + if len(self.memory) < self.batch_size: + return + state, action, reward, next_state, done = self.memory.sample(self.batch_size) + state = torch.FloatTensor(state).to(self.device) + next_state = torch.FloatTensor(next_state).to(self.device) + action = torch.FloatTensor(action).to(self.device) + reward = torch.FloatTensor(reward).unsqueeze(1).to(self.device) + done = torch.FloatTensor(np.float32(done)).unsqueeze(1).to(self.device) + expected_q_value = self.soft_q_net(state, action) + expected_value = self.value_net(state) + new_action, log_prob, z, mean, log_std = self.policy_net.evaluate(state) + + + target_value = self.target_value_net(next_state) + next_q_value = reward + (1 - done) * gamma * target_value + q_value_loss = self.soft_q_criterion(expected_q_value, next_q_value.detach()) + + expected_new_q_value = self.soft_q_net(state, new_action) + next_value = expected_new_q_value - log_prob + value_loss = self.value_criterion(expected_value, next_value.detach()) + + log_prob_target = expected_new_q_value - expected_value + policy_loss = (log_prob * (log_prob - log_prob_target).detach()).mean() + + + mean_loss = mean_lambda * mean.pow(2).mean() + std_loss = std_lambda * log_std.pow(2).mean() + z_loss = z_lambda * z.pow(2).sum(1).mean() + + policy_loss += mean_loss + std_loss + z_loss + + self.soft_q_optimizer.zero_grad() + q_value_loss.backward() + self.soft_q_optimizer.step() + + self.value_optimizer.zero_grad() + value_loss.backward() + self.value_optimizer.step() + + self.policy_optimizer.zero_grad() + policy_loss.backward() + self.policy_optimizer.step() + + for target_param, param in zip(self.target_value_net.parameters(), self.value_net.parameters()): + target_param.data.copy_( + target_param.data * (1.0 - soft_tau) + param.data * soft_tau + ) + def save(self, path): + torch.save(self.value_net.state_dict(), path + "sac_value") + torch.save(self.value_optimizer.state_dict(), path + "sac_value_optimizer") + torch.save(self.soft_q_net.state_dict(), path + "sac_soft_q") + torch.save(self.soft_q_optimizer.state_dict(), path + "sac_soft_q_optimizer") + + torch.save(self.policy_net.state_dict(), path + "sac_policy") + torch.save(self.policy_optimizer.state_dict(), path + "sac_policy_optimizer") + + def load(self, path): + self.value_net.load_state_dict(torch.load(path + "sac_value")) + self.value_optimizer.load_state_dict(torch.load(path + "sac_value_optimizer")) + self.target_value_net = copy.deepcopy(self.value_net) + + self.soft_q_net.load_state_dict(torch.load(path + "sac_soft_q")) + self.soft_q_optimizer.load_state_dict(torch.load(path + "sac_soft_q_optimizer")) + + self.policy_net.load_state_dict(torch.load(path + "sac_policy")) + self.policy_optimizer.load_state_dict(torch.load(path + "sac_policy_optimizer")) \ No newline at end of file diff --git a/codes/SoftActorCritic/task0.py b/codes/SoftActorCritic/task0.py new file mode 100644 index 0000000..e910749 --- /dev/null +++ b/codes/SoftActorCritic/task0.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: JiangJi +Email: johnjim0816@gmail.com +Date: 2021-04-29 12:59:22 +LastEditor: JiangJi +LastEditTime: 2021-12-22 16:27:13 +Discription: +Environment: +''' +import sys,os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import gym +import torch +import datetime + +from SoftActorCritic.env_wrapper import NormalizedActions +from SoftActorCritic.sac import SAC +from common.utils import save_results, make_dir +from common.utils import plot_rewards + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 +algo_name = 'SAC' # 算法名称 +env_name = 'Pendulum-v1' # 环境名称 +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + +class SACConfig: + def __init__(self) -> None: + self.algo_name = algo_name + self.env_name = env_name # 环境名称 + self.device= device + self.train_eps = 300 + self.test_eps = 20 + self.max_steps = 500 # 每回合的最大步数 + self.gamma = 0.99 + self.mean_lambda=1e-3 + self.std_lambda=1e-3 + self.z_lambda=0.0 + self.soft_tau=1e-2 + self.value_lr = 3e-4 + self.soft_q_lr = 3e-4 + self.policy_lr = 3e-4 + self.capacity = 1000000 + self.hidden_dim = 256 + self.batch_size = 128 + + +class PlotConfig: + def __init__(self) -> None: + self.algo_name = algo_name # 算法名称 + self.env_name = env_name # 环境名称 + self.device= device + self.result_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/results/' # 保存结果的路径 + self.model_path = curr_path + "/outputs/" + self.env_name + \ + '/' + curr_time + '/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + +def env_agent_config(cfg,seed=1): + env = NormalizedActions(gym.make(cfg.env_name)) + env.seed(seed) + action_dim = env.action_space.shape[0] + state_dim = env.observation_space.shape[0] + agent = SAC(state_dim,action_dim,cfg) + return env,agent + +def train(cfg,env,agent): + print('开始训练!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.train_eps): + ep_reward = 0 # 记录一回合内的奖励 + state = env.reset() # 重置环境,返回初始状态 + for i_step in range(cfg.max_steps): + action = agent.policy_net.get_action(state) + next_state, reward, done, _ = env.step(action) + agent.memory.push(state, action, reward, next_state, done) + agent.update() + state = next_state + ep_reward += reward + if done: + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + if (i_ep+1)%10 == 0: + print(f'回合:{i_ep+1}/{cfg.train_eps}, 奖励:{ep_reward:.3f}') + print('完成训练!') + return rewards, ma_rewards + +def test(cfg,env,agent): + print('开始测试!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo_name}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(cfg.test_eps): + state = env.reset() + ep_reward = 0 + for i_step in range(cfg.max_steps): + action = agent.policy_net.get_action(state) + next_state, reward, done, _ = env.step(action) + state = next_state + ep_reward += reward + if done: + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + print(f"回合:{i_ep+1}/{cfg.test_eps},奖励:{ep_reward:.1f}") + print('完成测试!') + return rewards, ma_rewards + +if __name__ == "__main__": + cfg=SACConfig() + plot_cfg = PlotConfig() + # 训练 + env, agent = env_agent_config(cfg, seed=1) + rewards, ma_rewards = train(cfg, env, agent) + make_dir(plot_cfg.result_path, plot_cfg.model_path) # 创建保存结果和模型路径的文件夹 + agent.save(path=plot_cfg.model_path) # 保存模型 + save_results(rewards, ma_rewards, tag='train', + path=plot_cfg.result_path) # 保存结果 + plot_rewards(rewards, ma_rewards, plot_cfg, tag="train") # 画出结果 + # 测试 + env, agent = env_agent_config(cfg, seed=10) + agent.load(path=plot_cfg.model_path) # 导入模型 + rewards, ma_rewards = test(cfg, env, agent) + save_results(rewards, ma_rewards, tag='test', path=plot_cfg.result_path) # 保存结果 + plot_rewards(rewards, ma_rewards, plot_cfg, tag="test") # 画出结果 + + + + diff --git a/codes/SoftActorCritic/task0_train.ipynb b/codes/SoftActorCritic/task0_train.ipynb new file mode 100644 index 0000000..14be84e --- /dev/null +++ b/codes/SoftActorCritic/task0_train.ipynb @@ -0,0 +1,221 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from pathlib import Path\n", + "curr_path = str(Path().absolute())\n", + "parent_path = str(Path().absolute().parent)\n", + "sys.path.append(parent_path) # add current terminal path to sys.path" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import gym\n", + "import torch\n", + "import datetime\n", + "\n", + "from SAC.env import NormalizedActions\n", + "from SAC.agent import SAC\n", + "from common.utils import save_results, make_dir\n", + "from common.plot import plot_rewards\n", + "\n", + "curr_time = datetime.datetime.now().strftime(\"%Y%m%d-%H%M%S\") # obtain current time" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "class SACConfig:\n", + " def __init__(self) -> None:\n", + " self.algo = 'SAC'\n", + " self.env = 'Pendulum-v0'\n", + " self.result_path = curr_path+\"/outputs/\" +self.env+'/'+curr_time+'/results/' # path to save results\n", + " self.model_path = curr_path+\"/outputs/\" +self.env+'/'+curr_time+'/models/' # path to save models\n", + " self.train_eps = 300\n", + " self.train_steps = 500\n", + " self.test_eps = 50\n", + " self.eval_steps = 500\n", + " self.gamma = 0.99\n", + " self.mean_lambda=1e-3\n", + " self.std_lambda=1e-3\n", + " self.z_lambda=0.0\n", + " self.soft_tau=1e-2\n", + " self.value_lr = 3e-4\n", + " self.soft_q_lr = 3e-4\n", + " self.policy_lr = 3e-4\n", + " self.capacity = 1000000\n", + " self.hidden_dim = 256\n", + " self.batch_size = 128\n", + " self.device=torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def env_agent_config(cfg,seed=1):\n", + " env = NormalizedActions(gym.make(\"Pendulum-v0\"))\n", + " env.seed(seed)\n", + " action_dim = env.action_space.shape[0]\n", + " state_dim = env.observation_space.shape[0]\n", + " agent = SAC(state_dim,action_dim,cfg)\n", + " return env,agent" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "def train(cfg,env,agent):\n", + " print('Start to train !')\n", + " print(f'Env: {cfg.env}, Algorithm: {cfg.algo}, Device: {cfg.device}')\n", + " rewards = []\n", + " ma_rewards = [] # moveing average reward\n", + " for i_ep in range(cfg.train_eps):\n", + " state = env.reset()\n", + " ep_reward = 0\n", + " for i_step in range(cfg.train_steps):\n", + " action = agent.policy_net.get_action(state)\n", + " next_state, reward, done, _ = env.step(action)\n", + " agent.memory.push(state, action, reward, next_state, done)\n", + " agent.update()\n", + " state = next_state\n", + " ep_reward += reward\n", + " if done:\n", + " break\n", + " if (i_ep+1)%10==0:\n", + " print(f\"Episode:{i_ep+1}/{cfg.train_eps}, Reward:{ep_reward:.3f}\")\n", + " rewards.append(ep_reward)\n", + " if ma_rewards:\n", + " ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward)\n", + " else:\n", + " ma_rewards.append(ep_reward) \n", + " print('Complete training!')\n", + " return rewards, ma_rewards" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def eval(cfg,env,agent):\n", + " print('Start to eval !')\n", + " print(f'Env: {cfg.env}, Algorithm: {cfg.algo}, Device: {cfg.device}')\n", + " rewards = []\n", + " ma_rewards = [] # moveing average reward\n", + " for i_ep in range(cfg.test_eps):\n", + " state = env.reset()\n", + " ep_reward = 0\n", + " for i_step in range(cfg.eval_steps):\n", + " action = agent.policy_net.get_action(state)\n", + " next_state, reward, done, _ = env.step(action)\n", + " state = next_state\n", + " ep_reward += reward\n", + " if done:\n", + " break\n", + " if (i_ep+1)%10==0:\n", + " print(f\"Episode:{i_ep+1}/{cfg.train_eps}, Reward:{ep_reward:.3f}\")\n", + " rewards.append(ep_reward)\n", + " if ma_rewards:\n", + " ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward)\n", + " else:\n", + " ma_rewards.append(ep_reward) \n", + " print('Complete evaling!')\n", + " return rewards, ma_rewards\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "ename": "DeprecatedEnv", + "evalue": "Env Pendulum-v0 not found (valid versions include ['Pendulum-v1'])", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m~/anaconda3/envs/py37/lib/python3.7/site-packages/gym/envs/registration.py\u001b[0m in \u001b[0;36mspec\u001b[0;34m(self, path)\u001b[0m\n\u001b[1;32m 157\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 158\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0menv_specs\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mid\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 159\u001b[0m \u001b[0;32mexcept\u001b[0m \u001b[0mKeyError\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mKeyError\u001b[0m: 'Pendulum-v0'", + "\nDuring handling of the above exception, another exception occurred:\n", + "\u001b[0;31mDeprecatedEnv\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0;31m# train\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 5\u001b[0;31m \u001b[0menv\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0magent\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0menv_agent_config\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcfg\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mseed\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 6\u001b[0m \u001b[0mrewards\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mma_rewards\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtrain\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcfg\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0menv\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0magent\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 7\u001b[0m \u001b[0mmake_dir\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcfg\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mresult_path\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcfg\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmodel_path\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36menv_agent_config\u001b[0;34m(cfg, seed)\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0menv_agent_config\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcfg\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mseed\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0menv\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mNormalizedActions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgym\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmake\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Pendulum-v0\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 3\u001b[0m \u001b[0menv\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mseed\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mseed\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[0maction_dim\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0menv\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0maction_space\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mshape\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mstate_dim\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0menv\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mobservation_space\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mshape\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/anaconda3/envs/py37/lib/python3.7/site-packages/gym/envs/registration.py\u001b[0m in \u001b[0;36mmake\u001b[0;34m(id, **kwargs)\u001b[0m\n\u001b[1;32m 233\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 234\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mmake\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mid\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 235\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mregistry\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmake\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mid\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 236\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 237\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/anaconda3/envs/py37/lib/python3.7/site-packages/gym/envs/registration.py\u001b[0m in \u001b[0;36mmake\u001b[0;34m(self, path, **kwargs)\u001b[0m\n\u001b[1;32m 126\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 127\u001b[0m \u001b[0mlogger\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minfo\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"Making new env: %s\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mpath\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 128\u001b[0;31m \u001b[0mspec\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mspec\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpath\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 129\u001b[0m \u001b[0menv\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mspec\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmake\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 130\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0menv\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m~/anaconda3/envs/py37/lib/python3.7/site-packages/gym/envs/registration.py\u001b[0m in \u001b[0;36mspec\u001b[0;34m(self, path)\u001b[0m\n\u001b[1;32m 185\u001b[0m raise error.DeprecatedEnv(\n\u001b[1;32m 186\u001b[0m \"Env {} not found (valid versions include {})\".format(\n\u001b[0;32m--> 187\u001b[0;31m \u001b[0mid\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmatching_envs\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 188\u001b[0m )\n\u001b[1;32m 189\u001b[0m )\n", + "\u001b[0;31mDeprecatedEnv\u001b[0m: Env Pendulum-v0 not found (valid versions include ['Pendulum-v1'])" + ] + } + ], + "source": [ + "if __name__ == \"__main__\":\n", + " cfg=SACConfig()\n", + " \n", + " # train\n", + " env,agent = env_agent_config(cfg,seed=1)\n", + " rewards, ma_rewards = train(cfg, env, agent)\n", + " make_dir(cfg.result_path, cfg.model_path)\n", + " agent.save(path=cfg.model_path)\n", + " save_results(rewards, ma_rewards, tag='train', path=cfg.result_path)\n", + " plot_rewards(rewards, ma_rewards, tag=\"train\",\n", + " algo=cfg.algo, path=cfg.result_path)\n", + " # eval\n", + " env,agent = env_agent_config(cfg,seed=10)\n", + " agent.load(path=cfg.model_path)\n", + " rewards,ma_rewards = eval(cfg,env,agent)\n", + " save_results(rewards,ma_rewards,tag='eval',path=cfg.result_path)\n", + " plot_rewards(rewards,ma_rewards,tag=\"eval\",env=cfg.env,algo = cfg.algo,path=cfg.result_path)\n" + ] + } + ], + "metadata": { + "interpreter": { + "hash": "fe38df673a99c62a9fea33a7aceda74c9b65b12ee9d076c5851d98b692a4989a" + }, + "kernelspec": { + "display_name": "Python 3.7.10 64-bit ('mujoco': conda)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.10" + }, + "metadata": { + "interpreter": { + "hash": "fd81e6a9e450d5c245c1a0b5da0b03c89c450f614a13afa2acb1654375922756" + } + }, + "orig_nbformat": 2 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/codes/TD3/README.md b/codes/TD3/README.md new file mode 100644 index 0000000..8001e9c --- /dev/null +++ b/codes/TD3/README.md @@ -0,0 +1 @@ +这是对[Implementation of Twin Delayed Deep Deterministic Policy Gradients (TD3)](https://arxiv.org/abs/1802.09477)的复现 \ No newline at end of file diff --git a/codes/TD3/agent.py b/codes/TD3/agent.py new file mode 100644 index 0000000..91939a6 --- /dev/null +++ b/codes/TD3/agent.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: JiangJi +Email: johnjim0816@gmail.com +Date: 2021-12-22 10:40:05 +LastEditor: JiangJi +LastEditTime: 2021-12-22 10:43:55 +Discription: +''' +import copy +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from TD3.memory import ReplayBuffer + +class Actor(nn.Module): + + def __init__(self, input_dim, output_dim, max_action): + '''[summary] + + Args: + input_dim (int): 输入维度,这里等于state_dim + output_dim (int): 输出维度,这里等于action_dim + max_action (int): action的最大值 + ''' + super(Actor, self).__init__() + + self.l1 = nn.Linear(input_dim, 256) + self.l2 = nn.Linear(256, 256) + self.l3 = nn.Linear(256, output_dim) + self.max_action = max_action + + def forward(self, state): + + a = F.relu(self.l1(state)) + a = F.relu(self.l2(a)) + return self.max_action * torch.tanh(self.l3(a)) + + +class Critic(nn.Module): + def __init__(self, input_dim, output_dim): + super(Critic, self).__init__() + + # Q1 architecture + self.l1 = nn.Linear(input_dim + output_dim, 256) + self.l2 = nn.Linear(256, 256) + self.l3 = nn.Linear(256, 1) + + # Q2 architecture + self.l4 = nn.Linear(input_dim + output_dim, 256) + self.l5 = nn.Linear(256, 256) + self.l6 = nn.Linear(256, 1) + + + def forward(self, state, action): + sa = torch.cat([state, action], 1) + + q1 = F.relu(self.l1(sa)) + q1 = F.relu(self.l2(q1)) + q1 = self.l3(q1) + + q2 = F.relu(self.l4(sa)) + q2 = F.relu(self.l5(q2)) + q2 = self.l6(q2) + return q1, q2 + + + def Q1(self, state, action): + sa = torch.cat([state, action], 1) + + q1 = F.relu(self.l1(sa)) + q1 = F.relu(self.l2(q1)) + q1 = self.l3(q1) + return q1 + + +class TD3(object): + def __init__( + self, + input_dim, + output_dim, + max_action, + cfg, + ): + self.max_action = max_action + self.gamma = cfg.gamma + self.lr = cfg.lr + self.policy_noise = cfg.policy_noise + self.noise_clip = cfg.noise_clip + self.policy_freq = cfg.policy_freq + self.batch_size = cfg.batch_size + self.device = cfg.device + self.total_it = 0 + + self.actor = Actor(input_dim, output_dim, max_action).to(self.device) + self.actor_target = copy.deepcopy(self.actor) + self.actor_optimizer = torch.optim.Adam(self.actor.parameters(), lr=3e-4) + + self.critic = Critic(input_dim, output_dim).to(self.device) + self.critic_target = copy.deepcopy(self.critic) + self.critic_optimizer = torch.optim.Adam(self.critic.parameters(), lr=3e-4) + self.memory = ReplayBuffer(input_dim, output_dim) + + def choose_action(self, state): + state = torch.FloatTensor(state.reshape(1, -1)).to(self.device) + return self.actor(state).cpu().data.numpy().flatten() + + def update(self): + self.total_it += 1 + + # Sample replay buffer + state, action, next_state, reward, not_done = self.memory.sample(self.batch_size) + + with torch.no_grad(): + # Select action according to policy and add clipped noise + noise = ( + torch.randn_like(action) * self.policy_noise + ).clamp(-self.noise_clip, self.noise_clip) + + next_action = ( + self.actor_target(next_state) + noise + ).clamp(-self.max_action, self.max_action) + + # Compute the target Q value + target_Q1, target_Q2 = self.critic_target(next_state, next_action) + target_Q = torch.min(target_Q1, target_Q2) + target_Q = reward + not_done * self.gamma * target_Q + + # Get current Q estimates + current_Q1, current_Q2 = self.critic(state, action) + + # Compute critic loss + critic_loss = F.mse_loss(current_Q1, target_Q) + F.mse_loss(current_Q2, target_Q) + + # Optimize the critic + self.critic_optimizer.zero_grad() + critic_loss.backward() + self.critic_optimizer.step() + + # Delayed policy updates + if self.total_it % self.policy_freq == 0: + + # Compute actor losse + actor_loss = -self.critic.Q1(state, self.actor(state)).mean() + + # Optimize the actor + self.actor_optimizer.zero_grad() + actor_loss.backward() + self.actor_optimizer.step() + + # Update the frozen target models + for param, target_param in zip(self.critic.parameters(), self.critic_target.parameters()): + target_param.data.copy_(self.lr * param.data + (1 - self.lr) * target_param.data) + + for param, target_param in zip(self.actor.parameters(), self.actor_target.parameters()): + target_param.data.copy_(self.lr * param.data + (1 - self.lr) * target_param.data) + + + def save(self, path): + torch.save(self.critic.state_dict(), path + "td3_critic") + torch.save(self.critic_optimizer.state_dict(), path + "td3_critic_optimizer") + + torch.save(self.actor.state_dict(), path + "td3_actor") + torch.save(self.actor_optimizer.state_dict(), path + "td3_actor_optimizer") + + + def load(self, path): + self.critic.load_state_dict(torch.load(path + "td3_critic")) + self.critic_optimizer.load_state_dict(torch.load(path + "td3_critic_optimizer")) + self.critic_target = copy.deepcopy(self.critic) + + self.actor.load_state_dict(torch.load(path + "td3_actor")) + self.actor_optimizer.load_state_dict(torch.load(path + "td3_actor_optimizer")) + self.actor_target = copy.deepcopy(self.actor) + diff --git a/codes/TD3/memory.py b/codes/TD3/memory.py new file mode 100644 index 0000000..7e2671c --- /dev/null +++ b/codes/TD3/memory.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-04-13 11:00:13 +LastEditor: John +LastEditTime: 2021-04-15 01:25:14 +Discription: +Environment: +''' +import numpy as np +import torch + + +class ReplayBuffer(object): + def __init__(self, state_dim, action_dim, max_size=int(1e6)): + self.max_size = max_size + self.ptr = 0 + self.size = 0 + self.state = np.zeros((max_size, state_dim)) + self.action = np.zeros((max_size, action_dim)) + self.next_state = np.zeros((max_size, state_dim)) + self.reward = np.zeros((max_size, 1)) + self.not_done = np.zeros((max_size, 1)) + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + def push(self, state, action, next_state, reward, done): + self.state[self.ptr] = state + self.action[self.ptr] = action + self.next_state[self.ptr] = next_state + self.reward[self.ptr] = reward + self.not_done[self.ptr] = 1. - done + self.ptr = (self.ptr + 1) % self.max_size + self.size = min(self.size + 1, self.max_size) + + def sample(self, batch_size): + ind = np.random.randint(0, self.size, size=batch_size) + return ( + torch.FloatTensor(self.state[ind]).to(self.device), + torch.FloatTensor(self.action[ind]).to(self.device), + torch.FloatTensor(self.next_state[ind]).to(self.device), + torch.FloatTensor(self.reward[ind]).to(self.device), + torch.FloatTensor(self.not_done[ind]).to(self.device) + ) \ No newline at end of file diff --git a/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_actor b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_actor new file mode 100644 index 0000000..2b3b481 Binary files /dev/null and b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_actor differ diff --git a/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_actor_optimizer b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_actor_optimizer new file mode 100644 index 0000000..9bb6195 Binary files /dev/null and b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_actor_optimizer differ diff --git a/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_critic b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_critic new file mode 100644 index 0000000..cccfb71 Binary files /dev/null and b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_critic differ diff --git a/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_critic_optimizer b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_critic_optimizer new file mode 100644 index 0000000..1446c66 Binary files /dev/null and b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/models/td3_critic_optimizer differ diff --git a/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/results/ma_rewards_train.npy b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/results/ma_rewards_train.npy new file mode 100644 index 0000000..96d40db Binary files /dev/null and b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/results/ma_rewards_train.npy differ diff --git a/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/results/rewards_curve_train.png b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/results/rewards_curve_train.png new file mode 100644 index 0000000..e310371 Binary files /dev/null and b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/results/rewards_curve_train.png differ diff --git a/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/results/rewards_train.npy b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/results/rewards_train.npy new file mode 100644 index 0000000..718e407 Binary files /dev/null and b/codes/TD3/outputs/HalfCheetah-v2/20210416-130341/results/rewards_train.npy differ diff --git a/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_actor b/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_actor new file mode 100644 index 0000000..40533d9 Binary files /dev/null and b/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_actor differ diff --git a/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_actor_optimizer b/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_actor_optimizer new file mode 100644 index 0000000..e91a68f Binary files /dev/null and b/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_actor_optimizer differ diff --git a/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_critic b/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_critic new file mode 100644 index 0000000..ef6b3e5 Binary files /dev/null and b/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_critic differ diff --git a/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_critic_optimizer b/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_critic_optimizer new file mode 100644 index 0000000..8094beb Binary files /dev/null and b/codes/TD3/outputs/Pendulum-v1/20211119-123814/models/td3_critic_optimizer differ diff --git a/codes/TD3/outputs/Pendulum-v1/20211119-123814/results/train_ma_rewards.npy b/codes/TD3/outputs/Pendulum-v1/20211119-123814/results/train_ma_rewards.npy new file mode 100644 index 0000000..288eb69 Binary files /dev/null and b/codes/TD3/outputs/Pendulum-v1/20211119-123814/results/train_ma_rewards.npy differ diff --git a/codes/TD3/outputs/Pendulum-v1/20211119-123814/results/train_rewards.npy b/codes/TD3/outputs/Pendulum-v1/20211119-123814/results/train_rewards.npy new file mode 100644 index 0000000..5bdee4a Binary files /dev/null and b/codes/TD3/outputs/Pendulum-v1/20211119-123814/results/train_rewards.npy differ diff --git a/codes/TD3/outputs/Pendulum-v1/20211119-123814/results/train_rewards_curve.png b/codes/TD3/outputs/Pendulum-v1/20211119-123814/results/train_rewards_curve.png new file mode 100644 index 0000000..31e873c Binary files /dev/null and b/codes/TD3/outputs/Pendulum-v1/20211119-123814/results/train_rewards_curve.png differ diff --git a/codes/TD3/outputs/Reacher-v2/20210415-021952/ma_rewards_train.npy b/codes/TD3/outputs/Reacher-v2/20210415-021952/ma_rewards_train.npy new file mode 100644 index 0000000..017dbba Binary files /dev/null and b/codes/TD3/outputs/Reacher-v2/20210415-021952/ma_rewards_train.npy differ diff --git a/codes/TD3/outputs/Reacher-v2/20210415-021952/rewards_curve_train.png b/codes/TD3/outputs/Reacher-v2/20210415-021952/rewards_curve_train.png new file mode 100644 index 0000000..098872d Binary files /dev/null and b/codes/TD3/outputs/Reacher-v2/20210415-021952/rewards_curve_train.png differ diff --git a/codes/TD3/outputs/Reacher-v2/20210415-021952/rewards_train.npy b/codes/TD3/outputs/Reacher-v2/20210415-021952/rewards_train.npy new file mode 100644 index 0000000..3ef20c3 Binary files /dev/null and b/codes/TD3/outputs/Reacher-v2/20210415-021952/rewards_train.npy differ diff --git a/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_actor b/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_actor new file mode 100644 index 0000000..10e7154 Binary files /dev/null and b/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_actor differ diff --git a/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_actor_optimizer b/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_actor_optimizer new file mode 100644 index 0000000..ac8989e Binary files /dev/null and b/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_actor_optimizer differ diff --git a/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_critic b/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_critic new file mode 100644 index 0000000..5e16302 Binary files /dev/null and b/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_critic differ diff --git a/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_critic_optimizer b/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_critic_optimizer new file mode 100644 index 0000000..3b7d759 Binary files /dev/null and b/codes/TD3/outputs/Reacher-v2/20210415-021952/td3_critic_optimizer differ diff --git a/codes/TD3/task0_eval.py b/codes/TD3/task0_eval.py new file mode 100644 index 0000000..0420dce --- /dev/null +++ b/codes/TD3/task0_eval.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: JiangJi +Email: johnjim0816@gmail.com +Date: 2021-04-23 20:36:23 +LastEditor: JiangJi +LastEditTime: 2021-04-23 20:37:22 +Discription: +Environment: +''' +import sys,os +curr_path = os.path.dirname(__file__) +parent_path=os.path.dirname(curr_path) +sys.path.append(parent_path) # add current terminal path to sys.path + +import torch +import gym +import numpy as np +import datetime + + +from TD3.agent import TD3 +from common.plot import plot_rewards +from common.utils import save_results,make_dir + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # obtain current time + +class TD3Config: + def __init__(self) -> None: + self.algo = 'TD3 and Random' + self.env = 'HalfCheetah-v2' + self.seed = 0 + self.result_path = curr_path+"/results/" +self.env+'/'+curr_time+'/results/' # path to save results + self.model_path = curr_path+"/results/" +self.env+'/'+curr_time+'/models/' # path to save models + self.start_timestep = 25e3 # Time steps initial random policy is used + self.eval_freq = 5e3 # How often (time steps) we evaluate + self.max_timestep = 200000 # Max time steps to run environment + self.expl_noise = 0.1 # Std of Gaussian exploration noise + self.batch_size = 256 # Batch size for both actor and critic + self.gamma = 0.99 # gamma factor + self.lr = 0.0005 # Target network update rate + self.policy_noise = 0.2 # Noise added to target policy during critic update + self.noise_clip = 0.5 # Range to clip target policy noise + self.policy_freq = 2 # Frequency of delayed policy updates + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +# Runs policy for X episodes and returns average reward +# A fixed seed is used for the eval environment +def eval(env_name,agent, seed, eval_episodes=50): + eval_env = gym.make(env_name) + eval_env.seed(seed + 100) + rewards,ma_rewards =[],[] + for i_episode in range(eval_episodes): + ep_reward = 0 + state, done = eval_env.reset(), False + while not done: + eval_env.render() + action = agent.choose_action(np.array(state)) + state, reward, done, _ = eval_env.step(action) + ep_reward += reward + print(f"Episode:{i_episode+1}, Reward:{ep_reward:.3f}") + rewards.append(ep_reward) + # 计算滑动窗口的reward + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + return rewards,ma_rewards + +if __name__ == "__main__": + cfg = TD3Config() + env = gym.make(cfg.env) + env.seed(cfg.seed) # Set seeds + torch.manual_seed(cfg.seed) + np.random.seed(cfg.seed) + state_dim = env.observation_space.shape[0] + action_dim = env.action_space.shape[0] + max_action = float(env.action_space.high[0]) + td3= TD3(state_dim,action_dim,max_action,cfg) + cfg.model_path = './TD3/results/HalfCheetah-v2/20210416-130341/models/' + td3.load(cfg.model_path) + td3_rewards,td3_ma_rewards = eval(cfg.env,td3,cfg.seed) + make_dir(cfg.result_path,cfg.model_path) + save_results(td3_rewards,td3_ma_rewards,tag='eval',path=cfg.result_path) + plot_rewards({'td3_rewards':td3_rewards,'td3_ma_rewards':td3_ma_rewards,},tag="eval",env=cfg.env,algo = cfg.algo,path=cfg.result_path) + # cfg.result_path = './TD3/results/HalfCheetah-v2/20210416-130341/' + # agent.load(cfg.result_path) + # eval(cfg.env,agent, cfg.seed) \ No newline at end of file diff --git a/codes/TD3/task0_train.py b/codes/TD3/task0_train.py new file mode 100644 index 0000000..11e2adf --- /dev/null +++ b/codes/TD3/task0_train.py @@ -0,0 +1,173 @@ +import sys,os +curr_path = os.path.dirname(__file__) +parent_path=os.path.dirname(curr_path) +sys.path.append(parent_path) # add current terminal path to sys.path + +import torch +import gym +import numpy as np +import datetime + + +from TD3.agent import TD3 +from common.plot import plot_rewards +from common.utils import save_results,make_dir + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # obtain current time + + +class TD3Config: + def __init__(self) -> None: + self.algo = 'TD3' + self.env = 'HalfCheetah-v2' + self.seed = 0 + self.result_path = curr_path+"/results/" +self.env+'/'+curr_time+'/results/' # path to save results + self.model_path = curr_path+"/results/" +self.env+'/'+curr_time+'/models/' # path to save models + self.start_timestep = 25e3 # Time steps initial random policy is used + self.eval_freq = 5e3 # How often (time steps) we evaluate + # self.train_eps = 800 + self.max_timestep = 4000000 # Max time steps to run environment + self.expl_noise = 0.1 # Std of Gaussian exploration noise + self.batch_size = 256 # Batch size for both actor and critic + self.gamma = 0.99 # gamma factor + self.lr = 0.0005 # Target network update rate + self.policy_noise = 0.2 # Noise added to target policy during critic update + self.noise_clip = 0.5 # Range to clip target policy noise + self.policy_freq = 2 # Frequency of delayed policy updates + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +# Runs policy for X episodes and returns average reward +# A fixed seed is used for the eval environment +def eval(env,agent, seed, eval_episodes=10): + eval_env = gym.make(env) + eval_env.seed(seed + 100) + avg_reward = 0. + for _ in range(eval_episodes): + state, done = eval_env.reset(), False + while not done: + # eval_env.render() + action = agent.choose_action(np.array(state)) + state, reward, done, _ = eval_env.step(action) + avg_reward += reward + avg_reward /= eval_episodes + print("---------------------------------------") + print(f"Evaluation over {eval_episodes} episodes: {avg_reward:.3f}") + print("---------------------------------------") + return avg_reward + +def train(cfg,env,agent): + # Evaluate untrained policy + evaluations = [eval(cfg.env,agent, cfg.seed)] + state, done = env.reset(), False + ep_reward = 0 + ep_timesteps = 0 + episode_num = 0 + rewards = [] + ma_rewards = [] # moveing average reward + for t in range(int(cfg.max_timestep)): + ep_timesteps += 1 + # Select action randomly or according to policy + if t < cfg.start_timestep: + action = env.action_space.sample() + else: + action = ( + agent.choose_action(np.array(state)) + + np.random.normal(0, max_action * cfg.expl_noise, size=action_dim) + ).clip(-max_action, max_action) + # Perform action + next_state, reward, done, _ = env.step(action) + done_bool = float(done) if ep_timesteps < env._max_episode_steps else 0 + # Store data in replay buffer + agent.memory.push(state, action, next_state, reward, done_bool) + state = next_state + ep_reward += reward + # Train agent after collecting sufficient data + if t >= cfg.start_timestep: + agent.update() + if done: + # +1 to account for 0 indexing. +0 on ep_timesteps since it will increment +1 even if done=True + print(f"Episode:{episode_num+1}, Episode T:{ep_timesteps}, Reward:{ep_reward:.3f}") + # Reset environment + state, done = env.reset(), False + rewards.append(ep_reward) + # 计算滑动窗口的reward + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + ep_reward = 0 + ep_timesteps = 0 + episode_num += 1 + # Evaluate episode + if (t + 1) % cfg.eval_freq == 0: + evaluations.append(eval(cfg.env,agent, cfg.seed)) + return rewards, ma_rewards +# def train(cfg,env,agent): +# evaluations = [eval(cfg.env,agent,cfg.seed)] +# ep_reward = 0 +# tot_timestep = 0 +# rewards = [] +# ma_rewards = [] # moveing average reward +# for i_ep in range(int(cfg.train_eps)): +# state, done = env.reset(), False +# ep_reward = 0 +# ep_timestep = 0 +# while not done: +# ep_timestep += 1 +# tot_timestep +=1 +# # Select action randomly or according to policy +# if tot_timestep < cfg.start_timestep: +# action = env.action_space.sample() +# else: +# action = ( +# agent.choose_action(np.array(state)) +# + np.random.normal(0, max_action * cfg.expl_noise, size=action_dim) +# ).clip(-max_action, max_action) +# # action = ( +# # agent.choose_action(np.array(state)) +# # + np.random.normal(0, max_action * cfg.expl_noise, size=action_dim) +# # ).clip(-max_action, max_action) +# # Perform action +# next_state, reward, done, _ = env.step(action) +# done_bool = float(done) if ep_timestep < env._max_episode_steps else 0 + +# # Store data in replay buffer +# agent.memory.push(state, action, next_state, reward, done_bool) +# state = next_state +# ep_reward += reward +# # Train agent after collecting sufficient data +# if tot_timestep >= cfg.start_timestep: +# agent.update() +# print(f"Episode:{i_ep}/{cfg.train_eps}, Episode Timestep:{ep_timestep}, Reward:{ep_reward:.3f}") +# rewards.append(ep_reward) +# # 计算滑动窗口的reward +# if ma_rewards: +# ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) +# else: +# ma_rewards.append(ep_reward) +# # Evaluate episode +# if (i_ep+1) % cfg.eval_freq == 0: +# evaluations.append(eval(cfg.env,agent, cfg.seed)) +# return rewards,ma_rewards + + +if __name__ == "__main__": + cfg = TD3Config() + env = gym.make(cfg.env) + env.seed(cfg.seed) # Set seeds + torch.manual_seed(cfg.seed) + np.random.seed(cfg.seed) + state_dim = env.observation_space.shape[0] + action_dim = env.action_space.shape[0] + max_action = float(env.action_space.high[0]) + agent = TD3(state_dim,action_dim,max_action,cfg) + rewards,ma_rewards = train(cfg,env,agent) + make_dir(cfg.result_path,cfg.model_path) + agent.save(path=cfg.model_path) + save_results(rewards,ma_rewards,tag='train',path=cfg.result_path) + plot_rewards(rewards,ma_rewards,tag="train",env=cfg.env,algo = cfg.algo,path=cfg.result_path) + # cfg.result_path = './TD3/results/HalfCheetah-v2/20210416-130341/' + # agent.load(cfg.result_path) + # eval(cfg.env,agent, cfg.seed) + + diff --git a/codes/TD3/task1_eval.py b/codes/TD3/task1_eval.py new file mode 100644 index 0000000..ae17681 --- /dev/null +++ b/codes/TD3/task1_eval.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: JiangJi +Email: johnjim0816@gmail.com +Date: 2021-04-23 20:36:23 +LastEditor: JiangJi +LastEditTime: 2021-04-28 10:14:33 +Discription: +Environment: +''' +import sys,os +curr_path = os.path.dirname(__file__) +parent_path=os.path.dirname(curr_path) +sys.path.append(parent_path) # add current terminal path to sys.path + +import torch +import gym +import numpy as np +import datetime + + +from TD3.agent import TD3 +from common.plot import plot_rewards +from common.utils import save_results,make_dir + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # obtain current time + +class TD3Config: + def __init__(self) -> None: + self.algo = 'TD3' + self.env = 'Pendulum-v0' + self.seed = 0 + self.result_path = curr_path+"/results/" +self.env+'/'+curr_time+'/results/' # path to save results + self.model_path = curr_path+"/results/" +self.env+'/'+curr_time+'/models/' # path to save models + self.batch_size = 256 # Batch size for both actor and critic + self.gamma = 0.99 # gamma factor + self.lr = 0.0005 # Target network update rate + self.policy_noise = 0.2 # Noise added to target policy during critic update + self.noise_clip = 0.5 # Range to clip target policy noise + self.policy_freq = 2 # Frequency of delayed policy updates + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +# Runs policy for X episodes and returns average reward +# A fixed seed is used for the eval environment +def eval(env_name,agent, seed, eval_episodes=50): + eval_env = gym.make(env_name) + eval_env.seed(seed + 100) + rewards,ma_rewards =[],[] + for i_episode in range(eval_episodes): + ep_reward = 0 + state, done = eval_env.reset(), False + while not done: + # eval_env.render() + action = agent.choose_action(np.array(state)) + state, reward, done, _ = eval_env.step(action) + ep_reward += reward + print(f"Episode:{i_episode+1}, Reward:{ep_reward:.3f}") + rewards.append(ep_reward) + # 计算滑动窗口的reward + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + return rewards,ma_rewards + +if __name__ == "__main__": + cfg = TD3Config() + env = gym.make(cfg.env) + env.seed(cfg.seed) # Set seeds + torch.manual_seed(cfg.seed) + np.random.seed(cfg.seed) + state_dim = env.observation_space.shape[0] + action_dim = env.action_space.shape[0] + max_action = float(env.action_space.high[0]) + td3= TD3(state_dim,action_dim,max_action,cfg) + cfg.model_path = './TD3/results/Pendulum-v0/20210428-092059/models/' + cfg.result_path = './TD3/results/Pendulum-v0/20210428-092059/results/' + td3.load(cfg.model_path) + rewards,ma_rewards = eval(cfg.env,td3,cfg.seed) + make_dir(cfg.result_path,cfg.model_path) + save_results(rewards,ma_rewards,tag='eval',path=cfg.result_path) + plot_rewards(rewards,ma_rewards,tag="train",env=cfg.env,algo = cfg.algo,path=cfg.result_path) \ No newline at end of file diff --git a/codes/TD3/task1_train.py b/codes/TD3/task1_train.py new file mode 100644 index 0000000..9780f76 --- /dev/null +++ b/codes/TD3/task1_train.py @@ -0,0 +1,122 @@ +import sys,os +curr_path = os.path.dirname(os.path.abspath(__file__)) # 当前文件所在绝对路径 +parent_path = os.path.dirname(curr_path) # 父路径 +sys.path.append(parent_path) # 添加路径到系统路径 + +import torch +import gym +import numpy as np +import datetime + +from TD3.agent import TD3 +from common.plot import plot_rewards +from common.utils import save_results,make_dir + +curr_time = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") # 获取当前时间 + + +class TD3Config: + def __init__(self) -> None: + self.algo = 'TD3' # 算法名称 + self.env_name = 'Pendulum-v1' # 环境名称 + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 检测GPU + self.train_eps = 600 # 训练的回合数 + self.start_timestep = 25e3 # Time steps initial random policy is used + self.epsilon_start = 50 # Episodes initial random policy is used + self.eval_freq = 10 # How often (episodes) we evaluate + self.max_timestep = 100000 # Max time steps to run environment + self.expl_noise = 0.1 # Std of Gaussian exploration noise + self.batch_size = 256 # Batch size for both actor and critic + self.gamma = 0.9 # gamma factor + self.lr = 0.0005 # 学习率 + self.policy_noise = 0.2 # Noise added to target policy during critic update + self.noise_clip = 0.3 # Range to clip target policy noise + self.policy_freq = 2 # Frequency of delayed policy updates +class PlotConfig(TD3Config): + def __init__(self) -> None: + super().__init__() + self.result_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/results/' # 保存结果的路径 + self.model_path = curr_path+"/outputs/" + self.env_name + \ + '/'+curr_time+'/models/' # 保存模型的路径 + self.save = True # 是否保存图片 + + + +# Runs policy for X episodes and returns average reward +# A fixed seed is used for the eval environment +def eval(env,agent, seed, eval_episodes=10): + eval_env = gym.make(env) + eval_env.seed(seed + 100) + avg_reward = 0. + for _ in range(eval_episodes): + state, done = eval_env.reset(), False + while not done: + # eval_env.render() + action = agent.choose_action(np.array(state)) + state, reward, done, _ = eval_env.step(action) + avg_reward += reward + avg_reward /= eval_episodes + print("---------------------------------------") + print(f"Evaluation over {eval_episodes} episodes: {avg_reward:.3f}") + print("---------------------------------------") + return avg_reward + +def train(cfg,env,agent): + print('开始训练!') + print(f'环境:{cfg.env_name}, 算法:{cfg.algo}, 设备:{cfg.device}') + rewards = [] # 记录所有回合的奖励 + ma_rewards = [] # 记录所有回合的滑动平均奖励 + for i_ep in range(int(cfg.train_eps)): + ep_reward = 0 + ep_timesteps = 0 + state, done = env.reset(), False + while not done: + ep_timesteps += 1 + # Select action randomly or according to policy + if i_ep < cfg.epsilon_start: + action = env.action_space.sample() + else: + action = ( + agent.choose_action(np.array(state)) + + np.random.normal(0, max_action * cfg.expl_noise, size=action_dim) + ).clip(-max_action, max_action) + # Perform action + next_state, reward, done, _ = env.step(action) + done_bool = float(done) if ep_timesteps < env._max_episode_steps else 0 + # Store data in replay buffer + agent.memory.push(state, action, next_state, reward, done_bool) + state = next_state + ep_reward += reward + # Train agent after collecting sufficient data + if i_ep+1 >= cfg.epsilon_start: + agent.update() + if (i_ep+1)%10 == 0: + print('回合:{}/{}, 奖励:{:.2f}'.format(i_ep+1, cfg.train_eps, ep_reward)) + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(0.9*ma_rewards[-1]+0.1*ep_reward) + else: + ma_rewards.append(ep_reward) + print('完成训练!') + return rewards, ma_rewards + + +if __name__ == "__main__": + cfg = TD3Config() + plot_cfg = PlotConfig() + env = gym.make(cfg.env_name) + env.seed(1) # 随机种子 + torch.manual_seed(1) + np.random.seed(1) + state_dim = env.observation_space.shape[0] + action_dim = env.action_space.shape[0] + max_action = float(env.action_space.high[0]) + agent = TD3(state_dim,action_dim,max_action,cfg) + rewards,ma_rewards = train(cfg,env,agent) + make_dir(plot_cfg.result_path,plot_cfg.model_path) + agent.save(path=plot_cfg.model_path) + save_results(rewards,ma_rewards,tag='train',path=plot_cfg.result_path) + plot_rewards(rewards,ma_rewards,plot_cfg,tag="train") + + diff --git a/codes/assets/image-20200820174307301.png b/codes/assets/image-20200820174307301.png new file mode 100644 index 0000000..1197da0 Binary files /dev/null and b/codes/assets/image-20200820174307301.png differ diff --git a/codes/assets/image-20200820174814084.png b/codes/assets/image-20200820174814084.png new file mode 100644 index 0000000..4c9e3dc Binary files /dev/null and b/codes/assets/image-20200820174814084.png differ diff --git a/codes/common/atari_wrappers.py b/codes/common/atari_wrappers.py new file mode 100644 index 0000000..48dab94 --- /dev/null +++ b/codes/common/atari_wrappers.py @@ -0,0 +1,284 @@ +import numpy as np +import os +os.environ.setdefault('PATH', '') +from collections import deque +import gym +from gym import spaces +import cv2 +cv2.ocl.setUseOpenCL(False) +from .wrappers import TimeLimit + + +class NoopResetEnv(gym.Wrapper): + def __init__(self, env, noop_max=30): + """Sample initial states by taking random number of no-ops on reset. + No-op is assumed to be action 0. + """ + gym.Wrapper.__init__(self, env) + self.noop_max = noop_max + self.override_num_noops = None + self.noop_action = 0 + assert env.unwrapped.get_action_meanings()[0] == 'NOOP' + + def reset(self, **kwargs): + """ Do no-op action for a number of steps in [1, noop_max].""" + self.env.reset(**kwargs) + if self.override_num_noops is not None: + noops = self.override_num_noops + else: + noops = self.unwrapped.np_random.randint(1, self.noop_max + 1) #pylint: disable=E1101 + assert noops > 0 + obs = None + for _ in range(noops): + obs, _, done, _ = self.env.step(self.noop_action) + if done: + obs = self.env.reset(**kwargs) + return obs + + def step(self, ac): + return self.env.step(ac) + +class FireResetEnv(gym.Wrapper): + def __init__(self, env): + """Take action on reset for environments that are fixed until firing.""" + gym.Wrapper.__init__(self, env) + assert env.unwrapped.get_action_meanings()[1] == 'FIRE' + assert len(env.unwrapped.get_action_meanings()) >= 3 + + def reset(self, **kwargs): + self.env.reset(**kwargs) + obs, _, done, _ = self.env.step(1) + if done: + self.env.reset(**kwargs) + obs, _, done, _ = self.env.step(2) + if done: + self.env.reset(**kwargs) + return obs + + def step(self, ac): + return self.env.step(ac) + +class EpisodicLifeEnv(gym.Wrapper): + def __init__(self, env): + """Make end-of-life == end-of-episode, but only reset on true game over. + Done by DeepMind for the DQN and co. since it helps value estimation. + """ + gym.Wrapper.__init__(self, env) + self.lives = 0 + self.was_real_done = True + + def step(self, action): + obs, reward, done, info = self.env.step(action) + self.was_real_done = done + # check current lives, make loss of life terminal, + # then update lives to handle bonus lives + lives = self.env.unwrapped.ale.lives() + if lives < self.lives and lives > 0: + # for Qbert sometimes we stay in lives == 0 condition for a few frames + # so it's important to keep lives > 0, so that we only reset once + # the environment advertises done. + done = True + self.lives = lives + return obs, reward, done, info + + def reset(self, **kwargs): + """Reset only when lives are exhausted. + This way all states are still reachable even though lives are episodic, + and the learner need not know about any of this behind-the-scenes. + """ + if self.was_real_done: + obs = self.env.reset(**kwargs) + else: + # no-op step to advance from terminal/lost life state + obs, _, _, _ = self.env.step(0) + self.lives = self.env.unwrapped.ale.lives() + return obs + +class MaxAndSkipEnv(gym.Wrapper): + def __init__(self, env, skip=4): + """Return only every `skip`-th frame""" + gym.Wrapper.__init__(self, env) + # most recent raw observations (for max pooling across time steps) + self._obs_buffer = np.zeros((2,)+env.observation_space.shape, dtype=np.uint8) + self._skip = skip + + def step(self, action): + """Repeat action, sum reward, and max over last observations.""" + total_reward = 0.0 + done = None + for i in range(self._skip): + obs, reward, done, info = self.env.step(action) + if i == self._skip - 2: self._obs_buffer[0] = obs + if i == self._skip - 1: self._obs_buffer[1] = obs + total_reward += reward + if done: + break + # Note that the observation on the done=True frame + # doesn't matter + max_frame = self._obs_buffer.max(axis=0) + + return max_frame, total_reward, done, info + + def reset(self, **kwargs): + return self.env.reset(**kwargs) + +class ClipRewardEnv(gym.RewardWrapper): + def __init__(self, env): + gym.RewardWrapper.__init__(self, env) + + def reward(self, reward): + """Bin reward to {+1, 0, -1} by its sign.""" + return np.sign(reward) + + +class WarpFrame(gym.ObservationWrapper): + def __init__(self, env, width=84, height=84, grayscale=True, dict_space_key=None): + """ + Warp frames to 84x84 as done in the Nature paper and later work. + If the environment uses dictionary observations, `dict_space_key` can be specified which indicates which + observation should be warped. + """ + super().__init__(env) + self._width = width + self._height = height + self._grayscale = grayscale + self._key = dict_space_key + if self._grayscale: + num_colors = 1 + else: + num_colors = 3 + + new_space = gym.spaces.Box( + low=0, + high=255, + shape=(self._height, self._width, num_colors), + dtype=np.uint8, + ) + if self._key is None: + original_space = self.observation_space + self.observation_space = new_space + else: + original_space = self.observation_space.spaces[self._key] + self.observation_space.spaces[self._key] = new_space + assert original_space.dtype == np.uint8 and len(original_space.shape) == 3 + + def observation(self, obs): + if self._key is None: + frame = obs + else: + frame = obs[self._key] + + if self._grayscale: + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY) + frame = cv2.resize( + frame, (self._width, self._height), interpolation=cv2.INTER_AREA + ) + if self._grayscale: + frame = np.expand_dims(frame, -1) + + if self._key is None: + obs = frame + else: + obs = obs.copy() + obs[self._key] = frame + return obs + + +class FrameStack(gym.Wrapper): + def __init__(self, env, k): + """Stack k last frames. + Returns lazy array, which is much more memory efficient. + See Also + -------- + baselines.common.atari_wrappers.LazyFrames + """ + gym.Wrapper.__init__(self, env) + self.k = k + self.frames = deque([], maxlen=k) + shp = env.observation_space.shape + self.observation_space = spaces.Box(low=0, high=255, shape=(shp[:-1] + (shp[-1] * k,)), dtype=env.observation_space.dtype) + + def reset(self): + ob = self.env.reset() + for _ in range(self.k): + self.frames.append(ob) + return self._get_ob() + + def step(self, action): + ob, reward, done, info = self.env.step(action) + self.frames.append(ob) + return self._get_ob(), reward, done, info + + def _get_ob(self): + assert len(self.frames) == self.k + return LazyFrames(list(self.frames)) + +class ScaledFloatFrame(gym.ObservationWrapper): + def __init__(self, env): + gym.ObservationWrapper.__init__(self, env) + self.observation_space = gym.spaces.Box(low=0, high=1, shape=env.observation_space.shape, dtype=np.float32) + + def observation(self, observation): + # careful! This undoes the memory optimization, use + # with smaller replay buffers only. + return np.array(observation).astype(np.float32) / 255.0 + +class LazyFrames(object): + def __init__(self, frames): + """This object ensures that common frames between the observations are only stored once. + It exists purely to optimize memory usage which can be huge for DQN's 1M frames replay + buffers. + This object should only be converted to numpy array before being passed to the model. + You'd not believe how complex the previous solution was.""" + self._frames = frames + self._out = None + + def _force(self): + if self._out is None: + self._out = np.concatenate(self._frames, axis=-1) + self._frames = None + return self._out + + def __array__(self, dtype=None): + out = self._force() + if dtype is not None: + out = out.astype(dtype) + return out + + def __len__(self): + return len(self._force()) + + def __getitem__(self, i): + return self._force()[i] + + def count(self): + frames = self._force() + return frames.shape[frames.ndim - 1] + + def frame(self, i): + return self._force()[..., i] + +def make_atari(env_id, max_episode_steps=None): + env = gym.make(env_id) + assert 'NoFrameskip' in env.spec.id + env = NoopResetEnv(env, noop_max=30) + env = MaxAndSkipEnv(env, skip=4) + if max_episode_steps is not None: + env = TimeLimit(env, max_episode_steps=max_episode_steps) + return env + +def wrap_deepmind(env, episode_life=True, clip_rewards=True, frame_stack=False, scale=False): + """Configure environment for DeepMind-style Atari. + """ + if episode_life: + env = EpisodicLifeEnv(env) + if 'FIRE' in env.unwrapped.get_action_meanings(): + env = FireResetEnv(env) + env = WarpFrame(env) + if scale: + env = ScaledFloatFrame(env) + if clip_rewards: + env = ClipRewardEnv(env) + if frame_stack: + env = FrameStack(env, 4) + return env \ No newline at end of file diff --git a/codes/common/memory.py b/codes/common/memory.py new file mode 100644 index 0000000..a238696 --- /dev/null +++ b/codes/common/memory.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +@Author: John +@Email: johnjim0816@gmail.com +@Date: 2020-06-10 15:27:16 +@LastEditor: John +LastEditTime: 2021-09-15 14:52:37 +@Discription: +@Environment: python 3.7.7 +''' +import random +class ReplayBuffer: + def __init__(self, capacity): + self.capacity = capacity # 经验回放的容量 + self.buffer = [] # 缓冲区 + self.position = 0 + + def push(self, state, action, reward, next_state, done): + ''' 缓冲区是一个队列,容量超出时去掉开始存入的转移(transition) + ''' + if len(self.buffer) < self.capacity: + self.buffer.append(None) + self.buffer[self.position] = (state, action, reward, next_state, done) + self.position = (self.position + 1) % self.capacity + + def sample(self, batch_size): + batch = random.sample(self.buffer, batch_size) # 随机采出小批量转移 + state, action, reward, next_state, done = zip(*batch) # 解压成状态,动作等 + return state, action, reward, next_state, done + + def __len__(self): + ''' 返回当前存储的量 + ''' + return len(self.buffer) + diff --git a/codes/common/model.py b/codes/common/model.py new file mode 100644 index 0000000..4ab0b8b --- /dev/null +++ b/codes/common/model.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-12 21:14:12 +LastEditor: John +LastEditTime: 2021-09-15 13:21:03 +Discription: +Environment: +''' +import torch +import torch.nn as nn +import torch.nn.functional as F +from torch.distributions import Categorical + +class MLP(nn.Module): + def __init__(self, input_dim,output_dim,hidden_dim=128): + """ 初始化q网络,为全连接网络 + input_dim: 输入的特征数即环境的状态维度 + output_dim: 输出的动作维度 + """ + super(MLP, self).__init__() + self.fc1 = nn.Linear(input_dim, hidden_dim) # 输入层 + self.fc2 = nn.Linear(hidden_dim,hidden_dim) # 隐藏层 + self.fc3 = nn.Linear(hidden_dim, output_dim) # 输出层 + + def forward(self, x): + # 各层对应的激活函数 + x = F.relu(self.fc1(x)) + x = F.relu(self.fc2(x)) + return self.fc3(x) + +class Critic(nn.Module): + def __init__(self, n_obs, action_dim, hidden_size, init_w=3e-3): + super(Critic, self).__init__() + + self.linear1 = nn.Linear(n_obs + action_dim, hidden_size) + self.linear2 = nn.Linear(hidden_size, hidden_size) + self.linear3 = nn.Linear(hidden_size, 1) + # 随机初始化为较小的值 + self.linear3.weight.data.uniform_(-init_w, init_w) + self.linear3.bias.data.uniform_(-init_w, init_w) + + def forward(self, state, action): + # 按维数1拼接 + x = torch.cat([state, action], 1) + x = F.relu(self.linear1(x)) + x = F.relu(self.linear2(x)) + x = self.linear3(x) + return x + +class Actor(nn.Module): + def __init__(self, n_obs, action_dim, hidden_size, init_w=3e-3): + super(Actor, self).__init__() + self.linear1 = nn.Linear(n_obs, hidden_size) + self.linear2 = nn.Linear(hidden_size, hidden_size) + self.linear3 = nn.Linear(hidden_size, action_dim) + + self.linear3.weight.data.uniform_(-init_w, init_w) + self.linear3.bias.data.uniform_(-init_w, init_w) + + def forward(self, x): + x = F.relu(self.linear1(x)) + x = F.relu(self.linear2(x)) + x = torch.tanh(self.linear3(x)) + return x + +class ActorCritic(nn.Module): + def __init__(self, state_dim, action_dim, hidden_dim=256): + super(ActorCritic, self).__init__() + self.critic = nn.Sequential( + nn.Linear(state_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, 1) + ) + + self.actor = nn.Sequential( + nn.Linear(state_dim, hidden_dim), + nn.ReLU(), + nn.Linear(hidden_dim, action_dim), + nn.Softmax(dim=1), + ) + + def forward(self, x): + value = self.critic(x) + probs = self.actor(x) + dist = Categorical(probs) + return dist, value \ No newline at end of file diff --git a/codes/common/multiprocessing_env.py b/codes/common/multiprocessing_env.py new file mode 100644 index 0000000..28c8aba --- /dev/null +++ b/codes/common/multiprocessing_env.py @@ -0,0 +1,153 @@ +# 该代码来自 openai baseline,用于多线程环境 +# https://github.com/openai/baselines/tree/master/baselines/common/vec_env + +import numpy as np +from multiprocessing import Process, Pipe + +def worker(remote, parent_remote, env_fn_wrapper): + parent_remote.close() + env = env_fn_wrapper.x() + while True: + cmd, data = remote.recv() + if cmd == 'step': + ob, reward, done, info = env.step(data) + if done: + ob = env.reset() + remote.send((ob, reward, done, info)) + elif cmd == 'reset': + ob = env.reset() + remote.send(ob) + elif cmd == 'reset_task': + ob = env.reset_task() + remote.send(ob) + elif cmd == 'close': + remote.close() + break + elif cmd == 'get_spaces': + remote.send((env.observation_space, env.action_space)) + else: + raise NotImplementedError + +class VecEnv(object): + """ + An abstract asynchronous, vectorized environment. + """ + def __init__(self, num_envs, observation_space, action_space): + self.num_envs = num_envs + self.observation_space = observation_space + self.action_space = action_space + + def reset(self): + """ + Reset all the environments and return an array of + observations, or a tuple of observation arrays. + If step_async is still doing work, that work will + be cancelled and step_wait() should not be called + until step_async() is invoked again. + """ + pass + + def step_async(self, actions): + """ + Tell all the environments to start taking a step + with the given actions. + Call step_wait() to get the results of the step. + You should not call this if a step_async run is + already pending. + """ + pass + + def step_wait(self): + """ + Wait for the step taken with step_async(). + Returns (obs, rews, dones, infos): + - obs: an array of observations, or a tuple of + arrays of observations. + - rews: an array of rewards + - dones: an array of "episode done" booleans + - infos: a sequence of info objects + """ + pass + + def close(self): + """ + Clean up the environments' resources. + """ + pass + + def step(self, actions): + self.step_async(actions) + return self.step_wait() + + +class CloudpickleWrapper(object): + """ + Uses cloudpickle to serialize contents (otherwise multiprocessing tries to use pickle) + """ + def __init__(self, x): + self.x = x + def __getstate__(self): + import cloudpickle + return cloudpickle.dumps(self.x) + def __setstate__(self, ob): + import pickle + self.x = pickle.loads(ob) + + +class SubprocVecEnv(VecEnv): + def __init__(self, env_fns, spaces=None): + """ + envs: list of gym environments to run in subprocesses + """ + self.waiting = False + self.closed = False + nenvs = len(env_fns) + self.nenvs = nenvs + self.remotes, self.work_remotes = zip(*[Pipe() for _ in range(nenvs)]) + self.ps = [Process(target=worker, args=(work_remote, remote, CloudpickleWrapper(env_fn))) + for (work_remote, remote, env_fn) in zip(self.work_remotes, self.remotes, env_fns)] + for p in self.ps: + p.daemon = True # if the main process crashes, we should not cause things to hang + p.start() + for remote in self.work_remotes: + remote.close() + + self.remotes[0].send(('get_spaces', None)) + observation_space, action_space = self.remotes[0].recv() + VecEnv.__init__(self, len(env_fns), observation_space, action_space) + + def step_async(self, actions): + for remote, action in zip(self.remotes, actions): + remote.send(('step', action)) + self.waiting = True + + def step_wait(self): + results = [remote.recv() for remote in self.remotes] + self.waiting = False + obs, rews, dones, infos = zip(*results) + return np.stack(obs), np.stack(rews), np.stack(dones), infos + + def reset(self): + for remote in self.remotes: + remote.send(('reset', None)) + return np.stack([remote.recv() for remote in self.remotes]) + + def reset_task(self): + for remote in self.remotes: + remote.send(('reset_task', None)) + return np.stack([remote.recv() for remote in self.remotes]) + + def close(self): + if self.closed: + return + if self.waiting: + for remote in self.remotes: + remote.recv() + for remote in self.remotes: + remote.send(('close', None)) + for p in self.ps: + p.join() + self.closed = True + + def __len__(self): + return self.nenvs \ No newline at end of file diff --git a/codes/common/utils.py b/codes/common/utils.py new file mode 100644 index 0000000..6027804 --- /dev/null +++ b/codes/common/utils.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-12 16:02:24 +LastEditor: John +LastEditTime: 2021-11-30 18:39:19 +Discription: +Environment: +''' +import os +import numpy as np +from pathlib import Path +import matplotlib.pyplot as plt +import seaborn as sns + +from matplotlib.font_manager import FontProperties # 导入字体模块 + +def chinese_font(): + ''' 设置中文字体,注意需要根据自己电脑情况更改字体路径,否则还是默认的字体 + ''' + try: + font = FontProperties( + fname='/System/Library/Fonts/STHeiti Light.ttc', size=15) # fname系统字体路径,此处是mac的 + except: + font = None + return font + +def plot_rewards_cn(rewards, ma_rewards, plot_cfg, tag='train'): + ''' 中文画图 + ''' + sns.set() + plt.figure() + plt.title(u"{}环境下{}算法的学习曲线".format(plot_cfg.env_name, + plot_cfg.algo_name), fontproperties=chinese_font()) + plt.xlabel(u'回合数', fontproperties=chinese_font()) + plt.plot(rewards) + plt.plot(ma_rewards) + plt.legend((u'奖励', u'滑动平均奖励',), loc="best", prop=chinese_font()) + if plot_cfg.save: + plt.savefig(plot_cfg.result_path+f"{tag}_rewards_curve_cn") + # plt.show() + + +def plot_rewards(rewards, ma_rewards, plot_cfg, tag='train'): + sns.set() + plt.figure() # 创建一个图形实例,方便同时多画几个图 + plt.title("learning curve on {} of {} for {}".format( + plot_cfg.device, plot_cfg.algo_name, plot_cfg.env_name)) + plt.xlabel('epsiodes') + plt.plot(rewards, label='rewards') + plt.plot(ma_rewards, label='ma rewards') + plt.legend() + if plot_cfg.save: + plt.savefig(plot_cfg.result_path+"{}_rewards_curve".format(tag)) + plt.show() + + +def plot_losses(losses, algo="DQN", save=True, path='./'): + sns.set() + plt.figure() + plt.title("loss curve of {}".format(algo)) + plt.xlabel('epsiodes') + plt.plot(losses, label='rewards') + plt.legend() + if save: + plt.savefig(path+"losses_curve") + plt.show() + + +def save_results(rewards, ma_rewards, tag='train', path='./results'): + ''' 保存奖励 + ''' + np.save(path+'{}_rewards.npy'.format(tag), rewards) + np.save(path+'{}_ma_rewards.npy'.format(tag), ma_rewards) + print('结果保存完毕!') + + +def make_dir(*paths): + ''' 创建文件夹 + ''' + for path in paths: + Path(path).mkdir(parents=True, exist_ok=True) + + +def del_empty_dir(*paths): + ''' 删除目录下所有空文件夹 + ''' + for path in paths: + dirs = os.listdir(path) + for dir in dirs: + if not os.listdir(os.path.join(path, dir)): + os.removedirs(os.path.join(path, dir)) diff --git a/codes/common/wrappers.py b/codes/common/wrappers.py new file mode 100644 index 0000000..4793b36 --- /dev/null +++ b/codes/common/wrappers.py @@ -0,0 +1,29 @@ +import gym + +class TimeLimit(gym.Wrapper): + def __init__(self, env, max_episode_steps=None): + super(TimeLimit, self).__init__(env) + self._max_episode_steps = max_episode_steps + self._elapsed_steps = 0 + + def step(self, ac): + observation, reward, done, info = self.env.step(ac) + self._elapsed_steps += 1 + if self._elapsed_steps >= self._max_episode_steps: + done = True + info['TimeLimit.truncated'] = True + return observation, reward, done, info + + def reset(self, **kwargs): + self._elapsed_steps = 0 + return self.env.reset(**kwargs) + +class ClipActionsWrapper(gym.Wrapper): + def step(self, action): + import numpy as np + action = np.nan_to_num(action) + action = np.clip(action, self.action_space.low, self.action_space.high) + return self.env.step(action) + + def reset(self, **kwargs): + return self.env.reset(**kwargs) \ No newline at end of file diff --git a/codes/envs/README.md b/codes/envs/README.md new file mode 100644 index 0000000..e93fba0 --- /dev/null +++ b/codes/envs/README.md @@ -0,0 +1,6 @@ +## 环境汇总 + +[OpenAI Gym](./gym_info.md) +[MuJoCo](./mujoco_info.md) + + diff --git a/codes/envs/assets/action_grid.png b/codes/envs/assets/action_grid.png new file mode 100644 index 0000000..7759f8b Binary files /dev/null and b/codes/envs/assets/action_grid.png differ diff --git a/codes/envs/assets/gym_info_20211130180023.png b/codes/envs/assets/gym_info_20211130180023.png new file mode 100644 index 0000000..723b67f Binary files /dev/null and b/codes/envs/assets/gym_info_20211130180023.png differ diff --git a/codes/envs/assets/image-20200820174307301.png b/codes/envs/assets/image-20200820174307301.png new file mode 100644 index 0000000..1197da0 Binary files /dev/null and b/codes/envs/assets/image-20200820174307301.png differ diff --git a/codes/envs/assets/image-20200820174814084.png b/codes/envs/assets/image-20200820174814084.png new file mode 100644 index 0000000..4c9e3dc Binary files /dev/null and b/codes/envs/assets/image-20200820174814084.png differ diff --git a/codes/envs/assets/image-20201007211441036.png b/codes/envs/assets/image-20201007211441036.png new file mode 100644 index 0000000..ae5b0f8 Binary files /dev/null and b/codes/envs/assets/image-20201007211441036.png differ diff --git a/codes/envs/assets/image-20201007211858925.png b/codes/envs/assets/image-20201007211858925.png new file mode 100644 index 0000000..0bbb5b2 Binary files /dev/null and b/codes/envs/assets/image-20201007211858925.png differ diff --git a/codes/envs/assets/image-20210429150622353.png b/codes/envs/assets/image-20210429150622353.png new file mode 100644 index 0000000..1216b4c Binary files /dev/null and b/codes/envs/assets/image-20210429150622353.png differ diff --git a/codes/envs/assets/image-20210429150630806.png b/codes/envs/assets/image-20210429150630806.png new file mode 100644 index 0000000..45107d5 Binary files /dev/null and b/codes/envs/assets/image-20210429150630806.png differ diff --git a/codes/envs/assets/track_big.png b/codes/envs/assets/track_big.png new file mode 100644 index 0000000..f7b3dc1 Binary files /dev/null and b/codes/envs/assets/track_big.png differ diff --git a/codes/envs/blackjack.py b/codes/envs/blackjack.py new file mode 100644 index 0000000..6946895 --- /dev/null +++ b/codes/envs/blackjack.py @@ -0,0 +1,122 @@ +import gym +from gym import spaces +from gym.utils import seeding + +def cmp(a, b): + return int((a > b)) - int((a < b)) + +# 1 = Ace, 2-10 = Number cards, Jack/Queen/King = 10 +deck = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10, 10, 10] + + +def draw_card(np_random): + return np_random.choice(deck) + + +def draw_hand(np_random): + return [draw_card(np_random), draw_card(np_random)] + + +def usable_ace(hand): # Does this hand have a usable ace? + return 1 in hand and sum(hand) + 10 <= 21 + + +def sum_hand(hand): # Return current hand total + if usable_ace(hand): + return sum(hand) + 10 + return sum(hand) + + +def is_bust(hand): # Is this hand a bust? + return sum_hand(hand) > 21 + + +def score(hand): # What is the score of this hand (0 if bust) + return 0 if is_bust(hand) else sum_hand(hand) + + +def is_natural(hand): # Is this hand a natural blackjack? + return sorted(hand) == [1, 10] + + +class BlackjackEnv(gym.Env): + """Simple blackjack environment + Blackjack is a card game where the goal is to obtain cards that sum to as + near as possible to 21 without going over. They're playing against a fixed + dealer. + Face cards (Jack, Queen, King) have point value 10. + Aces can either count as 11 or 1, and it's called 'usable' at 11. + This game is placed with an infinite deck (or with replacement). + The game starts with each (player and dealer) having one face up and one + face down card. + The player can request additional cards (hit=1) until they decide to stop + (stick=0) or exceed 21 (bust). + After the player sticks, the dealer reveals their facedown card, and draws + until their sum is 17 or greater. If the dealer goes bust the player wins. + If neither player nor dealer busts, the outcome (win, lose, draw) is + decided by whose sum is closer to 21. The reward for winning is +1, + drawing is 0, and losing is -1. + The observation of a 3-tuple of: the players current sum, + the dealer's one showing card (1-10 where 1 is ace), + and whether or not the player holds a usable ace (0 or 1). + This environment corresponds to the version of the blackjack problem + described in Example 5.1 in Reinforcement Learning: An Introduction + by Sutton and Barto (1998). + https://webdocs.cs.ualberta.ca/~sutton/book/the-book.html + """ + def __init__(self, natural=False): + self.action_space = spaces.Discrete(2) + self.observation_space = spaces.Tuple(( + spaces.Discrete(32), + spaces.Discrete(11), + spaces.Discrete(2))) + self._seed() + + # Flag to payout 1.5 on a "natural" blackjack win, like casino rules + # Ref: http://www.bicyclecards.com/how-to-play/blackjack/ + self.natural = natural + # Start the first game + self._reset() # Number of + self.action_dim = 2 + + def reset(self): + return self._reset() + + def step(self, action): + return self._step(action) + + def _seed(self, seed=None): + self.np_random, seed = seeding.np_random(seed) + return [seed] + + def _step(self, action): + assert self.action_space.contains(action) + if action: # hit: add a card to players hand and return + self.player.append(draw_card(self.np_random)) + if is_bust(self.player): + done = True + reward = -1 + else: + done = False + reward = 0 + else: # stick: play out the dealers hand, and score + done = True + while sum_hand(self.dealer) < 17: + self.dealer.append(draw_card(self.np_random)) + reward = cmp(score(self.player), score(self.dealer)) + if self.natural and is_natural(self.player) and reward == 1: + reward = 1.5 + return self._get_obs(), reward, done, {} + + def _get_obs(self): + return (sum_hand(self.player), self.dealer[0], usable_ace(self.player)) + + def _reset(self): + self.dealer = draw_hand(self.np_random) + self.player = draw_hand(self.np_random) + + # Auto-draw another card if the score is less than 12 + while sum_hand(self.player) < 12: + self.player.append(draw_card(self.np_random)) + + return self._get_obs() diff --git a/codes/envs/cliff_walking.py b/codes/envs/cliff_walking.py new file mode 100644 index 0000000..73e33c7 --- /dev/null +++ b/codes/envs/cliff_walking.py @@ -0,0 +1,84 @@ +import numpy as np +import sys +from gym.envs.toy_text import discrete + + +UP = 0 +RIGHT = 1 +DOWN = 2 +LEFT = 3 + +class CliffWalkingEnv(discrete.DiscreteEnv): + + metadata = {'render.modes': ['human', 'ansi']} + + def _limit_coordinates(self, coord): + coord[0] = min(coord[0], self.shape[0] - 1) + coord[0] = max(coord[0], 0) + coord[1] = min(coord[1], self.shape[1] - 1) + coord[1] = max(coord[1], 0) + return coord + + def _calculate_transition_prob(self, current, delta): + new_position = np.array(current) + np.array(delta) + new_position = self._limit_coordinates(new_position).astype(int) + new_state = np.ravel_multi_index(tuple(new_position), self.shape) + reward = -100.0 if self._cliff[tuple(new_position)] else -1.0 + is_done = self._cliff[tuple(new_position)] or (tuple(new_position) == (3,11)) + return [(1.0, new_state, reward, is_done)] + + def __init__(self): + self.shape = (4, 12) + + nS = np.prod(self.shape) + action_dim = 4 + + # Cliff Location + self._cliff = np.zeros(self.shape, dtype=np.bool) + self._cliff[3, 1:-1] = True + + # Calculate transition probabilities + P = {} + for s in range(nS): + position = np.unravel_index(s, self.shape) + P[s] = { a : [] for a in range(action_dim) } + P[s][UP] = self._calculate_transition_prob(position, [-1, 0]) + P[s][RIGHT] = self._calculate_transition_prob(position, [0, 1]) + P[s][DOWN] = self._calculate_transition_prob(position, [1, 0]) + P[s][LEFT] = self._calculate_transition_prob(position, [0, -1]) + + # We always start in state (3, 0) + isd = np.zeros(nS) + isd[np.ravel_multi_index((3,0), self.shape)] = 1.0 + + super(CliffWalkingEnv, self).__init__(nS, action_dim, P, isd) + + def render(self, mode='human', close=False): + self._render(mode, close) + + def _render(self, mode='human', close=False): + if close: + return + + outfile = StringIO() if mode == 'ansi' else sys.stdout + + for s in range(self.nS): + position = np.unravel_index(s, self.shape) + # print(self.s) + if self.s == s: + output = " x " + elif position == (3,11): + output = " T " + elif self._cliff[position]: + output = " C " + else: + output = " o " + + if position[1] == 0: + output = output.lstrip() + if position[1] == self.shape[1] - 1: + output = output.rstrip() + output += "\n" + + outfile.write(output) + outfile.write("\n") diff --git a/codes/envs/gridworld.py b/codes/envs/gridworld.py new file mode 100644 index 0000000..c4fd512 --- /dev/null +++ b/codes/envs/gridworld.py @@ -0,0 +1,125 @@ +import io +import numpy as np +import sys +from gym.envs.toy_text import discrete + +UP = 0 +RIGHT = 1 +DOWN = 2 +LEFT = 3 + +class GridworldEnv(discrete.DiscreteEnv): + """ + Grid World environment from Sutton's Reinforcement Learning book chapter 4. + You are an agent on an MxN grid and your goal is to reach the terminal + state at the top left or the bottom right corner. + + For example, a 4x4 grid looks as follows: + + T o o o + o x o o + o o o o + o o o T + + x is your position and T are the two terminal states. + + You can take actions in each direction (UP=0, RIGHT=1, DOWN=2, LEFT=3). + Actions going off the edge leave you in your current state. + You receive a reward of -1 at each step until you reach a terminal state. + """ + + metadata = {'render.modes': ['human', 'ansi']} + + def __init__(self, shape=[4,4]): + if not isinstance(shape, (list, tuple)) or not len(shape) == 2: + raise ValueError('shape argument must be a list/tuple of length 2') + + self.shape = shape + + nS = np.prod(shape) + action_dim = 4 + + MAX_Y = shape[0] + MAX_X = shape[1] + + P = {} + grid = np.arange(nS).reshape(shape) + it = np.nditer(grid, flags=['multi_index']) + + while not it.finished: + s = it.iterindex + y, x = it.multi_index + + # P[s][a] = (prob, next_state, reward, is_done) + P[s] = {a : [] for a in range(action_dim)} + + is_done = lambda s: s == 0 or s == (nS - 1) + reward = 0.0 if is_done(s) else -1.0 + + # We're stuck in a terminal state + if is_done(s): + P[s][UP] = [(1.0, s, reward, True)] + P[s][RIGHT] = [(1.0, s, reward, True)] + P[s][DOWN] = [(1.0, s, reward, True)] + P[s][LEFT] = [(1.0, s, reward, True)] + # Not a terminal state + else: + ns_up = s if y == 0 else s - MAX_X + ns_right = s if x == (MAX_X - 1) else s + 1 + ns_down = s if y == (MAX_Y - 1) else s + MAX_X + ns_left = s if x == 0 else s - 1 + P[s][UP] = [(1.0, ns_up, reward, is_done(ns_up))] + P[s][RIGHT] = [(1.0, ns_right, reward, is_done(ns_right))] + P[s][DOWN] = [(1.0, ns_down, reward, is_done(ns_down))] + P[s][LEFT] = [(1.0, ns_left, reward, is_done(ns_left))] + + it.iternext() + + # Initial state distribution is uniform + isd = np.ones(nS) / nS + + # We expose the model of the environment for educational purposes + # This should not be used in any model-free learning algorithm + self.P = P + + super(GridworldEnv, self).__init__(nS, action_dim, P, isd) + + def _render(self, mode='human', close=False): + """ Renders the current gridworld layout + + For example, a 4x4 grid with the mode="human" looks like: + T o o o + o x o o + o o o o + o o o T + where x is your position and T are the two terminal states. + """ + if close: + return + + outfile = io.StringIO() if mode == 'ansi' else sys.stdout + + grid = np.arange(self.nS).reshape(self.shape) + it = np.nditer(grid, flags=['multi_index']) + while not it.finished: + s = it.iterindex + y, x = it.multi_index + + if self.s == s: + output = " x " + elif s == 0 or s == self.nS - 1: + output = " T " + else: + output = " o " + + if x == 0: + output = output.lstrip() + if x == self.shape[1] - 1: + output = output.rstrip() + + outfile.write(output) + + if x == self.shape[1] - 1: + outfile.write("\n") + + it.iternext() diff --git a/codes/envs/gridworld_env.py b/codes/envs/gridworld_env.py new file mode 100644 index 0000000..31d968f --- /dev/null +++ b/codes/envs/gridworld_env.py @@ -0,0 +1,195 @@ +# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# -*- coding: utf-8 -*- + +import gym +import turtle +import numpy as np + +# turtle tutorial : https://docs.python.org/3.3/library/turtle.html + + +def GridWorld(gridmap=None, is_slippery=False): + if gridmap is None: + gridmap = ['SFFF', 'FHFH', 'FFFH', 'HFFG'] + env = gym.make("FrozenLake-v0", desc=gridmap, is_slippery=False) + env = FrozenLakeWapper(env) + return env + + +class FrozenLakeWapper(gym.Wrapper): + def __init__(self, env): + gym.Wrapper.__init__(self, env) + self.max_y = env.desc.shape[0] + self.max_x = env.desc.shape[1] + self.t = None + self.unit = 50 + + def draw_box(self, x, y, fillcolor='', line_color='gray'): + self.t.up() + self.t.goto(x * self.unit, y * self.unit) + self.t.color(line_color) + self.t.fillcolor(fillcolor) + self.t.setheading(90) + self.t.down() + self.t.begin_fill() + for _ in range(4): + self.t.forward(self.unit) + self.t.right(90) + self.t.end_fill() + + def move_player(self, x, y): + self.t.up() + self.t.setheading(90) + self.t.fillcolor('red') + self.t.goto((x + 0.5) * self.unit, (y + 0.5) * self.unit) + + def render(self): + if self.t == None: + self.t = turtle.Turtle() + self.wn = turtle.Screen() + self.wn.setup(self.unit * self.max_x + 100, + self.unit * self.max_y + 100) + self.wn.setworldcoordinates(0, 0, self.unit * self.max_x, + self.unit * self.max_y) + self.t.shape('circle') + self.t.width(2) + self.t.speed(0) + self.t.color('gray') + for i in range(self.desc.shape[0]): + for j in range(self.desc.shape[1]): + x = j + y = self.max_y - 1 - i + if self.desc[i][j] == b'S': # Start + self.draw_box(x, y, 'white') + elif self.desc[i][j] == b'F': # Frozen ice + self.draw_box(x, y, 'white') + elif self.desc[i][j] == b'G': # Goal + self.draw_box(x, y, 'yellow') + elif self.desc[i][j] == b'H': # Hole + self.draw_box(x, y, 'black') + else: + self.draw_box(x, y, 'white') + self.t.shape('turtle') + + x_pos = self.s % self.max_x + y_pos = self.max_y - 1 - int(self.s / self.max_x) + self.move_player(x_pos, y_pos) + + +class CliffWalkingWapper(gym.Wrapper): + def __init__(self, env): + gym.Wrapper.__init__(self, env) + self.t = None + self.unit = 50 + self.max_x = 12 + self.max_y = 4 + + def draw_x_line(self, y, x0, x1, color='gray'): + assert x1 > x0 + self.t.color(color) + self.t.setheading(0) + self.t.up() + self.t.goto(x0, y) + self.t.down() + self.t.forward(x1 - x0) + + def draw_y_line(self, x, y0, y1, color='gray'): + assert y1 > y0 + self.t.color(color) + self.t.setheading(90) + self.t.up() + self.t.goto(x, y0) + self.t.down() + self.t.forward(y1 - y0) + + def draw_box(self, x, y, fillcolor='', line_color='gray'): + self.t.up() + self.t.goto(x * self.unit, y * self.unit) + self.t.color(line_color) + self.t.fillcolor(fillcolor) + self.t.setheading(90) + self.t.down() + self.t.begin_fill() + for i in range(4): + self.t.forward(self.unit) + self.t.right(90) + self.t.end_fill() + + def move_player(self, x, y): + self.t.up() + self.t.setheading(90) + self.t.fillcolor('red') + self.t.goto((x + 0.5) * self.unit, (y + 0.5) * self.unit) + + def render(self): + if self.t == None: + self.t = turtle.Turtle() + self.wn = turtle.Screen() + self.wn.setup(self.unit * self.max_x + 100, + self.unit * self.max_y + 100) + self.wn.setworldcoordinates(0, 0, self.unit * self.max_x, + self.unit * self.max_y) + self.t.shape('circle') + self.t.width(2) + self.t.speed(0) + self.t.color('gray') + for _ in range(2): + self.t.forward(self.max_x * self.unit) + self.t.left(90) + self.t.forward(self.max_y * self.unit) + self.t.left(90) + for i in range(1, self.max_y): + self.draw_x_line( + y=i * self.unit, x0=0, x1=self.max_x * self.unit) + for i in range(1, self.max_x): + self.draw_y_line( + x=i * self.unit, y0=0, y1=self.max_y * self.unit) + + for i in range(1, self.max_x - 1): + self.draw_box(i, 0, 'black') + self.draw_box(self.max_x - 1, 0, 'yellow') + self.t.shape('turtle') + + x_pos = self.s % self.max_x + y_pos = self.max_y - 1 - int(self.s / self.max_x) + self.move_player(x_pos, y_pos) + + +if __name__ == '__main__': + # 环境1:FrozenLake, 可以配置冰面是否是滑的 + # 0 left, 1 down, 2 right, 3 up + env = gym.make("FrozenLake-v0", is_slippery=False) + env = FrozenLakeWapper(env) + + # 环境2:CliffWalking, 悬崖环境 + # env = gym.make("CliffWalking-v0") # 0 up, 1 right, 2 down, 3 left + # env = CliffWalkingWapper(env) + + # 环境3:自定义格子世界,可以配置地图, S为出发点Start, F为平地Floor, H为洞Hole, G为出口目标Goal + # gridmap = [ + # 'SFFF', + # 'FHFF', + # 'FFFF', + # 'HFGF' ] + # env = GridWorld(gridmap) + + env.reset() + for step in range(10): + action = np.random.randint(0, 4) + obs, reward, done, info = env.step(action) + print('step {}: action {}, obs {}, reward {}, done {}, info {}'.format(\ + step, action, obs, reward, done, info)) + # env.render() # 渲染一帧图像 \ No newline at end of file diff --git a/codes/envs/gym_info.md b/codes/envs/gym_info.md new file mode 100644 index 0000000..49da18f --- /dev/null +++ b/codes/envs/gym_info.md @@ -0,0 +1,50 @@ +# OpenAi Gym 环境说明 +## 基础控制 + +### [CartPole v0](https://github.com/openai/gym/wiki/CartPole-v0) + +image-20200820174307301 + +通过向左或向右推车能够实现平衡,所以动作空间由两个动作组成。每进行一个step就会给一个reward,如果无法保持平衡那么done等于true,本次episode失败。理想状态下,每个episode至少能进行200个step,也就是说每个episode的reward总和至少为200,step数目至少为200 + +### CartPole-v1 + +```CartPole v1```环境其实跟```CartPole v0```是一模一样的,区别在于每回合最大步数(max_episode_steps)以及奖励阈值(reward_threshold),如下是相关源码: + +![](assets/gym_info_20211130180023.png) + +这里先解释一下奖励阈值(reward_threshold),即Gym设置的一个合格标准,比如对于```CartPole v0```如果算法能够将奖励收敛到195以上,说明该算法合格。但实际上```CartPole v0```的每回合最大步数(max_episode_steps)是200,每步的奖励最大是1,也就是每回合最大奖励是200,比Gym设置的奖励阈值高。笔者猜测这是Gym可能是给算法学习者们设置的一个参考线,而实际中在写算法时并不会用到这个算法阈值,所以可以忽略。 + +再看每回合最大步数,可以看到```CartPole v1```的步数更长,相应的奖励要求更高,可以理解为```v1```是```v0```的难度升级版。 + + +### [Pendulum-v0](https://github.com/openai/gym/wiki/Pendulum-v0) + +注:gym 0.18.0之后版本中Pendulum-v0已经改为Pendulum-v1 +image-20200820174814084 + +钟摆以随机位置开始,目标是将其摆动,使其保持向上直立。动作空间是连续的,值的区间为[-2,2]。每个step给的reward最低为-16.27,最高为0。目前最好的成绩是100个episode的reward之和为-123.11 ± 6.86。 + +### + +悬崖寻路问题(CliffWalking)是指在一个4 x 12的网格中,智能体以网格的左下角位置为起点,以网格的下角位置为终点,目标是移动智能体到达终点位置,智能体每次可以在上、下、左、右这4个方向中移动一步,每移动一步会得到-1单位的奖励。 + +image-20201007211441036 + +如图,红色部分表示悬崖,数字代表智能体能够观测到的位置信息,即observation,总共会有0-47等48个不同的值,智能体再移动中会有以下限制: + +* 智能体不能移出网格,如果智能体想执行某个动作移出网格,那么这一步智能体不会移动,但是这个操作依然会得到-1单位的奖励 + +* 如果智能体“掉入悬崖” ,会立即回到起点位置,并得到-100单位的奖励 + +* 当智能体移动到终点时,该回合结束,该回合总奖励为各步奖励之和 + +实际的仿真界面如下: + +image-20201007211858925 + +由于从起点到终点最少需要13步,每步得到-1的reward,因此最佳训练算法下,每个episode下reward总和应该为-13。 + +## 参考 + +[Gym环境相关源码](https://github.com/openai/gym/tree/master/gym/envs) \ No newline at end of file diff --git a/codes/envs/mujoco_info.md b/codes/envs/mujoco_info.md new file mode 100644 index 0000000..aaa8cbb --- /dev/null +++ b/codes/envs/mujoco_info.md @@ -0,0 +1,42 @@ +# MuJoCo + +MuJoCo(Multi-Joint dynamics with Contact)是一个物理模拟器,可以用于机器人控制优化等研究。安装见[Mac安装MuJoCo以及mujoco_py](https://blog.csdn.net/JohnJim0/article/details/115656392?spm=1001.2014.3001.5501) + + + +## HalfCheetah-v2 + + + +该环境基于mujoco仿真引擎,该环境的目的是使一只两只脚的“猎豹”跑得越快越好(下面图谷歌HalfCheetah-v2的,https://gym.openai.com/envs/HalfCheetah-v2/)。 + +image-20210429150630806 + +动作空间:Box(6,),一只脚需要控制三个关节一共6个关节,每个关节的运动范围为[-1, 1]。 + +状态空间:Box(17, ),包含各种状态,每个值的范围为![img](assets/9cd6ae68c9aad008ede4139da358ec26.svg),主要描述“猎豹”本身的姿态等信息。 + +回报定义:每一步的回报与这一步的中猎豹的速度和猎豹行动的消耗有关,定义回报的代码如下。 + +```python +def step(self, action): + xposbefore = self.sim.data.qpos[0] + self.do_simulation(action, self.frame_skip) + xposafter = self.sim.data.qpos[0] + ob = self._get_obs() + reward_ctrl = - 0.1 * np.square(action).sum() + reward_run = (xposafter - xposbefore)/self.dt + # =========== reward =========== + reward = reward_ctrl + reward_run + # =========== reward =========== + done = False + return ob, reward, done, dict(reward_run=reward_run, reward_ctrl=reward_ctrl) +``` + +当猎豹无法控制平衡而倒下时,一个回合(episode)结束。 + +但是这个环境有一些问题,目前经过搜索并不知道一个回合的reward上限,实验中训练好的episode能跑出平台之外: + +image-20210429150622353 + +加上时间有限,所以训练中reward一直处于一个平缓上升的状态,本人猜测这可能是gym的一个bug。 \ No newline at end of file diff --git a/codes/envs/racetrack_env.md b/codes/envs/racetrack_env.md new file mode 100644 index 0000000..c5e2d7f --- /dev/null +++ b/codes/envs/racetrack_env.md @@ -0,0 +1,37 @@ +## The Racetrack Environment +We have implemented a custom environment called "Racetrack" for you to use during this piece of coursework. It is inspired by the environment described in the course textbook (Reinforcement Learning, Sutton & Barto, 2018, Exercise 5.12), but is not exactly the same. + +### Environment Description + +Consider driving a race car around a turn on a racetrack. In order to complete the race as quickly as possible, you would want to drive as fast as you can but, to avoid running off the track, you must slow down while turning. + +In our simplified racetrack environment, the agent is at one of a discrete set of grid positions. The agent also has a discrete speed in two directions, $x$ and $y$. So the state is represented as follows: +$$(\text{position}_y, \text{position}_x, \text{velocity}_y, \text{velocity}_x)$$ + +The agent collects a reward of -1 at each time step, an additional -10 for leaving the track (i.e., ending up on a black grid square in the figure below), and an additional +10 for reaching the finish line (any of the red grid squares). The agent starts each episode in a randomly selected grid-square on the starting line (green grid squares) with a speed of zero in both directions. At each time step, the agent can change its speed in both directions. Each speed can be changed by +1, -1 or 0, giving a total of nine actions. For example, the agent may increase its speed in the $x$ direction by -1 and its speed in the $y$ direction by +1. The agent's speed cannot be greater than +10 or less than -10 in either direction. + + + + +The agent's next state is determined by its current grid square, its current speed in two directions, and the changes it makes to its speed in the two directions. This environment is stochastic. When the agent tries to change its speed, no change occurs (in either direction) with probability 0.2. In other words, 20% of the time, the agent's action is ignored and the car's speed remains the same in both directions. + +If the agent leaves the track, it is returned to a random start grid-square and has its speed set to zero in both directions; the episode continues. An episode ends only when the agent transitions to a goal grid-square. + + + +### Environment Implementation +We have implemented the above environment in the `racetrack_env.py` file, for you to use in this coursework. Please use this implementation instead of writing your own, and please do not modify the environment. + +We provide a `RacetrackEnv` class for your agents to interact with. The class has the following methods: +- **`reset()`** - this method initialises the environment, chooses a random starting state, and returns it. This method should be called before the start of every episode. +- **`step(action)`** - this method takes an integer action (more on this later), and executes one time-step in the environment. It returns a tuple containing the next state, the reward collected, and whether the next state is a terminal state. +- **`render(sleep_time)`** - this method renders a matplotlib graph representing the environment. It takes an optional float parameter giving the number of seconds to display each time-step. This method is useful for testing and debugging, but should not be used during training since it is *very* slow. **Do not use this method in your final submission**. +- **`get_actions()`** - a simple method that returns the available actions in the current state. Always returns a list containing integers in the range [0-8] (more on this later). + +In our code, states are represented as Python tuples - specifically a tuple of four integers. For example, if the agent is in a grid square with coordinates ($Y = 2$, $X = 3$), and is moving zero cells vertically and one cell horizontally per time-step, the state is represented as `(2, 3, 0, 1)`. Tuples of this kind will be returned by the `reset()` and `step(action)` methods. + +There are nine actions available to the agent in each state, as described above. However, to simplify your code, we have represented each of the nine actions as an integer in the range [0-8]. The table below shows the index of each action, along with the corresponding changes it will cause to the agent's speed in each direction. + + + +For example, taking action 8 will increase the agent's speed in the $x$ direction, but decrease its speed in the $y$ direction. \ No newline at end of file diff --git a/codes/envs/racetrack_env.py b/codes/envs/racetrack_env.py new file mode 100644 index 0000000..d6684f5 --- /dev/null +++ b/codes/envs/racetrack_env.py @@ -0,0 +1,260 @@ +# Please do not make changes to this file - it will be overwritten with a clean +# version when your work is marked. +# +# This file contains code for the racetrack environment that you will be using +# as part of the second part of the CM50270: Reinforcement Learning coursework. + +import time +import random +import numpy as np +import os +import matplotlib.pyplot as plt +import matplotlib.patheffects as pe +from IPython.display import clear_output + +from matplotlib import colors + +class RacetrackEnv(object) : + """ + Class representing a race-track environment inspired by exercise 5.12 in Sutton & Barto 2018 (p.111). + Please do not make changes to this class - it will be overwritten with a clean version when it comes to marking. + + The dynamics of this environment are detailed in this coursework exercise's jupyter notebook, although I have + included rather verbose comments here for those of you who are interested in how the environment has been + implemented (though this should not impact your solution code). + + If you find any *bugs* with this code, please let me know immediately - thank you for finding them, sorry that I didn't! + However, please do not suggest optimisations - some things have been purposely simplified for readability's sake. + """ + + + ACTIONS_DICT = { + 0 : (1, -1), # Acc Vert., Brake Horiz. + 1 : (1, 0), # Acc Vert., Hold Horiz. + 2 : (1, 1), # Acc Vert., Acc Horiz. + 3 : (0, -1), # Hold Vert., Brake Horiz. + 4 : (0, 0), # Hold Vert., Hold Horiz. + 5 : (0, 1), # Hold Vert., Acc Horiz. + 6 : (-1, -1), # Brake Vert., Brake Horiz. + 7 : (-1, 0), # Brake Vert., Hold Horiz. + 8 : (-1, 1) # Brake Vert., Acc Horiz. + } + + + CELL_TYPES_DICT = { + 0 : "track", + 1 : "wall", + 2 : "start", + 3 : "goal" + } + + + def __init__(self) : + # Load racetrack map from file. + self.track = np.flip(np.loadtxt(os.path.dirname(__file__)+"/track.txt", dtype = int), axis = 0) + + + # Discover start grid squares. + self.initial_states = [] + for y in range(self.track.shape[0]) : + for x in range(self.track.shape[1]) : + if (self.CELL_TYPES_DICT[self.track[y, x]] == "start") : + self.initial_states.append((y, x)) + + + self.is_reset = False + + #print("Racetrack Environment File Loaded Successfully.") + #print("Be sure to call .reset() before starting to initialise the environment and get an initial state!") + + + def step(self, action : int) : + """ + Takes a given action in the environment's current state, and returns a next state, + reward, and whether the next state is terminal or not. + + Arguments: + action {int} -- The action to take in the environment's current state. Should be an integer in the range [0-8]. + + Raises: + RuntimeError: Raised when the environment needs resetting.\n + TypeError: Raised when an action of an invalid type is given.\n + ValueError: Raised when an action outside the range [0-8] is given.\n + + Returns: + A tuple of:\n + {(int, int, int, int)} -- The next state, a tuple of (y_pos, x_pos, y_velocity, x_velocity).\n + {int} -- The reward earned by taking the given action in the current environment state.\n + {bool} -- Whether the environment's next state is terminal or not.\n + + """ + + # Check whether a reset is needed. + if (not self.is_reset) : + raise RuntimeError(".step() has been called when .reset() is needed.\n" + + "You need to call .reset() before using .step() for the first time, and after an episode ends.\n" + + ".reset() initialises the environment at the start of an episode, then returns an initial state.") + + # Check that action is the correct type (either a python integer or a numpy integer). + if (not (isinstance(action, int) or isinstance(action, np.integer))) : + raise TypeError("action should be an integer.\n" + + "action value {} of type {} was supplied.".format(action, type(action))) + + # Check that action is an allowed value. + if (action < 0 or action > 8) : + raise ValueError("action must be an integer in the range [0-8] corresponding to one of the legal actions.\n" + + "action value {} was supplied.".format(action)) + + + # Update Velocity. + # With probability, 0.85 update velocity components as intended. + if (np.random.uniform() < 0.8) : + (d_y, d_x) = self.ACTIONS_DICT[action] + # With probability, 0.15 Do not change velocity components. + else : + (d_y, d_x) = (0, 0) + + self.velocity = (self.velocity[0] + d_y, self.velocity[1] + d_x) + + # Keep velocity within bounds (-10, 10). + if (self.velocity[0] > 10) : + self.velocity[0] = 10 + elif (self.velocity[0] < -10) : + self.velocity[0] = -10 + if (self.velocity[1] > 10) : + self.velocity[1] = 10 + elif (self.velocity[1] < -10) : + self.velocity[1] = -10 + + # Update Position. + new_position = (self.position[0] + self.velocity[0], self.position[1] + self.velocity[1]) + + reward = 0 + terminal = False + + # If position is out-of-bounds, return to start and set velocity components to zero. + if (new_position[0] < 0 or new_position[1] < 0 or new_position[0] >= self.track.shape[0] or new_position[1] >= self.track.shape[1]) : + self.position = random.choice(self.initial_states) + self.velocity = (0, 0) + reward -= 10 + # If position is in a wall grid-square, return to start and set velocity components to zero. + elif (self.CELL_TYPES_DICT[self.track[new_position]] == "wall") : + self.position = random.choice(self.initial_states) + self.velocity = (0, 0) + reward -= 10 + # If position is in a track grid-squre or a start-square, update position. + elif (self.CELL_TYPES_DICT[self.track[new_position]] in ["track", "start"]) : + self.position = new_position + # If position is in a goal grid-square, end episode. + elif (self.CELL_TYPES_DICT[self.track[new_position]] == "goal") : + self.position = new_position + reward += 10 + terminal = True + # If this gets reached, then the student has touched something they shouldn't have. Naughty! + else : + raise RuntimeError("You've met with a terrible fate, haven't you?\nDon't modify things you shouldn't!") + + # Penalise every timestep. + reward -= 1 + + # Require a reset if the current state is terminal. + if (terminal) : + self.is_reset = False + + # Return next state, reward, and whether the episode has ended. + return (self.position[0], self.position[1], self.velocity[0], self.velocity[1]), reward, terminal + + + def reset(self) : + """ + Resets the environment, ready for a new episode to begin, then returns an initial state. + The initial state will be a starting grid square randomly chosen using a uniform distribution, + with both components of the velocity being zero. + + Returns: + {(int, int, int, int)} -- an initial state, a tuple of (y_pos, x_pos, y_velocity, x_velocity). + """ + + # Pick random starting grid-square. + self.position = random.choice(self.initial_states) + + # Set both velocity components to zero. + self.velocity = (0, 0) + + self.is_reset = True + + return (self.position[0], self.position[1], self.velocity[0], self.velocity[1]) + + + def render(self, sleep_time : float = 0.1) : + """ + Renders a pretty matplotlib plot representing the current state of the environment. + Calling this method on subsequent timesteps will update the plot. + This is VERY VERY SLOW and wil slow down training a lot. Only use for debugging/testing. + + Arguments: + sleep_time {float} -- How many seconds (or partial seconds) you want to wait on this rendered frame. + + """ + # Turn interactive mode on. + plt.ion() + fig = plt.figure(num = "env_render") + ax = plt.gca() + ax.clear() + clear_output(wait = True) + + # Prepare the environment plot and mark the car's position. + env_plot = np.copy(self.track) + env_plot[self.position] = 4 + env_plot = np.flip(env_plot, axis = 0) + + # Plot the gridworld. + cmap = colors.ListedColormap(["white", "black", "green", "red", "yellow"]) + bounds = list(range(6)) + norm = colors.BoundaryNorm(bounds, cmap.N) + ax.imshow(env_plot, cmap = cmap, norm = norm, zorder = 0) + + # Plot the velocity. + if (not self.velocity == (0, 0)) : + ax.arrow(self.position[1], self.track.shape[0] - 1 - self.position[0], self.velocity[1], -self.velocity[0], + path_effects=[pe.Stroke(linewidth=1, foreground='black')], color = "yellow", width = 0.1, length_includes_head = True, zorder = 2) + + # Set up axes. + ax.grid(which = 'major', axis = 'both', linestyle = '-', color = 'k', linewidth = 2, zorder = 1) + ax.set_xticks(np.arange(-0.5, self.track.shape[1] , 1)); + ax.set_xticklabels([]) + ax.set_yticks(np.arange(-0.5, self.track.shape[0], 1)); + ax.set_yticklabels([]) + + # Draw everything. + #fig.canvas.draw() + #fig.canvas.flush_events() + + plt.show() + + # Sleep if desired. + if (sleep_time > 0) : + time.sleep(sleep_time) + + + def get_actions(self) : + """ + Returns the available actions in the current state - will always be a list + of integers in the range [0-8]. + """ + return [*self.ACTIONS_DICT] + +# num_steps = 1000000 + +# env = RacetrackEnv() +# state = env.reset() +# print(state) + +# for _ in range(num_steps) : + +# next_state, reward, terminal = env.step(random.choice(env.get_actions())) +# print(next_state) +# env.render() + +# if (terminal) : +# _ = env.reset() diff --git a/codes/envs/snake/README.md b/codes/envs/snake/README.md new file mode 100644 index 0000000..b49b4e8 --- /dev/null +++ b/codes/envs/snake/README.md @@ -0,0 +1,38 @@ +# 贪吃蛇 + +贪吃蛇是一个起源于1976年的街机游戏 Blockade,玩家控制蛇上下左右吃到食物并将身体增长,吃到食物后移动速度逐渐加快,直到碰到墙体或者蛇的身体算游戏结束。 + +![image-20200901202636603](img/image-20200901202636603.png) + +如图,本次任务整个游戏版面大小为560X560,绿色部分就是我们的智能体贪吃蛇,红色方块就是食物,墙位于四周,一旦食物被吃掉,会在下一个随机位置刷出新的食物。蛇的每一节以及食物的大小为40X40,除开墙体(厚度也为40),蛇可以活动的范围为480X480,也就是12X12的栅格。环境的状态等信息如下: + +* state:为一个元组,包含(adjoining_wall_x, adjoining_wall_y, food_dir_x, food_dir_y, adjoining_body_top, adjoining_body_bottom, adjoining_body_left, adjoining_body_right). + + * [adjoining_wall_x, adjoining_wall_y]:提供蛇头是否与墙体相邻的信息,具体包含9个状态 + + adjoining_wall_x:0表示x轴方向蛇头无墙体相邻,1表示有墙在蛇头左边,2表示有墙在右边adjoining_wall_y:0表示y轴方向蛇头无墙体相邻,1表示有墙在蛇头上边,2表示有墙在下边 + + 注意[0,0]也包括蛇跑出480X480范围的情况 + + * [food_dir_x, food_dir_y]:表示食物与蛇头的位置关系 + + food_dir_x:0表示食物与蛇头同在x轴上,1表示食物在蛇头左侧(不一定相邻),2表示在右边 + + food_dir_y:0表示食物与蛇头同在y轴上,1表示食物在蛇头上面,2表示在下面 + + * [adjoining_body_top, adjoining_body_bottom, adjoining_body_left, adjoining_body_right]:用以检查蛇的身体是否在蛇头的附近 + + adjoining_body_top:1表示蛇头上边有蛇的身体,0表示没有 + + adjoining_body_bottom:1表示蛇头下边有蛇的身体,0表示没有 + + adjoining_body_left:1表示蛇头左边有蛇的身体,0表示没有 + + adjoining_body_right:1表示蛇头右边有蛇的身体,0表示没有 + +* action:即上下左右 + +* reward:如果吃到食物给一个+1的reward,如果蛇没了就-1,其他情况给-0.1的reward + + + diff --git a/codes/envs/snake/agent.py b/codes/envs/snake/agent.py new file mode 100644 index 0000000..b32de9d --- /dev/null +++ b/codes/envs/snake/agent.py @@ -0,0 +1,106 @@ +import numpy as np +import utils +import random +import math + + +class Agent: + + def __init__(self, actions, Ne, C, gamma): + self.actions = actions + self.Ne = Ne # used in exploration function + self.C = C + self.gamma = gamma + + # Create the Q and N Table to work with + self.Q = utils.create_q_table() + self.N = utils.create_q_table() + self.reset() + + def train(self): + self._train = True + + def eval(self): + self._train = False + + # At the end of training save the trained model + def save_model(self, model_path): + utils.save(model_path, self.Q) + + # Load the trained model for evaluation + def load_model(self, model_path): + self.Q = utils.load(model_path) + + def reset(self): + self.points = 0 + self.s = None + self.a = None + + def f(self,u,n): + if n < self.Ne: + return 1 + return u + + def R(self,points,dead): + if dead: + return -1 + elif points > self.points: + return 1 + return -0.1 + + def get_state(self, state): + # [adjoining_wall_x, adjoining_wall_y] + adjoining_wall_x = int(state[0] == utils.WALL_SIZE) + 2 * int(state[0] == utils.DISPLAY_SIZE - utils.WALL_SIZE) + adjoining_wall_y = int(state[1] == utils.WALL_SIZE) + 2 * int(state[1] == utils.DISPLAY_SIZE - utils.WALL_SIZE) + # [food_dir_x, food_dir_y] + food_dir_x = 1 + int(state[0] < state[3]) - int(state[0] == state[3]) + food_dir_y = 1 + int(state[1] < state[4]) - int(state[1] == state[4]) + # [adjoining_body_top, adjoining_body_bottom, adjoining_body_left, adjoining_body_right] + adjoining_body = [(state[0] - body_state[0], state[1] - body_state[1]) for body_state in state[2]] + adjoining_body_top = int([0, utils.GRID_SIZE] in adjoining_body) + adjoining_body_bottom = int([0, -utils.GRID_SIZE] in adjoining_body) + adjoining_body_left = int([utils.GRID_SIZE, 0] in adjoining_body) + adjoining_body_right = int([-utils.GRID_SIZE, 0] in adjoining_body) + return adjoining_wall_x, adjoining_wall_y, food_dir_x, food_dir_y, adjoining_body_top, adjoining_body_bottom, adjoining_body_left, adjoining_body_right + + + def update(self, _state, points, dead): + if self.s: + maxq = max(self.Q[_state]) + reward = self.R(points,dead) + alpha = self.C / (self.C + self.N[self.s][self.a]) + self.Q[self.s][self.a] += alpha * (reward + self.gamma * maxq - self.Q[self.s][self.a]) + self.N[self.s][self.a] += 1.0 + + def choose_action(self, state, points, dead): + ''' + :param state: a list of [snake_head_x, snake_head_y, snake_body, food_x, food_y] from environment. + :param points: float, the current points from environment + :param dead: boolean, if the snake is dead + :return: the index of action. 0,1,2,3 indicates up,down,left,right separately + Return the index of action the snake needs to take, according to the state and points known from environment. + Tips: you need to discretize the state to the state space defined on the webpage first. + (Note that [adjoining_wall_x=0, adjoining_wall_y=0] is also the case when snake runs out of the 480x480 board) + ''' + + _state = self.get_state(state) + Qs = self.Q[_state][:] + + if self._train: + self.update(_state, points, dead) + if dead: + self.reset() + return + Ns = self.N[_state] + Fs = [self.f(Qs[a], Ns[a]) for a in self.actions] + action = np.argmax(Fs) + self.s = _state + self.a = action + else: + if dead: + self.reset() + return + action = np.argmax(Qs) + + self.points = points + return action diff --git a/codes/envs/snake/checkpoint.npy b/codes/envs/snake/checkpoint.npy new file mode 100644 index 0000000..591d49e Binary files /dev/null and b/codes/envs/snake/checkpoint.npy differ diff --git a/codes/envs/snake/checkpoint1.npy b/codes/envs/snake/checkpoint1.npy new file mode 100644 index 0000000..84b54ca Binary files /dev/null and b/codes/envs/snake/checkpoint1.npy differ diff --git a/codes/envs/snake/checkpoint2.npy b/codes/envs/snake/checkpoint2.npy new file mode 100644 index 0000000..4614eb7 Binary files /dev/null and b/codes/envs/snake/checkpoint2.npy differ diff --git a/codes/envs/snake/checkpoint3.npy b/codes/envs/snake/checkpoint3.npy new file mode 100644 index 0000000..8737b4c Binary files /dev/null and b/codes/envs/snake/checkpoint3.npy differ diff --git a/codes/envs/snake/example_assignment_and_report2.pdf b/codes/envs/snake/example_assignment_and_report2.pdf new file mode 100644 index 0000000..84008c0 Binary files /dev/null and b/codes/envs/snake/example_assignment_and_report2.pdf differ diff --git a/codes/envs/snake/main.py b/codes/envs/snake/main.py new file mode 100644 index 0000000..16776ad --- /dev/null +++ b/codes/envs/snake/main.py @@ -0,0 +1,185 @@ +import pygame +from pygame.locals import * +import argparse + +from agent import Agent +from snake_env import SnakeEnv +import utils +import time + +def get_args(): + parser = argparse.ArgumentParser(description='CS440 MP4 Snake') + + parser.add_argument('--human', default = False, action="store_true", + help='making the game human playable - default False') + + parser.add_argument('--model_name', dest="model_name", type=str, default="checkpoint3.npy", + help='name of model to save if training or to load if evaluating - default q_agent') + + parser.add_argument('--train_episodes', dest="train_eps", type=int, default=10000, + help='number of training episodes - default 10000') + + parser.add_argument('--test_episodes', dest="test_eps", type=int, default=1000, + help='number of testing episodes - default 1000') + + parser.add_argument('--show_episodes', dest="show_eps", type=int, default=10, + help='number of displayed episodes - default 10') + + parser.add_argument('--window', dest="window", type=int, default=100, + help='number of episodes to keep running stats for during training - default 100') + + parser.add_argument('--Ne', dest="Ne", type=int, default=40, + help='the Ne parameter used in exploration function - default 40') + + parser.add_argument('--C', dest="C", type=int, default=40, + help='the C parameter used in learning rate - default 40') + + parser.add_argument('--gamma', dest="gamma", type=float, default=0.2, + help='the gamma paramter used in learning rate - default 0.7') + + parser.add_argument('--snake_head_x', dest="snake_head_x", type=int, default=200, + help='initialized x position of snake head - default 200') + + parser.add_argument('--snake_head_y', dest="snake_head_y", type=int, default=200, + help='initialized y position of snake head - default 200') + + parser.add_argument('--food_x', dest="food_x", type=int, default=80, + help='initialized x position of food - default 80') + + parser.add_argument('--food_y', dest="food_y", type=int, default=80, + help='initialized y position of food - default 80') + cfg = parser.parse_args() + return cfg + +class Application: + def __init__(self, args): + self.args = args + self.env = SnakeEnv(args.snake_head_x, args.snake_head_y, args.food_x, args.food_y) + self.agent = Agent(self.env.get_actions(), args.Ne, args.C, args.gamma) + + def execute(self): + if not self.args.human: + if self.args.train_eps != 0: + self.train() + self.eval() + self.show_games() + + def train(self): + print("Train Phase:") + self.agent.train() + window = self.args.window + self.points_results = [] + first_eat = True + start = time.time() + + for game in range(1, self.args.train_eps + 1): + state = self.env.get_state() + dead = False + action = self.agent.choose_action(state, 0, dead) + while not dead: + state, points, dead = self.env.step(action) + + # For debug convenience, you can check if your Q-table mathches ours for given setting of parameters + # (see Debug Convenience part on homework 4 web page) + if first_eat and points == 1: + self.agent.save_model(utils.CHECKPOINT) + first_eat = False + + action = self.agent.choose_action(state, points, dead) + + + points = self.env.get_points() + self.points_results.append(points) + if game % self.args.window == 0: + print( + "Games:", len(self.points_results) - window, "-", len(self.points_results), + "Points (Average:", sum(self.points_results[-window:])/window, + "Max:", max(self.points_results[-window:]), + "Min:", min(self.points_results[-window:]),")", + ) + self.env.reset() + print("Training takes", time.time() - start, "seconds") + self.agent.save_model(self.args.model_name) + + def eval(self): + print("Evaling Phase:") + self.agent.eval() + self.agent.load_model(self.args.model_name) + points_results = [] + start = time.time() + + for game in range(1, self.args.test_eps + 1): + state = self.env.get_state() + dead = False + action = self.agent.choose_action(state, 0, dead) + while not dead: + state, points, dead = self.env.step(action) + action = self.agent.choose_action(state, points, dead) + points = self.env.get_points() + points_results.append(points) + self.env.reset() + + print("Testing takes", time.time() - start, "seconds") + print("Number of Games:", len(points_results)) + print("Average Points:", sum(points_results)/len(points_results)) + print("Max Points:", max(points_results)) + print("Min Points:", min(points_results)) + + def show_games(self): + print("Display Games") + self.env.display() + pygame.event.pump() + self.agent.eval() + points_results = [] + end = False + for game in range(1, self.args.show_eps + 1): + state = self.env.get_state() + dead = False + action = self.agent.choose_action(state, 0, dead) + count = 0 + while not dead: + count +=1 + pygame.event.pump() + keys = pygame.key.get_pressed() + if keys[K_ESCAPE] or self.check_quit(): + end = True + break + state, points, dead = self.env.step(action) + # Qlearning agent + if not self.args.human: + action = self.agent.choose_action(state, points, dead) + # for human player + else: + for event in pygame.event.get(): + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_UP: + action = 2 + elif event.key == pygame.K_DOWN: + action = 3 + elif event.key == pygame.K_LEFT: + action = 1 + elif event.key == pygame.K_RIGHT: + action = 0 + if end: + break + self.env.reset() + points_results.append(points) + print("Game:", str(game)+"/"+str(self.args.show_eps), "Points:", points) + if len(points_results) == 0: + return + print("Average Points:", sum(points_results)/len(points_results)) + + def check_quit(self): + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return True + return False + + +def main(): + cfg = get_args() + app = Application(cfg) + app.execute() + +if __name__ == "__main__": + main() diff --git a/codes/envs/snake/q_agent.npy b/codes/envs/snake/q_agent.npy new file mode 100644 index 0000000..75ef415 Binary files /dev/null and b/codes/envs/snake/q_agent.npy differ diff --git a/codes/envs/snake/snake_env.py b/codes/envs/snake/snake_env.py new file mode 100644 index 0000000..a4afe0a --- /dev/null +++ b/codes/envs/snake/snake_env.py @@ -0,0 +1,202 @@ +import random +import pygame +import utils + +class SnakeEnv: + def __init__(self, snake_head_x, snake_head_y, food_x, food_y): + self.game = Snake(snake_head_x, snake_head_y, food_x, food_y) + self.render = False + + def get_actions(self): + return self.game.get_actions() + + def reset(self): + return self.game.reset() + + def get_points(self): + return self.game.get_points() + + def get_state(self): + return self.game.get_state() + + def step(self, action): + state, points, dead = self.game.step(action) + if self.render: + self.draw(state, points, dead) + # return state, reward, done + return state, points, dead + + def draw(self, state, points, dead): + snake_head_x, snake_head_y, snake_body, food_x, food_y = state + self.display.fill(utils.BLUE) + pygame.draw.rect( self.display, utils.BLACK, + [ + utils.GRID_SIZE, + utils.GRID_SIZE, + utils.DISPLAY_SIZE - utils.GRID_SIZE * 2, + utils.DISPLAY_SIZE - utils.GRID_SIZE * 2 + ]) + + # draw snake head + pygame.draw.rect( + self.display, + utils.GREEN, + [ + snake_head_x, + snake_head_y, + utils.GRID_SIZE, + utils.GRID_SIZE + ], + 3 + ) + # draw snake body + for seg in snake_body: + pygame.draw.rect( + self.display, + utils.GREEN, + [ + seg[0], + seg[1], + utils.GRID_SIZE, + utils.GRID_SIZE, + ], + 1 + ) + # draw food + pygame.draw.rect( + self.display, + utils.RED, + [ + food_x, + food_y, + utils.GRID_SIZE, + utils.GRID_SIZE + ] + ) + + text_surface = self.font.render("Points: " + str(points), True, utils.BLACK) + text_rect = text_surface.get_rect() + text_rect.center = ((280),(25)) + self.display.blit(text_surface, text_rect) + pygame.display.flip() + if dead: + # slow clock if dead + self.clock.tick(1) + else: + self.clock.tick(5) + + return + + + def display(self): + pygame.init() + pygame.display.set_caption('MP4: Snake') + self.clock = pygame.time.Clock() + pygame.font.init() + + self.font = pygame.font.Font(pygame.font.get_default_font(), 15) + self.display = pygame.display.set_mode((utils.DISPLAY_SIZE, utils.DISPLAY_SIZE), pygame.HWSURFACE) + self.draw(self.game.get_state(), self.game.get_points(), False) + self.render = True + +class Snake: + def __init__(self, snake_head_x, snake_head_y, food_x, food_y): + self.init_snake_head_x,self.init_snake_head_y = snake_head_x,snake_head_y # 蛇头初始位置 + self.init_food_x, self.init_food_y = food_x, food_y # 食物初始位置 + self.reset() + + def reset(self): + self.points = 0 + self.snake_head_x, self.snake_head_y = self.init_snake_head_x, self.init_snake_head_y + self.food_x, self.food_y = self.init_food_x, self.init_food_y + self.snake_body = [] # 蛇身的位置集合 + + def get_points(self): + return self.points + + def get_actions(self): + return [0, 1, 2, 3] + + def get_state(self): + return [ + self.snake_head_x, + self.snake_head_y, + self.snake_body, + self.food_x, + self.food_y + ] + + def move(self, action): + '''根据action指令移动蛇头,并返回是否撞死 + ''' + delta_x = delta_y = 0 + if action == 0: # 上 + delta_x = utils.GRID_SIZE + elif action == 1: + delta_x = - utils.GRID_SIZE + elif action == 2: + delta_y = - utils.GRID_SIZE + elif action == 3: + delta_y = utils.GRID_SIZE + old_body_head = None + if len(self.snake_body) == 1: + old_body_head = self.snake_body[0] + + self.snake_body.append((self.snake_head_x, self.snake_head_y)) + self.snake_head_x += delta_x + self.snake_head_y += delta_y + + if len(self.snake_body) > self.points: # 说明没有吃到食物 + del(self.snake_body[0]) + + self.handle_eatfood() + + # 蛇长大于1时,蛇头与蛇身任一位置重叠则看作蛇与自身相撞 + if len(self.snake_body) >= 1: + for seg in self.snake_body: + if self.snake_head_x == seg[0] and self.snake_head_y == seg[1]: + return True + + # 蛇长为1时,如果蛇头与之前的位置重复则看作蛇与自身相撞 + if len(self.snake_body) == 1: + if old_body_head == (self.snake_head_x, self.snake_head_y): + return True + + # 蛇头是否撞墙 + if (self.snake_head_x < utils.GRID_SIZE or self.snake_head_y < utils.GRID_SIZE or + self.snake_head_x + utils.GRID_SIZE > utils.DISPLAY_SIZE-utils.GRID_SIZE or self.snake_head_y + utils.GRID_SIZE > utils.DISPLAY_SIZE-utils.GRID_SIZE): + return True + + return False + + def step(self, action): + is_dead = self.move(action) + return self.get_state(), self.get_points(), is_dead + + def handle_eatfood(self): + if (self.snake_head_x == self.food_x) and (self.snake_head_y == self.food_y): + self.random_food() + self.points += 1 + + def random_food(self): + '''生成随机位置的食物 + ''' + max_x = (utils.DISPLAY_SIZE - utils.WALL_SIZE - utils.GRID_SIZE) + max_y = (utils.DISPLAY_SIZE - utils.WALL_SIZE - utils.GRID_SIZE) + + self.food_x = random.randint(utils.WALL_SIZE, max_x)//utils.GRID_SIZE * utils.GRID_SIZE + self.food_y = random.randint(utils.WALL_SIZE, max_y)//utils.GRID_SIZE * utils.GRID_SIZE + + while self.check_food_on_snake(): # 食物不能生成在蛇身上 + self.food_x = random.randint(utils.WALL_SIZE, max_x)//utils.GRID_SIZE * utils.GRID_SIZE + self.food_y = random.randint(utils.WALL_SIZE, max_y)//utils.GRID_SIZE * utils.GRID_SIZE + + def check_food_on_snake(self): + if self.food_x == self.snake_head_x and self.food_y == self.snake_head_y: + return True + for seg in self.snake_body: + if self.food_x == seg[0] and self.food_y == seg[1]: + return True + return False + + diff --git a/codes/envs/snake/utils.py b/codes/envs/snake/utils.py new file mode 100644 index 0000000..01c9b00 --- /dev/null +++ b/codes/envs/snake/utils.py @@ -0,0 +1,55 @@ +import numpy as np +DISPLAY_SIZE = 560 +GRID_SIZE = 40 +WALL_SIZE = 40 +WHITE = (255, 255, 255) +RED = (255, 0, 0) +BLUE = (72, 61, 139) +BLACK = (0, 0, 0) +GREEN = (0, 255, 0) + +NUM_ADJOINING_WALL_X_STATES=3 +NUM_ADJOINING_WALL_Y_STATES=3 +NUM_FOOD_DIR_X=3 +NUM_FOOD_DIR_Y=3 +NUM_ADJOINING_BODY_TOP_STATES=2 +NUM_ADJOINING_BODY_BOTTOM_STATES=2 +NUM_ADJOINING_BODY_LEFT_STATES=2 +NUM_ADJOINING_BODY_RIGHT_STATES=2 +NUM_ACTIONS = 4 + +CHECKPOINT = 'checkpoint.npy' + +def create_q_table(): + return np.zeros((NUM_ADJOINING_WALL_X_STATES, NUM_ADJOINING_WALL_Y_STATES, NUM_FOOD_DIR_X, NUM_FOOD_DIR_Y, + NUM_ADJOINING_BODY_TOP_STATES, NUM_ADJOINING_BODY_BOTTOM_STATES, NUM_ADJOINING_BODY_LEFT_STATES, + NUM_ADJOINING_BODY_RIGHT_STATES, NUM_ACTIONS)) + +def sanity_check(arr): + if (type(arr) is np.ndarray and + arr.shape==(NUM_ADJOINING_WALL_X_STATES, NUM_ADJOINING_WALL_Y_STATES, NUM_FOOD_DIR_X, NUM_FOOD_DIR_Y, + NUM_ADJOINING_BODY_TOP_STATES, NUM_ADJOINING_BODY_BOTTOM_STATES, NUM_ADJOINING_BODY_LEFT_STATES, + NUM_ADJOINING_BODY_RIGHT_STATES,NUM_ACTIONS)): + return True + else: + return False + +def save(filename, arr): + if sanity_check(arr): + np.save(filename,arr) + return True + else: + print("Failed to save model") + return False + +def load(filename): + try: + arr = np.load(filename) + if sanity_check(arr): + print("Loaded model successfully") + return arr + print("Model loaded is not in the required format") + return None + except: + print("Filename doesnt exist") + return None \ No newline at end of file diff --git a/codes/envs/stochastic_mdp.py b/codes/envs/stochastic_mdp.py new file mode 100644 index 0000000..5770fa5 --- /dev/null +++ b/codes/envs/stochastic_mdp.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# coding=utf-8 +''' +Author: John +Email: johnjim0816@gmail.com +Date: 2021-03-24 22:12:19 +LastEditor: John +LastEditTime: 2021-03-26 17:12:43 +Discription: +Environment: +''' +import numpy as np +import random + + +class StochasticMDP: + def __init__(self): + self.end = False + self.curr_state = 2 + self.action_dim = 2 + self.state_dim = 6 + self.p_right = 0.5 + + def reset(self): + self.end = False + self.curr_state = 2 + state = np.zeros(self.state_dim) + state[self.curr_state - 1] = 1. + return state + + def step(self, action): + if self.curr_state != 1: + if action == 1: + if random.random() < self.p_right and self.curr_state < self.state_dim: + self.curr_state += 1 + else: + self.curr_state -= 1 + + if action == 0: + self.curr_state -= 1 + if self.curr_state == self.state_dim: + self.end = True + + state = np.zeros(self.state_dim) + state[self.curr_state - 1] = 1. + + if self.curr_state == 1: + if self.end: + return state, 1.00, True, {} + else: + return state, 1.00/100.00, True, {} + else: + return state, 0.0, False, {} diff --git a/codes/envs/track.txt b/codes/envs/track.txt new file mode 100644 index 0000000..4bbe230 --- /dev/null +++ b/codes/envs/track.txt @@ -0,0 +1,15 @@ +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 0 0 0 0 0 3 3 3 3 3 1 +1 1 1 1 1 1 0 0 0 0 0 0 0 3 3 3 3 3 1 +1 1 1 1 1 0 0 0 0 0 0 0 0 3 3 3 3 3 1 +1 1 1 1 0 0 0 0 0 0 0 0 0 3 3 3 3 3 1 +1 1 1 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 +1 1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 +1 1 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 2 2 2 2 1 1 1 1 1 1 1 1 1 1 1 1 1 1 +1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 \ No newline at end of file diff --git a/codes/envs/windy_gridworld.py b/codes/envs/windy_gridworld.py new file mode 100644 index 0000000..ac9c66a --- /dev/null +++ b/codes/envs/windy_gridworld.py @@ -0,0 +1,82 @@ +import gym +import numpy as np +import sys +from gym.envs.toy_text import discrete + +UP = 0 +RIGHT = 1 +DOWN = 2 +LEFT = 3 + +class WindyGridworldEnv(discrete.DiscreteEnv): + + metadata = {'render.modes': ['human', 'ansi']} + + def _limit_coordinates(self, coord): + coord[0] = min(coord[0], self.shape[0] - 1) + coord[0] = max(coord[0], 0) + coord[1] = min(coord[1], self.shape[1] - 1) + coord[1] = max(coord[1], 0) + return coord + + def _calculate_transition_prob(self, current, delta, winds): + new_position = np.array(current) + np.array(delta) + np.array([-1, 0]) * winds[tuple(current)] + new_position = self._limit_coordinates(new_position).astype(int) + new_state = np.ravel_multi_index(tuple(new_position), self.shape) + is_done = tuple(new_position) == (3, 7) + return [(1.0, new_state, -1.0, is_done)] + + def __init__(self): + self.shape = (7, 10) + + nS = np.prod(self.shape) + action_dim = 4 + + # Wind strength + winds = np.zeros(self.shape) + winds[:,[3,4,5,8]] = 1 + winds[:,[6,7]] = 2 + + # Calculate transition probabilities + P = {} + for s in range(nS): + position = np.unravel_index(s, self.shape) + P[s] = { a : [] for a in range(action_dim) } + P[s][UP] = self._calculate_transition_prob(position, [-1, 0], winds) + P[s][RIGHT] = self._calculate_transition_prob(position, [0, 1], winds) + P[s][DOWN] = self._calculate_transition_prob(position, [1, 0], winds) + P[s][LEFT] = self._calculate_transition_prob(position, [0, -1], winds) + + # We always start in state (3, 0) + isd = np.zeros(nS) + isd[np.ravel_multi_index((3,0), self.shape)] = 1.0 + + super(WindyGridworldEnv, self).__init__(nS, action_dim, P, isd) + + def render(self, mode='human', close=False): + self._render(mode, close) + + def _render(self, mode='human', close=False): + if close: + return + + outfile = StringIO() if mode == 'ansi' else sys.stdout + + for s in range(self.nS): + position = np.unravel_index(s, self.shape) + # print(self.s) + if self.s == s: + output = " x " + elif position == (3,7): + output = " T " + else: + output = " o " + + if position[1] == 0: + output = output.lstrip() + if position[1] == self.shape[1] - 1: + output = output.rstrip() + output += "\n" + + outfile.write(output) + outfile.write("\n") diff --git a/docs/.ipynb_checkpoints/Untitled-checkpoint.ipynb b/docs/.ipynb_checkpoints/Untitled-checkpoint.ipynb new file mode 100644 index 0000000..7fec515 --- /dev/null +++ b/docs/.ipynb_checkpoints/Untitled-checkpoint.ipynb @@ -0,0 +1,6 @@ +{ + "cells": [], + "metadata": {}, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..1e92f06 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,96 @@ +# 蘑菇书EasyRL + +李宏毅老师的《深度强化学习》是强化学习领域经典的中文视频之一。李老师幽默风趣的上课风格让晦涩难懂的强化学习理论变得轻松易懂,他会通过很多有趣的例子来讲解强化学习理论。比如老师经常会用玩 Atari 游戏的例子来讲解强化学习算法。此外,为了教程的完整性,我们整理了周博磊老师的《强化学习纲要》、李科浇老师的《世界冠军带你从零实践强化学习》以及多个强化学习的经典资料作为补充。对于想入门强化学习又想看中文讲解的人来说绝对是非常推荐的。 + +本教程也称为“蘑菇书”,寓意是希望此书能够为读者注入活力,让读者“吃”下这本蘑菇之后,能够饶有兴致地探索强化学习,像马里奥那样愈加强大,继而在人工智能领域觅得意外的收获。 + +## 使用说明 + +* 第 4 章到第 11 章为[李宏毅《深度强化学习》](http://speech.ee.ntu.edu.tw/~tlkagk/courses_MLDS18.html)的部分; +* 第 1 章和第 2 章根据[《强化学习纲要》](https://github.com/zhoubolei/introRL)整理而来; +* 第 3 章和第 12 章根据[《世界冠军带你从零实践强化学习》](https://aistudio.baidu.com/aistudio/education/group/info/1335) 整理而来。 + +## 纸质版 + + + +购买链接:[京东](https://item.jd.com/13075567.html) | [当当](http://product.dangdang.com/29374163.html) + +勘误表:https://datawhalechina.github.io/easy-rl/#/errata + +豆瓣评分:https://book.douban.com/subject/35781275/ + +## 在线阅读(内容实时更新) + +地址:https://datawhalechina.github.io/easy-rl/ + +## 最新版PDF下载 + +地址:https://github.com/datawhalechina/easy-rl/releases + +国内地址(推荐国内读者使用):https://pan.baidu.com/s/1y6WLaLM5ChMhK1zZ9RoceQ 提取码: tyxb + +压缩版(推荐网速较差的读者使用,文件小,图片分辨率较低):https://pan.baidu.com/s/1DM84K1ckN16jwHU3-3oxGw 提取码: an48 + +## 纸质版和PDF版的区别 + +PDF版本是全书初稿,人民邮电出版社的编辑老师们对初稿进行了反复修缮,最终诞生了纸质书籍,在此向人民邮电出版社的编辑老师的认真严谨表示衷心的感谢!(附:校对样稿) + +
+ +## 内容导航 + +| 章节 | 习题 | 相关项目 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| [第一章 强化学习概述](https://datawhalechina.github.io/easy-rl/#/chapter1/chapter1) | [第一章 习题](https://datawhalechina.github.io/easy-rl/#/chapter1/chapter1_questions&keywords) | | +| [第二章 马尔可夫决策过程 (MDP)](https://datawhalechina.github.io/easy-rl/#/chapter2/chapter2) | [第二章 习题](https://datawhalechina.github.io/easy-rl/#/chapter2/chapter2_questions&keywords) | | +| [第三章 表格型方法](https://datawhalechina.github.io/easy-rl/#/chapter3/chapter3) | [第三章 习题](https://datawhalechina.github.io/easy-rl/#/chapter3/chapter3_questions&keywords) | [Q-learning算法实战](https://datawhalechina.github.io/easy-rl/#/chapter3/project1) | +| [第四章 策略梯度](https://datawhalechina.github.io/easy-rl/#/chapter4/chapter4) | [第四章 习题](https://datawhalechina.github.io/easy-rl/#/chapter4/chapter4_questions&keywords) | | +| [第五章 近端策略优化 (PPO) 算法](https://datawhalechina.github.io/easy-rl/#/chapter5/chapter5) | [第五章 习题](https://datawhalechina.github.io/easy-rl/#/chapter5/chapter5_questions&keywords) | | +| [第六章 DQN (基本概念)](https://datawhalechina.github.io/easy-rl/#/chapter6/chapter6) | [第六章 习题](https://datawhalechina.github.io/easy-rl/#/chapter6/chapter6_questions&keywords) | | +| [第七章 DQN (进阶技巧)](https://datawhalechina.github.io/easy-rl/#/chapter7/chapter7) | [第七章 习题](https://datawhalechina.github.io/easy-rl/#/chapter7/chapter7_questions&keywords) | [DQN算法实战](https://datawhalechina.github.io/easy-rl/#/chapter7/project2) | +| [第八章 DQN (连续动作)](https://datawhalechina.github.io/easy-rl/#/chapter8/chapter8) | [第八章 习题](https://datawhalechina.github.io/easy-rl/#/chapter8/chapter8_questions&keywords) | | +| [第九章 演员-评论家算法](https://datawhalechina.github.io/easy-rl/#/chapter9/chapter9) | [第九章 习题](https://datawhalechina.github.io/easy-rl/#/chapter9/chapter9_questions&keywords) | | +| [第十章 稀疏奖励](https://datawhalechina.github.io/easy-rl/#/chapter10/chapter10) | [第十章 习题](https://datawhalechina.github.io/easy-rl/#/chapter10/chapter10_questions&keywords) | | +| [第十一章 模仿学习](https://datawhalechina.github.io/easy-rl/#/chapter11/chapter11) | [第十一章 习题](https://datawhalechina.github.io/easy-rl/#/chapter11/chapter11_questions&keywords) | | +| [第十二章 深度确定性策略梯度 (DDPG) 算法](https://datawhalechina.github.io/easy-rl/#/chapter12/chapter12) | [第十二章 习题](https://datawhalechina.github.io/easy-rl/#/chapter12/chapter12_questions&keywords) | [DDPG算法实战](https://datawhalechina.github.io/easy-rl/#/chapter12/project3) | +| [第十三章 AlphaStar 论文解读](https://datawhalechina.github.io/easy-rl/#/chapter13/chapter13) | | | +## 算法实战 + +[点击](https://github.com/datawhalechina/easy-rl/tree/master/codes)或者跳转```codes```文件夹下进入算法实战 + +## 贡献者 + + + + + + + + + +
+ pic
+ Qi Wang +

教程设计(第1~12章)
中国科学院大学

+
+ pic
+ Yiyuan Yang +

习题设计&第13章
清华大学

+
+ pic
+ John Jim +

算法实战
北京大学

+
+ + +## 致谢 + +特别感谢 [@Sm1les](https://github.com/Sm1les)、[@LSGOMYP](https://github.com/LSGOMYP) 对本项目的帮助与支持。 + +## 关注我们 +
Datawhale是一个专注AI领域的开源组织,以“for the learner,和学习者一起成长”为愿景,构建对学习者最有价值的开源学习社区。关注我们,一起学习成长。
+ +## LICENSE +知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 + diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100755 index 0000000..12e53a7 --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,51 @@ +- 目录 +- [第一章 强化学习概述](chapter1/chapter1) +- [第一章 习题](chapter1/chapter1_questions&keywords) +- [第二章 马尔可夫决策过程 (MDP)](chapter2/chapter2) +- [第二章 习题](chapter2/chapter2_questions&keywords) +- [第三章 表格型方法](chapter3/chapter3) +- [第三章 习题](chapter3/chapter3_questions&keywords) +- [项目一 使用 Q-learning 解决悬崖寻路问题](chapter3/project1) +- [第四章 策略梯度](chapter4/chapter4) +- [第四章 习题](chapter4/chapter4_questions&keywords) +- [第五章 近端策略优化 (PPO) 算法](chapter5/chapter5) +- [第五章 习题](chapter5/chapter5_questions&keywords) +- [第六章 DQN (基本概念)](chapter6/chapter6) +- [第六章 习题](chapter6/chapter6_questions&keywords) +- [第七章 DQN (进阶技巧)](chapter7/chapter7) +- [第七章 习题](chapter7/chapter7_questions&keywords) +- [项目二 使用 DQN 实现 CartPole-v0](chapter7/project2) +- [第八章 DQN (连续动作)](chapter8/chapter8) +- [第八章 习题](chapter8/chapter8_questions&keywords) +- [第九章 演员-评论家算法](chapter9/chapter9) +- [第九章 习题](chapter9/chapter9_questions&keywords) +- [第十章 稀疏奖励](chapter10/chapter10) +- [第十章 习题](chapter10/chapter10_questions&keywords) +- [第十一章 模仿学习](chapter11/chapter11) +- [第十一章 习题](chapter11/chapter11_questions&keywords) +- [第十二章 深度确定性策略梯度 (DDPG) 算法](chapter12/chapter12) +- [第十二章 习题](chapter12/chapter12_questions&keywords) +- [项目三 使用 Policy-Based 方法实现 Pendulum-v0](chapter12/project3) +- [第十三章 AlphaStar 论文解读](chapter13/chapter13) + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/chapter1/chapter1.md b/docs/chapter1/chapter1.md new file mode 100644 index 0000000..bf13479 --- /dev/null +++ b/docs/chapter1/chapter1.md @@ -0,0 +1,688 @@ +# Reinforement Learning + +## Reinforcement Learning + +![](img/1.1.png ':size=450') + +**强化学习讨论的问题是一个智能体(agent) 怎么在一个复杂不确定的环境(environment)里面去极大化它能获得的奖励。** 示意图由两部分组成:agent 和 environment。在强化学习过程中,agent 跟 environment 一直在交互。Agent 在环境里面获取到状态,agent 会利用这个状态输出一个动作(action),一个决策。然后这个决策会放到环境之中去,环境会根据 agent 采取的决策,输出下一个状态以及当前的这个决策得到的奖励。Agent 的目的就是为了尽可能多地从环境中获取奖励。 + +![](img/1.2.png ':size=500') + +**我们可以把强化学习跟监督学习做一个对比。** + +* 举个图片分类的例子,`监督学习(supervised learning)`就是说我们有一大堆标注的数据,比如车、飞机、凳子这些标注的图片,这些图片都要满足独立同分布(i.i.d.),就是它们之间是没有关联的。 + +* 然后我们训练一个分类器,比如说右边这个神经网络。为了分辨出这个图片是车辆还是飞机,训练过程中,我们把真实的标签给了这个网络。当这个网络做出一个错误的预测,比如现在输入了汽车的图片,它预测出来是飞机。我们就会直接告诉它,你这个预测是错误的,正确的标签应该是车。然后我们把这个错误写成一个`损失函数(loss function)`,通过反向传播(Backpropagation)来训练这个网络。 +* 所以在监督学习过程中,有两个假设: + * 输入的数据(标注的数据)都是没有关联的,尽可能没有关联。因为如果有关联的话,这个网络是不好学习的。 + * 我们告诉学习器(learner)正确的标签是什么,这样它可以通过正确的标签来修正自己的预测。 + +> 通常假设样本空间中全体样本服从一个未知分布,我们获得的每个样本都是独立地从这个分布上采样获得的,即独立同分布(independent and identically distributed,简称 i.i.d.)。 + +![](img/1.3.png ':size=300') + +在强化学习里面,这两点其实都不满足。举一个 Atari Breakout 游戏的例子,这是一个打砖块的游戏,控制木板左右移动把球反弹到上面来消除砖块。 + +* 在游戏过程中,大家可以发现这个 agent 得到的观测不是个独立同分布的分布,上一帧下一帧其实有非常强的连续性。这就是说,得到的数据是相关的时间序列数据,不满足独立同分布。 +* 另外一点,在玩游戏的过程中,你并没有立刻获得反馈,没有告诉你哪个动作是正确动作。比如你现在把这个木板往右移,那么只会使得这个球往上或者往左上去一点,你并不会得到立刻的反馈。所以强化学习这么困难的原因是没有得到很好的反馈,然后你依然希望 agent 在这个环境里面学习。 + +![](img/1.4.png ':size=500') + +强化学习的训练数据就是这样一个玩游戏的过程。你从第一步开始,采取一个决策,比如说你把这个往右移,接到这个球了。第二步你又做出决策,得到的训练数据是一个玩游戏的序列。 + +比如现在是在第三步,你把这个序列放进去,你希望这个网络可以输出一个决策,在当前的这个状态应该输出往右移或者往左移。这里有个问题:我们没有标签来说明你现在这个动作是正确还是错误,必须等到游戏结束才可能说明,这个游戏可能十秒过后才结束。现在这个动作到底对最后游戏结束能赢是否有帮助,其实是不清楚的。这里就面临`延迟奖励(Delayed Reward)`,所以就使得训练这个网络非常困难。 + +**我们对比下强化学习和监督学习。** + +* 强化学习输入的是序列数据,而不是像监督学习里面这些样本都是独立的。 +* 学习器并没有被告诉你每一步正确的行为应该是什么。学习器需要自己去发现哪些行为可以得到最多的奖励,只能通过不停地尝试来发现最有利的动作。 +* Agent 获得自己能力的过程中,其实是通过不断地试错探索(trial-and-error exploration)。 + * 探索(exploration)和利用(exploitation)是强化学习里面非常核心的一个问题。 + * 探索:你会去尝试一些新的行为,这些新的行为有可能会使你得到更高的奖励,也有可能使你一无所有。 + * 利用:采取你已知的可以获得最大奖励的行为,你就重复执行这个动作就可以了,因为你已经知道可以获得一定的奖励。 + * 因此,我们需要在探索和利用之间取得一个权衡,这也是在监督学习里面没有的情况。 +* 在强化学习过程中,没有非常强的监督者(supervisor),只有一个`奖励信号(reward signal)`,并且这个奖励信号是延迟的,就是环境会在很久以后告诉你之前你采取的行为到底是不是有效的。Agent 在这个强化学习里面学习的话就非常困难,因为你没有得到即时反馈。当你采取一个行为过后,如果是监督学习,你就立刻可以获得一个指引,就说你现在做出了一个错误的决定,那么正确的决定应该是谁。而在强化学习里面,环境可能会告诉你这个行为是错误的,但是它并没有告诉你正确的行为是什么。而且更困难的是,它可能是在一两分钟过后告诉你错误,它再告诉你之前的行为到底行不行。所以这也是强化学习和监督学习不同的地方。 + +通过跟监督学习比较,我们可以总结出强化学习的一些特征。 + +* 强化学习有这个 `试错探索(trial-and-error exploration)`,它需要通过探索环境来获取对环境的理解。 +* 强化学习 agent 会从环境里面获得延迟的奖励。 +* 在强化学习的训练过程中,时间非常重要。因为你得到的数据都是有时间关联的(sequential data),而不是独立同分布的。在机器学习中,如果观测数据有非常强的关联,其实会使得这个训练非常不稳定。这也是为什么在监督学习中,我们希望数据尽量是独立同分布,这样就可以消除数据之间的相关性。 +* Agent 的行为会影响它随后得到的数据,这一点是非常重要的。在我们训练 agent 的过程中,很多时候我们也是通过正在学习的这个 agent 去跟环境交互来得到数据。所以如果在训练过程中,这个 agent 的模型很快死掉了,那会使得我们采集到的数据是非常糟糕的,这样整个训练过程就失败了。所以在强化学习里面一个非常重要的问题就是怎么让这个 agent 的行为一直稳定地提升。 + +为什么我们关注强化学习,其中非常重要的一点就是强化学习得到的模型可以有超人类的表现。 + +* 监督学习获取的这些监督数据,其实是让人来标注的。比如说 ImageNet 的图片都是人类标注的。那么我们就可以确定这个算法的上限(upper bound)就是人类的表现,人类的这个标注结果决定了它永远不可能超越人类。 +* 但是对于强化学习,它在环境里面自己探索,有非常大的潜力,它可以获得超越人的能力的这个表现,比如谷歌 DeepMind 的 AlphaGo 这样一个强化学习的算法可以把人类最强的棋手都打败。 + +这里给大家举一些在现实生活中强化学习的例子。 + +* 在自然界中,羚羊其实也是在做一个强化学习,它刚刚出生的时候,可能都不知道怎么站立,然后它通过试错的一个尝试,三十分钟过后,它就可以跑到每小时 36 公里,很快地适应了这个环境。 +* 你也可以把股票交易看成一个强化学习的问题,就怎么去买卖来使你的收益极大化。 +* 玩雅达利游戏或者一些电脑游戏,也是一个强化学习的过程。 + +![](img/1.9.png ':size=350') + +上图是强化学习的一个经典例子,就是雅达利的一个叫 Pong 的游戏。这个游戏就是把这个球拍到左边,然后左边这个选手需要把这个球拍到右边。训练好的一个强化学习 agent 和正常的选手有区别,强化学习的 agent 会一直在做这种无意义的一些振动,而正常的选手不会出现这样的行为。 + +![](img/1.10.png ':size=450') + +在这个 pong 的游戏里面,决策其实就是两个动作:往上或者往下。如果强化学习是通过学习一个 policy network 来分类的话,其实就是输入当前帧的图片,policy network 就会输出所有决策的可能性。 + +![](img/1.11.png ':size=450') + +对于监督学习,我们可以直接告诉 agent 正确的标签是什么。但在这种游戏情况下面,我们并不知道它的正确的标签是什么。 + +![](img/1.12.png ':size=450') + +在强化学习里面,我们是通过让它尝试去玩这个游戏,然后直到游戏结束过后,再去说你前面的一系列动作到底是正确还是错误。 + +![](img/1.13.png ':size=450') + +* 上图的过程是 `rollout` 的一个过程。Rollout 的意思是从当前帧去生成很多局的游戏。 + +* 当前的 agent 去跟环境交互,你就会得到一堆观测。你可以把每一个观测看成一个`轨迹(trajectory)`。轨迹就是当前帧以及它采取的策略,即状态和动作的一个序列: + $$ + \tau=\left(s_{0}, a_{0}, s_{1}, a_{1}, \ldots\right) + $$ + +* 最后结束过后,你会知道你到底有没有把这个球击到对方区域,对方没有接住,你是赢了还是输了。我们可以通过观测序列以及最终奖励(eventual reward)来训练这个 agent ,使它尽可能地采取可以获得这个最终奖励的动作。 + +* 一场游戏叫做一个 `episode(回合)` 或者 `trial(试验)`。 + +![](img/1.14.png ':size=500') + +强化学习是有一定的历史的,只是最近大家把强化学习跟深度学习结合起来,就形成了`深度强化学习(Deep Reinforcemet Learning)`。深度强化学习 = 深度学习 + 强化学习。这里做一个类比,把它类比于这个传统的计算机视觉以及深度计算机视觉。 + +* 传统的计算机视觉由两个过程组成。 + * 给定一张图,我们先要提取它的特征,用一些设计好的特征(feature),比如说 HOG、DPM。 + * 提取这些特征后,我们再单独训练一个分类器。这个分类器可以是 SVM、Boosting,然后就可以辨别这张图片是狗还是猫。 +* 2012年,Krizhevsky等人提出了AlexNet,AlexNet在ImageNet分类比赛中取得冠军,迅速引起了人们对于卷积神经网络的广泛关注。 +大家就把特征提取以及分类两者合到一块儿去了,就是训练一个神经网络。这个神经网络既可以做特征提取,也可以做分类。它可以实现这种端到端的训练,它里面的参数可以在每一个阶段都得到极大的优化,这样就得到了一个非常重要的突破。 + +![](img/1.15.png ':size=500') + +我们可以把神经网络放到强化学习里面。 + +* Standard RL:之前的强化学习,比如 TD-Gammon 玩 backgammon 这个游戏,它其实是设计特征,然后通过训练价值函数的一个过程,就是它先设计了很多手工的特征,这个手工特征可以描述现在整个状态。得到这些特征过后,它就可以通过训练一个分类网络或者分别训练一个价值估计函数来做出决策。 +* Deep RL:现在我们有了深度学习,有了神经网络,那么大家也把这个过程改进成一个端到端训练(end-to-end training)的过程。你直接输入这个状态,我们不需要去手工地设计这个特征,就可以让它直接输出动作。那么就可以用一个神经网络来拟合我们这里的价值函数或策略网络,省去了特征工程(feature engineering)的过程。 + +为什么强化学习在这几年就用到各种应用中去,比如玩游戏以及机器人的一些应用,并且可以击败人类的最好棋手。 + +这有如下几点原因: + +* 我们有了更多的算力(computation power),有了更多的 GPU,可以更快地做更多的试错的尝试。 +* 通过这种不同尝试使得 agent 在这个环境里面获得很多信息,然后可以在这个环境里面取得很大的奖励。 +* 我们有了这个端到端的一个训练,可以把特征提取和价值估计或者决策一块来优化,这样就可以得到了一个更强的决策网络。 + +![](img/1.17.png) + + 接下来给大家再看一些强化学习里面比较有意思的例子。 + +1. **[DeepMind 研发的一个走路的 agent](https://www.youtube.com/watch?v=gn4nRCC9TwQ)。**这个 agent 往前走一步,你就会得到一个 reward。这个 agent 有不同的这个形态,可以学到很多有意思的功能。比如怎么跨越这个障碍物,就像那个蜘蛛那样的 agent 。怎么跨越障碍物,像这个人有双腿一样, 这个 agent 往前走。以及像这个人形的 agent,怎么在一个曲折的道路上面往前走。这个结果也是非常有意思,这个人形 agent 会把手举得非常高,因为它这个手的功能就是为了使它身体保持平衡,这样它就可以更快地在这个环境里面往前跑,而且这里你也可以增加这个环境的难度,加入一些扰动,这个 agent 就会变得更鲁棒。 +2. **[机械臂抓取](https://ai.googleblog.com/2016/03/deep-learning-for-robots-learning-from.html)。**因为机械臂的应用自动去强化学习需要大量的 rollout,所以它这里就有好多机械臂,分布式系统可以让这个机械臂尝试抓取不同的物体。你发现这个盘子里面物体的形状、形态其实都是不同的,这样就可以让这个机械臂学到一个统一的行为。然后在不同的抓取物下面都可以采取最优的一个抓取特征。你的这个抓取的物件形态存在很多不同,一些传统的这个抓取算法就没法把所有物体都抓起来,因为你对每一个物体都需要做一个建模,这样的话就是非常花时间。但是通过强化学习,你就可以学到一个统一的抓取算法,在不同物体上它都可以适用。 +3. **[OpenAI 做的一个机械臂翻魔方](https://www.youtube.com/watch?v=jwSbzNHGflM)。**这里它们 18 年的时候先设计了这个手指的一个机械臂,让它可以通过翻动手指,使得手中的这个木块达到一个预定的设定。人的手指其实非常精细,怎么使得这个机械手臂也具有这样灵活的能力就一直是个问题。它们通过这个强化学习在一个虚拟环境里面先训练,让 agent 能翻到特定的这个方向,再把它应用到真实的手臂之中。这在强化学习里面是一个比较常用的做法,就是你先在虚拟环境里面得到一个很好的 agent,然后再把它使用到真实的这个机器人中。因为真实的机械手臂通常都是非常容易坏,而且非常贵,你没法大批量地购买。2019 年对手臂进一步改进了,这个手臂可以玩魔方了。这个结果也非常有意思,到后面,这个魔方就被恢复成了个六面都是一样的结构了。 +4. **[一个穿衣服的 agent](https://www.youtube.com/watch?v=ixmE5nt2o88) ,就是训练这个 agent 穿衣服。**因为很多时候你要在电影或者一些动画实现人穿衣服的场景,通过手写执行命令让机器人穿衣服其实非常困难。很多时候穿衣服也是一个非常精细的操作,那么它们这个工作就是训练这个强化学习 agent,然后就可以实现这个穿衣功能。你还可以在这里面加入一些扰动,然后 agent 可以抗扰动。可能会有失败的情况(failure case), agent 就穿不进去,就卡在这个地方。 + +## Introduction to Sequential Decision Making + +### Agent and Environment + +![](img/1.18.png ':size=450') + +接下来我们讲`序列决策(Sequential Decision Making)过程`。 + +强化学习研究的问题是 agent 跟环境交互,上图左边画的是一个 agent,agent 一直在跟环境进行交互。这个 agent 把它输出的动作给环境,环境取得这个动作过后,会进行到下一步,然后会把下一步的观测跟它上一步是否得到奖励返还给 agent。 + +通过这样的交互过程会产生很多观测,agent 的目的是从这些观测之中学到能极大化奖励的策略。 + +### Reward + +奖励是由环境给的一个标量的反馈信号(scalar feedback signal),这个信号显示了 agent 在某一步采取了某个策略的表现如何。 + +强化学习的目的就是为了最大化 agent 可以获得的奖励,agent 在这个环境里面存在的目的就是为了极大化它的期望的累积奖励(expected cumulative reward)。 + +不同的环境,奖励也是不同的。这里给大家举一些奖励的例子。 + +* 比如说一个下象棋的选手,他的目的其实就为了赢棋。奖励是说在最后棋局结束的时候,他知道会得到一个正奖励或者负奖励。 +* 羚羊站立也是一个强化学习过程,它得到的奖励就是它是否可以最后跟它妈妈一块离开或者它被吃掉。 +* 在股票管理里面,奖励定义由你的股票获取的收益跟损失决定。 +* 在玩雅达利游戏的时候,奖励就是你有没有在增加游戏的分数,奖励本身的稀疏程度决定了这个游戏的难度。 + +### Sequential Decision Making + +![](img/1.21.png ':size=500') + +在一个强化学习环境里面,agent 的目的就是选取一系列的动作来极大化它的奖励,所以这些采取的动作必须有长期的影响。但在这个过程里面,它的奖励其实是被延迟了,就是说你现在采取的某一步决策可能要等到时间很久过后才知道这一步到底产生了什么样的影响。 + +这里一个示意图就是我们玩这个 Atari 的 Pong 游戏,你可能只有到最后游戏结束过后,才知道这个球到底有没有击打过去。中间你采取的 up 或 down 行为,并不会直接产生奖励。强化学习里面一个重要的课题就是近期奖励和远期奖励的一个权衡(trade-off)。怎么让 agent 取得更多的长期奖励是强化学习的问题。 + + +在跟环境的交互过程中,agent 会获得很多观测。在每一个观测会采取一个动作,它也会得到一个奖励。**所以历史是观测(observation)、行为、奖励的序列:** +$$ +H_{t}=O_{1}, R_{1}, A_{1}, \ldots, A_{t-1}, O_{t}, R_{t} +$$ +Agent 在采取当前动作的时候会依赖于它之前得到的这个历史,**所以你可以把整个游戏的状态看成关于这个历史的函数:** +$$ +S_{t}=f\left(H_{t}\right) +$$ +Q: 状态和观测有什么关系? + +A: `状态(state)` $s$ 是对世界的完整描述,不会隐藏世界的信息。`观测(observation)` $o$ 是对状态的部分描述,可能会遗漏一些信息。在 deep RL 中,我们几乎总是用一个实值的向量、矩阵或者更高阶的张量来表示状态和观测。举个例子,我们可以用 RGB 像素值的矩阵来表示一个视觉的观测,我们可以用机器人关节的角度和速度来表示一个机器人的状态。 + +环境有自己的函数 $S_{t}^{e}=f^{e}\left(H_{t}\right)$ 来更新状态,在 agent 的内部也有一个函数 $S_{t}^{a}=f^{a}\left(H_{t}\right)$ 来更新状态。当 agent 的状态跟环境的状态等价的时候,我们就说这个环境是 `full observability`,就是全部可以观测。换句话说,当 agent 能够观察到环境的所有状态时,我们称这个环境是`完全可观测的(fully observed)`。在这种情况下面,强化学习通常被建模成一个 Markov decision process(MDP)的问题。在 MDP 中, $O_{t}=S_{t}^{e}=S_{t}^{a}$。 + +但是有一种情况是 agent 得到的观测并不能包含环境运作的所有状态,因为在这个强化学习的设定里面,环境的状态才是真正的所有状态。 + +* 比如 agent 在玩这个 black jack 这个游戏,它能看到的其实是牌面上的牌。 +* 或者在玩雅达利游戏的时候,观测到的只是当前电视上面这一帧的信息,你并没有得到游戏内部里面所有的运作状态。 + +也就是说当 agent 只能看到部分的观测,我们就称这个环境是`部分可观测的(partially observed)`。在这种情况下面,强化学习通常被建模成一个 POMDP 的问题。 + +`部分可观测马尔可夫决策过程(Partially Observable Markov Decision Processes, POMDP)`是一个马尔可夫决策过程的泛化。POMDP 依然具有马尔可夫性质,但是假设智能体无法感知环境的状态 $s$,只能知道部分观测值 $o$。比如在自动驾驶中,智能体只能感知传感器采集的有限的环境信息。 + +POMDP 可以用一个 7 元组描述:$(S,A,T,R,\Omega,O,\gamma)$,其中 $S$ 表示状态空间,为隐变量,$A$ 为动作空间,$T(s'|s,a)$ 为状态转移概率,$R$ 为奖励函数,$\Omega(o|s,a)$ 为观测概率,$O$ 为观测空间,$\gamma$ 为折扣系数。 + +## Action Spaces + +不同的环境允许不同种类的动作。在给定的环境中,有效动作的集合经常被称为`动作空间(action space)`。像 Atari 和 Go 这样的环境有`离散动作空间(discrete action spaces)`,在这个动作空间里,agent 的动作数量是有限的。在其他环境,比如在物理世界中控制一个 agent,在这个环境中就有`连续动作空间(continuous action spaces)` 。在连续空间中,动作是实值的向量。 + +例如, + +* 走迷宫机器人如果只有东南西北这 4 种移动方式,则其为离散动作空间; +* 如果机器人向 $360^{\circ}$ 中的任意角度都可以移动,则为连续动作空间。 + +## Major Components of an RL Agent + +对于一个强化学习 agent,它可能有一个或多个如下的组成成分: + +* `策略函数(policy function)`,agent 会用这个函数来选取下一步的动作。 + +* `价值函数(value function)`,我们用价值函数来对当前状态进行估价,它就是说你进入现在这个状态,可以对你后面的收益带来多大的影响。当这个价值函数大的时候,说明你进入这个状态越有利。 + +* `模型(model)`,模型表示了 agent 对这个环境的状态进行了理解,它决定了这个世界是如何进行的。 + +### Policy + +我们深入看这三个组成成分的一些细节。 + +Policy 是 agent 的行为模型,它决定了这个 agent 的行为,它其实是一个函数,把输入的状态变成行为。这里有两种 policy: + +* 一种是 `stochastic policy(随机性策略)`,它就是 $\pi$ 函数 $\pi(a | s)=P\left[A_{t}=a | S_{t}=s\right]$ 。当你输入一个状态 $s$ 的时候,输出是一个概率。这个概率就是你所有行为的一个概率,然后你可以进一步对这个概率分布进行采样,得到真实的你采取的行为。比如说这个概率可能是有 70% 的概率往左,30% 的概率往右,那么你通过采样就可以得到一个 action。 +* 一种是 `deterministic policy(确定性策略)`,就是说你这里有可能只是采取它的极大化,采取最有可能的动作,即 $a^{*}=\arg \underset{a}{\max} \pi(a \mid s)$。 你现在这个概率就是事先决定好的。 + +![](img/1.26.png) + +从 Atari 游戏来看的话,策略函数的输入就是游戏的一帧,它的输出决定你是往左走或者是往右走。 + +通常情况下,强化学习一般使用`随机性策略`。随机性策略有很多优点: + +* 在学习时可以通过引入一定随机性来更好地探索环境; + +* 随机性策略的动作具有多样性,这一点在多个智能体博弈时也非常重要。采用确定性策略的智能体总是对同样的环境做出相同的动作,会导致它的策略很容易被对手预测。 + +### Value Function +**价值函数是未来奖励的一个预测,用来评估状态的好坏**。 + +价值函数里面有一个 `discount factor(折扣因子)`,我们希望尽可能在短的时间里面得到尽可能多的奖励。如果我们说十天过后,我给你 100 块钱,跟我现在给你 100 块钱,你肯定更希望我现在就给你 100 块钱,因为你可以把这 100 块钱存在银行里面,你就会有一些利息。所以我们就通过把这个折扣因子放到价值函数的定义里面,价值函数的定义其实是一个期望,如下式所示: +$$ +v_{\pi}(s) \doteq \mathbb{E}_{\pi}\left[G_{t} \mid S_{t}=s\right]=\mathbb{E}_{\pi}\left[\sum_{k=0}^{\infty} \gamma^{k} R_{t+k+1} \mid S_{t}=s\right], \text { for all } s \in \mathcal{S} +$$ +这里有一个期望 $\mathbb{E}_{\pi}$,这里有个小角标是 $\pi$ 函数,这个 $\pi$ 函数就是说在我们已知某一个策略函数的时候,到底可以得到多少的奖励。 + +我们还有一种价值函数:Q 函数。Q 函数里面包含两个变量:状态和动作,其定义如下式所示: +$$ +q_{\pi}(s, a) \doteq \mathbb{E}_{\pi}\left[G_{t} \mid S_{t}=s, A_{t}=a\right]=\mathbb{E}_{\pi}\left[\sum_{k=0}^{\infty} \gamma^{k} R_{t+k+1} \mid S_{t}=s, A_{t}=a\right] +$$ +所以你未来可以获得多少的奖励,它的这个期望取决于你当前的状态和当前的行为。这个 Q 函数是强化学习算法里面要学习的一个函数。因为当我们得到这个 Q 函数后,进入某一种状态,它最优的行为就可以通过这个 Q 函数来得到。 + +### Model +第三个组成部分是模型,**模型决定了下一个状态会是什么样的,就是说下一步的状态取决于你当前的状态以及你当前采取的行为。**它由两个部分组成, + +* 概率:这个转移状态之间是怎么转移的,如下式所示: + +$$ +\mathcal{P}_{s s^{\prime}}^{a}=\mathbb{P}\left[S_{t+1}=s^{\prime} \mid S_{t}=s, A_{t}=a\right] +$$ + +* 奖励函数:当你在当前状态采取了某一个行为,可以得到多大的奖励,如下式所示: +$$ +\mathcal{R}_{s}^{a}=\mathbb{E}\left[R_{t+1} \mid S_{t}=s, A_{t}=a\right] +$$ + + +![](img/1.29.png ':size=300') + +当我们有了这三个组成部分过后,就形成了一个 `马尔可夫决策过程(Markov Decision Process)`。这个决策过程可视化了状态之间的转移以及采取的行为。 + +![](img/1.30.png ':size=300') + +我们来看一个走迷宫的例子。 + +* 要求 agent 从 start 开始,然后到达 goal 的位置。 +* 每走一步,你就会得到 -1 的奖励。 +* 可以采取的动作是往上下左右走。 +* 当前状态用现在 agent 所在的位置来描述。 + +![](img/1.31.png ':size=300') + +* 我们可以用不同的强化学习算法来解这个环境。 +* 如果采取的是 `基于策略的(policy-based)RL`,当学习好了这个环境过后,在每一个状态,我们就会得到一个最佳的行为。 + +* 比如说现在在第一格开始的时候,我们知道它最佳行为是往右走,然后第二格的时候,得到的最佳策略是往上走,第三格是往右走。通过这个最佳的策略,我们就可以最快地到达终点。 + +![](img/1.32.png ':size=300') + +* 如果换成 `基于价值的(value-based)RL` 这个算法,利用价值函数来作为导向,我们就会得到另外一种表征,这里就表征了你每一个状态会返回一个价值。 + +* 比如说你在 start 位置的时候,价值是 -16,因为你最快可以 16 步到达终点。因为每走一步会减一,所以你这里的价值是 -16。 +* 当我们快接近最后终点的时候,这个数字变得越来越大。在拐角的时候,比如要现在在第二格 -15。然后 agent 会看上下,它看到上面值变大了,变成 -14 了,它下面是 -16,那么 agent 肯定就会采取一个往上走的策略。所以通过这个学习的值的不同,我们可以抽取出现在最佳的策略。 + +## Types of RL Agents + +**根据 agent 学习的东西不同,我们可以把 agent 进行归类。** + +* `基于价值的 agent(value-based agent)`。 + * 这一类 agent 显式地学习的是价值函数, + * 隐式地学习了它的策略。策略是从我们学到的价值函数里面推算出来的。 +* `基于策略的 agent(policy-based agent)`。 + * 这一类 agent 直接去学习 policy,就是说你直接给它一个状态,它就会输出这个动作的概率。 + * 在基于策略的 agent 里面并没有去学习它的价值函数。 +* 把 value-based 和 policy-based 结合起来就有了 `Actor-Critic agent`。这一类 agent 把它的策略函数和价值函数都学习了,然后通过两者的交互得到一个最佳的行为。 + +Q: 基于策略迭代和基于价值迭代的强化学习方法有什么区别? + +A: 对于一个状态转移概率已知的马尔可夫决策过程,我们可以使用动态规划算法来求解;从决策方式来看,强化学习又可以划分为基于策略迭代的方法和基于价值迭代的方法。`决策方式`是智能体在给定状态下从动作集合中选择一个动作的依据,它是静态的,不随状态变化而变化。 + +在`基于策略迭代`的强化学习方法中,智能体会`制定一套动作策略`(确定在给定状态下需要采取何种动作),并根据这个策略进行操作。强化学习算法直接对策略进行优化,使制定的策略能够获得最大的奖励。 + +而在`基于价值迭代`的强化学习方法中,智能体不需要制定显式的策略,它`维护一个价值表格或价值函数`,并通过这个价值表格或价值函数来选取价值最大的动作。基于价值迭代的方法只能应用在不连续的、离散的环境下(如围棋或某些游戏领域),对于行为集合规模庞大、动作连续的场景(如机器人控制领域),其很难学习到较好的结果(此时基于策略迭代的方法能够根据设定的策略来选择连续的动作)。 + +基于价值迭代的强化学习算法有 Q-learning、 Sarsa 等,而基于策略迭代的强化学习算法有策略梯度算法等。此外, Actor-Critic 算法同时使用策略和价值评估来做出决策,其中,智能体会根据策略做出动作,而价值函数会对做出的动作给出价值,这样可以在原有的策略梯度算法的基础上加速学习过程,取得更好的效果。 + +**另外,我们是可以通过 agent 到底有没有学习这个环境模型来分类。** + +* `model-based(有模型)` RL agent,它通过学习这个状态的转移来采取动作。 +* ` model-free(免模型)` RL agent,它没有去直接估计这个状态的转移,也没有得到环境的具体转移变量。它通过学习价值函数和策略函数进行决策。Model-free 的模型里面没有一个环境转移的模型。 + +我们可以用马尔可夫决策过程来定义强化学习任务,并表示为四元组 $$,即状态集合、动作集合、状态转移函数和奖励函数。如果这四元组中所有元素均已知,且状态集合和动作集合在有限步数内是有限集,则机器可以对真实环境进行建模,构建一个虚拟世界来模拟真实环境的状态和交互反应。 + +具体来说,当智能体知道状态转移函数 $P(s_{t+1}|s_t,a_t)$ 和奖励函数 $R(s_t,a_t)$ 后,它就能知道在某一状态下执行某一动作后能带来的奖励和环境的下一状态,这样智能体就不需要在真实环境中采取动作,直接在虚拟世界中学习和规划策略即可。这种学习方法称为`有模型学习`。 + +![](img/1.35.png ':size=400') + +上图是有模型强化学习的流程图。 + +然而在实际应用中,智能体并不是那么容易就能知晓 MDP 中的所有元素的。**通常情况下,状态转移函数和奖励函数很难估计,甚至连环境中的状态都可能是未知的,这时就需要采用免模型学习。**免模型学习没有对真实环境进行建模,智能体只能在真实环境中通过一定的策略来执行动作,等待奖励和状态迁移,然后根据这些反馈信息来更新行为策略,这样反复迭代直到学习到最优策略。 + +Q: 有模型强化学习和免模型强化学习有什么区别? + +A: 针对是否需要对真实环境建模,强化学习可以分为有模型学习和免模型学习。 + +* 有模型学习是指根据环境中的经验,构建一个虚拟世界,同时在真实环境和虚拟世界中学习; + +* 免模型学习是指不对环境进行建模,直接与真实环境进行交互来学习到最优策略。 + +总的来说,有模型学习相比于免模型学习仅仅多出一个步骤,即对真实环境进行建模。因此,一些有模型的强化学习方法,也可以在免模型的强化学习方法中使用。在实际应用中,如果不清楚该用有模型强化学习还是免模型强化学习,可以先思考一下,在智能体执行动作前,是否能对下一步的状态和奖励进行预测,如果可以,就能够对环境进行建模,从而采用有模型学习。 + +免模型学习通常属于数据驱动型方法,需要大量的采样来估计状态、动作及奖励函数,从而优化动作策略。例如,在 Atari 平台上的 Space Invader 游戏中,免模型的深度强化学习需要大约 2 亿帧游戏画面才能学到比较理想的效果。相比之下,有模型学习可以在一定程度上缓解训练数据匮乏的问题,因为智能体可以在虚拟世界中行训练。 + +免模型学习的泛化性要优于有模型学习,原因是有模型学习算需要对真实环境进行建模,并且虚拟世界与真实环境之间可能还有差异,这限制了有模型学习算法的泛化性。 + +有模型的强化学习方法可以对环境建模,使得该类方法具有独特魅力,即“想象能力”。在免模型学习中,智能体只能一步一步地采取策略,等待真实环境的反馈;而有模型学习可以在虚拟世界中预测出所有将要发生的事,并采取对自己最有利的策略。 + +**目前,大部分深度强化学习方法都采用了免模型学习**,这是因为: + +* 免模型学习更为简单直观且有丰富的开源资料,像 DQN、AlphaGo 系列等都采用免模型学习; +* 在目前的强化学习研究中,大部分情况下环境都是静态的、可描述的,智能体的状态是离散的、可观察的(如 Atari 游戏平台),这种相对简单确定的问题并不需要评估状态转移函数和奖励函数,直接采用免模型学习,使用大量的样本进行训练就能获得较好的效果。 + +![](img/1.36.png ':size=400') + +把几类模型放到同一个饼图里面。饼图有三个组成部分:价值函数、策略和模型。按一个 agent 具不具有三者中的两者或者一者可以把它分成很多类。 + +## Learning and Planning + +Learning 和 Planning 是序列决策的两个基本问题。 + +![](img/learning.png ':size=450') + +在强化学习中,环境初始时是未知的,agent 不知道环境如何工作,agent 通过不断地与环境交互,逐渐改进策略。 + +![](img/planning.png ':size=450') + +在 plannning 中,环境是已知的,我们被告知了整个环境的运作规则的详细信息。Agent 能够计算出一个完美的模型,并且在不需要与环境进行任何交互的时候进行计算。Agent 不需要实时地与环境交互就能知道未来环境,只需要知道当前的状态,就能够开始思考,来寻找最优解。 + +在这个游戏中,规则是制定的,我们知道选择 left 之后环境将会产生什么变化。我们完全可以通过已知的变化规则,来在内部进行模拟整个决策过程,无需与环境交互。 + +一个常用的强化学习问题解决思路是,先学习环境如何工作,也就是了解环境工作的方式,即学习得到一个模型,然后利用这个模型进行规划。 + +## Exploration and Exploitation + +在强化学习里面,`探索` 和`利用` 是两个很核心的问题。 + +* 探索是说我们怎么去探索这个环境,通过尝试不同的行为来得到一个最佳的策略,得到最大奖励的策略。 + +* 利用是说我们不去尝试新的东西,就采取已知的可以得到很大奖励的行为。 + +因为在刚开始的时候强化学习 agent 不知道它采取了某个行为会发生什么,所以它只能通过试错去探索。所以探索就是在试错来理解采取的这个行为到底可不可以得到好的奖励。利用是说我们直接采取已知的可以得到很好奖励的行为。所以这里就面临一个权衡,怎么通过牺牲一些短期的奖励来获得行为的理解,从而学习到更好的策略。 + +下面举一些探索和利用的例子。 + +* 以选择餐馆为例, + * 利用:我们直接去你最喜欢的餐馆,因为你去过这个餐馆很多次了,所以你知道这里面的菜都非常可口。 + * 探索:你把手机拿出来,你直接搜索一个新的餐馆,然后去尝试它到底好不好吃。你有可能对这个新的餐馆非常不满意,钱就浪费了。 + +* 以做广告为例, + * 利用:我们直接采取最优的这个广告策略。 + * 探索:我们换一种广告策略,看看这个新的广告策略到底可不可以得到奖励。 + +* 以挖油为例, + * 利用:我们直接在已知的地方挖油,我们就可以确保挖到油。 + * 探索:我们在一个新的地方挖油,就有很大的概率,你可能不能发现任何油,但也可能有比较小的概率可以发现一个非常大的油田。 +* 以玩游戏为例, + * 利用:你总是采取某一种策略。比如说,你可能打街霸,你采取的策略可能是蹲在角落,然后一直触脚。这个策略很可能可以奏效,但可能遇到特定的对手就失效。 + * 探索:你可能尝试一些新的招式,有可能你会发出大招来,这样就可能一招毙命。 + +### K-armed Bandit +![](img/1.39.png ':size=280') + +与监督学习不同,强化学习任务的最终奖赏是在多步动作之后才能观察到,这里我们不妨先考虑比较简单的情形:最大化单步奖赏,即仅考虑一步操作。需注意的是,即便在这样的简化情形下,强化学习仍与监督学习有显著不同,因为机器需通过尝试来发现各个动作产生的结果,而没有训练数据告诉机器应当做哪个动作。 + +想要最大化单步奖赏需考虑两个方面:一是需知道每个动作带来的奖赏,二是要执行奖赏最大的动作。若每个动作对应的奖赏是一个确定值,那么尝试遍所有的动作便能找出奖赏最大的动作。然而,更一般的情形是,一个动作的奖赏值是来自于一个概率分布,仅通过一次尝试并不能确切地获得平均奖赏值。 + +实际上,单步强化学习任务对应了一个理论模型,即` K-臂赌博机(K-armed bandit)`。K-臂赌博机也被称为 `多臂赌博机(Multi-armed bandit) `。如上图所示,K-摇臂赌博机有 K 个摇臂,赌徒在投入一个硬币后可选择按下其中一个摇臂,每个摇臂以一定的概率吐出硬币,但这个概率赌徒并不知道。赌徒的目标是通过一定的策略最大化自己的奖赏,即获得最多的硬币。 + +* 若仅为获知每个摇臂的期望奖赏,则可采用`仅探索(exploration-only)法`:将所有的尝试机会平均分配给每个摇臂(即轮流按下每个摇臂),最后以每个摇臂各自的平均吐币概率作为其奖赏期望的近似估计。 + +* 若仅为执行奖赏最大的动作,则可采用`仅利用(exploitation-only)法`:按下目前最优的(即到目前为止平均奖赏最大的)摇臂,若有多个摇臂同为最优,则从中随机选取一个。 + +显然,仅探索法能很好地估计每个摇臂的奖赏,却会失去很多选择最优摇臂的机会;仅利用法则相反,它没有很好地估计摇臂期望奖赏,很可能经常选不到最优摇臂。因此,这两种方法都难以使最终的累积奖赏最大化。 + +事实上,探索(即估计摇臂的优劣)和利用(即选择当前最优摇臂)这两者是矛盾的,因为尝试次数(即总投币数)有限,加强了一方则会自然削弱另一方,这就是强化学习所面临的`探索-利用窘境(Exploration-Exploitation dilemma)`。显然,想要累积奖赏最大,则必须在探索与利用之间达成较好的折中。 + +## Experiment with Reinforcement Learning +强化学习是一个理论跟实践结合的机器学习分支,需要去推导很多算法公式,去理解它算法背后的一些数学原理。另外一方面,上机实践通过实现算法,在很多实验环境里面去探索这个算法是不是可以得到预期效果也是一个非常重要的过程。 + +在[这个链接](https://github.com/cuhkrlcourse/RLexample)里面,公布了一些 RL 相关的代码,利用了 Python 和深度学习的一些包(主要是用 PyTorch 为主)。 + +你可以直接调用现有的包来实践。现在有很多深度学习的包可以用,比如 PyTorch、TensorFlow、Keras,熟练使用这里面的两三种,就可以实现非常多的功能。所以你并不需要从头去造轮子。 + + [OpenAI](https://openai.com/) 是一个非盈利的人工智能研究公司。Open AI 公布了非常多的学习资源以及算法资源,他们之所以叫 Open AI,就是他们把所有开发的算法都 open source 出来。 + +### Gym + +![](img/1.44.png ':size=450') + +[OpenAI Gym](https://gym.openai.com/) 是一个环境仿真库,里面包含了很多现有的环境。针对不同的场景,我们可以选择不同的环境, + +* 离散控制场景(输出的动作是可数的,比如 Pong 游戏中输出的向上或向下动作):一般使用 Atari 环境评估 +* 连续控制场景(输出的动作是不可数的,比如机器人走路时不仅有方向,还要角度,角度就是不可数的,是一个连续的量 ):一般使用 mujoco 环境评估 + +`Gym Retro` 是对 Gym 环境的进一步扩展,包含了更多的一些游戏。 + +我们可以通过 pip 来安装 Gym: + +```bash +pip install gym +``` + +在 Python 环境中导入Gym,如果不报错,就可以认为 Gym 安装成功。 + +```bash +$python +>>>import gym +``` + +![](img/1.45.png ':size=450') + +```python +import gym +env = gym.make("Taxi-v3") +observation = env.reset() +agent = load_agent() +for step in range(100): + action = agent(observation) + observation, reward, done, info = env.step(action) +``` + +强化学习的这个交互就是由 agent 跟环境进行交互。所以算法的 interface 也是用这个来表示。比如说我们现在安装了 OpenAI Gym。 + +1. 我们就可以直接调入 Taxi-v3 的环境,就建立了这个环境。 + +2. 初始化这个环境过后,就可以进行交互了。 +3. Agent 得到这个观测过后,它就会输出一个 action。 +4. 这个动作会被环境拿进去执行这个 step,然后环境就会往前走一步,返回新的 observation、reward 以及一个 flag variable `done` ,`done` 决定这个游戏是不是结束了。 + +几行代码就实现了强化学习的框架。 + +![](img/1.46.png ':size=400') + +在 OpenAI Gym 里面有很经典的控制类游戏。 + +* 比如说 Acrobot 就是把两节铁杖甩了立起来。 +* CartPole 是通过控制一个平板,让木棍立起来。 +* MountainCar 是通过前后移动这个车,让它到达这个旗子的位置。 + +大家可以点[这个链接](https://gym.openai.com/envs/#classic_control)看一看这些环境。在刚开始测试强化学习的时候,可以选择这些简单环境,因为这些环境可以在一两分钟之内见到一个效果。 + +![](img/1.47.png) + +这里我们看一下 CartPole 的这个环境。对于这个环境,有两个动作,Cart 往左移还是往右移。这里得到了观测: + +* 这个车当前的位置, +* Cart 当前往左往右移的速度, +* 这个杆的角度以及杆的最高点的速度。 + +如果 observation 越详细,就可以更好地描述当前这个所有的状态。这里有 reward 的定义,如果能多保留一步,你就会得到一个奖励,所以你需要在尽可能多的时间存活来得到更多的奖励。当这个杆的角度大于某一个角度(没能保持平衡)或者这个车已经出到外面的时候,游戏就结束了,你就输了。所以这个 agent 的目的就是为了控制木棍,让它尽可能地保持平衡以及尽可能保持在这个环境的中央。 + +```python +import gym # 导入 Gym 的 Python 接口环境包 +env = gym.make('CartPole-v0') # 构建实验环境 +env.reset() # 重置一个 episode +for _ in range(1000): + env.render() # 显示图形界面 + action = env.action_space.sample() # 从动作空间中随机选取一个动作 + env.step(action) # 用于提交动作,括号内是具体的动作 +env.close() # 关闭环境 +``` + +注意:如果绘制了实验的图形界面窗口,那么关闭该窗口的最佳方式是调用`env.close()`。试图直接关闭图形界面窗口可能会导致内存不能释放,甚至会导致死机。 + +当你执行这段代码时,机器人会完全无视那根本该立起来的杆子,驾驶着小车朝某个方向一通跑,直到不见踪影,这是因为我们还没开始训练机器人。 + +Gym 中的小游戏,大部分都可以用一个普通的实数或者向量来充当动作。打印 `env.action_space.sample()` 的返回值,能看到输出为 1 或者 0。 + +`env.action_space.sample()`的含义是,在该游戏的所有动作空间里随机选择一个作为输出。在这个例子中,意思就是,动作只有两个:0 和 1,一左一右。 + +`env.step()`这个方法的作用不止于此,它还有四个返回值,分别是`observation`、`reward`、`done`、`info`。 + +* `observation(object)`是状态信息,是在游戏中观测到的屏幕像素值或者盘面状态描述信息。 +* `reward(float)`是奖励值,即 action 提交以后能够获得的奖励值。这个奖励值因游戏的不同而不同,但总体原则是,对完成游戏有帮助的动作会获得比较高的奖励值。 +* `done(boolean)`表示游戏是否已经完成。如果完成了,就需要重置游戏并开始一个新的 episode。 +* `info(dict)`是一些比较原始的用于诊断和调试的信息,或许对训练有帮助。不过,OpenAI 团队在评价你提交的机器人时,是不允许使用这些信息的。 + +在每个训练中都要使用的返回值有 observation、reward、done。但 observation 的结构会由于游戏的不同而发生变化。以 CartPole-v0 小游戏为例,我们修改下代码: + +```python +import gym +env = gym.make('CartPole-v0') +env.reset() +for _ in range(1000): + env.render() + action = env.action_space.sample() + observation, reward, done, info = env.step(action) + print(observation) +env.close() +``` + +输出: + +``` +[ 0.01653398 0.19114579 0.02013859 -0.28050058] +[ 0.0203569 -0.00425755 0.01452858 0.01846535] +[ 0.02027175 -0.19958481 0.01489789 0.31569658] +...... +``` + +从输出可以看出这是一个四维的 Observation。在其他游戏中会有维度很多的情况。 + +`env.step()`完成了一个完整的 $S \to A \to R \to S'$ 过程。我们只要不断观测这样的过程,并让机器在其中用相应的算法完成训练,就能得到一个高质量的强化学习模型。 + +想要查看当前 Gym 库已经注册了哪些环境,可以使用以下代码: + +```python +from gym import envs +env_specs = envs.registry.all() +envs_ids = [env_spec.id for env_spec in env_specs] +print(envs_ids) +``` + +每个环境都定义了自己的观测空间和动作空间。环境 env 的观测空间用`env.observation_space`表示,动作空间用 `env.action_space `表示。观测空间和动作空间既可以是离散空间(即取值是有限个离散的值),也可以是连续空间(即取值是连续的)。在 Gym 库中,离散空间一般用`gym.spaces.Discrete`类表示,连续空间用`gym.spaces.Box`类表示。 + +例如,环境`'MountainCar-v0'`的观测空间是`Box(2,)`,表示观测可以用 2 个 float 值表示;环境`'MountainCar-v0'`的动作空间是`Dicrete(3)`,表示动作取值自`{0,1,2}`。对于离散空间,`gym.spaces.Discrete`类实例的成员 n 表示有几个可能的取值;对于连续空间,`Box`类实例的成员 low 和 high 表示每个浮点数的取值范围。 + +### MountainCar-v0 Example + +接下来,我们通过一个例子来学习如何与 Gym 库进行交互。我们选取 `小车上山(MountainCar-v0)`作为例子。 + +首先我们来看看这个任务的观测空间和动作空间: + +```python +import gym +env = gym.make('MountainCar-v0') +print('观测空间 = {}'.format(env.observation_space)) +print('动作空间 = {}'.format(env.action_space)) +print('观测范围 = {} ~ {}'.format(env.observation_space.low, + env.observation_space.high)) +print('动作数 = {}'.format(env.action_space.n)) +``` + +输出: + +``` +观测空间 = Box(2,) +动作空间 = Discrete(3) +观测范围 = [-1.2 -0.07] ~ [0.6 0.07] +动作数 = 3 +``` + +由输出可知,观测空间是形状为 (2,) 的浮点型 np.array,动作空间是取 {0,1,2} 的 int 型数值。 + +接下来考虑智能体。智能体往往是我们自己实现的。我们可以实现一个智能体类:`BespokeAgent类`,代码如下所示: + +```python +class BespokeAgent: + def __init__(self, env): + pass + + def decide(self, observation): # 决策 + position, velocity = observation + lb = min(-0.09 * (position + 0.25) ** 2 + 0.03, + 0.3 * (position + 0.9) ** 4 - 0.008) + ub = -0.07 * (position + 0.38) ** 2 + 0.07 + if lb < velocity < ub: + action = 2 + else: + action = 0 + return action # 返回动作 + + def learn(self, *args): # 学习 + pass + +agent = BespokeAgent(env) +``` + +智能体的 `decide()` 方法实现了决策功能,而 `learn()` 方法实现了学习功能。`BespokeAgent`类是一个比较简单的类,它只能根据给定的数学表达式进行决策,不能有效学习。所以它并不是一个真正意义上的强化学习智能体类。但是,用于演示智能体和环境的交互已经足够了。 + +接下来我们试图让智能体与环境交互,代码如下所示: + +```python +def play_montecarlo(env, agent, render=False, train=False): + episode_reward = 0. # 记录回合总奖励,初始化为0 + observation = env.reset() # 重置游戏环境,开始新回合 + while True: # 不断循环,直到回合结束 + if render: # 判断是否显示 + env.render() # 显示图形界面,图形界面可以用 env.close() 语句关闭 + action = agent.decide(observation) + next_observation, reward, done, _ = env.step(action) # 执行动作 + episode_reward += reward # 收集回合奖励 + if train: # 判断是否训练智能体 + agent.learn(observation, action, reward, done) # 学习 + if done: # 回合结束,跳出循环 + break + observation = next_observation + return episode_reward # 返回回合总奖励 +``` + +上面代码中的 `play_montecarlo` 函数可以让智能体和环境交互一个回合。这个函数有 4 个参数: + +* `env` 是环境类 +* `agent` 是智能体类 +* `render`是 bool 类型变量,指示在运行过程中是否要图形化显示。如果函数参数 render为 True,那么在交互过程中会调用 `env.render()` 以显示图形化界面,而这个界面可以通过调用 `env.close()` 关闭。 +* `train`是 bool 类型的变量,指示在运行过程中是否训练智能体。在训练过程中应当设置为 True,以调用 `agent.learn()` 函数;在测试过程中应当设置为 False,使得智能体不变。 + +这个函数有一个返回值 `episode_reward`,是 float 类型的数值,表示智能体与环境交互一个回合的回合总奖励。 + +接下来,我们使用下列代码让智能体和环境交互一个回合,并在交互过程中图形化显示,可用 `env.close()` 语句关闭图形化界面。 + +```python +env.seed(0) # 设置随机数种子,只是为了让结果可以精确复现,一般情况下可删去 +episode_reward = play_montecarlo(env, agent, render=True) +print('回合奖励 = {}'.format(episode_reward)) +env.close() # 此语句可关闭图形界面 +``` + +输出: + +``` +回合奖励 = -105.0 +``` + +为了系统评估智能体的性能,下列代码求出了连续交互 100 回合的平均回合奖励。 + +```python +episode_rewards = [play_montecarlo(env, agent) for _ in range(100)] +print('平均回合奖励 = {}'.format(np.mean(episode_rewards))) +``` + +输出: + +``` +平均回合奖励 = -102.61 +``` + +小车上山环境有一个参考的回合奖励值 -110,如果当连续 100 个回合的平均回合奖励大于 -110,则认为这个任务被解决了。`BespokeAgent` 类对应的策略的平均回合奖励大概就在 -110 左右。 + +测试 agent 在 Gym 库中某个任务的性能时,学术界一般最关心 100 个回合的平均回合奖励。至于为什么是 100 个回合而不是其他回合数(比如 128 个回合),完全是习惯使然,没有什么特别的原因。对于有些环境,还会指定一个参考的回合奖励值,当连续 100 个回合的奖励大于指定的值时,就认为这个任务被解决了。但是,并不是所有的任务都指定了这样的值。对于没有指定值的任务,就无所谓任务被解决了或者没有被解决。 + +总结一下 Gym 的用法:使用 `env=gym.make(环境名)` 取出环境,使用 `env.reset()`初始化环境,使用`env.step(动作)`执行一步环境,使用 `env.render()`显示环境,使用 `env.close()` 关闭环境。 + +最后提一下,Gym 有对应的[官方文档](https://gym.openai.com/docs/),大家可以阅读文档来学习 Gym。 + +## References + +* [百面深度学习](https://book.douban.com/subject/35043939/) +* [强化学习:原理与Python实现](https://book.douban.com/subject/34478302/) + +* [强化学习基础 David Silver 笔记](https://zhuanlan.zhihu.com/c_135909947) +* [David Silver 强化学习公开课中文讲解及实践](https://zhuanlan.zhihu.com/reinforce) +* [UCL Course on RL(David Silver)](https://www.davidsilver.uk/teaching/) + +* [白话强化学习与PyTorch](https://book.douban.com/subject/34809676/) + +* [OpenAI Spinning Up ](https://spinningup.openai.com/en/latest/spinningup/rl_intro.html#) + +* [神经网络与深度学习](https://nndl.github.io/) + +* [机器学习](https://book.douban.com/subject/26708119//) + + + + + + + + + + + diff --git a/docs/chapter1/chapter1_questions&keywords.md b/docs/chapter1/chapter1_questions&keywords.md new file mode 100644 index 0000000..4a57b13 --- /dev/null +++ b/docs/chapter1/chapter1_questions&keywords.md @@ -0,0 +1,113 @@ +# Chapter1 强化学习概述 + +## 1 Keywords + +- **强化学习(Reinforcement Learning)**:Agent可以在与复杂且不确定的Environment进行交互时,尝试使所获得的Reward最大化的计算算法。 +- **Action**: Environment接收到的Agent当前状态的输出。 +- **State**:Agent从Environment中获取到的状态。 +- **Reward**:Agent从Environment中获取的反馈信号,这个信号指定了Agent在某一步采取了某个策略以后是否得到奖励。 +- **Exploration**:在当前的情况下,继续尝试**新的**Action,其有可能会使你得到更高的这个奖励,也有可能使你一无所有。 +- **Exploitation**:在当前的情况下,继续尝试**已知的**可以获得最大Reward的过程,即重复执行这个 Action 就可以了。 +- **深度强化学习(Deep Reinforcement Learning)**:不需要手工设计特征,仅需要输入State让系统直接输出Action的一个end-to-end training的强化学习方法。通常使用神经网络来拟合 value function 或者 policy network。 +- **Full observability、fully observed和partially observed**:当Agent的状态跟Environment的状态等价的时候,我们就说现在Environment是full observability(全部可观测),当Agent能够观察到Environment的所有状态时,我们称这个环境是fully observed(完全可观测)。一般我们的Agent不能观察到Environment的所有状态时,我们称这个环境是partially observed(部分可观测)。 +- **POMDP(Partially Observable Markov Decision Processes)**:部分可观测马尔可夫决策过程,即马尔可夫决策过程的泛化。POMDP 依然具有马尔可夫性质,但是假设智能体无法感知环境的状态 $s$,只能知道部分观测值 $o$。 +- **Action space(discrete action spaces and continuous action spaces)**:在给定的Environment中,有效动作的集合经常被称为动作空间(Action space),Agent的动作数量是有限的动作空间为离散动作空间(discrete action spaces),反之,称为连续动作空间(continuous action spaces)。 +- **policy-based(基于策略的)**:Agent会制定一套动作策略(确定在给定状态下需要采取何种动作),并根据这个策略进行操作。强化学习算法直接对策略进行优化,使制定的策略能够获得最大的奖励。 +- **valued-based(基于价值的)**:Agent不需要制定显式的策略,它维护一个价值表格或价值函数,并通过这个价值表格或价值函数来选取价值最大的动作。 +- **model-based(有模型结构)**:Agent通过学习状态的转移来采取措施。 +- **model-free(无模型结构)**:Agent没有去直接估计状态的转移,也没有得到Environment的具体转移变量。它通过学习 value function 和 policy function 进行决策。 + +## 2 Questions + +- 强化学习的基本结构是什么? + + 答:本质上是Agent和Environment间的交互。具体地,当Agent在Environment中得到当前时刻的State,Agent会基于此状态输出一个Action。然后这个Action会加入到Environment中去并输出下一个State和当前的这个Action得到的Reward。Agent在Environment里面存在的目的就是为了极大它的期望积累的Reward。 + +- 强化学习相对于监督学习为什么训练会更加困难?(强化学习的特征) + + 答: + + 1. 强化学习处理的多是序列数据,其很难像监督学习的样本一样满足**IID(独立同分布)**条件。 + + 2. 强化学习有奖励的延迟(Delay Reward),即在Agent的action作用在Environment中时,Environment对于Agent的State的**奖励的延迟**(Delayed Reward),使得反馈不及时。 + 3. 相比于监督学习有正确的label,可以通过其修正自己的预测,强化学习相当于一个“试错”的过程,其完全根据Environment的“**反馈**”更新对自己最有利的Action。 + +- 强化学习的基本特征有哪些? + + 答: + + 1. 有**trial-and-error exploration**的过程,即需要通过探索Environment来获取对这个Environment的理解。 + 2. 强化学习的Agent会从Environment里面获得**延迟**的Reward。 + 3. 强化学习的训练过程中**时间**非常重要,因为数据都是有时间关联的,而不是像监督学习一样是IID分布的。 + 4. 强化学习中Agent的Action会**影响**它随后得到的**反馈**。 + +- 近几年强化学习发展迅速的原因? + + 答: + + 1. **算力(GPU、TPU)的提升**,我们可以更快地做更多的 trial-and-error 的尝试来使得Agent在Environment里面获得很多信息,取得更大的Reward。 + + 2. 我们有了深度强化学习这样一个端到端的训练方法,可以把特征提取和价值估计或者决策一起优化,这样就可以得到一个更强的决策网络。 + +- 状态和观测有什么关系? + + 答:状态(state)是对世界的**完整描述**,不会隐藏世界的信息。观测(observation)是对状态的**部分描述**,可能会遗漏一些信息。在深度强化学习中,我们几乎总是用一个实值向量、矩阵或者更高阶的张量来表示状态和观测。 + +- 对于一个强化学习 Agent,它由什么组成? + + 答: + + 1. **策略函数(policy function)**,Agent会用这个函数来选取它下一步的动作,包括**随机性策略(stochastic policy)**和**确定性策略(deterministic policy)**。 + + 2. **价值函数(value function)**,我们用价值函数来对当前状态进行评估,即进入现在的状态,到底可以对你后面的收益带来多大的影响。当这个价值函数大的时候,说明你进入这个状态越有利。 + + 3. **模型(model)**,其表示了 Agent 对这个Environment的状态进行的理解,它决定了这个系统是如何进行的。 + +- 根据强化学习 Agent 的不同,我们可以将其分为哪几类? + + 答: + + 1. **基于价值函数的Agent**。 显式学习的就是价值函数,隐式的学习了它的策略。因为这个策略是从我们学到的价值函数里面推算出来的。 + 2. **基于策略的Agent**。它直接去学习 policy,就是说你直接给它一个 state,它就会输出这个动作的概率。然后在这个 policy-based agent 里面并没有去学习它的价值函数。 + 3. 然后另外还有一种 Agent 是把这两者结合。把 value-based 和 policy-based 结合起来就有了 **Actor-Critic agent**。这一类 Agent 就把它的策略函数和价值函数都学习了,然后通过两者的交互得到一个更佳的状态。 + +- 基于策略迭代和基于价值迭代的强化学习方法有什么区别? + + 答: + + 1. 基于策略迭代的强化学习方法,agent会制定一套动作策略(确定在给定状态下需要采取何种动作),并根据这个策略进行操作。强化学习算法直接对策略进行优化,使制定的策略能够获得最大的奖励;基于价值迭代的强化学习方法,agent不需要制定显式的策略,它维护一个价值表格或价值函数,并通过这个价值表格或价值函数来选取价值最大的动作。 + 2. 基于价值迭代的方法只能应用在不连续的、离散的环境下(如围棋或某些游戏领域),对于行为集合规模庞大、动作连续的场景(如机器人控制领域),其很难学习到较好的结果(此时基于策略迭代的方法能够根据设定的策略来选择连续的动作); + 3. 基于价值迭代的强化学习算法有 Q-learning、 Sarsa 等,而基于策略迭代的强化学习算法有策略梯度算法等。 + 4. 此外, Actor-Critic 算法同时使用策略和价值评估来做出决策,其中,智能体会根据策略做出动作,而价值函数会对做出的动作给出价值,这样可以在原有的策略梯度算法的基础上加速学习过程,取得更好的效果。 + +- 有模型(model-based)学习和免模型(model-free)学习有什么区别? + + 答:针对是否需要对真实环境建模,强化学习可以分为有模型学习和免模型学习。 + 有模型学习是指根据环境中的经验,构建一个虚拟世界,同时在真实环境和虚拟世界中学习;免模型学习是指不对环境进行建模,直接与真实环境进行交互来学习到最优策略。总的来说,有模型学习相比于免模型学习仅仅多出一个步骤,即对真实环境进行建模。免模型学习通常属于数据驱动型方法,需要大量的采样来估计状态、动作及奖励函数,从而优化动作策略。免模型学习的泛化性要优于有模型学习,原因是有模型学习算需要对真实环境进行建模,并且虚拟世界与真实环境之间可能还有差异,这限制了有模型学习算法的泛化性。 + +- 强化学习的通俗理解 + + 答:environment 跟 reward function 不是我们可以控制的,environment 跟 reward function 是在开始学习之前,就已经事先给定的。我们唯一能做的事情是调整 actor 里面的 policy,使得 actor 可以得到最大的 reward。Actor 里面会有一个 policy, 这个 policy 决定了actor 的行为。Policy 就是给一个外界的输入,然后它会输出 actor 现在应该要执行的行为。 + +## 3 Something About Interview + +- 高冷的面试官: 看来你对于RL还是有一定了解的,那么可以用一句话谈一下你对于强化学习的认识吗? + + 答: 强化学习包含环境,动作和奖励三部分,其本质是agent通过与环境的交互,使得其作出的action所得到的决策得到的总的奖励达到最大,或者说是期望最大。 + +- 高冷的面试官: 你认为强化学习与监督学习和无监督学习有什么区别? + + 答: 首先强化学习和无监督学习是不需要标签的,而监督学习需要许多有标签的样本来进行模型的构建;对于强化学习与无监督学习,无监督学习是直接对于给定的数据进行建模,寻找数据(特征)给定的隐藏的结构,一般对应的聚类问题,而强化学习需要通过延迟奖励学习策略来得到"模型"对于正确目标的远近(通过奖励惩罚函数进行判断),这里我们可以将奖励惩罚函数视为正确目标的一个稀疏、延迟形式。另外强化学习处理的多是序列数据,样本之间通常具有强相关性,但其很难像监督学习的样本一样满足IID条件。 + +- 高冷的面试官: 根据你上面介绍的内容,你认为强化学习的使用场景有哪些呢? + + 答: 七个字的话就是多序列决策问题。或者说是对应的模型未知,需要通过学习逐渐逼近真实模型的问题并且当前的动作会影响环境的状态,即服从马尔可夫性的问题。同时应满足所有状态是可重复到达的(满足可学习型的)。 + +- 高冷的面试官: 强化学习中所谓的损失函数与DL中的损失函数有什么区别呀? + + 答: DL中的loss function目的是使预测值和真实值之间的差距最小,而RL中的loss function是是奖励和的期望最大。 + +- 高冷的面试官: 你了解model-free和model-based吗?两者有什么区别呢? + + 答: 两者的区别主要在于是否需要对于真实的环境进行建模, model-free不需要对于环境进行建模,直接与真实环境进行交互即可,所以其通常需要较大的数据或者采样工作来优化策略,这也帮助model-free对于真实环境具有更好的泛化性能; 而model-based 需要对于环境进行建模,同时再真实环境与虚拟环境中进行学习,如果建模的环境与真实环境的差异较大,那么会限制其泛化性能。现在通常使用model-free进行模型的构建工作。 + diff --git a/docs/chapter1/img/1.1.png b/docs/chapter1/img/1.1.png new file mode 100644 index 0000000..e36284a Binary files /dev/null and b/docs/chapter1/img/1.1.png differ diff --git a/docs/chapter1/img/1.10.png b/docs/chapter1/img/1.10.png new file mode 100644 index 0000000..532438e Binary files /dev/null and b/docs/chapter1/img/1.10.png differ diff --git a/docs/chapter1/img/1.11.png b/docs/chapter1/img/1.11.png new file mode 100644 index 0000000..0c2375a Binary files /dev/null and b/docs/chapter1/img/1.11.png differ diff --git a/docs/chapter1/img/1.12.png b/docs/chapter1/img/1.12.png new file mode 100644 index 0000000..814dc76 Binary files /dev/null and b/docs/chapter1/img/1.12.png differ diff --git a/docs/chapter1/img/1.13.png b/docs/chapter1/img/1.13.png new file mode 100644 index 0000000..718d85d Binary files /dev/null and b/docs/chapter1/img/1.13.png differ diff --git a/docs/chapter1/img/1.14.png b/docs/chapter1/img/1.14.png new file mode 100644 index 0000000..c3b2415 Binary files /dev/null and b/docs/chapter1/img/1.14.png differ diff --git a/docs/chapter1/img/1.15.png b/docs/chapter1/img/1.15.png new file mode 100644 index 0000000..3a66f69 Binary files /dev/null and b/docs/chapter1/img/1.15.png differ diff --git a/docs/chapter1/img/1.16.png b/docs/chapter1/img/1.16.png new file mode 100644 index 0000000..ffcd32a Binary files /dev/null and b/docs/chapter1/img/1.16.png differ diff --git a/docs/chapter1/img/1.17.png b/docs/chapter1/img/1.17.png new file mode 100644 index 0000000..4520633 Binary files /dev/null and b/docs/chapter1/img/1.17.png differ diff --git a/docs/chapter1/img/1.18.png b/docs/chapter1/img/1.18.png new file mode 100644 index 0000000..62c7ff6 Binary files /dev/null and b/docs/chapter1/img/1.18.png differ diff --git a/docs/chapter1/img/1.19.png b/docs/chapter1/img/1.19.png new file mode 100644 index 0000000..7bb1c7b Binary files /dev/null and b/docs/chapter1/img/1.19.png differ diff --git a/docs/chapter1/img/1.2.png b/docs/chapter1/img/1.2.png new file mode 100644 index 0000000..8214682 Binary files /dev/null and b/docs/chapter1/img/1.2.png differ diff --git a/docs/chapter1/img/1.20.png b/docs/chapter1/img/1.20.png new file mode 100644 index 0000000..a5ebccb Binary files /dev/null and b/docs/chapter1/img/1.20.png differ diff --git a/docs/chapter1/img/1.21.png b/docs/chapter1/img/1.21.png new file mode 100644 index 0000000..e5f0f0c Binary files /dev/null and b/docs/chapter1/img/1.21.png differ diff --git a/docs/chapter1/img/1.22.png b/docs/chapter1/img/1.22.png new file mode 100644 index 0000000..d646ee1 Binary files /dev/null and b/docs/chapter1/img/1.22.png differ diff --git a/docs/chapter1/img/1.23.png b/docs/chapter1/img/1.23.png new file mode 100644 index 0000000..94f82f8 Binary files /dev/null and b/docs/chapter1/img/1.23.png differ diff --git a/docs/chapter1/img/1.24.png b/docs/chapter1/img/1.24.png new file mode 100644 index 0000000..aa12547 Binary files /dev/null and b/docs/chapter1/img/1.24.png differ diff --git a/docs/chapter1/img/1.25.png b/docs/chapter1/img/1.25.png new file mode 100644 index 0000000..48a9268 Binary files /dev/null and b/docs/chapter1/img/1.25.png differ diff --git a/docs/chapter1/img/1.26.png b/docs/chapter1/img/1.26.png new file mode 100644 index 0000000..e2584b3 Binary files /dev/null and b/docs/chapter1/img/1.26.png differ diff --git a/docs/chapter1/img/1.27.png b/docs/chapter1/img/1.27.png new file mode 100644 index 0000000..ba7cd96 Binary files /dev/null and b/docs/chapter1/img/1.27.png differ diff --git a/docs/chapter1/img/1.28.png b/docs/chapter1/img/1.28.png new file mode 100644 index 0000000..346c89c Binary files /dev/null and b/docs/chapter1/img/1.28.png differ diff --git a/docs/chapter1/img/1.29.png b/docs/chapter1/img/1.29.png new file mode 100644 index 0000000..a46b39f Binary files /dev/null and b/docs/chapter1/img/1.29.png differ diff --git a/docs/chapter1/img/1.3.png b/docs/chapter1/img/1.3.png new file mode 100644 index 0000000..e737bc6 Binary files /dev/null and b/docs/chapter1/img/1.3.png differ diff --git a/docs/chapter1/img/1.30.png b/docs/chapter1/img/1.30.png new file mode 100644 index 0000000..a00d6b0 Binary files /dev/null and b/docs/chapter1/img/1.30.png differ diff --git a/docs/chapter1/img/1.31.png b/docs/chapter1/img/1.31.png new file mode 100644 index 0000000..8819bb5 Binary files /dev/null and b/docs/chapter1/img/1.31.png differ diff --git a/docs/chapter1/img/1.32.png b/docs/chapter1/img/1.32.png new file mode 100644 index 0000000..fa19878 Binary files /dev/null and b/docs/chapter1/img/1.32.png differ diff --git a/docs/chapter1/img/1.33.png b/docs/chapter1/img/1.33.png new file mode 100644 index 0000000..8f72a7a Binary files /dev/null and b/docs/chapter1/img/1.33.png differ diff --git a/docs/chapter1/img/1.34.png b/docs/chapter1/img/1.34.png new file mode 100644 index 0000000..579d9da Binary files /dev/null and b/docs/chapter1/img/1.34.png differ diff --git a/docs/chapter1/img/1.35.png b/docs/chapter1/img/1.35.png new file mode 100644 index 0000000..3b3d848 Binary files /dev/null and b/docs/chapter1/img/1.35.png differ diff --git a/docs/chapter1/img/1.36.png b/docs/chapter1/img/1.36.png new file mode 100644 index 0000000..3bc31ac Binary files /dev/null and b/docs/chapter1/img/1.36.png differ diff --git a/docs/chapter1/img/1.37.png b/docs/chapter1/img/1.37.png new file mode 100644 index 0000000..5f94a01 Binary files /dev/null and b/docs/chapter1/img/1.37.png differ diff --git a/docs/chapter1/img/1.38.png b/docs/chapter1/img/1.38.png new file mode 100644 index 0000000..03951c2 Binary files /dev/null and b/docs/chapter1/img/1.38.png differ diff --git a/docs/chapter1/img/1.39.png b/docs/chapter1/img/1.39.png new file mode 100644 index 0000000..6f52a11 Binary files /dev/null and b/docs/chapter1/img/1.39.png differ diff --git a/docs/chapter1/img/1.4.png b/docs/chapter1/img/1.4.png new file mode 100644 index 0000000..3eb81f1 Binary files /dev/null and b/docs/chapter1/img/1.4.png differ diff --git a/docs/chapter1/img/1.40.png b/docs/chapter1/img/1.40.png new file mode 100644 index 0000000..373dc02 Binary files /dev/null and b/docs/chapter1/img/1.40.png differ diff --git a/docs/chapter1/img/1.41.png b/docs/chapter1/img/1.41.png new file mode 100644 index 0000000..b3db399 Binary files /dev/null and b/docs/chapter1/img/1.41.png differ diff --git a/docs/chapter1/img/1.42.png b/docs/chapter1/img/1.42.png new file mode 100644 index 0000000..9aa5c7d Binary files /dev/null and b/docs/chapter1/img/1.42.png differ diff --git a/docs/chapter1/img/1.43.png b/docs/chapter1/img/1.43.png new file mode 100644 index 0000000..b720d49 Binary files /dev/null and b/docs/chapter1/img/1.43.png differ diff --git a/docs/chapter1/img/1.44.png b/docs/chapter1/img/1.44.png new file mode 100644 index 0000000..fafc170 Binary files /dev/null and b/docs/chapter1/img/1.44.png differ diff --git a/docs/chapter1/img/1.45.png b/docs/chapter1/img/1.45.png new file mode 100644 index 0000000..e36284a Binary files /dev/null and b/docs/chapter1/img/1.45.png differ diff --git a/docs/chapter1/img/1.46.png b/docs/chapter1/img/1.46.png new file mode 100644 index 0000000..abf2552 Binary files /dev/null and b/docs/chapter1/img/1.46.png differ diff --git a/docs/chapter1/img/1.47.png b/docs/chapter1/img/1.47.png new file mode 100644 index 0000000..bce609e Binary files /dev/null and b/docs/chapter1/img/1.47.png differ diff --git a/docs/chapter1/img/1.5.png b/docs/chapter1/img/1.5.png new file mode 100644 index 0000000..5fb5f56 Binary files /dev/null and b/docs/chapter1/img/1.5.png differ diff --git a/docs/chapter1/img/1.6.png b/docs/chapter1/img/1.6.png new file mode 100644 index 0000000..20433e1 Binary files /dev/null and b/docs/chapter1/img/1.6.png differ diff --git a/docs/chapter1/img/1.7.png b/docs/chapter1/img/1.7.png new file mode 100644 index 0000000..df13685 Binary files /dev/null and b/docs/chapter1/img/1.7.png differ diff --git a/docs/chapter1/img/1.8.png b/docs/chapter1/img/1.8.png new file mode 100644 index 0000000..3defb23 Binary files /dev/null and b/docs/chapter1/img/1.8.png differ diff --git a/docs/chapter1/img/1.9.png b/docs/chapter1/img/1.9.png new file mode 100644 index 0000000..3892956 Binary files /dev/null and b/docs/chapter1/img/1.9.png differ diff --git a/docs/chapter1/img/learning.png b/docs/chapter1/img/learning.png new file mode 100644 index 0000000..d3cb87f Binary files /dev/null and b/docs/chapter1/img/learning.png differ diff --git a/docs/chapter1/img/planning.png b/docs/chapter1/img/planning.png new file mode 100644 index 0000000..c620811 Binary files /dev/null and b/docs/chapter1/img/planning.png differ diff --git a/docs/chapter10/chapter10.md b/docs/chapter10/chapter10.md new file mode 100644 index 0000000..aabf421 --- /dev/null +++ b/docs/chapter10/chapter10.md @@ -0,0 +1,97 @@ +# Sparse Reward +实际上用 reinforcement learning learn agent 的时候,多数的时候 agent 都是没有办法得到 reward 的。在没有办法得到 reward 的情况下,训练 agent 是非常困难的。举例来说,假设你要训练一个机器手臂,然后桌上有一个螺丝钉跟螺丝起子,那你要训练它用螺丝起子把螺丝钉栓进去,这个很难,为什么?因为一开始你的 agent 是什么都不知道的,它唯一能够做不同的 action 的原因是 exploration。举例来说,你在做 Q-learning 的时候,会有一些随机性,让它去采取一些过去没有采取过的 action,那你要随机到说,它把螺丝起子捡起来,再把螺丝栓进去,然后就会得到 reward 1,这件事情是永远不可能发生的。所以,不管你的 actor 做了什么事情,它得到 reward 永远都是 0,对它来说不管采取什么样的 action 都是一样糟或者是一样的好。所以,它最后什么都不会学到。 + +如果环境中的 reward 非常 sparse,reinforcement learning 的问题就会变得非常的困难,但是人类可以在非常 sparse 的 reward 上面去学习。我们的人生通常多数的时候,我们就只是活在那里,都没有得到什么 reward 或是 penalty。但是,人还是可以采取各种各式各样的行为。所以,一个真正厉害的 AI 应该能够在 sparse reward 的情况下也学到要怎么跟这个环境互动。 + +我们可以通过三个方向来解决 sparse reward 的问题。 + +## Reward Shaping +![](img/10.1.png) + +第一个方向是 `reward shaping`。**Reward shaping 的意思是说环境有一个固定的 reward,它是真正的 reward,但是为了让 agent 学出来的结果是我们要的样子,我们刻意地设计了一些 reward 来引导我们的 agent。** + +举例来说,如果是把小孩当成一个 agent 的话。那一个小孩,他可以 take 两个 actions,一个 action 是他可以出去玩,那他出去玩的话,在下一秒钟它会得到 reward 1。但是他在月考的时候,成绩可能会很差。所以在100 个小时之后呢,他会得到 reward -100。然后,他也可以决定要念书,然后在下一个时间,因为他没有出去玩,所以他觉得很不爽,所以他得到 reward -1。但是在 100 个小时后,他可以得到 reward 100。但对一个小孩来说,他可能就会想要 take play 而不是 take study。我们计算的是 accumulated reward,但也许对小孩来说,他的 discount factor 会很大,所以他就不太在意未来的reward。而且因为他是一个小孩,他还没有很多 experience,所以他的 Q-function estimate 是非常不精准的。所以要他去 estimate 很远以后会得到的 accumulated reward,他其实是预测不出来的。所以这时候大人就要引导他,怎么引导呢?就骗他说,如果你坐下来念书我就给你吃一个棒棒糖。所以,对他来说,下一个时间点会得到的 reward 就变成是positive 的。所以他就觉得说,也许 take 这个 study 是比 play 好的。虽然这并不是真正的 reward,而是其他人骗他的reward,告诉他说你采取这个 action 是好的。Reward shaping 的概念是一样的,简单来说,就是你自己想办法 design 一些 reward,它不是环境真正的 reward。在玩 Atari 游戏里面,真的 reward 是游戏主机给你的 reward,但你自己去设计一些 reward 好引导你的 machine,做你想要它做的事情。 + +![](img/10.2.png) + +举例来说,这个例子是 Facebook 玩 VizDoom 的 agent。VizDoom 是一个第一人射击游戏,在这个射击游戏中,杀了敌人就得到 positive reward,被杀就得到 negative reward。他们设计了一些新的 reward,用新的 reward 来引导 agent 让他们做得更好,这不是游戏中真正的 reward。比如说掉血就扣 0.05 的分数,弹药减少就扣分,捡到补给包就加分,呆在原地就扣分,移动就加分。 活着会扣一个很小的分数,因为不这样做的话,machine 会只想活着,一直躲避敌人,这样会让 machine 好战一点。表格中的参数都是调出来的。 + +Reward shaping 是有问题的,因为我们需要 domain knowledge,举例来说,机器人想要学会的事情是把蓝色的板子从这个柱子穿过去。机器人很难学会,我们可以做 reward shaping。一个貌似合理的说法是,蓝色的板子离柱子越近,reward 越大。但是 machine 靠近的方式会有问题,它会用蓝色的板子打柱子。而我们要把蓝色板子放在柱子上面去,才能把蓝色板子穿过柱子。 这种 reward shaping 的方式是没有帮助的,那至于什么 reward shaping 有帮助,什么 reward shaping 没帮助,会变成一个 domain knowledge,你要去调的。 + +### Curiosity +![](img/10.3.png) + +接下来就是介绍各种你可以自己加进去,in general 看起来是有用的 reward。举例来说,一个技术是给 machine 加上 curiosity,所以叫 `curiosity driven reward`。如上图所示,我们有一个 reward function,它给你某一个 state,给你某一个 action,它就会评断说在这个 state 采取这个 action 得到多少的 reward。那我们当然希望 total reward 越大越好。 + +在 curiosity driven 的这种技术里面,你会加上一个新的 reward function。这个新的 reward function 叫做 `ICM(intrinsic curiosity module)`,它就是要给机器加上好奇心。ICM 会吃 3 个东西,它会吃 state $s_1$、action $a_1$ 和 state $s_2$。根据 $s_1$ 、$a_1$、$s_2$,它会 output 另外一个 reward $r_1^i$。对 machine 来说,total reward 并不是只有 r 而已,还有 $r^i$。它不是只有把所有的 r 都加起来,它还把所有 $r^i$ 加起来当作 total reward。所以,它在跟环境互动的时候,它不是只希望 r 越大越好,它还同时希望 $r^i$ 越大越好,它希望从 ICM 的 module 里面得到的 reward 越大越好。ICM 就代表了一种 curiosity。 + + +![](img/10.4.png) + +怎么设计这个 ICM ?这个是最原始的设计。这个设计是这样,curiosity module 就是 input 3 个东西,input 现在的 state,input 在这个 state 采取的 action,然后 input 下一个 state $s_{t+1}$。接下来会 output 一个 reward $r^i_t$。那这个 $r^i_t$ 是怎么算出来的呢?在 ICM 里面,你有一个 network,这个 network 会 take $a_t$ 跟$s_t$,然后去 output $\hat{s}_{t+1}$,也就是这个 network 根据 $a_t$ 和 $s_t$ 去 predict $\hat{s}_{t+1}$ 。接下来再看说,这个 network 的预测 $\hat{s}_{t+1}$ 跟真实的情况 $s_{t+1}$ 像不像,越不像那得到的 reward 就越大。所以这个 reward $r_t^i$ 的意思是说,如果未来的 state 越难被预测的话,那得到的 reward 就越大。这就是鼓励 machine 去冒险,现在采取这个 action,未来会发生什么事越没有办法预测的话,这个 action 的 reward 就大。所以如果有这样子的 ICM,machine 就会倾向于采取一些风险比较大的 action,它想要去探索未知的世界,它想要去看看说,假设某一个 state 是它没有办法预测,它会特别去想要采取那个 state,这可以增加 machine exploration 的能力。 + +这个 network 1 其实是另外 train 出来的。Training 的时候,这个 network 1,你会给它 $a_t$、 $s_t$、 $s_{t+1}$,然后让这个network 1 去学说 given $a_t, s_t$,怎么 predict $\hat{s}_{t+1}$。Apply 到 agent 互动的时候,其实要把 ICM module fix 住。其实,这一整个想法里面是有一个问题的。这个问题是某一些 state它很难被预测并不代表它就是好的,它就应该要去被尝试的。举例来说,俄罗斯轮盘的结果也是没有办法预测的,并不代表说,人应该每天去玩俄罗斯轮盘这样子。所以只是鼓励 machine 去冒险是不够的,因为如果光是只有这个 network 的架构,machine 只知道说什么东西它无法预测。如果在某一个 state 采取某一个 action,它无法预测接下来结果,它就会采取那个 action,但并不代表这样的结果一定是好的。举例来说,可能在某个游戏里面,背景会有风吹草动,会有树叶飘动。那也许树叶飘动这件事情,是很难被预测的,对 machine 来说它在某一个 state 什么都不做,看着树叶飘动,然后,发现这个树叶飘动是没有办法预测的,接下来它就会一直站在那边,看树叶飘动。所以说,光是有好奇心是不够的,还要让它知道说,什么事情是真正重要的。 + +![](img/10.5.png) + +怎么让 machine 知道说什么事情是真正重要的?你要加上另外一个 module,我们要 learn 一个`feature extractor`,黄色的格子代表 feature extractor,它是 input 一个 state,然后 output 一个 feature vector 来代表这个 state,那我们期待这个 feature extractor 可以把那种没有意义的画面,state 里面没有意义的东西把它过滤掉,比如说风吹草动、白云的飘动、树叶的飘动这种没有意义的东西直接把它过滤掉, + +假设这个 feature extractor 真的可以把无关紧要的东西过滤掉以后,network 1 实际上做的事情是,给它一个 actor,给它一个 state $s_t$ 的 feature representation,让它预测 state $s_{t+1}$ 的 feature representation。接下来我们再看说,这个预测的结果跟真正的 state $s_{t+1}$ 的 feature representation 像不像,越不像,reward 就越大。怎么 learn 这个 feature extractor 呢?让这个 feature extractor 可以把无关紧要的事情滤掉呢?这边的 learn 法就是 learn 另外一个 network 2。这个 network 2 是吃 $\phi(s_t)$、$\phi(s_{t+1})$ 这两个 vector 当做 input,然后接下来它要 predict action a 是什么,然后它希望呢这个 action a 跟真正的 action a 越接近越好。这个 network 2 会 output 一个 action,它 output 说,从 state $s_t$ 跳到 state $s_{t+1}$,要采取哪一个 action 才能够做到,那希望这个 action 跟真正的 action 越接近越好。加上这个 network 2 的好处就是因为要用 $\phi(s_t)$、$\phi(s_{t+1})$ 预测 action。所以,今天我们抽出来的 feature 跟预测 action 这件事情是有关的。所以风吹草动等与 machine 要采取的 action 无关的东西就会被滤掉,就不会被放在抽出来的 vector representation 里面。 + + + +## Curriculum Learning + +![](img/10.6.png) +第二个方向是 `curriculum learning` 。Curriculum learning 不是 reinforcement learning 所独有的概念,其实在 machine learning,尤其是 deep learning 里面,你都会用到 curriculum learning 的概念。举例来说,curriculum learning 的意思是说,你为机器的学习做规划,你给他喂 training data 的时候,是有顺序的,通常都是由简单到难。就好比说,假设你今天要交一个小朋友作微积分,他做错就打他一巴掌,这样他永远都不会做对,太难了。你要先教他九九乘法,然后才教他微积分。所以 curriculum learning 的意思就是在教机器的时候,从简单的题目教到难的题目。就算不是 reinforcement learning,一般在 train deep network 的时候,你有时候也会这么做。举例来说,在 train RNN 的时候,已经有很多的文献都 report 说,你给机器先看短的 sequence,再慢慢给它长的 sequence,通常可以学得比较好。那用在 reinforcement learning 里面,你就是要帮机器规划一下它的课程,从最简单的到最难的。 + +* 举例来说,在 Facebook 玩 VizDoom 的 agent 里面,Facebook 玩 VizDoom 的 agent 蛮强的。他们在参加这个 VizDoom 的比赛,机器的 VizDoom 比赛是得第一名的,他们是有为机器规划课程的。先从课程 0 一直上到课程 7。在这个课程里面,怪物的速度跟血量是不一样的。所以,在越进阶的课程里面,怪物的速度越快,然后他的血量越多。在 paper 里面也有讲说,如果直接上课程 7,machine 是学不起来的。你就是要从课程 0 一路玩上去,这样 machine 才学得起来。 + +* 再举个例子,把蓝色的板子穿过柱子,怎么让机器一直从简单学到难呢? + * 如第一张图所示,也许一开始机器初始的时候,它的板子就已经在柱子上了。这个时候,机器要做的事情只有把蓝色的板子压下去,就结束了。这比较简单,它应该很快就学的会。它只有往上跟往下这两个选择嘛,往下就得到 reward,就结束了,他也不知道学的是什么。 + * 如第二张图所示,这边就是把板子挪高一点,挪高一点,所以它有时候会很笨的往上拉,然后把板子拿出来了。如果它压板子学得会的话,拿板子也比较有机会学得会。假设它现在学的到说,只要板子接近柱子,它就可以把这个板子压下去的话。接下来,你再让它学更 general 的 case。 + * 如第三张图所示,一开始,让板子离柱子远一点。然后,板子放到柱子上面的时候,它就会知道把板子压下去,这个就是 curriculum learning 的概念。当然 curriculum learning 有点 ad hoc(特别),就是需要人去为机器设计它的课程。 + +### Reverse Curriculum Generation + +![](img/10.7.png) + +有一个比较 general 的方法叫做 `Reverse Curriculum Generation`。你可以用一个比较通用的方法来帮机器设计课程,这个比较通用的方法是怎么样呢?假设你现在一开始有一个 state $s_g$,这是你的 gold state,也就是最后最理想的结果。如果拿刚才那个板子和柱子的实验作为例子的话,就把板子放到柱子里面,这样子叫做 gold state。你就已经完成了,或者你让机器去抓东西,你训练一个机器手臂抓东西,抓到东西以后叫做 gold state。接下来你根据你的 gold state 去找其他的 state,这些其他的 state 跟 gold state 是比较接近的。举例来说,如果是让机器抓东西的例子里面,你的机器手臂可能还没有抓到东西。假设这些跟 gold state 很近的 state 叫做 $s_1$。你的机械手臂还没有抓到东西,但它离 gold state 很近,那这个叫做$s_1$。至于什么叫做近,这是 case dependent,你要根据你的 task 来 design 说怎么从 $s_g$ sample 出 $s_1$。如果是机械手臂的例子,可能就比较好想。其他例子可能就比较难想。接下来呢,你再从这些 $s_1$ 开始做互动,看它能不能够达到 gold state $s_g$,那每一个 state,你跟环境做互动的时候,你都会得到一个 reward R。 + +![](img/10.8.png) + +接下来,我们把 reward 特别极端的 case 去掉。Reward 特别极端的 case 的意思就是说那些 case 太简单或是太难了。如果 reward 很大,代表说这个 case 太简单了,就不用学了,因为机器已经会了,它可以得到很大的 reward。如果 reward 太小,代表这个 case 太难了,依照机器现在的能力这个课程太难了,它学不会,所以就不要学这个,所以只找一些 reward 适中的 case。 + +什么叫做适中,这个就是你要调的参数,找一些 reward 适中的 case。接下来,再根据这些 reward 适中的 case 去 sample 出更多的 state。假设你一开始,你机械手臂在这边,可以抓的到以后。接下来,就再离远一点,看看能不能够抓得到,又抓的到以后,再离远一点,看看能不能抓得到。这是一个有用的方法,它叫做`Reverse Curriculum learning`。刚才讲的是 curriculum learning,就是你要为机器规划它学习的顺序。而 reverse curriculum learning 是从 gold state 去反推,就是说你原来的目标是长这个样子,我们从目标去反推,所以这个叫做 reverse。 + +## Hierarchical RL + +![](img/10.9.png) + +第三个方向是`分层强化学习(hierarchical reinforcement learning,HRL)`。分层强化学习是说,我们有好几个 agent。然后,有一些 agent 负责比较 high level 的东西,它负责订目标,然后它订完目标以后,再分配给其他的 agent,去把它执行完成。 + +这样的想法其实也是很合理的。因为人在一生之中,并不是时时刻刻都在做决定。举例来说,假设你想要写一篇 paper,你会说就我先想个梗这样子,然后想完梗以后,你还要跑个实验。跑完实验以后,你还要写。写完以后呢,你还要这个去发表。每一个动作下面又还会再细分,比如说怎么跑实验呢?你要先 collect data,collect 完 data 以后,你要再 label,你要弄一个 network,然后又 train 不起来,要 train 很多次。然后重新 design network 架构好几次,最后才把 network train 起来。 + +所以,我们要完成一个很大的 task 的时候,我们并不是从非常底层的那些 action 开始想起,我们其实是有个 plan。我们先想说,如果要完成这个最大的任务,那接下来要拆解成哪些小任务。每一个小任务要再怎么拆解成小小的任务。举例来说,叫你直接写一本书可能很困难,但叫你先把一本书拆成好几个章节,每个章节拆成好几段,每一段又拆成好几个句子,每一个句子又拆成好几个词汇,这样你可能就比较写得出来,这个就是分层的 reinforcement learning 的概念。 + +这边是举一个例子,就是假设校长、教授和研究生通通都是 agent。那今天假设我们只要进入百大就可以得到 reward。假设进入百大的话,校长就要提出愿景告诉其他的 agent 说,现在你要达到什么样的目标。那校长的愿景可能就是说教授每年都要发三篇期刊。然后接下来这些 agent 都是有分层的,所以上面的 agent,他的动作就是提出愿景这样。那他把他的愿景传给下一层的 agent,下一层的 agent 就把这个愿景吃下去。如果他下面还有其他人的话,它就会提出新的愿景。比如说,校长要教授发期刊,但其实教授自己也是不做实验的。所以,教授也只能够叫下面的研究生做实验。所以教授就提出愿景,就做出实验的规划,然后研究生才是真的去执行这个实验的人。然后,真的把实验做出来,最后大家就可以得到reward。那现在是这样子的,在 learn 的时候,其实每一个 agent 都会 learn。那他们的整体的目标就是要达到最后的reward。那前面的这些 agent,他提出来的 actions 就是愿景这样。你如果是玩游戏的话,他提出来的就是,我现在想要产生这样的游戏画面。但是,假设他提出来的愿景是下面的 agent 达不到的,那就会被讨厌。举例来说,教授对研究生都一直逼迫研究生做一些很困难的实验,研究生都做不出来的话,研究生就会跑掉,所以他就会得到一个 penalty。所以如果今天下层的 agent 没有办法达到上层 agent 所提出来的 goal 的话,上层的 agent 就会被讨厌,它就会得到一个 negative reward。所以他要避免提出那些愿景是底下的 agent 所做不到的。那每一个 agent 都是把上层的 agent 所提出来的愿景当作输入,然后决定他自己要产生什么输出。 + +但是你知道说,就算你看到上面的的愿景说,叫你做这一件事情。你最后也不一定能做成这一件事情。假设本来教授目标是要写期刊,但是不知道怎么回事,他就要变成一个 YouTuber。这个 paper 里面的 solution,我觉得非常有趣。给大家做一个参考,这其实本来的目标是要写期刊,但却变成 YouTuber,那怎么办呢? 把原来的愿景改成变成 YouTuber 就行了,在 paper 里面就是这么做的,为什么这么做呢? 因为虽然本来的愿景是要写期刊,但是后来变成 YouTuber,难道这些动作都浪费了吗? 不是,这些动作是没有被浪费的。我们就假设说,本来的愿景其实就是要成为 YouTuber,那你就知道成为 YouTuber 要怎做了。这个是分层 RL,是可以做得起来的 tip。 + +![](img/10.10.png) + + +上图是真实的例子。实际上呢,这里面就做了一些比较简单的游戏,这个是走迷宫,蓝色是 agent,蓝色的 agent 要走到黄色的目标。这边也是,这个单摆要碰到黄色的球。那愿景是什么呢? + +在这个 task 里面,它只有两个 agent ,下层的一个 agent 负责决定说要怎么走,上层的 agent 就负责提出愿景。虽然,实际上你可以用很多层,但 paper 就用了两层。 + +走迷宫的例子是说粉红色的这个点代表的就是愿景。上层这个 agent,它告诉蓝色的这个 agent 说,你现在的第一个目标是先走到这个地方,蓝色的 agent 走到以后,再说你的新的目标是走到这里。蓝色的 agent 再走到以后,新的目标在这里。接下来又跑到这边,最后希望蓝色的 agent 就可以走到黄色的这个位置。 + +单摆的例子也一样,就是粉红色的这个点代表的是上层的 agent 所提出来的愿景,所以这个 agent 先摆到这边,接下来,新的愿景又跑到这边,所以它又摆到这里。然后,新的愿景又跑到上面。然后又摆到上面,最后就走到黄色的位置了。这个就是 hierarchical 的 reinforcement learning。 + +最后总结下分层强化学习。分层强化学习是指将一个复杂的强化学习问题分解成多个小的、简单的子问题,每个子问题都可以单独用马尔可夫决策过程来建模。这样,我们可以将智能体的策略分为高层次策略和低层次策略,高层次策略根据当前状态决定如何执行低层次策略。这样,智能体就可以解决一些非常复杂的任务。 + +## References + +* [神经网络与深度学习](https://nndl.github.io/) + diff --git a/docs/chapter10/chapter10_questions&keywords.md b/docs/chapter10/chapter10_questions&keywords.md new file mode 100644 index 0000000..df15277 --- /dev/null +++ b/docs/chapter10/chapter10_questions&keywords.md @@ -0,0 +1,27 @@ +# Chapter10 Sparse Reward + +## 1 Keywords + +- **reward shaping:** 在我们的agent与environment进行交互时,我们人为的设计一些reward,从而“指挥”agent,告诉其采取哪一个action是最优的,而这个reward并不是environment对应的reward,这样可以提高我们estimate Q-function时的准确性。 +- **ICM(intrinsic curiosity module):** 其代表着curiosity driven这个技术中的增加新的reward function以后的reward function。 +- **curriculum learning:** 一种广义的用在RL的训练agent的方法,其在input训练数据的时候,采取由易到难的顺序进行input,也就是认为设计它的学习过程,这个方法在ML和DL中都会普遍使用。 +- **reverse curriculum learning:** 相较于上面的curriculum learning,其为更general的方法。其从最终最理想的state(我们称之为gold state)开始,依次去寻找距离gold state最近的state作为想让agent达到的阶段性的“理想”的state,当然我们应该在此过程中有意的去掉一些极端的case(太简单、太难的case)。综上,reverse curriculum learning 是从 gold state 去反推,就是说你原来的目标是长这个样子,我们从我们的目标去反推,所以这个叫做 reverse curriculum learning。 +- **hierarchical (分层) reinforcement learning:** 将一个大型的task,横向或者纵向的拆解成多个 agent去执行。其中,有一些agent 负责比较high level 的东西,负责订目标,然后它订完目标以后,再分配给其他的 agent把它执行完成。(看教程的 hierarchical reinforcement learning部分的示例就会比较明了) + +## 2 Questions + +- 解决sparse reward的方法有哪些? + + 答:Reward Shaping、curiosity driven reward、(reverse)curriculum learning 、Hierarchical Reinforcement learning等等。 + +- reward shaping方法存在什么主要问题? + + 答:主要的一个问题是我们人为设计的reward需要domain knowledge,需要我们自己设计出符合environment与agent更好的交互的reward,这需要不少的经验知识,需要我们根据实际的效果进行调整。 + +- ICM是什么?我们应该如何设计这个ICM? + + 答:ICM全称为intrinsic curiosity module。其代表着curiosity driven这个技术中的增加新的reward function以后的reward function。具体来说,ICM在更新计算时会考虑三个新的东西,分别是 state $s_1$、action $a_1$ 和 state $s_2$。根据$s_1$ 、$a_1$、 $a_2$,它会 output 另外一个新的 reward $r_1^i$。所以在ICM中我们total reward 并不是只有 r 而已,还有 $r^i$。它不是只有把所有的 r 都加起来,它还把所有 $r^i$ 加起来当作total reward。所以,它在跟环境互动的时候,它不是只希望 r 越大越好,它还同时希望 $r^i$ 越大越好,它希望从 ICM 的 module 里面得到的 reward 越大越好。ICM 就代表了一种curiosity。 + + 对于如何设计ICM,ICM的input就像前面所说的一样包括三部分input 现在的 state $s_1$,input 在这个 state 采取的 action $a_1$,然后接 input 下一个 state $s_{t+1}$,对应的output就是reward $r_1^i$,input到output的映射是通过network构建的,其使用 $s_1$ 和 $a_1$ 去预测 $\hat{s}_{t+1}$ ,然后继续评判预测的$\hat{s}_{t+1}$和真实的$s_{t+1}$像不像,越不相同得到的reward就越大。通俗来说这个reward就是,如果未来的状态越难被预测的话,那么得到的reward就越大。这也就是curiosity的机制,倾向于让agent做一些风险比较大的action,从而增加其machine exploration的能力。 + + 同时为了进一步增强network的表达能力,我们通常讲ICM的input优化为feature extractor,这个feature extractor模型的input就是state,output是一个特征向量,其可以表示这个state最主要、重要的特征,把没有意义的东西过滤掉。 diff --git a/docs/chapter10/img/10.1.png b/docs/chapter10/img/10.1.png new file mode 100644 index 0000000..f394ac0 Binary files /dev/null and b/docs/chapter10/img/10.1.png differ diff --git a/docs/chapter10/img/10.10.png b/docs/chapter10/img/10.10.png new file mode 100644 index 0000000..8a4edbc Binary files /dev/null and b/docs/chapter10/img/10.10.png differ diff --git a/docs/chapter10/img/10.2.png b/docs/chapter10/img/10.2.png new file mode 100644 index 0000000..e1dda19 Binary files /dev/null and b/docs/chapter10/img/10.2.png differ diff --git a/docs/chapter10/img/10.3.png b/docs/chapter10/img/10.3.png new file mode 100644 index 0000000..1d4b264 Binary files /dev/null and b/docs/chapter10/img/10.3.png differ diff --git a/docs/chapter10/img/10.4.png b/docs/chapter10/img/10.4.png new file mode 100644 index 0000000..5e1d209 Binary files /dev/null and b/docs/chapter10/img/10.4.png differ diff --git a/docs/chapter10/img/10.5.png b/docs/chapter10/img/10.5.png new file mode 100644 index 0000000..487f858 Binary files /dev/null and b/docs/chapter10/img/10.5.png differ diff --git a/docs/chapter10/img/10.6.png b/docs/chapter10/img/10.6.png new file mode 100644 index 0000000..50599e9 Binary files /dev/null and b/docs/chapter10/img/10.6.png differ diff --git a/docs/chapter10/img/10.7.png b/docs/chapter10/img/10.7.png new file mode 100644 index 0000000..1c08fad Binary files /dev/null and b/docs/chapter10/img/10.7.png differ diff --git a/docs/chapter10/img/10.8.png b/docs/chapter10/img/10.8.png new file mode 100644 index 0000000..e9a38f5 Binary files /dev/null and b/docs/chapter10/img/10.8.png differ diff --git a/docs/chapter10/img/10.9.png b/docs/chapter10/img/10.9.png new file mode 100644 index 0000000..813ebfb Binary files /dev/null and b/docs/chapter10/img/10.9.png differ diff --git a/docs/chapter11/chapter11.md b/docs/chapter11/chapter11.md new file mode 100644 index 0000000..9ee0433 --- /dev/null +++ b/docs/chapter11/chapter11.md @@ -0,0 +1,107 @@ +# 模仿学习 +`模仿学习(imitation learning,IL)`讨论的问题是:假设我们连奖励都没有,那要怎么办呢?模仿学习又叫做`示范学习(learning from demonstration)`,`学徒学习(apprenticeship learning)`,`观察学习(learning by watching)`。在模仿学习里面,你有一些专家的示范,那机器也可以跟环境互动,但它没有办法从环境里面得到任何的奖励,它只能看着专家的示范来学习什么是好,什么是不好。其实,多数的情况,我们都没有办法真的从环境里面得到非常明确的奖励。举例来说,如果是棋类游戏或者是电玩,你有非常明确的奖励。但是其实多数的任务,都是没有奖励的。以聊天机器人为例,机器跟人聊天,聊得怎么样算是好,聊得怎么样算是不好,你无法给出明确的奖励。所以很多任务是根本就没有办法给出奖励的。 + +虽然没有办法给出奖励,但是收集专家的示范是可以做到的。举例来说,在自动驾驶汽车里面,虽然你没有办法给出自动驾驶汽车的奖励,但你可以收集很多人类开车的纪录。在聊天机器人里面,你可能没有办法定义什么叫做好的对话,什么叫做不好的对话。但是收集很多人的对话当作范例,这一件事情也是可行的。 + +所以模仿学习的使用性非常高。假设你不知道该怎么定义奖励,你就可以收集到专家的示范。如果你可以收集到一些范例的话,你可以收集到一些很厉害的智能体(比如人)跟环境实际上的互动的话,那你就可以考虑模仿学习这个技术。在模仿学习里面,我们介绍两个方法。第一个叫做`行为克隆(behavior cloning,BC)`,第二个叫做`逆强化学习(inverse reinforcement learning,IRL)` 或者叫做`逆最优控制(inverse optimal control)`。 + +## 行为克隆 +其实行为克隆跟监督学习是一模一样的。如下图所示,以自动驾驶汽车为例,你可以收集到人开自动驾驶汽车的所有资料,比如说可以通过行车记录器进行收集。看到下图的观测的时候,人会决定向前。机器就采取跟人一样的行为,也向前,就结束了。这个就叫做行为克隆,专家做什么,机器就做一模一样的事。 + +怎么让机器学会跟专家一模一样的行为呢?我们可以把它当作一个监督学习的问题,去收集很多行车记录器,然后再收集人在具体情境下会采取什么样的行为(训练数据)。你知道说人在状态$s_1$ 会采取动作$a_1$,人在状态$s_2$ 会采取动作$a_2$。人在状态, $s_3$ 会采取动作$a_3$。接下来,你就学习一个网络。这个网络就是演员,它输入$s_i$ 的时候,你就希望它的输出 是$a_i$,就这样结束了。它就是一个的监督学习的问题。 + +![](img/11.2.png ':size=400') + +行为克隆虽然非常简单,但它的问题是如果你只收集专家的资料,你可能看过的观测会是非常有限的。举例来说,如下图所示,假设你要学习一部自动驾驶汽车,自动驾驶汽车就是要过这个弯道。如果是专家的话,它就是把车顺着这个红线就开过去了。但假设智能体很笨,它开着开着就撞墙了,它永远不知道撞墙这种状况要怎么处理。因为训练数据里面从来没有撞过墙,所以它根本就不知道撞墙这一种情况要怎么处理。打电玩也是一样,让人去玩马里奥(Mario),那专家可能非常强,它从来不会跳不上水管,所以机器根本不知道跳不上水管时要怎么处理。 + + +所以光是做行为克隆是不够的,只观察专家的行为是不够的,需要一个招数,这个招数叫作`数据集聚合(dataset aggregation,DAgger)`。 + +![](img/11.3.png ':size=300') + +我们会希望收集更多样性的数据,而不是只收集专家所看到的观测。我们会希望能够收集专家在各种极端的情况下,它会采取什么样的行为。如下图所示,以自动驾驶汽车为例的话,假设一开始,我们有演员 $\pi_1$,并且让 $\pi_1$去开这个车,但车上坐了一个专家。这个专家会不断地告诉机器说,如果在这个情境里面,我会怎么样开。所以 $\pi_1$ 自己开自己的,但是专家会不断地表示它的想法。比如说,一开始的时候,专家可能说往前走。在拐弯的时候,专家可能就会说往右转。但 $\pi_1$ 是不管专家的指令的,所以它会继续去撞墙。虽然专家说往右转,但是不管它怎么下指令都是没有用的,$\pi_1$ 会自己做自己的事情,因为我们要做的记录的是说,专家在 $\pi_1$ 看到这种观测的情况下,它会做什么样的反应。这个方法显然是有一些问题的,因为你每开一次自动驾驶汽车就会牺牲一个人。那你用这个方法,你牺牲一个专家以后,你就会知道,人类在这样子的状态下,在快要撞墙的时候,会采取什么样的行为。再把这个数据拿去训练新的 $\pi_2$。这个过程就反复继续下去,这个方法就叫做数据集聚合。 + +![](img/11.4.png ':size=300') + +行为克隆还有一个问题:机器会完全模仿专家的行为,不管专家的行为是否有道理,就算没有道理,没有什么用的,就算这是专家本身的习惯,机器也会硬把它记下来。如果机器确实可以记住所有专家的行为,也许还好。因为如果专家这么做,有些行为是多余的。但是没有问题,假设机器的行为可以完全仿造专家行为,也就算了,它就是跟专家一样得好,只是做一些多余的事。但问题是机器是一个网络,网络的容量是有限的。就算给网络训练数据,它在训练数据上得到的正确率往往也不是 100%,它有些事情是学不起来的。这个时候,什么该学,什么不该学就变得很重要。 + +举例来说,如下图所示,在学习中文的时候,老师有语音、行为和知识,但其实只有语音部分是重要的,知识的部分是不重要的。也许机器只能够学一件事,也许它就只学到了语音,那没有问题。如果它只学到了手势,这样子就有问题了。所以让机器学习什么东西是需要模仿,什么东西是不需要模仿,这件事情是重要的。而单纯的行为克隆就没有把这件事情学进来,因为机器只是复制专家所有的行为而已,它不知道哪些行为是重要,是对接下来有影响的,哪些行为是不重要的,是对接下来是没有影响的。 + +![](img/11.5.png ':size=450') + +行为克隆还有一个问题:在做行为克隆的时候,训练数据跟测试数据是不匹配的。我们可以用数据集聚合的方法来缓解这个问题。在训练跟测试的时候,数据分布其实是不一样的。因为在强化学习里面,动作会影响到接下来所看到的状态。我们是先有状态$s_1$,然后采取动作$a_1$,动作$a_1$ 其实会决定接下来你看到什么样的状态$s_2$。所以在强化学习里面有一个很重要的特征,就是你采取了动作会影响你接下来所看到的状态,也就是会影响状态的分布。如果做了行为克隆的话,我们只能观察到专家$\hat{\pi}$的一堆状态跟动作的对$(s,a)$。 + +然后我们希望可以学习一个 $\pi^*$,我们希望 $\pi^*$ 跟 $\hat{\pi}$ 越接近越好。如果 $\pi^*$ 可以跟 $\hat{\pi}$ 一模一样的话,训练的时候看到的状态跟测试的时候所看到的状态会是一样的。因为虽然动作会影响我们看到的状态,但假设两个策略一模一样, 在同一个状态都会采取同样的动作,那你接下来所看到的状态都会是一样的。但问题就是你很难让学习出来的策略跟专家的策略一模一样。专家可是一个人,网络要跟人一模一样,有点困难。 + +如果 $\pi^*$ 跟 $\hat{\pi}$ 有一点误差。这个误差在一般监督学习问题里面,每一个样本(example)都是独立的,也许还好。但对强化学习的问题来说,可能在某个地方就是失之毫厘,差之千里。可能在某个地方,也许机器没有办法完全复制专家的行为,它复制的差了一点点,也许最后得到的结果就会差很多这样。所以行为克隆并不能够完全解决模仿学习这件事情,我们就有另外一个比较好的做法叫做逆强化学习。 + +## 逆强化学习 +为什么叫逆强化学习,因为原来的强化学习里面,有一个环境和一个奖励函数。根据环境和奖励函数,通过强化学习这个技术,你会找到一个演员,你会学习出一个最优演员。但逆强化学习刚好是相反的,你没有奖励函数,你只有一堆专家的示范。但你还是有环境的。逆强化学习的做法是说假设我们现在有一堆专家的示范,我们用 $\hat{\tau}$ 来代表专家的示范。如果是在玩电玩的话,每一个 $\tau$ 就是一个很会玩电玩的人玩一场游戏的纪录,如果是自动驾驶汽车的话,就是人开自动驾驶汽车的纪录。这一边就是专家的示范,每一个 $\tau$ 是一个轨迹。 + + +把所有专家示范收集起来,然后,使用逆强化学习这个技术。使用逆强化学习技术的时候,机器是可以跟环境互动的。但它得不到奖励。它的奖励必须要从专家那边推出来,有了环境和专家示范以后,去反推出奖励函数长什么样子。之前强化学习是由奖励函数反推出什么样的动作、演员是最好的。逆强化学习是反过来,我们有专家的示范,我们相信它是不错的,我就反推说,专家是因为什么样的奖励函数才会采取这些行为。你有了奖励函数以后,接下来,你就可以套用一般的强化学习的方法去找出最优演员。所以逆强化学习是先找出奖励函数,找出奖励函数以后,再去用强化学习找出最优演员。 + +把这个奖励函数学习出来,相较于原来的强化学习有什么样好处。一个可能的好处是也许奖励函数是比较简单的。也许,虽然这个专家的行为非常复杂,但也许简单的奖励函数就可以导致非常复杂的行为。一个例子就是也许人类本身的奖励函数就只有活着这样,每多活一秒,你就加一分。但人类有非常复杂的行为,但是这些复杂的行为,都只是围绕着要从这个奖励函数里面得到分数而已。有时候很简单的奖励函数也许可以推导出非常复杂的行为。 + + +逆强化学习实际上是怎么做的呢?如下图所示,首先,我们有一个专家$\hat{\pi}$,这个专家去跟环境互动,给我们很多轨迹:{$\hat{\tau_1}$,$\hat{\tau_2}$,...,$\hat{\tau_N}$}。如果是玩游戏的话,就让某一个电玩高手,去玩 $N$ 场游戏。把 $N$ 场游戏的状态跟动作的序列都记录下来。接下来,你有一个演员 $\pi$,一开始演员很烂,这个演员也去跟环境互动。它也去玩了 $N$ 场游戏,它也有 $N$ 场游戏的纪录。接下来,我们要反推出奖励函数。怎么推出奖励函数呢?原则就是专家永远是最棒的,是先射箭,再画靶的概念。 +专家去玩一玩游戏,得到这一些游戏的纪录,演员也去玩一玩游戏,得到这些游戏的纪录。接下来,你要定一个奖励函数,这个奖励函数的原则就是专家得到的分数要比演员得到的分数高(先射箭,再画靶),所以我们就学习出一个奖励函数。你就找出一个奖励函数。这个奖励函数会使专家所得到的奖励大过于演员所得到的奖励。你有了新的奖励函数以后,就可以套用一般强化学习的方法去学习一个演员,这个演员会针对奖励函数去最大化它的奖励。它也会采取一大堆的动作。但是这个演员虽然可以最大化这个奖励函数,采取一大堆的行为,得到一大堆游戏的纪录。 + +但接下来,我们就改奖励函数。这个演员就会很生气,它已经可以在这个奖励函数得到高分。但是它得到高分以后,我们就改奖励函数,仍然让专家可以得到比演员更高的分数。这个就是逆强化学习。有了新的奖励函数以后,根据这个新的奖励函数,你就可以得到新的演员,新的演员再去跟环境做一下互动,它跟环境做互动以后, 你又会重新定义奖励函数,让专家得到的奖励比演员大。 + +怎么让专家得到的奖励大过演员呢?如下图所示,其实我们在学习的时候,奖励函数也许就是神经网络。这个神经网络就是输入 $\tau$,输出就是应该要给这个 $\tau$ 多少的分数。或者说,你假设觉得输入整个 $\tau$ 太难了。因为 $\tau$ 是 $s$ 和 $a$ 的一个很长的序列。也许它就是输入一个 $s$ 和 $a$ 的对,然后输出一个实数。把整个 $\tau$ 会得到的实数都加起来就得到 $R(\tau)$。在训练的时候,对于 $\left\{\hat{\tau}_{1}, \hat{\tau}_{2}, \cdots, \hat{\tau}_{N}\right\}$,我们希望它输出的 $R$ 越大越好。对于 $\left\{\tau_{1}, \tau_{2}, \cdots, \tau_{N}\right\}$,我们就希望 $R$ 的值越小越好。 + +![](img/11.7.png ':size=450') + +什么叫做一个最好的奖励函数。最后你学习出来的奖励函数应该就是专家和演员在这个奖励函数都会得到一样高的分数。最终奖励函数没有办法分辨出谁应该会得到比较高的分数。通常在训练的时候,你会迭代地去做。最早的逆强化学习对奖励函数有些限制,它是假设奖励函数是线性的(linear) 。如果奖励函数是线性的话,你可以证明这个算法会收敛(converge)。但是如果不是线性的,你就没有办法证明说它会收敛。 + +逆强化学习的框架如下图所示,其实我们只要把逆强化学习中的演员看成生成器,把奖励函数看成判别器,它就是 GAN。所以逆强化学习会不会收敛这个问题就等于是问说 GAN 会不会收敛。如果你已经实现过,你会知道不一定会收敛。但除非你对 $R$ 下一个非常严格的限制,如果 $R$ 是一个一般的网络的话,你就会有很大的麻烦。 + +![](img/11.8.png ':size=450') + +我们可以把逆强化学习跟 GAN 比较一下。 +如下图所示,GAN 里面,我们有一堆很好的图、一个生成器和一个判别器。一开始生成器不知道要产生什么样的图,它就乱画。判别器的工作就是给画的图打分,专家画的图就是高分,生成器画的图就是低分。生成器会想办法去骗过判别器,生成器会希望判别器 也会给它画的图高分。整个过程跟逆强化学习是一模一样的。画的图就是专家的示范。生成器就是 演员,生成器画很多图,演员 会去跟环境互动,产生很多轨迹。这些轨迹 跟环境互动的记录,游戏的纪录其实就是等于 GAN 里面的这些图。然后你学习一个奖励函数。奖励函数就是判别器。奖励函数要给专家的示范高分,给演员互动的结果低分。 +接下来,演员会想办法,从这个已经学习出来的奖励函数里面得到高分,然后迭代地去循环。跟 GAN 其实是一模一样的,我们只是换个说法而已。 + +![](img/11.9.png ':size=450') + +逆强化学习有很多的应用,比如可以用开来自动驾驶汽车,有人用这个技术来学开自动驾驶汽车的不同风格。每个人在开车的时候会有不同风格,举例来说,能不能够压到线,能不能够倒退,要不要遵守交通规则等等。每个人的风格是不同的,然后用逆强化学习可以让自动驾驶汽车学会各种不同的开车风格。 + +下图是文献上真实的例子。在这个例子里面,逆强化学习有一个有趣的地方,通常你不需要太多的训练数据,因为训练数据往往都是个位数。因为逆强化学习只是一种示范,只是一种范例,实际上机器可以去跟环境互动非常多次。所以在逆强化学习的文献, 往往会看到说只用几笔数据就训练出一些有趣的结果。 +比如说,在这个例子里面是要让自动驾驶汽车学会在停车场里面停。这边的示范是这样,蓝色是终点,自动驾驶汽车要开到蓝色终点停车。给机器只看一行的四个示范,然后让它去学怎么样开车,最后它就可以学出,在红色的终点位置,如果它要停车的话,它会这样开。给机器看不同的示范,最后它学出来开车的风格就会不太一样。举例来说,上图第二行是不守规矩的开车方式,因为它会开到道路之外,这边,它会穿过其它的车,然后从这边开进去。所以机器就会学到说,不一定要走在道路上,它可以走非道路的地方。上图第三行是倒退来停车,机器也会学会说,它可以倒退, + +![](img/11.11.png ':size=450') + +这种技术也可以拿来训练机器人。你可以让机器人,做一些你想要它做的动作,过去如果你要训练机器人,做你想要它做的动作,其实是比较麻烦的。怎么麻烦呢?过去如果你要操控机器的手臂,你要花很多力气去写程序才让机器做一件很简单的事看。假设你有模仿学习的技术,你可以让人做一下示范,然后机器就跟着人的示范来进行学习,比如学会摆盘子,拉着机器人的手去摆盘子,机器自己动。让机器学会倒水,人只教它 20 次,杯子每次放的位置不太一样。用这种方法来教机械手臂。 + +## 第三人称视角模仿学习 + +其实还有很多相关的研究,如下图所示,举例来说,你在教机械手臂的时候,要注意就是也许机器看到的视野跟人看到的视野是不太一样的。在刚才那个例子里面,人跟机器的动作是一样的。但是在未来的世界里面,也许机器是看着人的行为学的。刚才是人拉着,假设你要让机器学会打高尔夫球,在刚才的例子里面就是人拉着机器人手臂去打高尔夫球,但是在未来有没有可能机器就是看着人打高尔夫球,它自己就学会打高尔夫球了呢?但这个时候,要注意的事情是机器的视野跟它真正去采取这个行为的时候的视野是不一样的。机器必须了解到当它是第三人的视角的时候,看到另外一个人在打高尔夫球,跟它实际上自己去打高尔夫球的时候,看到的视野显然是不一样的。但它怎么把它是第三人称视角所观察到的经验把它泛化到它是第一人称视角的时候所采取的行为,这就需要用到`第三人称视角模仿学习(third person imitation learning)`的技术。 + +![](img/11.13.png ':size=450') + +这个怎么做呢?它的技术其实也是不只是用到模仿学习,它用到了`领域对抗训练(domain-adversarial Training)`。我们在讲领域对抗训练的时候,我们有讲说这也是一个 GAN 的技术。如下图 所示,我们希望有一个提取器,有两个不同领域(domain)的图像,通过特征提取器以后,没有办法分辨出它来自哪一个领域。其实第一人称视角和第三人称视角,模仿学习用的技术其实也是一样的,希望学习一个特征提取器,机器在第三人称的时候跟它在第一人称的时候看到的视野其实是一样的,就是把最重要的东西抽出来就好了。 + +![](img/11.14.png ':size=450') + +## 序列生成和聊天机器人 +在讲序列生成对抗网络(sequence GAN)的时候,我们有讲过句子生成(sentence generation)跟聊天机器人。那其实句子生成或聊天机器人 也可以想成是模仿学习。 +如下图所示,机器在模仿人写的句子,你在写句子的时候,你写下去的每一个字都想成是一个动作,所有的字合起来就是一个回合。举例来说,句子生成里面,你会给机器看很多人类写的文字。你要让机器学会写诗,那你就要给它看唐诗三百首。人类写的文字其实就是专家的示范。每一个词汇其实就是一个动作。你让机器做句子生成的时候,其实就是在模仿专家的轨迹。聊天机器人也是一样,在聊天机器人 里面你会收集到很多人互动对话的纪录,那些就是专家的示范。 + +如果我们单纯用最大似然(maximum likelihood)这个技术来最大化会得到似然(likelihood),这个其实就是行为克隆。行为克隆就是看到一个状态,接下来预测我们会得到什么样的动作,有一个标准答案(ground truth)告诉机器说什么样的动作是最好的。在做似然的时候也是一样,给定句子已经产生的部分。接下来机器要预测说接下来要写哪一个字才是最好的。所以,其实最大似然在做序列生成(sequence generation)的时候,它对应到模仿学习里面就是行为克隆。只有最大似然是不够的,我们想要用序列生成对抗网络。其实序列生成对抗网络就是对应到逆强化学习,逆强化学习就是一种 GAN 的技术。你把逆强化学习的技术放在句子生成,放到聊天机器人里面,其实就是序列生成对抗网络跟它的种种的变形。 + +![](img/11.15.png ':size=450') + +## References + +* [机器学习](https://book.douban.com/subject/26708119//) +* [模仿学习(Imitation Learning)介绍](https://zhuanlan.zhihu.com/p/25688750) + + + + + + + + + diff --git a/docs/chapter11/chapter11_questions&keywords.md b/docs/chapter11/chapter11_questions&keywords.md new file mode 100644 index 0000000..9b60b89 --- /dev/null +++ b/docs/chapter11/chapter11_questions&keywords.md @@ -0,0 +1,36 @@ +# Chapter11 Imitation Learning + +## 1 Keywords + +- **Imitation learning:** 其讨论我们没有reward或者无法定义reward但是有与environment进行交互时怎么进行agent的学习。这与我们平时处理的问题中的情况有些类似,因为通常我们无法从环境中得到明确的reward。Imitation learning 又被称为 learning from demonstration (示范学习) ,apprenticeship learning (学徒学习),learning by watching (观察学习)等。 +- **Behavior Cloning:** 类似于ML中的监督学习,通过收集expert的state与action的对应信息,训练我们的network(actor)。在使用时input state时,得到对应的outpur action。 +- **Dataset Aggregation:** 用来应对在Behavior Cloning中expert提供不到的data,其希望收集expert在各种极端state下expert的action。 +- **Inverse Reinforcement learning(IRL):** Inverse Reinforcement Learning 是先找出 reward function,再去用 Reinforcement Learning 找出 optimal actor。这么做是因为我们没有环境中reward,但是我们有expert 的demonstration,使用IRL,我们可以推断expert 是因为什么样的 reward function 才会采取这些action。有了reward function 以后,接下来,就可以套用一般的 reinforcement learning 的方法去找出 optimal actor。 +- **Third Person Imitation Learning:** 一种把第三人称视角所观察到的经验 generalize 到第一人称视角的经验的技术。 + +## 2 Questions + +- 对于Imitation Learning 的方法有哪些? + + 答:Behavior Cloning、Inverse Reinforcement Learning(IRL)或者称为Inverse Optimal Control。 + +- Behavior Cloning存在哪些问题呢?我们可以如何处理呢? + + 答: + + 1. 首先,如果只收集expert 的data(看到某一个state输出的action),你可能看过的 observation 会是非常 limited。所以我们要收集expert在各种极端state下的action,或者说是要收集更多的、复杂的data,可以使用教程中提到的Dataset Aggregation。 + 2. 另外,使用传统意义上的Behavior Cloning的话,机器会完全 copy expert 的行为,不管 expert 的行为是否有道理,就算没有道理,没有什么用的,这是expert 本身的习惯,机器也会硬把它记下来。我们的agent是一个 machine,它是一个 network,network 的capacity 是有限的。就算给 network training data,它在training data 上得到的正确率往往也不是100%,他有些事情是学不起来的。这个时候,什么该学,什么不该学就变得很重要。不过极少数expert的行为是没有意义的,但是至少也不会产生较坏的影响。 + 3. 还有,在做 Behavior Cloning 的时候,training data 跟 testing data 是 mismatch 的。我们可以用 Dataset Aggregation 的方法来缓解这个问题。这个问题是,在 training 跟 testing 的时候,data distribution 其实是不一样的。因为在 reinforcement learning 里面,action 会影响到接下来所看到的 state。我们是先有 state $s_1$,然后采取 action $a_1$,action $a_1$ 其实会决定接下来你看到什么样的 state $s_2$。所以在 reinforcement learning 里面有一个很重要的特征,就是你采取了 action 会影响你接下来所看到的 state。如果做了Behavior Cloning 的话,我们只能观察到 expert 的一堆 state 跟 action 的pair。然后我们希望可以 learn 一个 $\pi^*$,我们希望 $\pi^*$ 跟 $\hat{\pi}$ 越接近越好。如果 $\pi^*$ 可以跟 $\hat{\pi}$ 一模一样的话,你 training 的时候看到的 state 跟 testing 的时候所看到的 state 会是一样的,这样模型的泛化性能就会变得比较差。而且,如果你的 $\pi^*$ 跟 $\hat{\pi}$ 有一点误差。这个误差在一般 supervised learning problem 里面,每一个 example 都是 independent 的,也许还好。但对 reinforcement learning 的 problem 来说,可能在某个地方,也许 machine 没有办法完全复制 expert 的行为,也许最后得到的结果就会差很多。所以 Behavior Cloning 并不能够完全解决 Imatation learning 这件事情,我们可以使用另外一个比较好的做法叫做 Inverse Reinforcement Learning。 + + +- Inverse Reinforcement Learning 是怎么运行的呢? + + 答:首先,我们有一个 expert $\hat{\pi}$,这个 expert 去跟环境互动,给我们很多 $\hat{\tau_1}$ 到 $\hat{\tau_n}$,我们需要将其中的state、action这个序列都记录下来。然后对于actor $\pi$ 也需要进行一样的互动和序列的记录。接着我们需要指定一个reward function,并且保证expert对应的分数一定要比actor的要高,用过这个reward function继续learning更新我们的训练并且套用一般条件下的RL方法进行actor的更新。在这个过程中,我们也要同时进行我们一开始制定的reward function的更新,使得actor得得分越来越高,但是不超过expert的得分。最终的reward function应该让expert和actor对应的reward function都达到比较高的分数,并且从最终的reward function中无法分辨出谁应该得到比较高的分数。 + +- Inverse Reinforcement Learning 方法与GAN在图像生成中有什么异曲同工之处? + + 答:在GAN 中,我们有一些比较好的图片数据集,也有一个generator,一开始他根本不知道要产生什么样的图,只能随机生成。另外我们有一个discriminator,其用来给生成的图打分,expert 生成的图得分高,generator 生成的图得分低。有了discriminator 以后,generator 会想办法去骗过 discriminator。Generator 会希望 discriminator 也会给它生成得图高分。整个 process 跟 IRL 的过程是类似的。我们一一对应起来看: + + * 生成的图就是 expert 的 demonstration,generator 就是actor,generator 会生成很多的图并让actor 与环境进行互动,从而产生很多 trajectory。这些 trajectory 跟环境互动的记录等价于 GAN 里面的生成图。 + * 在IRL中 learn 的 reward function 就是 discriminator。Rewards function 要给 expert 的 demonstration 高分,给 actor 互动的结果低分。 + * 考虑两者的过程,在IRL中,actor 会想办法,从这个已经 learn 出来的 reward function 里面得到高分,然后 iterative 地去循环这其实是与 GAN 的过程是一致的。 diff --git a/docs/chapter11/img/11.1.png b/docs/chapter11/img/11.1.png new file mode 100644 index 0000000..a8c236a Binary files /dev/null and b/docs/chapter11/img/11.1.png differ diff --git a/docs/chapter11/img/11.10.png b/docs/chapter11/img/11.10.png new file mode 100644 index 0000000..5a968b8 Binary files /dev/null and b/docs/chapter11/img/11.10.png differ diff --git a/docs/chapter11/img/11.11.png b/docs/chapter11/img/11.11.png new file mode 100644 index 0000000..ec7aa9b Binary files /dev/null and b/docs/chapter11/img/11.11.png differ diff --git a/docs/chapter11/img/11.12.png b/docs/chapter11/img/11.12.png new file mode 100644 index 0000000..25c14f5 Binary files /dev/null and b/docs/chapter11/img/11.12.png differ diff --git a/docs/chapter11/img/11.13.png b/docs/chapter11/img/11.13.png new file mode 100644 index 0000000..a85fa78 Binary files /dev/null and b/docs/chapter11/img/11.13.png differ diff --git a/docs/chapter11/img/11.14.png b/docs/chapter11/img/11.14.png new file mode 100644 index 0000000..55ae11d Binary files /dev/null and b/docs/chapter11/img/11.14.png differ diff --git a/docs/chapter11/img/11.15.png b/docs/chapter11/img/11.15.png new file mode 100644 index 0000000..32c734b Binary files /dev/null and b/docs/chapter11/img/11.15.png differ diff --git a/docs/chapter11/img/11.2.png b/docs/chapter11/img/11.2.png new file mode 100644 index 0000000..b7011db Binary files /dev/null and b/docs/chapter11/img/11.2.png differ diff --git a/docs/chapter11/img/11.3.png b/docs/chapter11/img/11.3.png new file mode 100644 index 0000000..8b1354b Binary files /dev/null and b/docs/chapter11/img/11.3.png differ diff --git a/docs/chapter11/img/11.4.png b/docs/chapter11/img/11.4.png new file mode 100644 index 0000000..f9e7b87 Binary files /dev/null and b/docs/chapter11/img/11.4.png differ diff --git a/docs/chapter11/img/11.5.png b/docs/chapter11/img/11.5.png new file mode 100644 index 0000000..fe08508 Binary files /dev/null and b/docs/chapter11/img/11.5.png differ diff --git a/docs/chapter11/img/11.6.png b/docs/chapter11/img/11.6.png new file mode 100644 index 0000000..313329c Binary files /dev/null and b/docs/chapter11/img/11.6.png differ diff --git a/docs/chapter11/img/11.7.png b/docs/chapter11/img/11.7.png new file mode 100644 index 0000000..b3a9947 Binary files /dev/null and b/docs/chapter11/img/11.7.png differ diff --git a/docs/chapter11/img/11.8.png b/docs/chapter11/img/11.8.png new file mode 100644 index 0000000..36c0dc0 Binary files /dev/null and b/docs/chapter11/img/11.8.png differ diff --git a/docs/chapter11/img/11.9.png b/docs/chapter11/img/11.9.png new file mode 100644 index 0000000..c52ec16 Binary files /dev/null and b/docs/chapter11/img/11.9.png differ diff --git a/docs/chapter12/assets/image-20201015221602396.png b/docs/chapter12/assets/image-20201015221602396.png new file mode 100644 index 0000000..c10ebf4 Binary files /dev/null and b/docs/chapter12/assets/image-20201015221602396.png differ diff --git a/docs/chapter12/assets/moving_average_rewards_eval.png b/docs/chapter12/assets/moving_average_rewards_eval.png new file mode 100644 index 0000000..3e9c92f Binary files /dev/null and b/docs/chapter12/assets/moving_average_rewards_eval.png differ diff --git a/docs/chapter12/assets/moving_average_rewards_train.png b/docs/chapter12/assets/moving_average_rewards_train.png new file mode 100644 index 0000000..666e14d Binary files /dev/null and b/docs/chapter12/assets/moving_average_rewards_train.png differ diff --git a/docs/chapter12/assets/rewards_eval.png b/docs/chapter12/assets/rewards_eval.png new file mode 100644 index 0000000..f7b3c04 Binary files /dev/null and b/docs/chapter12/assets/rewards_eval.png differ diff --git a/docs/chapter12/assets/rewards_train.png b/docs/chapter12/assets/rewards_train.png new file mode 100644 index 0000000..ee4862f Binary files /dev/null and b/docs/chapter12/assets/rewards_train.png differ diff --git a/docs/chapter12/assets/steps_eval.png b/docs/chapter12/assets/steps_eval.png new file mode 100644 index 0000000..d6d77d7 Binary files /dev/null and b/docs/chapter12/assets/steps_eval.png differ diff --git a/docs/chapter12/assets/steps_train.png b/docs/chapter12/assets/steps_train.png new file mode 100644 index 0000000..c6a9675 Binary files /dev/null and b/docs/chapter12/assets/steps_train.png differ diff --git a/docs/chapter12/chapter12.md b/docs/chapter12/chapter12.md new file mode 100644 index 0000000..1783775 --- /dev/null +++ b/docs/chapter12/chapter12.md @@ -0,0 +1,174 @@ +# DDPG + +## 离散动作 vs. 连续动作 + +![](img/12.1.png) +离散动作与连续动作是相对的概念,一个是可数的,一个是不可数的。 + +* 离散动作有如下几个例子: + * 在 CartPole 环境中,可以有向左推小车、向右推小车两个动作。 + * 在 Frozen Lake 环境中,小乌龟可以有上下左右四个动作。 + * 在 Atari 的 Pong 游戏中,游戏有 6 个按键的动作可以输出。 +* 但在实际情况中,经常会遇到连续动作空间的情况,也就是输出的动作是不可数的。比如: + * 推小车力的大小, + * 选择下一时刻方向盘的转动角度, + * 四轴飞行器的四个螺旋桨给的电压的大小。 + +![](img/12.2.png) + +对于这些连续的动作控制空间,Q-learning、DQN 等算法是没有办法处理的。那我们怎么输出连续的动作呢,这个时候,万能的神经网络又出现了。 + +* 在离散动作的场景下,比如说我输出上、下或是停止这几个动作。有几个动作,神经网络就输出几个概率值,我们用 $\pi_\theta(a_t|s_t)$ 来表示这个随机性的策略。 + +* 在连续的动作场景下,比如说我要输出这个机器人手臂弯曲的角度,这样子的一个动作,我们就输出一个具体的浮点数。我们用 $\mu_{\theta}(s_t)$ 来代表这个确定性的策略。 + +我们再解释一下随机性策略跟确定性策略。 + +* 对随机性的策略来说,输入某一个状态 s,采取某一个 action 的可能性并不是百分之百,而是有一个概率 P 的,就好像抽奖一样,根据概率随机抽取一个动作。 +* 而对于确定性的策略来说,它没有概率的影响。当神经网络的参数固定下来了之后,输入同样的 state,必然输出同样的 action,这就是确定性的策略。 + +![](img/12.3.png) + +* 要输出离散动作的话,我们就是加一层 softmax 层来确保说所有的输出是动作概率,而且所有的动作概率加和为 1。 +* 要输出连续动作的话,一般可以在输出层这里加一层 tanh。 + * tanh 的图像的像右边这样子,它的作用就是把输出限制到 [-1,1] 之间。 + * 我们拿到这个输出后,就可以根据实际动作的范围再做一下缩放,然后再输出给环境。 + * 比如神经网络输出一个浮点数是 2.8,然后经过 tanh 之后,它就可以被限制在 [-1,1] 之间,它输出 0.99。假设小车速度的动作范围是 [-2,2] 之间,那我们就按比例从 [-1,1] 扩放到 [-2,2],0.99 乘 2,最终输出的就是 1.98,作为小车的速度或者说推小车的力输出给环境。 + +## DDPG(Deep Deterministic Policy Gradient) + +![](img/12.4.png) + +在连续控制领域,比较经典的强化学习算法就是 `深度确定性策略梯度(Deep Deterministic Policy Gradient,简称 DDPG)`。DDPG 的特点可以从它的名字当中拆解出来,拆解成 Deep、Deterministic 和 Policy Gradient。 + +* Deep 是因为用了神经网络; +* Deterministic 表示 DDPG 输出的是一个确定性的动作,可以用于连续动作的一个环境; + +* Policy Gradient 代表的是它用到的是策略网络。REINFORCE 算法每隔一个 episode 就更新一次,但 DDPG 网络是每个 step 都会更新一次 policy 网络,也就是说它是一个单步更新的 policy 网络。 + +DDPG 是 DQN 的一个扩展的版本。 + +* 在 DDPG 的训练中,它借鉴了 DQN 的技巧:目标网络和经验回放。 + +* 经验回放这一块跟 DQN 是一样的,但 target network 这一块的更新跟 DQN 有点不一样。 + +![](img/12.5.png) +**提出 DDPG 是为了让 DQN 可以扩展到连续的动作空间**,就是我们刚才提到的小车速度、角度和电压的电流量这样的连续值。 + +* DDPG 直接在 DQN 基础上加了一个策略网络来直接输出动作值,所以 DDPG 需要一边学习 Q 网络,一边学习策略网络。 +* Q 网络的参数用 $w$ 来表示。策略网络的参数用 $\theta$ 来表示。 +* 我们称这样的结构为 `Actor-Critic` 的结构。 + +![](img/12.6.png) +**通俗地解释一下 Actor-Critic 的结构**, + +* 策略网络扮演的就是 actor 的角色,它负责对外展示输出,输出舞蹈动作。 +* Q 网络就是评论家(critic),它会在每一个 step 都对 actor 输出的动作做一个评估,打一个分,估计一下 actor 的 action 未来能有多少收益,也就是去估计这个 actor 输出的这个 action 的 Q 值大概是多少,即 $Q_w(s,a)$。 Actor 就需要根据舞台目前的状态来做出一个 action。 +* 评论家就是评委,它需要根据舞台现在的状态和演员输出的 action 对 actor 刚刚的表现去打一个分数 $Q_w(s,a)$。 + * Actor 根据评委的打分来调整自己的策略,也就是更新 actor 的神经网络参数 $\theta$, 争取下次可以做得更好。 + * Critic 则是要根据观众的反馈,也就是环境的反馈 reward 来调整自己的打分策略,也就是要更新 critic 的神经网络的参数 $w$ ,它的目标是要让每一场表演都获得观众尽可能多的欢呼声跟掌声,也就是要最大化未来的总收益。 +* 最开始训练的时候,这两个神经网络参数是随机的。所以 critic 最开始是随机打分的,然后 actor 也跟着乱来,就随机表演,随机输出动作。但是由于我们有环境反馈的 reward 存在,所以 critic 的评分会越来越准确,也会评判的那个 actor 的表现会越来越好。 +* 既然 actor 是一个神经网络,是我们希望训练好的策略网络,那我们就需要计算梯度来去更新优化它里面的参数 $\theta$ 。简单的说,我们希望调整 actor 的网络参数,使得评委打分尽可能得高。注意,这里的 actor 是不管观众的,它只关注评委,它就是迎合评委的打分 $Q_w(s,a)$ 而已。 + +![](img/12.7.png) + +接下来就是类似 DQN。 + +* DQN 的最佳策略是想要学出一个很好的 Q 网络,学好这个网络之后,我们希望选取的那个动作使你的 Q 值最大。 + +* DDPG 的目的也是为了求解让 Q 值最大的那个 action。 + * Actor 只是为了迎合评委的打分而已,所以用来优化策略网络的梯度就是要最大化这个 Q 值,所以构造的 loss 函数就是让 Q 取一个负号。 + * 我们写代码的时候就是把这个 loss 函数扔到优化器里面,它就会自动最小化 loss,也就是最大化 Q。 + +这里要注意,除了策略网络要做优化,DDPG 还有一个 Q 网络也要优化。 + +* 评委一开始也不知道怎么评分,它也是在一步一步的学习当中,慢慢地去给出准确的打分。 +* 我们优化 Q 网络的方法其实跟 DQN 优化 Q 网络的方法是一样的,我们用真实的 reward $r$ 和下一步的 Q 即 Q' 来去拟合未来的收益 Q_target。 + +* 然后让 Q 网络的输出去逼近这个 Q_target。 + * 所以构造的 loss function 就是直接求这两个值的均方差。 + * 构造好 loss 后,我们就扔进去那个优化器,让它自动去最小化 loss 就好了。 + +![](img/12.8.png) + +我们可以把两个网络的 loss function 构造出来。 + +策略网络的 loss function 是一个复合函数。我们把 $a = \mu_\theta(s)$ 代进去,最终策略网络要优化的是策略网络的参数 $\theta$ 。Q 网络要优化的是 $Q_w(s,a)$ 和 Q_target 之间的一个均方差。 + +但是 Q 网络的优化存在一个和 DQN 一模一样的问题就是它后面的 Q_target 是不稳定的。此外,后面的 $Q_{\bar{w}}\left(s^{\prime}, a^{\prime}\right)$ 也是不稳定的,因为 $Q_{\bar{w}}\left(s^{\prime}, a^{\prime}\right)$ 也是一个预估的值。 + +**为了稳定这个 Q_target,DDPG 分别给 Q 网络和策略网络都搭建了 target network。** + +* target_Q 网络就为了来计算 Q_target 里面的 $Q_{\bar{w}}\left(s^{\prime}, a^{\prime}\right)$。 +* $Q_{\bar{w}}\left(s^{\prime}, a^{\prime}\right)$ 里面的需要的 next action $a'$ 就是通过 target_P 网络来去输出,即 $a^{\prime}=\mu_{\bar{\theta}}\left(s^{\prime}\right)$。 + +* 为了区分前面的 Q 网络和策略网络以及后面的 target_Q 网络和 target_P 策略网络,前面的网络的参数是 $w$,后面的网络的参数是 $\bar{w}$。 +* DDPG 有四个网络,策略网络的 target 网络 和 Q 网络的 target 网络就是颜色比较深的这两个,它只是为了让计算 Q_target 的时候能够更稳定一点而已。因为这两个网络也是固定一段时间的参数之后再跟评估网络同步一下最新的参数。 + +这里面训练需要用到的数据就是 $s,a,r,s'$,我们只需要用到这四个数据。我们就用 Replay Memory 把这些数据存起来,然后再 sample 进来训练就好了。这个经验回放的技巧跟 DQN 是一模一样的。注意,因为 DDPG 使用了经验回放这个技巧,所以 DDPG 是一个 `off-policy` 的算法。 + +### Exploration vs. Exploitation +DDPG 通过 off-policy 的方式来训练一个确定性策略。因为策略是确定的,如果 agent 使用同策略来探索,在一开始的时候,它会很可能不会尝试足够多的 action 来找到有用的学习信号。为了让 DDPG 的策略更好地探索,我们在训练的时候给它们的 action 加了噪音。DDPG 的原作者推荐使用时间相关的 [OU noise](https://en.wikipedia.org/wiki/Ornstein–Uhlenbeck_process),但最近的结果表明不相关的、均值为 0 的 Gaussian noise 的效果非常好。由于后者更简单,因此我们更喜欢使用它。为了便于获得更高质量的训练数据,你可以在训练过程中把噪声变小。 + +在测试的时候,为了查看策略利用它学到的东西的表现,我们不会在 action 中加噪音。 + +## Twin Delayed DDPG(TD3) + +![](img/12.9.png 'size=500') + +虽然 DDPG 有时表现很好,但它在超参数和其他类型的调整方面经常很敏感。DDPG 常见的问题是已经学习好的 Q 函数开始显著地高估 Q 值,然后导致策略被破坏了,因为它利用了 Q 函数中的误差。 + +我们可以拿实际的 Q 值跟这个 Q-network 输出的 Q 值进行对比。实际的 Q 值可以用 MC 来算。根据当前的 policy 采样 1000 条轨迹,得到 G 后取平均,得到实际的 Q 值。 + +`双延迟深度确定性策略梯度(Twin Delayed DDPG,简称 TD3)`通过引入三个关键技巧来解决这个问题: + +* **截断的双 Q 学习(Clipped Dobule Q-learning)** 。TD3 学习两个 Q-function(因此名字中有 “twin”)。TD3 通过最小化均方差来同时学习两个 Q-function:$Q_{\phi_1}$ 和 $Q_{\phi_2}$。两个 Q-function 都使用一个目标,两个 Q-function 中给出较小的值会被作为如下的 Q-target: + +$$ +y\left(r, s^{\prime}, d\right)=r+\gamma(1-d) \min _{i=1,2} Q_{\phi_{i, t a r g}}\left(s^{\prime}, a_{T D 3}\left(s^{\prime}\right)\right) +$$ + +* **延迟的策略更新(“Delayed” Policy Updates)** 。相关实验结果表明,同步训练动作网络和评价网络,却不使用目标网络,会导致训练过程不稳定;但是仅固定动作网络时,评价网络往往能够收敛到正确的结果。因此 TD3 算法以较低的频率更新动作网络,较高频率更新评价网络,通常每更新两次评价网络就更新一次策略。 +* **目标策略平滑(Target Policy smoothing)** 。TD3 引入了 smoothing 的思想。TD3 在目标动作中加入噪音,通过平滑 Q 沿动作的变化,使策略更难利用 Q 函数的误差。 + +这三个技巧加在一起,使得性能相比基线 DDPG 有了大幅的提升。 + + +目标策略平滑化的工作原理如下: + +$$ +a_{T D 3}\left(s^{\prime}\right)=\operatorname{clip}\left(\mu_{\theta, t a r g}\left(s^{\prime}\right)+\operatorname{clip}(\epsilon,-c, c), a_{\text {low }}, a_{\text {high }}\right) +$$ + +其中 $\epsilon$ 本质上是一个噪声,是从正态分布中取样得到的,即 $\epsilon \sim N(0,\sigma)$。 + +目标策略平滑化是一种正则化方法。 + +![](img/12.10.png) + +我们可以将 TD3 跟其他算法进行对比。这边作者自己实现的 DDPG(our DDPG) 和官方实现的 DDPG 的表现不一样,这说明 DDPG 对初始化和调参非常敏感。TD3 对参数不是这么敏感。在 TD3 的论文中,TD3 的性能比 SAC(Soft Actor-Critic) 高。但在 SAC 的论文中,SAC 的性能比 TD3 高,这是因为强化学习的很多算法估计对参数和初始条件敏感。 + +TD3 的作者给出了对应的实现:[TD3 Pytorch implementation](https://github.com/sfujim/TD3/),代码写得很棒,我们可以将其作为一个强化学习的标准库来学习。 + +### Exploration vs. Exploitation + +TD3 以 off-policy 的方式训练确定性策略。由于该策略是确定性的,因此如果智能体要探索策略,则一开始它可能不会尝试采取足够广泛的动作来找到有用的学习信号。为了使 TD3 策略更好地探索,我们在训练时在它们的动作中添加了噪声,通常是不相关的均值为零的高斯噪声。为了便于获取高质量的训练数据,你可以在训练过程中减小噪声的大小。 + +在测试时,为了查看策略对所学知识的利用程度,我们不会在动作中增加噪音。 + +## References + +* [百度强化学习](https://aistudio.baidu.com/aistudio/education/lessonvideo/460292) + +* [OpenAI Spinning Up ](https://spinningup.openai.com/en/latest/algorithms/ddpg.html#) + +* [Intro to Reinforcement Learning (强化学习纲要)](https://github.com/zhoubolei/introRL) + +* [天授文档](https://tianshou.readthedocs.io/zh/latest/index.html) + + + + + + + diff --git a/docs/chapter12/chapter12_questions&keywords.md b/docs/chapter12/chapter12_questions&keywords.md new file mode 100644 index 0000000..4b048f5 --- /dev/null +++ b/docs/chapter12/chapter12_questions&keywords.md @@ -0,0 +1,50 @@ +# Chapter12 DDPG + +## 1 Keywords + +- **DDPG(Deep Deterministic Policy Gradient):** 在连续控制领域经典的RL算法,是DQN在处理连续动作空间的一个扩充。具体地,从命名就可以看出,Deep是使用了神经网络;Deterministic 表示 DDPG 输出的是一个确定性的动作,可以用于连续动作的一个环境;Policy Gradient 代表的是它用到的是策略网络,并且每个 step 都会更新一次 policy 网络,也就是说它是一个单步更新的 policy 网络。其与DQN都有目标网络和经验回放的技巧,在经验回放部分是一致的,在目标网络的更新有些许不同。 + +## 2 Questions + +- 请解释随机性策略和确定性策略。 + + 答: + + - 对于随机性的策略 $\pi_\theta(a_t|s_t)$ ,我们输入某一个状态 s,采取某一个 action 的可能性并不是百分之百,而是有一个概率 P 的,就好像抽奖一样,根据概率随机抽取一个动作。 + - 对于确定性的策略 $\mu_{\theta}(s_t)$ ,其没有概率的影响。当神经网络的参数固定下来了之后,输入同样的state,必然输出同样的 action,这就是确定性的策略。 + +- 对于连续动作的控制空间和离散动作的控制空间,如果我们都采取使用Policy网络的话,分别应该如何操作? + + 答:首先需要说明的是,对于连续的动作控制空间,Q-learning、DQN等算法是没有办法处理的,所以我们需要使用神经网络进行处理,因为其可以既输出概率值 $\pi_\theta(a_t|s_t)$ ,也可以输出确定的策略 $\mu_{\theta}(s_t)$ 。 + + - 要输出离散动作的话,最后的output的激活函数使用 softmax 就可以实现。其可以保证输出是的动作概率,而且所有的动作概率加和为 1。 + - 要输出连续的动作的话,可以在输出层这里加一层 tanh激活函数。其作用可以把输出限制到 [-1,1] 之间。我们拿到这个输出后,就可以根据实际动作的一个范围再做一下缩放,然后再输出给环境。比如神经网络输出一个浮点数是 2.8,然后经过 tanh 之后,它就可以被限制在 [-1,1] 之间,它输出 0.99。然后假设说小车的一个速度的那个动作范围是 [-2,2] 之间,那我们就按比例从 [-1,1] 扩放到 [-2,2],0.99 乘 2,最终输出的就是1.98,作为小车的速度或者说推小车的力输出给环境。 + + + +## 3 Something About Interview + +- 高冷的面试官:请简述一下DDPG算法? + + 答:深度确定性策略梯度(Deep Deterministic Policy Gradient,简称 DDPG) 使用 Actor Critic 结构,但是输出的不是行为的概率,,而是具体的行为,用于连续动作的预测。优化的目的是为了将DQN扩展到连续的动作空间。另外,其字如其名: + + - Deep 是因为用了神经网络; + - Deterministic 表示 DDPG 输出的是一个确定性的动作,可以用于连续动作的一个环境; + - Policy Gradient 代表的是它用到的是策略网络。REINFORCE 算法每隔一个 episode 就更新一次,但 DDPG 网络是每个 step 都会更新一次 policy 网络,也就是说它是一个单步更新的 policy 网络。 + +- 高冷的面试官:你好,请问DDPG是on-policy还是off-policy,原因是什么呀? + + 答:off-policy。解释方法一,DDPG是优化的DQN,其使用了经验回放,所以为off-policy方法;解释方法二,因为DDPG为了保证一定的探索,对于输出动作加了一定的噪音,也就是说行为策略不再是优化的策略。 + +- 高冷的面试官:你是否了解过D4PG算法呢?描述一下吧。 + + 答:分布的分布式DDPG(Distributed Distributional DDPG ,简称 D4PG),相对于DDPG其优化部分为: + + - 分布式 critic: 不再只估计Q值的期望值,而是去估计期望Q值的分布, 即将期望Q值作为一个随机变量来进行估计。 + - N步累计回报: 当计算TD误差时,D4PG计算的是N步的TD目标值而不仅仅只有一步,这样就可以考虑未来更多步骤的回报。 + - 多个分布式并行actor:D4PG使用K个独立的演员并行收集训练样本并存储到同一个replay buffer中。 + - 优先经验回放(Prioritized Experience Replay,PER):使用一个非均匀概率 $\pi$ 从replay buffer中采样。 + + + + diff --git a/docs/chapter12/img/12.1.png b/docs/chapter12/img/12.1.png new file mode 100644 index 0000000..d2d8b08 Binary files /dev/null and b/docs/chapter12/img/12.1.png differ diff --git a/docs/chapter12/img/12.10.png b/docs/chapter12/img/12.10.png new file mode 100644 index 0000000..024bc93 Binary files /dev/null and b/docs/chapter12/img/12.10.png differ diff --git a/docs/chapter12/img/12.2.png b/docs/chapter12/img/12.2.png new file mode 100644 index 0000000..bc14e58 Binary files /dev/null and b/docs/chapter12/img/12.2.png differ diff --git a/docs/chapter12/img/12.3.png b/docs/chapter12/img/12.3.png new file mode 100644 index 0000000..be0f3f7 Binary files /dev/null and b/docs/chapter12/img/12.3.png differ diff --git a/docs/chapter12/img/12.4.png b/docs/chapter12/img/12.4.png new file mode 100644 index 0000000..7760c5b Binary files /dev/null and b/docs/chapter12/img/12.4.png differ diff --git a/docs/chapter12/img/12.5.png b/docs/chapter12/img/12.5.png new file mode 100644 index 0000000..4e6802a Binary files /dev/null and b/docs/chapter12/img/12.5.png differ diff --git a/docs/chapter12/img/12.6.png b/docs/chapter12/img/12.6.png new file mode 100644 index 0000000..76a90fe Binary files /dev/null and b/docs/chapter12/img/12.6.png differ diff --git a/docs/chapter12/img/12.7.png b/docs/chapter12/img/12.7.png new file mode 100644 index 0000000..825e6b8 Binary files /dev/null and b/docs/chapter12/img/12.7.png differ diff --git a/docs/chapter12/img/12.8.png b/docs/chapter12/img/12.8.png new file mode 100644 index 0000000..ade74f6 Binary files /dev/null and b/docs/chapter12/img/12.8.png differ diff --git a/docs/chapter12/img/12.9.png b/docs/chapter12/img/12.9.png new file mode 100644 index 0000000..be7b8ee Binary files /dev/null and b/docs/chapter12/img/12.9.png differ diff --git a/docs/chapter12/img/image-20200820174814084.png b/docs/chapter12/img/image-20200820174814084.png new file mode 100644 index 0000000..4c9e3dc Binary files /dev/null and b/docs/chapter12/img/image-20200820174814084.png differ diff --git a/docs/chapter12/img/moving_average_rewards-8929361.png b/docs/chapter12/img/moving_average_rewards-8929361.png new file mode 100644 index 0000000..a80fc99 Binary files /dev/null and b/docs/chapter12/img/moving_average_rewards-8929361.png differ diff --git a/docs/chapter12/project3.md b/docs/chapter12/project3.md new file mode 100644 index 0000000..ef4f275 --- /dev/null +++ b/docs/chapter12/project3.md @@ -0,0 +1,95 @@ +# 使用Policy-Based方法实现Pendulum-v0 + +使用Policy-Based方法比如DDPG等实现Pendulum-v0环境 + +## Pendulum-v0 + +![image-20200820174814084](img/image-20200820174814084.png) + +钟摆以随机位置开始,目标是将其摆动,使其保持向上直立。动作空间是连续的,值的区间为[-2,2]。每个step给的reward最低为-16.27,最高为0。 + +环境建立如下: + +```python +env = gym.make('Pendulum-v0') +env.seed(1) # 设置env随机种子 +n_states = env.observation_space.shape[0] # 获取总的状态数 +``` + +## 强化学习基本接口 + +```python +rewards = [] # 记录总的rewards +moving_average_rewards = [] # 记录总的经滑动平均处理后的rewards +ep_steps = [] +for i_episode in range(1, cfg.max_episodes+1): # cfg.max_episodes为最大训练的episode数 + state = env.reset() # reset环境状态 + ep_reward = 0 + for i_step in range(1, cfg.max_steps+1): # cfg.max_steps为每个episode的补偿 + action = agent.select_action(state) # 根据当前环境state选择action + next_state, reward, done, _ = env.step(action) # 更新环境参数 + ep_reward += reward + agent.memory.push(state, action, reward, next_state, done) # 将state等这些transition存入memory + state = next_state # 跳转到下一个状态 + agent.update() # 每步更新网络 + if done: + break + # 更新target network,复制DQN中的所有weights and biases + if i_episode % cfg.target_update == 0: # cfg.target_update为target_net的更新频率 + agent.target_net.load_state_dict(agent.policy_net.state_dict()) + print('Episode:', i_episode, ' Reward: %i' % + int(ep_reward), 'n_steps:', i_step, 'done: ', done,' Explore: %.2f' % agent.epsilon) + ep_steps.append(i_step) + rewards.append(ep_reward) + # 计算滑动窗口的reward + if i_episode == 1: + moving_average_rewards.append(ep_reward) + else: + moving_average_rewards.append( + 0.9*moving_average_rewards[-1]+0.1*ep_reward) +``` + +## 任务要求 + +训练并绘制reward以及滑动平均后的reward随episode的变化曲线图并记录超参数写成报告,图示如下: + +![rewards_train](assets/rewards_train.png) + +![moving_average_rewards_train](assets/moving_average_rewards_train.png) + +![steps_train](assets/steps_train.png) + +同时也可以绘制测试(eval)模型时的曲线: + +![rewards_eval](assets/rewards_eval.png) + +![moving_average_rewards_eval](assets/moving_average_rewards_eval.png) + +![steps_eval](assets/steps_eval.png) + +也可以[tensorboard](https://pytorch.org/docs/stable/tensorboard.html)查看结果,如下: + +![image-20201015221602396](assets/image-20201015221602396.png) + +### 注意 + +1. 本次环境action范围在[-2,2]之间,而神经网络中输出的激活函数tanh在[0,1],可以使用NormalizedActions(gym.ActionWrapper)的方法解决 +2. 由于本次环境为惯性系统,建议增加Ornstein-Uhlenbeck噪声提高探索率,可参考[知乎文章](https://zhuanlan.zhihu.com/p/96720878) +3. 推荐多次试验保存rewards,然后使用searborn绘制,可参考[CSDN](https://blog.csdn.net/JohnJim0/article/details/106715402) + +### 代码清单 + +**main.py**:保存强化学习基本接口,以及相应的超参数,可使用argparse + +**model.py**:保存神经网络,比如全链接网络 + +**ddpg.py**: 保存算法模型,主要包含select_action和update两个函数 + +**memory.py**:保存Replay Buffer + +**plot.py**:保存相关绘制函数 + +**noise.py**:保存噪声相关 + +[参考代码](https://github.com/datawhalechina/easy-rl/tree/master/codes/DDPG) + diff --git a/docs/chapter13/Paper-AlphaStar-Nature-2019.pdf b/docs/chapter13/Paper-AlphaStar-Nature-2019.pdf new file mode 100644 index 0000000..89cd12a Binary files /dev/null and b/docs/chapter13/Paper-AlphaStar-Nature-2019.pdf differ diff --git a/docs/chapter13/chapter13.md b/docs/chapter13/chapter13.md new file mode 100644 index 0000000..e8b7913 --- /dev/null +++ b/docs/chapter13/chapter13.md @@ -0,0 +1,170 @@ +# AlphaStar 论文解读 + +## AlphaStar以及背景简介 + +相比于之前的深蓝和go,对于星际争霸2等策略对战型游戏,使用AI与人类对战难度更大。比如在星际争霸2中,操作枯燥是众所周知的,要想在PVP中击败对方,就得要学会各种战术,各种微操和Timing。在游戏中你还得侦查对方的发展,做出正确判断进行转型,甚至要欺骗对方以达到战术目的。总而言之,想要上手这款游戏是非常困难的,对不起,DeepMind就做到了。 + +AlphaStar是DeepMind公司与暴雪使用深度强化学习技术进行PC与星际争霸2人类玩家进行对战的产品,其在近些年在星际争霸2中打败了职业选手以及99.8%的欧服玩家而被人所熟知。北京时间2019年1月25日凌晨2点,暴雪与谷歌DeepMind团队合作研究的星际争霸人工智能“AlphaStar”正式通过直播亮相。按照直播安排,AlphaStar与两位《星际争霸2》人类职业选手进行了5场比赛对决演示。加上并未在直播中演示的对决,在人类vs AlphaStar人工智能的共计11场比赛中,人类只取得了一场胜利。DeepMind也将研究工作发表在了2019年10月的《Nature》杂志上。我们也将对于这篇Paper进行深入的分析,下面是论文的链接: + +[Vinyals, Oriol, et al. "Grandmaster level in StarCraft II using multi-agent reinforcement learning." Nature (2019): 1-5.](https://www.nature.com/articles/s41586-019-1724-z?) + +## AlphaStar的模型输入输出是什么?——环境设计 + +构建DRL模型的第一部分就是构建输入输出,对于星际争霸2这个复杂的环境,paper第一步做的就是将游戏的环境抽象成为许多的数据信息。 + +### 状态(网络的输入) + +AlphaStar将星际争霸2的环境状态分为四部分,分别为实体信息(Entities)、地图信息(Map)、玩家数据信息(Player data)、游戏统计信息(Game statistics)。 + +![img1](img\13.1.png) + +- 第一部分:实体信息,例如当前时刻环境中有什么建筑、兵种等等,并且我们将每一个实体的属性信息以向量的形式表示,例如对于一个建筑,其当前时刻的向量中包含此建筑的血量、等级、位置以及冷却时间等等信息。所以对于当前帧的全部实体信息,环境会给神经网络 $N$ 个长度为 $K$ 的向量,各表示此刻智能体能够看见的 $N$ 个实体的具体信息。(向量信息) +- 第二部分:地图信息,这个比较好理解,也就是将地图中的信息以矩阵的形式送入神经网络中,来表示当前状态全局地图的信息。(向量信息或者说是图像信息) +- 第三部分:玩家数据信息,也就是当前状态下,玩家的等级、种族等等信息。(标量信息) +- 第四部分:游戏统计信息,相机的位置(小窗口的位置,区别于第二部分的全局地图信息),还有当前游戏的开始时间等等信息。(标量信息) + +### 动作(网络的输出) + +AlphaStar的动作信息主要分为六个部分,分别为动作类型(Action type)、选中的单元(Selected units)、目标(Target)、执行动作的队列(Queued)、是否重复(Repeat)、延时(Delay),每一个部分间是有关联的。 + +![img2](img\13.2.png) + +- 第一部分:动作类型,即下一次要进行的动作的类型是移动小兵、升级建筑还是移动小窗口的位置等等 +- 第二部分:选中的单元,即承接第一部分,例如我们要进行的动作类型是移动小兵,那么我们就应该选择具体“操作”哪一个小兵 +- 第三部分:目标,承接第二部分,我们操作小兵A后,是要去地图的某一个位置还是去攻击对手的哪一个目标等等,即选择目的地和攻击的对象 +- 第四部分:执行动作的队列,具体说是是否立即执行动作,对于小兵A,我们是到达目的地后直接进行攻击还是等待 +- 第五部分:是否重复做动作,如果需要小兵A持续攻击,那么就不需要再通过网络计算得到下一个的动作了,直接重复以上一个动作的相同的动作即可。 +- 第六部分:延时,也就是等候多久才接收网络的输入,可以理解为我们人类玩家的一个操作的延迟等等 + +## AlphaStar的计算模型是什么呢?——网络结构 + +上面我们说明了AlphaStar网络的输入和输出,即状态和动作,那么从状态怎么得到动作呢?其网络结构是怎么样的呢? + +![img3](img\13.3.png) + +### 输入部分 + +![img4](img\13.4.png) + +从上图的红框可以看出,模型的输入框架中主要有三个部分,即Scalar features(标量特征),例如前面叙述的玩家的等级、小窗口的位置等等信息、Entities(实体),是向量即前面所叙述的一个建筑一个兵的当前的所有属性信息、Minimap(地图),即上面说的图像的数据。 + +- 对于Scalar features(标量特征),使用多层感知器(MLP),就可以得到对应的向量,或者说是一个embedding的过程。 +- 对于Entities,使用NLP中常用的transformer作为encoder +- 对于Minimap,使用图像中常用的Resnet作为encoder,得到一个定长的向量。 + +### 中间过程 + +中间过程比较简单,即通过一个deep LSTM进行融合三种当前状态下的embedding进行下一时刻的embedding输出,并且将该结果分别送入ValueNetwork、Residual MLP以及Actoin type的后续的MLP中。 + +![img5](img\13.5.png) + +### 输出部分 + +正如前面介绍的,输出的动作是前后有关联的,按照顺序 + +![img6](img\13.6.png) + +- 首先是动作类型(Action type):使用Deep LSTM的embedding的向量作为输入,使用residual MLP得到Action type的softmax的输出结果,并传给下一个子模型进行embedding。 +- 然后是延时(Delay):使用上一个上面的embedding的结果以及Deep LSTM的结果一起输入MLP后得到结果,并传给下一个子模型进行embedding。 +- 接下来是执行动作的队列(Queued):使用delay的结果以及embedding的结果一起输入MLP后得到结果,并传给下一个子模型进行embedding。 +- 然后是选中的单元(Selected units):使用queued的结果、embedding的结果以及Entity encoder的全部结果(非平均的结果)一起送入到Pointer Network中得到结果,并传给下一个子模型进行embedding。这里的Pointer Netowrk为指针网络,即输入的是一个序列,输出是另外一个序列,并且,输出序列的元素来源于输入的序列,主要用于NLP中,在这里很适合与我们的Selected units的计算。 +- 接着是目标单元(Target unit)和目标区域(Target point)两者二选一进行,对于Target unit,使用attention机制得到最优的动作作用的一个对象,对于target point,使用反卷积神经网络,将embedding的向量,反卷积为map的大小,从而执行目标移动到某一点的对应的动作。 + +## 庞大的AlphaStar如何训练呢?——学习算法 + +对于上面复杂的模型,AlphaStar究竟如何来进行训练呢?总结下来一共分为4个部分,即监督学习(主要是解决训练的初始化问题)、强化学习、模仿学习(配合强化学习)以及多智能体学习和自学习(面向对战的具体问题),下面我们一一分析: + +### 监督学习 + +在训练一开始首先使用监督学习利用人类的数据进行一个比较好的初始化。模型的输入是收集到的人类的对局的信息,输出是训练好的神经网络。具体的做法是,对于收集到了人类的对局数据,在对于每一个时刻解码游戏的状态,将每一时刻的状态送入网络中得到以上每一个动作的概率分布,最终计算模型的输出以及人类数据的KL Loss,并以此进行网络的优化,其中在KL Loss中需要使用不同的 Loss 函数,例如,Action类型的输出,即分类问题的loss就需要使用Cross Entropy。而对于target location等类似于回归问题的就需要计算MSE。当然还有一些细节,大家可以自行阅读paper。总之,经过监督学习,我们的模型输出的概率分布就可以与人类玩家输出的概率分布类似。 + +### 强化学习 + +这里的目标就是通过优化策略使得期望的reward最大,即 +$$ +J(\pi_{\theta}) = \Epsilon_{\pi_{\theta}} \sum_{t=0}r(s_t,a_t) +$$ +但AlphaStar的训练的模型使用不是采样的模型,即off-policy的模型,这是因为其使用的架构为类似于IMPALA的结构,即Actor负责与环境交互并采样,learner负责优化网络并更新参数,而Actor和learner通常是异步进行计算的,并且由于前面介绍的输出的动作的类型空间复杂,所以导致我们的value function的拟合比较困难。 + +这里AlphaStar利用了以下的方式进行强化学习模型的构建: + +- 首先是采取了经典的Actor-critic的结构,使用策略网络给出当前状态下的智能体的动作,即计算$\pi(a_t|s_t)$ ,使用价值网络计算当前状态下的智能体的期望收益,即计算 $V(s_t) = \Epsilon \sum_{t'=t}r_{t'} = \Epsilon_{a_t}[r(s_t,a_t)+V(s_{t+1})]$。具体的计算方法是: + - 对于当前的状态 $s$ ,计算当前计算出的动作 $a$ 相对于“平均动作”所能额外获得的奖励。$A(s_t,a_t)=[r(s_t,a_t)+V(s_{t+1})]-V(s_t)$,即当前动作的预期收益减去当前状态的预期收益。在AlphaStar中,UPGO(Upgoing Policy Update)也得到了应用,即UPGO使用了一个迭代变量 $G_t$ 来取代原来的动作预期收益的 $r(s_t,a_t)+V(s_{t+1})$ ,即把未来乐观的信息纳入到我们额外奖励中,上式可改写为: + +$$ +A(s_t,a_t)=G_t-V(s_t) +$$ + +$$ +G_t=\left\{ +\begin{aligned} +r_t+G_{t+1} && Q(s_{s+1},a_{t+1})\geq V(s_{t+1}) \\ +r_t+V(s_{t+1}) && otherwise \\ +\end{aligned} +\right. +$$ + +- 在基于上面计算得到的action,更新策略梯度,即 $\nabla_{\theta}J = A(s_t,a_t)\nabla_{\theta}log \pi_{\theta}(a_t|s_t)$,在我们之前的笔记中也介绍了,如果基于 $\pi_{\theta}$ 的分布不好求解,或者说学习策略 $\pi_{\theta}$ 与采集策略 $\pi_{\mu}$ 不同的话,我们需要使用重要性采样的方法,即 $\nabla_{\theta}J = E_{\pi_{\mu}}\frac{\pi_{\theta} (a_t|s_t)}{\pi_{\mu} (a_t|s_t)} A^{\pi_{\theta}}(s_t,a_t)\nabla_{\theta}log \pi_{\theta}(a_t|s_t)$。当然我们还需防止 $\frac{\pi_{\theta} (a_t|s_t)}{\pi_{\mu} (a_t|s_t)}$ 出现无穷大的情况,我们需要使用V-trace限制重要性系数。这也是用于off-policy的一个更新方法,在 IMPALA 论文中的4.1小节有所体现。即将重要性系数的最大值限制为1,公式可表达如下: + +$$ +\nabla_{\theta}J = E_{\pi_{\mu}}\rho_tA^{\pi_{\theta}}(s_t,a_t)\nabla_{\theta}log \pi_{\theta}(a_t|s_t) +$$ + +$$ +\rho_t = min(\frac{\pi_{\theta} (a_t|s_t)}{\pi_{\mu} (a_t|s_t)},1) +$$ + +- 利用了TD($\lambda$) 来优化价值网络,并同时输入对手的数据。对于我们的价值函数 $V^{\pi_{\theta}}(s_t)=E_{\pi_{\theta}}\sum_{t'=t}\gamma^{t'-t}r(s_t,a_t)=E_{a_t\sim\pi_{\theta}(\cdot|s_t)}[r(s_t,a_t)+\gamma V(s_{t+1})]$,可以使用TD的方法计算MSE损失,有如下几种: + - $TD(0)$ ,表达式为 $L = [(r_t+\gamma V_{t+1})-V_t]^2$ ,即当前step的信息,有偏小方差 + - $TD(1)$也就是MC方法,表达式为 $L = [(\sum_{t'=t}^\infty\gamma^{t'-t}r_{t'})-V_t]^2$,即未来无穷个step的信息,无偏大方差 + - $TD(\lambda)$ ,以上两个方法的加权平均。即平衡当前step、下一个step到无穷个step后的结果 + - 已知对于 $\lambda \in (0,1)$, $(1-\lambda)+(1-\lambda)\lambda+(1-\lambda)\lambda ^2+...=1$ + - $R_t = \lim_{T\rightarrow\infty} (1-\lambda)(r_t+V_{t+1})+(1-\lambda)\lambda(r_t+\gamma r_{t+1}+\gamma^2 V_{t+2})+...$ + +### 模仿学习 + +使用模仿学习额外引入了监督学习Loss以及人类的统计量 $Z$ ,即对于Build order(建造顺序)、Build Units(建造单元)、Upgrades(升级)、Effects(技能)等信息进行了奖励。对于统计量 $Z$ ,本质来说是一系列的数据,将其作为输入信息输入到策略网络和价值网络中。另外对于人类信息的利用还体现在前面介绍的使用监督学习进行网络的预训练工作。 + +### 多智能体学习/自学习 + +自学习在AlphaGo中得到了应用也就是自己和自己玩,Alpha对此做了一些更新,即有优先级的虚拟自学习策略,对于虚拟自学习就是在训练过程中,每一些时间就进行存档,并随机均匀的从存档中选出对手与正在训练的智能体对战。而有优先级的虚拟自学习指的是优先挑选能击败我的或者说常能打败智能体的对手进行训练对战,评判指标就是概率。对于AlphaStar中,其训练的agent分为了三种, + +- Main Agent (主智能体),即正在训练的智能体及其祖先;其中有50%的概率从联盟中的所有人中挑选,使用有优先级的虚拟自学习策略,即能打败我的概率高,不能打败我的概率低,有35%的概率与自己对战,有15%的概率与能打败我的联盟利用者或者老的主智能体对战,通过利用了有优先级的虚拟自学习策略。 +- League Exploiter(联盟利用者):能打败联盟中的所有智能体的agent;其按照有优先级的虚拟自学习策略计算的概率与全联盟的对手训练,在以70%的胜率打败所有的agent或者距离上次存档 $2 \times10^9$ step后就保存,并且在存档的时候,有25%概率把场上的联盟利用者的策略重设成监督学习给出的初始化。 +- Main Exploiter(主利用者):能打败训练中的所有agent,在训练的过程中,随机从3个中挑1个主智能体,如果可以以高于10%的概率打败该agent就与其进行训练,如果不能就从其他的老主智能体中再挑选对手,当以70%的胜率打败全部三个正在学习的策略主智能体,或者距上次存档 $4 \times10^9 $ 个step之后就存,并且进行重设初始化的操作。 + +他们的区别在于: + +- 如何选取训练过程中对战的对象 +- 在什么情况下存档 (snapshot) 现在的策略 +- 以多大的概率将策略的参数重设为监督训练给出的初始化 + +## AlphaStar实验结果如何呢?——实验结果 + +### 宏观结果 + +![img7](img\13.7.png) + +图A为训练后的agent与人类对战的结果(天梯图),具体地,刚刚结束监督学习后的AlphaStar可以达到钻石级别,而训练到一半(20天)以及训练完结(40天)的AlphaStar可以达到GM的级别。AlphaStar已经可以击败绝大多数的普通玩家。 + +图B为不同种族间对战的胜率。 + +图C为《星际争霸II》报告的每分钟有效行动分布情况(EPM),其中蓝色为AlphaStar Final的结果,红色为人类选手的结果虚线显示平均值。 + +### 其他实验(消融实验) + +AlphaStar的论文中也使用了消融实验,即控制变量法,来进一步分析每一个约束条件对于对战结果的影响。下面举一个特别的例子: + +![img8](img\13.8.png) + +上面的图片表示的是人类对局数据的使用的情况。可以看到如果没有人类对局数据的情况下,数值仅仅为149,但是只要经过了简单的监督学习,对应的数值就可以达到936,当然使用人类初始化后的强化学习可以达到更好的效果,利用强化学习加监督学习的KL Loss的话可以达到接近于完整的利用人类统计量 $Z$ 的效果。可以分析出,AlphaStar中人类对局的数据对于整个model的表现是很重要的,其并没有完全像AlphaGo一样,可以不使用人类数据的情况。 + +## 关于AlphaStar的总结 + +### 总结 + +- AlphaStar设计了一个高度可融合图像、文本、标量等信息的神经网络架构,并且对于网络设计使用了Autoregressive解耦了结构化的action space。 +- 模仿学习和监督学习的内容,例如人类统计量 $Z$ 的计算方法 +- 复杂的DRL方法以及超复杂的训练策略 +- 当然了,大量的计算资源(Each agent was trained using 32 third-generation tensor processing units (TPUs 23 ) over 44 days. During league training almost 900 distinct players were created.) + diff --git a/docs/chapter13/img/13.1.png b/docs/chapter13/img/13.1.png new file mode 100644 index 0000000..d3f88a0 Binary files /dev/null and b/docs/chapter13/img/13.1.png differ diff --git a/docs/chapter13/img/13.2.png b/docs/chapter13/img/13.2.png new file mode 100644 index 0000000..bd14ba0 Binary files /dev/null and b/docs/chapter13/img/13.2.png differ diff --git a/docs/chapter13/img/13.3.png b/docs/chapter13/img/13.3.png new file mode 100644 index 0000000..aca0bee Binary files /dev/null and b/docs/chapter13/img/13.3.png differ diff --git a/docs/chapter13/img/13.4.png b/docs/chapter13/img/13.4.png new file mode 100644 index 0000000..b20f36f Binary files /dev/null and b/docs/chapter13/img/13.4.png differ diff --git a/docs/chapter13/img/13.5.png b/docs/chapter13/img/13.5.png new file mode 100644 index 0000000..b056976 Binary files /dev/null and b/docs/chapter13/img/13.5.png differ diff --git a/docs/chapter13/img/13.6.png b/docs/chapter13/img/13.6.png new file mode 100644 index 0000000..f40cce1 Binary files /dev/null and b/docs/chapter13/img/13.6.png differ diff --git a/docs/chapter13/img/13.7.png b/docs/chapter13/img/13.7.png new file mode 100644 index 0000000..de333c9 Binary files /dev/null and b/docs/chapter13/img/13.7.png differ diff --git a/docs/chapter13/img/13.8.png b/docs/chapter13/img/13.8.png new file mode 100644 index 0000000..630e8ac Binary files /dev/null and b/docs/chapter13/img/13.8.png differ diff --git a/docs/chapter2/chapter2.md b/docs/chapter2/chapter2.md new file mode 100644 index 0000000..18706b9 --- /dev/null +++ b/docs/chapter2/chapter2.md @@ -0,0 +1,889 @@ +# MDP + +本章给大家介绍马尔可夫决策过程。 + +* 在介绍马尔可夫决策过程之前,先介绍它的简化版本:马尔可夫链以及马尔可夫奖励过程,通过跟这两种过程的比较,我们可以更容易理解马尔可夫决策过程。 +* 第二部分会介绍马尔可夫决策过程中的 `policy evaluation`,就是当给定一个决策过后,怎么去计算它的价值函数。 +* 第三部分会介绍马尔可夫决策过程的控制,具体有两种算法:`policy iteration` 和 `value iteration`。 + +![](img/2.2.png) + +上图介绍了在强化学习里面 agent 跟 environment 之间的交互,agent 在得到环境的状态过后,它会采取动作,它会把这个采取的动作返还给环境。环境在得到 agent 的动作过后,它会进入下一个状态,把下一个状态传回 agent。在强化学习中,agent 跟环境就是这样进行交互的,这个交互过程是可以通过马尔可夫决策过程来表示的,所以马尔可夫决策过程是强化学习里面的一个基本框架。 + +在马尔可夫决策过程中,它的环境是全部可以观测的(`fully observable`)。但是很多时候环境里面有些量是不可观测的,但是这个部分观测的问题也可以转换成一个 MDP 的问题。 + +在介绍马尔可夫决策过程(Markov Decision Process,MDP)之前,先给大家梳理一下马尔可夫过程(Markov Process,MP)、马尔可夫奖励过程(Markov Reward Processes,MRP)。这两个过程是马尔可夫决策过程的一个基础。 + +## Markov Process(MP) + +### Markov Property + +如果一个状态转移是符合马尔可夫的,那就是说一个状态的下一个状态只取决于它当前状态,而跟它当前状态之前的状态都没有关系。 + +我们设状态的历史为 $h_{t}=\left\{s_{1}, s_{2}, s_{3}, \ldots, s_{t}\right\}$($h_t$ 包含了之前的所有状态),如果一个状态转移是符合马尔可夫的,也就是满足如下条件: +$$ +p\left(s_{t+1} \mid s_{t}\right) =p\left(s_{t+1} \mid h_{t}\right) \tag{1} +$$ + +$$ +p\left(s_{t+1} \mid s_{t}, a_{t}\right) =p\left(s_{t+1} \mid h_{t}, a_{t}\right) \tag{2} +$$ + +从当前 $s_t$ 转移到 $s_{t+1}$ 这个状态,它是直接就等于它之前所有的状态转移到 $s_{t+1}$。如果某一个过程满足`马尔可夫性质(Markov Property)`,就是说未来的转移跟过去是独立的,它只取决于现在。**马尔可夫性质是所有马尔可夫过程的基础。** + +### Markov Process/Markov Chain + +![](img/2.5.png ':size=500') + +首先看一看`马尔可夫链(Markov Chain)`。举个例子,这个图里面有四个状态,这四个状态从 $s_1,s_2,s_3,s_4$ 之间互相转移。比如说从 $s_1$ 开始, + +* $s_1$ 有 0.1 的概率继续存活在 $s_1$ 状态, +* 有 0.2 的概率转移到 $s_2$, +* 有 0.7 的概率转移到 $s_4$ 。 + +如果 $s_4$ 是我们当前状态的话, + +* 它有 0.3 的概率转移到 $s_2$ , +* 有 0.2 的概率转移到 $s_3$ , +* 有 0.5 的概率留在这里。 + +我们可以用`状态转移矩阵(State Transition Matrix)` $P$ 来描述状态转移 $p\left(s_{t+1}=s^{\prime} \mid s_{t}=s\right)$,如下式所示。 +$$ +P=\left[\begin{array}{cccc} +P\left(s_{1} \mid s_{1}\right) & P\left(s_{2} \mid s_{1}\right) & \ldots & P\left(s_{N} \mid s_{1}\right) \\ +P\left(s_{1} \mid s_{2}\right) & P\left(s_{2} \mid s_{2}\right) & \ldots & P\left(s_{N} \mid s_{2}\right) \\ +\vdots & \vdots & \ddots & \vdots \\ +P\left(s_{1} \mid s_{N}\right) & P\left(s_{2} \mid s_{N}\right) & \ldots & P\left(s_{N} \mid s_{N}\right) +\end{array}\right] +$$ +状态转移矩阵类似于一个 conditional probability,当我们知道当前我们在 $s_t$ 这个状态过后,到达下面所有状态的一个概念。所以它每一行其实描述了是从一个节点到达所有其它节点的概率。 + +### Example of MP + +![](img/2.6.png) + +上图是一个马尔可夫链的例子,我们这里有七个状态。比如说从 $s_1$ 开始到 $s_2$ ,它有 0.4 的概率,然后它有 0.6 的概率继续存活在它当前的状态。 $s_2$ 有 0.4 的概率到左边,有 0.4 的概率到 $s_3$ ,另外有 0.2 的概率存活在现在的状态,所以给定了这个状态转移的马尔可夫链后,我们可以对这个链进行采样,这样就会得到一串的轨迹。 + +下面我们有三个轨迹,都是从同一个起始点开始。假设还是从 $s_3$ 这个状态开始, + +* 第一条链先到了 $s_4$, 又到了 $s_5$,又往右到了 $s_6$ ,然后继续存活在 $s_6$ 状态。 +* 第二条链从 $s_3$ 开始,先往左走到了 $s_2$ 。然后它又往右走,又回到了$s_3$ ,然后它又往左走,然后再往左走到了 $s_1$ 。 +* 通过对这个状态的采样,我们生成了很多这样的轨迹。 + +## Markov Reward Process(MRP) + +**`马尔可夫奖励过程(Markov Reward Process, MRP)` 是马尔可夫链再加上了一个奖励函数。**在 MRP 中,转移矩阵和状态都是跟马尔可夫链一样的,多了一个`奖励函数(reward function)`。**奖励函数 $R$ 是一个期望**,就是说当你到达某一个状态的时候,可以获得多大的奖励,然后这里另外定义了一个 discount factor $\gamma$ 。如果状态数是有限的,$R$ 可以是一个向量。 + +### Example of MRP + +![](img/2.8.png) + +这里是我们刚才看的马尔可夫链,如果把奖励也放上去的话,就是说到达每一个状态,我们都会获得一个奖励。这里我们可以设置对应的奖励,比如说到达 $s_1$ 状态的时候,可以获得 5 的奖励,到达 $s_7$ 的时候,可以得到 10 的奖励,其它状态没有任何奖励。因为这里状态是有限的,所以我们可以用向量 $R=[5,0,0,0,0,0,10]$ 来表示这个奖励函数,这个向量表示了每个点的奖励大小。 + +我们通过一个形象的例子来理解 MRP。我们把一个纸船放到河流之中,那么它就会随着这个河流而流动,它自身是没有动力的。所以你可以把 MRP 看成是一个随波逐流的例子,当我们从某一个点开始的时候,这个纸船就会随着事先定义好的状态转移进行流动,它到达每个状态过后,我们就有可能获得一些奖励。 + +### Return and Value function + +这里我们进一步定义一些概念。 + +* `Horizon` 是指一个回合的长度(每个回合最大的时间步数),它是由有限个步数决定的。 + +* `Return(回报)` 说的是把奖励进行折扣后所获得的收益。Return 可以定义为奖励的逐步叠加,如下式所示: + +$$ +G_{t}=R_{t+1}+\gamma R_{t+2}+\gamma^{2} R_{t+3}+\gamma^{3} R_{t+4}+\ldots+\gamma^{T-t-1} R_{T} +$$ + +这里有一个叠加系数,越往后得到的奖励,折扣得越多。这说明我们其实更希望得到现有的奖励,未来的奖励就要把它打折扣。 + +* 当我们有了 return 过后,就可以定义一个状态的价值了,就是 `state value function`。对于 MRP,state value function 被定义成是 return 的期望,如下式所示: + $$ + \begin{aligned} + V_{t}(s) &=\mathbb{E}\left[G_{t} \mid s_{t}=s\right] \\ + &=\mathbb{E}\left[R_{t+1}+\gamma R_{t+2}+\gamma^{2} R_{t+3}+\ldots+\gamma^{T-t-1} R_{T} \mid s_{t}=s\right] + \end{aligned} + $$ + +$G_t$ 是之前定义的 `discounted return`,我们这里取了一个期望,期望就是说从这个状态开始,你有可能获得多大的价值。所以这个期望也可以看成是对未来可能获得奖励的当前价值的一个表现,就是当你进入某一个状态过后,你现在就有多大的价值。 + +### Why Discount Factor + +**这里解释一下为什么需要 discount factor。** + +* 有些马尔可夫过程是带环的,它并没有终结,我们想避免这个无穷的奖励。 +* 我们并没有建立一个完美的模拟环境的模型,也就是说,我们对未来的评估不一定是准确的,我们不一定完全信任我们的模型,因为这种不确定性,所以我们对未来的预估增加一个折扣。我们想把这个不确定性表示出来,希望尽可能快地得到奖励,而不是在未来某一个点得到奖励。 +* 如果这个奖励是有实际价值的,我们可能是更希望立刻就得到奖励,而不是后面再得到奖励(现在的钱比以后的钱更有价值)。 +* 在人的行为里面来说的话,大家也是想得到即时奖励。 +* 有些时候可以把这个系数设为 0,$\gamma=0$:我们就只关注了它当前的奖励。我们也可以把它设为 1,$\gamma=1$:对未来并没有折扣,未来获得的奖励跟当前获得的奖励是一样的。 + +Discount factor 可以作为强化学习 agent 的一个超参数来进行调整,然后就会得到不同行为的 agent。 + +![](img/2.11.png) + +这里我们再来看一看,在这个 MRP 里面,如何计算它的价值。这个 MRP 依旧是这个状态转移。它的奖励函数是定义成这样,它在进入第一个状态的时候会得到 5 的奖励,进入第七个状态的时候会得到 10 的奖励,其它状态都没有奖励。 + +我们现在可以计算每一个轨迹得到的奖励,比如我们对于这个 $s_4,s_5,s_6,s_7$ 轨迹的奖励进行计算,这里折扣系数是 0.5。 + +* 在 $s_4$ 的时候,奖励为零。 + +* 下一个状态 $s_5$ 的时候,因为我们已经到了下一步了,所以我们要把 $s_5$ 进行一个折扣,$s_5$ 本身也是没有奖励的。 +* 然后是到 $s_6$,也没有任何奖励,折扣系数应该是 $\frac{1}{4}$ 。 +* 到达 $s_7$ 后,我们获得了一个奖励,但是因为 $s_7$ 这个状态是未来才获得的奖励,所以我们要进行三次折扣。 + +所以对于这个轨迹,它的 return 就是一个 1.25,类似地,我们可以得到其它轨迹的 return 。 + +这里就引出了一个问题,当我们有了一些轨迹的实际 return,怎么计算它的价值函数。比如说我们想知道 $s_4$ 状态的价值,就是当你进入 $s_4$ 后,它的价值到底如何。一个可行的做法就是说我们可以产生很多轨迹,然后把这里的轨迹都叠加起来。比如我们可以从 $s_4$ 开始,采样生成很多轨迹,都把它的 return 计算出来,然后可以直接把它取一个平均作为你进入 $s_4$ 它的价值。这其实是一种计算价值函数的办法,通过这个蒙特卡罗采样的办法计算 $s_4$ 的状态。接下来会进一步介绍蒙特卡罗算法。 + +### Bellman Equation + +![](img/2.12.png) + +但是这里我们采取了另外一种计算方法,我们从这个价值函数里面推导出 `Bellman Equation(贝尔曼等式)`,如下式所示: +$$ +V(s)=\underbrace{R(s)}_{\text {Immediate reward }}+\underbrace{\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s\right) V\left(s^{\prime}\right)}_{\text {Discounted sum of future reward }} +$$ +其中: + +* $s'$ 可以看成未来的所有状态。 +* 转移 $P(s'|s)$ 是指从当前状态转移到未来状态的概率。 +* $V(s')$ 代表的是未来某一个状态的价值。我们从当前这个位置开始,有一定的概率去到未来的所有状态,所以我们要把这个概率也写上去,这个转移矩阵也写上去,然后我们就得到了未来状态,然后再乘以一个 $\gamma$,这样就可以把未来的奖励打折扣。 +* 第二部分可以看成是未来奖励的折扣总和(Discounted sum of future reward)。 + +**Bellman Equation 定义了当前状态跟未来状态之间的这个关系。** + +未来打了折扣的奖励加上当前立刻可以得到的奖励,就组成了这个 Bellman Equation。 + +#### Law of Total Expectation + +在推导 Bellman equation 之前,我们可以仿照`Law of Total Expectation(全期望公式)`的证明过程来证明下面的式子: +$$ +\mathbb{E}[V(s_{t+1})|s_t]=\mathbb{E}[\mathbb{E}[G_{t+1}|s_{t+1}]|s_t]=E[G_{t+1}|s_t] +$$ + +> Law of total expectation 也被称为 law of iterated expectations(LIE)。如果 $A_i$ 是样本空间的有限或可数的划分(partition),则全期望公式可以写成如下形式: +> $$ +> \mathrm{E}(X)=\sum_{i} \mathrm{E}\left(X \mid A_{i}\right) \mathrm{P}\left(A_{i}\right) +> $$ + +**证明:** + +为了记号简洁并且易读,我们丢掉了下标,令 $s=s_t,g'=G_{t+1},s'=s_{t+1}$。我们可以根据条件期望的定义来重写这个回报的期望为: +$$ +\begin{aligned} +\mathbb{E}\left[G_{t+1} \mid s_{t+1}\right] &=\mathbb{E}\left[g^{\prime} \mid s^{\prime}\right] \\ +&=\sum_{g^{\prime}} g^{\prime}~p\left(g^{\prime} \mid s^{\prime}\right) +\end{aligned} +$$ +> 如果 $X$ 和 $Y$ 都是离散型随机变量,则条件期望(Conditional Expectation)$E(X|Y=y)$的定义如下式所示: +> $$ +> \mathrm{E}(X \mid Y=y)=\sum_{x} x P(X=x \mid Y=y) +> $$ + +令 $s_t=s$,我们对 $\mathbb{E}\left[G_{t+1} \mid s_{t+1}\right]$ 求期望可得: +$$ +\begin{aligned} +\mathbb{E}\left[\mathbb{E}\left[G_{t+1} \mid s_{t+1}\right] \mid s_{t}\right] +&=\mathbb{E} \left[\mathbb{E}\left[g^{\prime} \mid s^{\prime}\right] \mid s\right]\\ +&=\mathbb{E} \left[\sum_{g^{\prime}} g^{\prime}~p\left(g^{\prime} \mid s^{\prime}\right)\mid s\right]\\ +&= \sum_{s^{\prime}}\sum_{g^{\prime}} g^{\prime}~p\left(g^{\prime} \mid s^{\prime},s\right)p(s^{\prime} \mid s)\\ +&=\sum_{s^{\prime}} \sum_{g^{\prime}} \frac{g^{\prime} p\left(g^{\prime} \mid s^{\prime}, s\right) p\left(s^{\prime} \mid s\right) p(s)}{p(s)} \\ +&=\sum_{s^{\prime}} \sum_{g^{\prime}} \frac{g^{\prime} p\left(g^{\prime} \mid s^{\prime}, s\right) p\left(s^{\prime}, s\right)}{p(s)} \\ +&=\sum_{s^{\prime}} \sum_{g^{\prime}} \frac{g^{\prime} p\left(g^{\prime}, s^{\prime}, s\right)}{p(s)} \\ +&=\sum_{s^{\prime}} \sum_{g^{\prime}} g^{\prime} p\left(g^{\prime}, s^{\prime} \mid s\right) \\ +&=\sum_{g^{\prime}} \sum_{s^{\prime}} g^{\prime} p\left(g^{\prime}, s^{\prime} \mid s\right) \\ +&=\sum_{g^{\prime}} g^{\prime} p\left(g^{\prime} \mid s\right) \\ +&=\mathbb{E}\left[g^{\prime} \mid s\right]=\mathbb{E}\left[G_{t+1} \mid s_{t}\right] +\end{aligned} +$$ + +#### Bellman Equation Derivation + +Bellman equation 的推导过程如下: +$$ +\begin{aligned} +V(s)&=\mathbb{E}\left[G_{t} \mid s_{t}=s\right]\\ +&=\mathbb{E}\left[R_{t+1}+\gamma R_{t+2}+\gamma^{2} R_{t+3}+\ldots \mid s_{t}=s\right] \\ +&=\mathbb{E}\left[R_{t+1}|s_t=s\right] +\gamma \mathbb{E}\left[R_{t+2}+\gamma R_{t+3}+\gamma^{2} R_{t+4}+\ldots \mid s_{t}=s\right]\\ +&=R(s)+\gamma \mathbb{E}[G_{t+1}|s_t=s] \\ +&=R(s)+\gamma \mathbb{E}[V(s_{t+1})|s_t=s]\\ +&=R(s)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s\right) V\left(s^{\prime}\right) +\end{aligned} +$$ + +>Bellman Equation 就是当前状态与未来状态的迭代关系,表示当前状态的值函数可以通过下个状态的值函数来计算。Bellman Equation 因其提出者、动态规划创始人 Richard Bellman 而得名 ,也叫作“动态规划方程”。 + +**Bellman Equation 定义了状态之间的迭代关系,如下式所示。** +$$ +V(s)=R(s)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s\right) V\left(s^{\prime}\right) +$$ +![](img/2.13.png) + +假设有一个马尔可夫转移矩阵是右边这个样子,Bellman Equation 描述的就是当前状态到未来状态的一个转移。假设我们当前是在 $s_1$, 那么它只可能去到三个未来的状态:有 0.1 的概率留在它当前这个位置,有 0.2 的概率去到 $s_2$ 状态,有 0.7 的概率去到 $s_4$ 的状态,所以我们要把这个转移乘以它未来的状态的价值,再加上它的 immediate reward 就会得到它当前状态的价值。**所以 Bellman Equation 定义的就是当前状态跟未来状态的一个迭代的关系。** + +我们可以把 Bellman Equation 写成一种矩阵的形式,如下式所示。 +$$ +\left[\begin{array}{c} +V\left(s_{1}\right) \\ +V\left(s_{2}\right) \\ +\vdots \\ +V\left(s_{N}\right) +\end{array}\right]=\left[\begin{array}{c} +R\left(s_{1}\right) \\ +R\left(s_{2}\right) \\ +\vdots \\ +R\left(s_{N}\right) +\end{array}\right]+\gamma\left[\begin{array}{cccc} +P\left(s_{1} \mid s_{1}\right) & P\left(s_{2} \mid s_{1}\right) & \ldots & P\left(s_{N} \mid s_{1}\right) \\ +P\left(s_{1} \mid s_{2}\right) & P\left(s_{2} \mid s_{2}\right) & \ldots & P\left(s_{N} \mid s_{2}\right) \\ +\vdots & \vdots & \ddots & \vdots \\ +P\left(s_{1} \mid s_{N}\right) & P\left(s_{2} \mid s_{N}\right) & \ldots & P\left(s_{N} \mid s_{N}\right) +\end{array}\right]\left[\begin{array}{c} +V\left(s_{1}\right) \\ +V\left(s_{2}\right) \\ +\vdots \\ +V\left(s_{N}\right) +\end{array}\right] +$$ +首先有这个转移矩阵。我们当前这个状态是一个向量 $[V(s_1),V(s_2),\cdots,V(s_N)]^T$。我们可以写成迭代的形式。我们每一行来看的话,$V$ 这个向量乘以了转移矩阵里面的某一行,再加上它当前可以得到的 reward,就会得到它当前的价值。 + +当我们把 Bellman Equation 写成矩阵形式后,可以直接求解: +$$ +\begin{aligned} +V &= R+ \gamma PV \\ +IV &= R+ \gamma PV \\ +(I-\gamma P)V &=R \\ +V&=(I-\gamma P)^{-1}R +\end{aligned} +$$ + +我们可以直接得到一个`解析解(analytic solution)`: +$$ +V=(I-\gamma P)^{-1} R +$$ +我们可以通过矩阵求逆把这个 V 的这个价值直接求出来。但是一个问题是这个矩阵求逆的过程的复杂度是 $O(N^3)$。所以当状态非常多的时候,比如说从十个状态到一千个状态,到一百万个状态。那么当我们有一百万个状态的时候,这个转移矩阵就会是个一百万乘以一百万的矩阵,这样一个大矩阵的话求逆是非常困难的,**所以这种通过解析解去求解的方法只适用于很小量的 MRP。** + +### Iterative Algorithm for Computing Value of a MRP + +接下来我们来求解这个价值函数。**我们可以通过迭代的方法来解这种状态非常多的 MRP(large MRPs),**比如说: + +* 动态规划的方法, +* 蒙特卡罗的办法(通过采样的办法去计算它), +* 时序差分学习(Temporal-Difference Learning)的办法。 `Temporal-Difference Learning` 叫 `TD Leanring`,它是动态规划和蒙特卡罗的一个结合。 + +![](img/2.16.png) + +**首先我们用蒙特卡罗(Monte Carlo)的办法来计算它的价值函数。**蒙特卡罗就是说当得到一个 MRP 过后,我们可以从某一个状态开始,把这个小船放进去,让它随波逐流,这样就会产生一个轨迹。产生了一个轨迹过后,就会得到一个奖励,那么就直接把它的折扣的奖励 $g$ 算出来。算出来过后就可以把它积累起来,得到 return $G_t$。 当积累到一定的轨迹数量过后,直接用 $G_t$ 除以轨迹数量,就会得到它的价值。 + +比如说我们要算 $s_4$ 状态的价值。 + +* 我们就可以从 $s_4$ 状态开始,随机产生很多轨迹,就是说产生很多小船,把小船扔到这个转移矩阵里面去,然后它就会随波逐流,产生轨迹。 +* 每个轨迹都会得到一个 return,我们得到大量的 return,比如说一百个、一千个 return ,然后直接取一个平均,那么就可以等价于现在 $s_4$ 这个价值,因为 $s_4$ 的价值 $V(s_4)$ 定义了你未来可能得到多少的奖励。这就是蒙特卡罗采样的方法。 + +![](img/2.17.png) + +**我们也可以用这个动态规划的办法**,一直去迭代它的 Bellman equation,让它最后收敛,我们就可以得到它的一个状态。所以在这里算法二就是一个迭代的算法,通过 `bootstrapping(自举)`的办法,然后去不停地迭代这个 Bellman Equation。当这个最后更新的状态跟你上一个状态变化并不大的时候,更新就可以停止,我们就可以输出最新的 $V'(s)$ 作为它当前的状态。所以这里就是把 Bellman Equation 变成一个 Bellman Update,这样就可以得到它的一个价值。 + +动态规划的方法基于后继状态值的估计来更新状态值的估计(算法二中的第 3 行用 $V'$ 来更新 $V$ )。也就是说,它们根据其他估算值来更新估算值。我们称这种基本思想为 bootstrapping。 + +>Bootstrap 本意是“解靴带”;这里是在使用徳国文学作品《吹牛大王历险记》中解靴带自助(拔靴自助)的典故,因此将其译为“自举”。 + +## Markov Decision Process(MDP) + +### MDP + +**相对于 MRP,`马尔可夫决策过程(Markov Decision Process)`多了一个 `decision`,其它的定义跟 MRP 都是类似的**: + +* 这里多了一个决策,多了一个动作。 +* 状态转移也多了一个条件,变成了 $P\left(s_{t+1}=s^{\prime} \mid s_{t}=s, a_{t}=a\right)$。你采取某一种动作,然后你未来的状态会不同。未来的状态不仅是依赖于你当前的状态,也依赖于在当前状态 agent 采取的这个动作。 +* 对于这个价值函数,它也是多了一个条件,多了一个你当前的这个动作,变成了 $R\left(s_{t}=s, a_{t}=a\right)=\mathbb{E}\left[r_{t} \mid s_{t}=s, a_{t}=a\right]$。你当前的状态以及你采取的动作会决定你在当前可能得到的奖励多少。 + +### Policy in MDP + +* Policy 定义了在某一个状态应该采取什么样的动作。 + +* 知道当前状态过后,我们可以把当前状态带入 policy function,然后就会得到一个概率,即 +$$ +\pi(a \mid s)=P\left(a_{t}=a \mid s_{t}=s\right) +$$ + +概率就代表了在所有可能的动作里面怎样采取行动,比如可能有 0.7 的概率往左走,有 0.3 的概率往右走,这是一个概率的表示。 + +* 另外这个策略也可能是确定的,它有可能是直接输出一个值。或者就直接告诉你当前应该采取什么样的动作,而不是一个动作的概率。 + +* 假设这个概率函数应该是稳定的(stationary),不同时间点,你采取的动作其实都是对这个 policy function 进行采样。 + +我们可以将 MRP 转换成 MDP。已知一个 MDP 和一个 policy $\pi$ 的时候,我们可以把 MDP 转换成 MRP。 + +在 MDP 里面,转移函数 $P(s'|s,a)$ 是基于它当前状态以及它当前的 action。因为我们现在已知它 policy function,就是说在每一个状态,我们知道它可能采取的动作的概率,那么就可以直接把这个 action 进行加和,直接把这个 a 去掉,那我们就可以得到对于 MRP 的一个转移,这里就没有 action。 + +$$ + P^{\pi}\left(s^{\prime} \mid s\right)=\sum_{a \in A} \pi(a \mid s) P\left(s^{\prime} \mid s, a\right) +$$ + +对于这个奖励函数,我们也可以把 action 拿掉,这样就会得到一个类似于 MRP 的奖励函数。 + +$$ +R^{\pi}(s)=\sum_{a \in A} \pi(a \mid s) R(s, a) +$$ + +### Comparison of MP/MRP and MDP + +![](img/2.21.png) + + + +**这里我们看一看,MDP 里面的状态转移跟 MRP 以及 MP 的一个差异。** + +* 马尔可夫过程的转移是直接就决定。比如当前状态是 s,那么就直接通过这个转移概率决定了下一个状态是什么。 +* 但对于 MDP,它的中间多了一层这个动作 a ,就是说在你当前这个状态的时候,首先要决定的是采取某一种动作,那么你会到了某一个黑色的节点。到了这个黑色的节点,因为你有一定的不确定性,当你当前状态决定过后以及你当前采取的动作过后,你到未来的状态其实也是一个概率分布。**所以在这个当前状态跟未来状态转移过程中这里多了一层决策性,这是 MDP 跟之前的马尔可夫过程很不同的一个地方。**在马尔可夫决策过程中,动作是由 agent 决定,所以多了一个 component,agent 会采取动作来决定未来的状态转移。 + +### Value function for MDP + +顺着 MDP 的定义,我们可以把 `状态-价值函数(state-value function)`,就是在 MDP 里面的价值函数也进行一个定义,它的定义是跟 MRP 是类似的,如式 (3) 所示: +$$ +v^{\pi}(s)=\mathbb{E}_{\pi}\left[G_{t} \mid s_{t}=s\right] \tag{3} +$$ +但是这里 expectation over policy,就是这个期望是基于你采取的这个 policy ,就当你的 policy 决定过后,**我们通过对这个 policy 进行采样来得到一个期望,那么就可以计算出它的这个价值函数。** + +这里我们另外引入了一个 `Q 函数(Q-function)`。Q 函数也被称为 `action-value function`。**Q 函数定义的是在某一个状态采取某一个动作,它有可能得到的这个 return 的一个期望**,如式 (4) 所示: +$$ +q^{\pi}(s, a)=\mathbb{E}_{\pi}\left[G_{t} \mid s_{t}=s, A_{t}=a\right] \tag{4} +$$ +这里期望其实也是 over policy function。所以你需要对这个 policy function 进行一个加和,然后得到它的这个价值。 +**对 Q 函数中的动作函数进行加和,就可以得到价值函数**,如式 (5) 所示: +$$ +v^{\pi}(s)=\sum_{a \in A} \pi(a \mid s) q^{\pi}(s, a) \tag{5} +$$ +#### Q-function Bellman Equation + +此处我们给出 Q 函数的 Bellman equation: + +$$ +\begin{aligned} +q(s,a)&=\mathbb{E}\left[G_{t} \mid s_{t}=s,a_{t}=a\right]\\ +&=\mathbb{E}\left[R_{t+1}+\gamma R_{t+2}+\gamma^{2} R_{t+3}+\ldots \mid s_{t}=s,a_{t}=a\right] \\ +&=\mathbb{E}\left[R_{t+1}|s_{t}=s,a_{t}=a\right] +\gamma \mathbb{E}\left[R_{t+2}+\gamma R_{t+3}+\gamma^{2} R_{t+4}+\ldots \mid s_{t}=s,a_{t}=a\right]\\ +&=R(s,a)+\gamma \mathbb{E}[G_{t+1}|s_{t}=s,a_{t}=a] \\ +&=R(s,a)+\gamma \mathbb{E}[V(s_{t+1})|s_{t}=s,a_{t}=a]\\ +&=R(s,a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s,a\right) V\left(s^{\prime}\right) +\end{aligned} +$$ + + +### Bellman Expectation Equation + +**我们可以把状态-价值函数和 Q 函数拆解成两个部分:即时奖励(immediate reward) 和后续状态的折扣价值(discounted value of successor state)。** + +通过对状态-价值函数进行一个分解,我们就可以得到一个类似于之前 MRP 的 Bellman Equation,这里叫 `Bellman Expectation Equation`,如式 (6) 所示: +$$ +v^{\pi}(s)=E_{\pi}\left[R_{t+1}+\gamma v^{\pi}\left(s_{t+1}\right) \mid s_{t}=s\right] \tag{6} +$$ +对于 Q 函数,我们也可以做类似的分解,也可以得到 Q 函数的 Bellman Expectation Equation,如式 (7) 所示: +$$ +q^{\pi}(s, a)=E_{\pi}\left[R_{t+1}+\gamma q^{\pi}\left(s_{t+1}, A_{t+1}\right) \mid s_{t}=s, A_{t}=a\right] \tag{7} +$$ +**Bellman expectation equation 定义了你当前状态跟未来状态之间的一个关联。** + +我们进一步进行一个简单的分解。 + +我们先给出等式 (8): +$$ +v^{\pi}(s)=\sum_{a \in A} \pi(a \mid s) q^{\pi}(s, a) \tag{8} +$$ +再给出等式 (9): +$$ +q^{\pi}(s, a)=R_{s}^{a}+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) v^{\pi}\left(s^{\prime}\right) \tag{9} +$$ +**等式 (8) 和等式 (9) 代表了价值函数跟 Q 函数之间的一个关联。** + +也可以把等式 (9) 插入等式 (8) 中,得到等式 (10): +$$ +v^{\pi}(s)=\sum_{a \in A} \pi(a \mid s)\left(R(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) v^{\pi}\left(s^{\prime}\right)\right) \tag{10} +$$ +**等式 (10) 代表了当前状态的价值跟未来状态价值之间的一个关联。** + +我们把等式 (8) 插入到等式 (9),就可以得到等式 (11): +$$ +q^{\pi}(s, a)=R(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) \sum_{a^{\prime} \in A} \pi\left(a^{\prime} \mid s^{\prime}\right) q^{\pi}\left(s^{\prime}, a^{\prime}\right) \tag{11} +$$ +**等式 (11) 代表了当前时刻的 Q 函数跟未来时刻的 Q 函数之间的一个关联。** + +**等式 (10) 和等式 (11) 是 Bellman expectation equation 的另一种形式。** + +### Backup Diagram + +![](img/2.25.png) + +这里有一个概念叫 `backup`。Backup 类似于 bootstrapping 之间这个迭代关系,就对于某一个状态,它的当前价值是跟它的未来价值线性相关的。 + +我们把上面这样的图称为 `backup diagram(备份图)`,因为它们图示的关系构成了更新或备份操作的基础,而这些操作是强化学习方法的核心。这些操作将价值信息从一个状态(或状态-动作对)的后继状态(或状态-动作对)转移回它。 + +每一个空心圆圈代表一个状态,每一个实心圆圈代表一个状态-动作对。 + + +$$ +v^{\pi}(s)=\sum_{a \in A} \pi(a \mid s)\left(R(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) v^{\pi}\left(s^{\prime}\right)\right) \tag{12} +$$ +如式 (12) 所示,我们这里有两层加和: + +* 第一层加和就是这个叶子节点,往上走一层的话,我们就可以把未来的价值($s'$ 的价值) backup 到黑色的节点。 +* 第二层加和是对 action 进行加和。得到黑色节点的价值过后,再往上 backup 一层,就会推到根节点的价值,即当前状态的价值。 + +![](img/state_value_function_backup.png ':size=650') + +上图是状态-价值函数的计算分解图,上图 B 计算公式为 +$$ +v^{\pi}(s)=\sum_{a \in A} \pi(a \mid s) q^{\pi}(s, a) \tag{i} +$$ +上图 B 给出了状态-价值函数与 Q 函数之间的关系。上图 C 计算 Q 函数为 +$$ +q^{\pi}(s,a)=R(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) v^{\pi}\left(s^{\prime}\right) \tag{ii} +$$ + +将式 (ii) 代入式 (i) 可得: +$$ +v^{\pi}(s)=\sum_{a \in A} \pi(a \mid s)\left(R(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) v^{\pi}\left(s^{\prime}\right)\right) +$$ +**所以 backup diagram 定义了未来下一时刻的状态-价值函数跟上一时刻的状态-价值函数之间的关联。** + +![](img/2.26.png) + +对于 Q 函数,我们也可以进行这样的一个推导。现在的根节点是这个 Q 函数的一个节点。Q 函数对应于黑色的节点。我们下一时刻的 Q 函数是叶子节点,有四个黑色节点。 +$$ +q^{\pi}(s, a)=R(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) \sum_{a^{\prime} \in A} \pi\left(a^{\prime} \mid s^{\prime}\right) q^{\pi}\left(s^{\prime}, a^{\prime}\right) \tag{13} +$$ +如式 (13) 所示,我们这里也有两个加和: + +* 第一层加和是先把这个叶子节点从黑色节点推到这个白色的节点,进了它的这个状态。 +* 当我们到达某一个状态过后,再对这个白色节点进行一个加和,这样就把它重新推回到当前时刻的一个 Q 函数。 + +![](img/q_function_backup.png ':size=650') + +在上图 C 中, +$$ +v^{\pi}\left(s^{\prime}\right)=\sum_{a^{\prime} \in A} \pi\left(a^{\prime} \mid s^{\prime}\right) q^{\pi}\left(s^{\prime}, a^{\prime}\right) \tag{iii} +$$ +将式 (iii) 代入式 (ii) 可得到 Q 函数: +$$ +q^{\pi}(s, a)=R(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) \sum_{a^{\prime} \in A} \pi\left(a^{\prime} \mid s^{\prime}\right) q^{\pi}\left(s^{\prime}, a^{\prime}\right) +$$ +**所以这个等式就决定了未来 Q 函数跟当前 Q 函数之间的这个关联。** + +### Policy Evaluation(Prediction) + +* 当我们知道一个 MDP 以及要采取的策略 $\pi$ ,计算价值函数 $v^{\pi}(s)$ 的过程就是 `policy evaluation`。就像我们在评估这个策略,我们会得到多大的奖励。 +* **Policy evaluation 在有些地方也被叫做 `(value) prediction`,也就是预测你当前采取的这个策略最终会产生多少的价值。** + +![](img/2.28.png) + +* MDP,你其实可以把它想象成一个摆渡的人在这个船上面,她可以控制这个船的移动,这样就避免了这个船随波逐流。因为在每一个时刻,这个人会决定采取什么样的一个动作,这样会把这个船进行导向。 + +* MRP 跟 MP 的话,这个纸的小船会随波逐流,然后产生轨迹。 +* MDP 的不同就是有一个 agent 去控制这个船,这样我们就可以尽可能多地获得奖励。 + +![](img/2.29.png) + +我们再看下 policy evaluation 的例子,怎么在决策过程里面计算它每一个状态的价值。 + +* 假设环境里面有两种动作:往左走和往右走。 +* 现在的奖励函数有两个变量:动作和状态。但我们这里规定,不管你采取什么动作,只要到达状态 $s_1$,就有 5 的奖励。只要你到达状态 $s_7$ 了,就有 10 的奖励,中间没有任何奖励。 +* 假设我们现在采取的一个策略,这个策略是说不管在任何状态,我们采取的策略都是往左走。假设价值折扣因子是零,那么对于确定性策略(deterministic policy),最后估算出的价值函数是一致的,即 + +$$ +V^{\pi}=[5,0,0,0,0,0,10] +$$ + +Q: 怎么得到这个结果? + +A: 我们可以直接在去 run 下面这个 iterative equation: +$$ +v_{k}^{\pi}(s)=r(s, \pi(s))+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, \pi(s)\right) v_{k-1}^{\pi}\left(s^{\prime}\right) +$$ +就把 Bellman expectation equation 拿到这边来,然后不停地迭代,最后它会收敛。收敛过后,它的值就是它每一个状态的价值。 + +![](img/2.30.png) + +再来看一个例子(practice 1),如果折扣因子是 0.5,我们可以通过下面这个等式进行迭代: +$$ +v_{t}^{\pi}(s)=\sum_{a} P(\pi(s)=a)\left(r(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) v_{t-1}^{\pi}\left(s^{\prime}\right)\right) +$$ +然后就会得到它的状态价值。 + +另外一个例子(practice 2),就是说我们现在采取的 policy 在每个状态下,有 0.5 的概率往左走,有 0.5 的概率往右走,那么放到这个状态里面去如何计算。其实也是把这个 Bellman expectation equation 拿出来,然后进行迭代就可以算出来了。一开始的时候,我们可以初始化,不同的 $v(s')$ 都会有一个值,放到 Bellman expectation equation 里面去迭代,然后就可以算出它的状态价值。 + +### Prediction and Control + +![](img/2.31.png) + +MDP 的 `prediction` 和 `control` 是 MDP 里面的核心问题。 + +* 预测问题: + * 输入:MDP $$ 和 policy $\pi$ 或者 MRP $$。 + * 输出:value function $v^{\pi}$。 + * Prediction 是说给定一个 MDP 以及一个 policy $\pi$ ,去计算它的 value function,就对于每个状态,它的价值函数是多少。 + +* 控制问题: + * 输入:MDP $$。 + * 输出:最佳价值函数(optimal value function) $v^*$ 和最佳策略(optimal policy) $\pi^*$。 + * Control 就是说我们去寻找一个最佳的策略,然后同时输出它的最佳价值函数以及最佳策略。 +* 在 MDP 里面,prediction 和 control 都可以通过动态规划去解决。 +* 要强调的是,这两者的区别就在于, + * 预测问题是**给定一个 policy**,我们要确定它的 value function 是多少。 + * 而控制问题是在**没有 policy 的前提下**,我们要确定最优的 value function 以及对应的决策方案。 +* **实际上,这两者是递进的关系,在强化学习中,我们通过解决预测问题,进而解决控制问题。** + +![](img/prediction_example.png) + +**举一个例子来说明 prediction 与 control 的区别。** + +首先是**预测问题**: + +* 在上图的方格中,我们规定从 A $\to$ A' 可以得到 +10 的奖励,从 B $\to$ B' 可以得到 +5 的奖励,其它步骤的奖励为 -1。 +* 现在,我们给定一个 policy:在任何状态中,它的行为模式都是随机的,也就是上下左右的概率各 25%。 +* 预测问题要做的就是,在这种决策模式下,我们的 value function 是什么。上图 b 是对应的 value function。 + +![](img/control_example.png) + + + +接着是**控制问题**: + +* 在控制问题中,问题背景与预测问题相同,唯一的区别就是:不再限制 policy。也就是说行为模式是未知的,我们要自己确定。 +* 所以我们通过解决控制问题,求得每一个状态的最优的 value function(如上图 b 所示),也得到了最优的 policy(如上图 c 所示)。 + +* 控制问题要做的就是,给定同样的条件,在所有可能的策略下最优的价值函数是什么?最优策略是什么? + +### Dynamic Programming + +`动态规划(Dynamic Programming,DP)`适合解决满足如下两个性质的问题: + +* `最优子结构(optimal substructure)`。最优子结构意味着,我们的问题可以拆分成一个个的小问题,通过解决这个小问题,最后,我们能够通过组合小问题的答案,得到大问题的答案,即最优的解。 +* `重叠子问题(Overlapping subproblems)`。重叠子问题意味着,子问题出现多次,并且子问题的解决方案能够被重复使用。 + +MDP 是满足动态规划的要求的, + +* 在 Bellman equation 里面,我们可以把它分解成一个递归的结构。当我们把它分解成一个递归的结构的时候,如果我们的子问题子状态能得到一个值,那么它的未来状态因为跟子状态是直接相连的,那我们也可以继续推算出来。 +* 价值函数就可以储存并重用它的最佳的解。 + +动态规划应用于 MDP 的规划问题(planning)而不是学习问题(learning),我们必须对环境是完全已知的(Model-Based),才能做动态规划,直观的说,就是要知道状态转移概率和对应的奖励才行 + +动态规划能够完成预测问题和控制问题的求解,是解 MDP prediction 和 control 一个非常有效的方式。 + +### Policy Evaluation on MDP + +**Policy evaluation 就是给定一个 MDP 和一个 policy,我们可以获得多少的价值。**就对于当前这个策略,我们可以得到多大的 value function。 + +这里有一个方法是说,我们直接把这个 `Bellman Expectation Backup` 拿过来,变成一个迭代的过程,这样反复迭代直到收敛。这个迭代过程可以看作是 `synchronous backup` 的过程。 + +> 同步备份(synchronous backup)是指每一次的迭代都会完全更新所有的状态,这样对于程序资源需求特别大。异步备份(asynchronous backup)的思想就是通过某种方式,使得每一次迭代不需要更新所有的状态,因为事实上,很多的状态也不需要被更新。 + +$$ +v_{t+1}(s)=\sum_{a \in \mathcal{A}} \pi(a \mid s)\left(R(s, a)+\gamma \sum_{s^{\prime} \in \mathcal{S}} P\left(s^{\prime} \mid s, a\right) v_{t}\left(s^{\prime}\right)\right) \tag{14} +$$ +* 等式 (14) 说的是说我们可以把 Bellman Expectation Backup 转换成一个动态规划的迭代。 +* 当我们得到上一时刻的 $v_t$ 的时候,就可以通过这个递推的关系来推出下一时刻的值。 +* 反复去迭代它,最后它的值就是从 $v_1,v_2$ 到最后收敛过后的这个值。这个值就是当前给定的 policy 对应的价值函数。 + +Policy evaluation 的核心思想就是把如下式所示的 Bellman expectation backup 拿出来反复迭代,然后就会得到一个收敛的价值函数的值。 +$$ +v_{t+1}(s)=\sum_{a \in \mathcal{A}} \pi(a \mid s)\left(R(s, a)+\gamma \sum_{s^{\prime} \in \mathcal{S}} P\left(s^{\prime} \mid s, a\right) v_{t}\left(s^{\prime}\right)\right) \tag{15} +$$ +因为已经给定了这个函数的 policy function,那我们可以直接把它简化成一个 MRP 的表达形式,这样的话,形式就更简洁一些,就相当于我们把这个 $a$ 去掉,如下式所示: +$$ +v_{t+1}(s)=R^{\pi}(s)+\gamma P^{\pi}\left(s^{\prime} \mid s\right) v_{t}\left(s^{\prime}\right) \tag{16} +$$ +这样它就只有价值函数跟转移函数了。通过去迭代这个更简化的一个函数,我们也可以得到它每个状态的价值。因为不管是在 MRP 以及 MDP,它的价值函数包含的这个变量都是只跟这个状态有关,就相当于进入某一个状态,未来可能得到多大的价值。 + +![](img/2.35.png) + +* 比如现在的环境是一个 small gridworld。这个 agent 的目的是从某一个状态开始,然后到达终点状态。它的终止状态就是左上角跟右下角,这里总共有 14 个状态,因为我们把每个位置用一个状态来表示。 +* 这个 agent 采取的动作,它的 policy function 就直接先给定了,它在每一个状态都是随机游走,它们在每一个状态就是上下左右行走。它在边缘状态的时候,比如说在第四号状态的时候,它往左走的话,它是依然存在第四号状态,我们加了这个限制。 + +* 这里我们给的奖励函数就是说你每走一步,就会得到 -1 的奖励,所以 agent 需要尽快地到达终止状态。 +* 状态之间的转移也是确定的。比如从第六号状态往上走,它就会直接到达第二号状态。很多时候有些环境是 `概率性的(probabilistic)`, 就是说 agent 在第六号状态,它选择往上走的时候,有可能地板是滑的,然后它可能滑到第三号状态或者第一号状态,这就是有概率的一个转移。但这里把这个环境进行了简化,从六号往上走,它就到了二号。 +* 所以直接用这个迭代来解它,因为我们已经知道每一个概率以及它的这个概率转移,那么就直接可以进行一个简短的迭代,这样就会算出它每一个状态的价值。 + +![](img/2.36.png) + +我们再来看一个动态的例子,首先推荐斯坦福大学的一个网站:[GridWorld: Dynamic Programming Demo](https://cs.stanford.edu/people/karpathy/reinforcejs/gridworld_dp.html) ,这个网站模拟了单步更新的过程中,所有格子的一个状态价值的变化过程。 + +![](img/2.37.png ':size=550') + +这里有很多格子,每个格子都代表了一个状态。在每个格子里面有一个初始值零。然后在每一个状态,它还有一些箭头,这个箭头就是说它在当前这个状态应该采取什么样的策略。我们这里采取一个随机的策略,不管它在哪一个状态,它上下左右的概率都是相同的。比如在某个状态,它都有上下左右 0.25 的概率采取某一个动作,所以它的动作是完全随机的。 + +在这样的环境里面,我们想计算它每一个状态的价值。我们也定义了它的 reward function,你可以看到有些状态上面有一个 R 的值。比如我们这边有些值是为负的,我们可以看到格子里面有几个 -1 的奖励,只有一个 +1 奖励的格子。在这个棋盘的中间这个位置,可以看到有一个 R 的值是 1.0,为正的一个价值函数。 所以每个状态对应了一个值,然后有一些状态没有任何值,就说明它的这个 reward function,它的奖励是为零的。 + +![](img/2.38.png ':size=550') + +我们开始做这个 policy evaluation,policy evaluation 是一个不停迭代的过程。当我们初始化的时候,所有的 $v(s)$ 都是 0。我们现在迭代一次,迭代一次过后,你发现有些状态上面,值已经产生了变化。比如有些状态的值的 R 为 -1,迭代一次过后,它就会得到 -1 的这个奖励。对于中间这个绿色的,因为它的奖励为正,所以它是 +1 的状态。 + +![](img/2.39.png ':size=550') + +所以当迭代第一次的时候,$v(s)$ 某些状态已经有些值的变化。 + +![](img/2.40.png ':size=550') + +* 我们再迭代一次(one sweep),然后发现它就从周围的状态也开始有值。因为周围状态跟之前有值的状态是临近的,所以它就相当于把旁边这个状态转移过来。所以当我们逐渐迭代的话,你会发现这个值一直在变换。 + +* 等迭代了很多次过后,很远的这些状态的价值函数已经有些值了,而且你可以发现它这里整个过程呈现逐渐扩散开的一个过程,这其实也是 policy evaluation 的一个可视化。 +* 当我们每一步在进行迭代的时候,远的状态就会得到了一些值,就逐渐从一些已经有奖励的这些状态,逐渐扩散,当你 run 很多次过后,它就逐渐稳定下来,最后值就会确定不变,这样收敛过后,每个状态上面的值就是它目前得到的这个 value function 的值。 + +### MDP Control + +![](img/2.41.png) + +Policy evaluation 是说给定一个 MDP 和一个 policy,我们可以估算出它的价值函数。**还有问题是说如果我们只有一个 MDP,如何去寻找一个最佳的策略,然后可以得到一个`最佳价值函数(Optimal Value Function)`。** + +Optimal Value Function 的定义如下式所示: +$$ +v^{*}(s)=\max _{\pi} v^{\pi}(s) +$$ +Optimal Value Function 是说,我们去搜索一种 policy $\pi$ 来让每个状态的价值最大。$v^*$ 就是到达每一个状态,它的值的极大化情况。 + +在这种极大化情况上面,我们得到的策略就可以说它是`最佳策略(optimal policy)`,如下式所示: +$$ +\pi^{*}(s)=\underset{\pi}{\arg \max }~ v^{\pi}(s) +$$ +Optimal policy 使得每个状态的价值函数都取得最大值。所以如果我们可以得到一个 optimal value function,就可以说某一个 MDP 的环境被解。在这种情况下,它的最佳的价值函数是一致的,就它达到的这个上限的值是一致的,但这里可能有多个最佳的 policy,就是说多个 policy 可以取得相同的最佳价值。 + +![](img/2.42.png) + +Q: 怎么去寻找这个最佳的 policy ? + +A: 当取得最佳的价值函数过后,我们可以通过对这个 Q 函数进行极大化,然后得到最佳策略。当所有东西都收敛过后,因为 Q 函数是关于状态跟动作的一个函数,所以在某一个状态采取一个动作,可以使得这个 Q 函数最大化,那么这个动作就应该是最佳的动作。所以如果我们能优化出一个 Q 函数,就可以直接在这个 Q 函数上面取一个让 Q 函数最大化的 action 的值,就可以提取出它的最佳策略。 + +![](img/2.43.png) + +最简单的策略搜索办法就是`穷举`。假设状态和动作都是有限的,那么每个状态我们可以采取这个 A 种动作的策略,那么总共就是 $|A|^{|S|}$ 个可能的 policy。那我们可以把策略都穷举一遍,然后算出每种策略的 value function,对比一下就可以得到最佳策略。 + +但是穷举非常没有效率,所以我们要采取其他方法。**搜索最佳策略有两种常用的方法:policy iteration 和 value iteration**。 + +![](img/2.44.png) + +**寻找这个最佳策略的过程就是 MDP control 过程**。MDP control 说的就是怎么去寻找一个最佳的策略来让我们得到一个最大的价值函数,如下式所示: +$$ +\pi^{*}(s)=\underset{\pi}{\arg \max } ~ v^{\pi}(s) +$$ +对于一个事先定好的 MDP 过程,当 agent 去采取最佳策略的时候,我们可以说最佳策略一般都是确定的,而且是稳定的(它不会随着时间的变化)。但是不一定是唯一的,多种动作可能会取得相同的这个价值。 + +**我们可以通过 policy iteration 和 value iteration 来解 MDP 的控制问题。** + +### Policy Iteration + +![](img/2.45.png) + +**Policy iteration 由两个步骤组成:policy evaluation 和 policy improvement。** + +* **第一个步骤是 policy evaluation**,当前我们在优化这个 policy $\pi$,在优化过程中得到一个最新的 policy。我们先保证这个 policy 不变,然后去估计它出来的这个价值。给定当前的 policy function 来估计这个 v 函数。 +* **第二个步骤是 policy improvement**,得到 v 函数过后,我们可以进一步推算出它的 Q 函数。得到 Q 函数过后,我们直接在 Q 函数上面取极大化,通过在这个 Q 函数上面做一个贪心的搜索来进一步改进它的策略。 +* 这两个步骤就一直是在迭代进行,所以在 policy iteration 里面,在初始化的时候,我们有一个初始化的 $V$ 和 $\pi$ ,然后就是在这两个过程之间迭代。 +* 左边这幅图上面的线就是我们当前 v 的值,下面的线是 policy 的值。 + * 跟踢皮球一样,我们先给定当前已有的这个 policy function,然后去算它的 v。 + * 算出 v 过后,我们会得到一个 Q 函数。Q 函数我们采取 greedy 的策略,这样就像踢皮球,踢回这个 policy 。 + * 然后进一步改进那个 policy ,得到一个改进的 policy 过后,它还不是最佳的,我们再进行 policy evaluation,然后又会得到一个新的 value function。基于这个新的 value function 再进行 Q 函数的极大化,这样就逐渐迭代,然后就会得到收敛。 + +![](img/2.46.png) + +这里再来看一下第二个步骤: `policy improvement`,我们是如何改进它的这个策略。得到这个 v 值过后,我们就可以通过这个 reward function 以及状态转移把它的这个 Q-function 算出来,如下式所示: +$$ +q^{\pi_{i}}(s, a)=R(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) v^{\pi_{i}}\left(s^{\prime}\right) +$$ +对于每一个状态,第二个步骤会得到它的新一轮的这个 policy ,就在每一个状态,我们去取使它得到最大值的 action,如下式所示: +$$ +\pi_{i+1}(s)=\underset{a}{\arg \max } ~q^{\pi_{i}}(s, a) +$$ +**你可以把 Q 函数看成一个 Q-table:** + +* 横轴是它的所有状态, +* 纵轴是它的可能的 action。 + +得到 Q 函数后,`Q-table`也就得到了。 + +那么对于某一个状态,每一列里面我们会取最大的那个值,最大值对应的那个 action 就是它现在应该采取的 action。所以 arg max 操作就说在每个状态里面采取一个 action,这个 action 是能使这一列的 Q 最大化的那个动作。 + +#### Bellman Optimality Equation + +![](img/2.47.png) + +当一直在采取 arg max 操作的时候,我们会得到一个单调的递增。通过采取这种 greedy,即 arg max 操作,我们就会得到更好的或者不变的 policy,而不会使它这个价值函数变差。所以当这个改进停止过后,我们就会得到一个最佳策略。 + +当改进停止过后,我们取它最大化的这个 action,它直接就会变成它的价值函数,如下式所示: +$$ +q^{\pi}\left(s, \pi^{\prime}(s)\right)=\max _{a \in \mathcal{A}} q^{\pi}(s, a)=q^{\pi}(s, \pi(s))=v^{\pi}(s) +$$ +所以我们有了一个新的等式: +$$ +v^{\pi}(s)=\max _{a \in \mathcal{A}} q^{\pi}(s, a) +$$ +上式被称为 `Bellman optimality equation`。从直觉上讲,Bellman optimality equation 表达了这样一个事实:最佳策略下的一个状态的价值必须等于在这个状态下采取最好动作得到的回报的期望。 + +**当 MDP 满足 Bellman optimality equation 的时候,整个 MDP 已经到达最佳的状态。**它到达最佳状态过后,对于这个 Q 函数,取它最大的 action 的那个值,就是直接等于它的最佳的 value function。只有当整个状态已经收敛过后,得到一个最佳的 policy 的时候,这个条件才是满足的。 + +![](img/2.49.png) + +最佳的价值函数到达过后,这个 Bellman optimlity equation 就会满足。 + +满足过后,就有这个 max 操作,如第一个等式所示: +$$ +v^{*}(s)=\max _{a} q^{*}(s, a) +$$ +当我们取最大的这个 action 的时候对应的值就是当前状态的最佳的价值函数。 + +另外,我们给出第二个等式,即 Q 函数的 Bellman equation: +$$ +q^{*}(s, a)=R(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) v^{*}\left(s^{\prime}\right) +$$ +**我们可以把第一个等式插入到第二个等式里面去**,如下式所示: +$$ +\begin{aligned} +q^{*}(s, a)&=R(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) v^{*}\left(s^{\prime}\right) \\ +&=R(s,a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) \max _{a} q^{*}(s', a') +\end{aligned} +$$ +我们就会得到 Q 函数之间的转移。它下一步这个状态,取了 max 这个值过后,就会跟它最佳的这个状态等价。 + +Q-learning 是基于 Bellman Optimality Equation 来进行的,当取它最大的这个状态的时候( $\underset{a'}{\max} q^{*}\left(s^{\prime}, a^{\prime}\right)$ ),它会满足下面这个等式: +$$ +q^{*}(s, a)=R(s, a)+\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) \max _{a^{\prime}} q^{*}\left(s^{\prime}, a^{\prime}\right) +$$ + +我们还可以把第二个等式插入到第一个等式,如下式所示: +$$ +\begin{aligned} +v^{*}(s)&=\max _{a} q^{*}(s, a) \\ +&=\max_{a} \mathbb{E}[G_t|s_t=s,a_t=a]\\ +&=\max_{a}\mathbb{E}[R_{t+1}+\gamma G_{t+1}|s_t=s,a_t=a]\\ +&=\max_{a}\mathbb{E}[R_{t+1}+\gamma v^*(s_{t+1})|s_t=s,a_t=a]\\ +&=\max_{a}\mathbb{E}[R_{t+1}]+ \max_a \mathbb{E}[\gamma v^*(s_{t+1})|s_t=s,a_t=a]\\ +&=\max_{a} R(s,a) + \max_a\gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) v^{*}\left(s^{\prime}\right)\\ +&=\max_{a} \left(R(s,a) + \gamma \sum_{s^{\prime} \in S} P\left(s^{\prime} \mid s, a\right) v^{*}\left(s^{\prime}\right)\right) +\end{aligned} +$$ +我们就会得到状态-价值函数的一个转移。 + +### Value Iteration + +#### Principle of Optimality + +我们从另一个角度思考问题,动态规划的方法将优化问题分成两个部分: + +* 第一步执行的是最优的 action; +* 之后后继的状态每一步都按照最优的 policy 去做,那么我最后的结果就是最优的。 + +**Principle of Optimality Theorem**: + +一个 policy $\pi(s|a)$ 在状态 $s$ 达到了最优价值,也就是 $v^{\pi}(s) = v^{*}(s)$ 成立,当且仅当: + +对于**任何**能够从 $s$ 到达的 $s'$,都已经达到了最优价值,也就是,对于所有的 $s'$,$v^{\pi}(s') = v^{*}(s')$ 恒成立。 + +#### Deterministic Value Iteration + +![](img/2.50.png) + + + +**Value iteration 就是把 Bellman Optimality Equation 当成一个 update rule 来进行,**如下式所示: +$$ +v(s) \leftarrow \max _{a \in \mathcal{A}}\left(R(s, a)+\gamma \sum_{s^{\prime} \in \mathcal{S}} P\left(s^{\prime} \mid s, a\right) v\left(s^{\prime}\right)\right) +$$ +之前我们说上面这个等式只有当整个 MDP 已经到达最佳的状态时才满足。但这里可以把它转换成一个 backup 的等式。Backup 就是说一个迭代的等式。**我们不停地去迭代 Bellman Optimality Equation,到了最后,它能逐渐趋向于最佳的策略,这是 value iteration 算法的精髓。** + +为了得到最佳的 $v^*$ ,对于每个状态的 $v^*$,我们直接把这个 Bellman Optimality Equation 进行迭代,迭代了很多次之后,它就会收敛。 + +![](img/2.51.png) + +* 我们使用 value iteration 算法是为了得到一个最佳的策略。 +* 解法:我们可以直接把 `Bellman Optimality backup` 这个等式拿进来进行迭代,迭代很多次,收敛过后得到的那个值就是它的最佳的值。 +* 这个算法开始的时候,它是先把所有值初始化,通过每一个状态,然后它会进行这个迭代。把等式 (22) 插到等式 (23) 里面,就是 Bellman optimality backup 的那个等式。有了等式 (22) 和等式 (23) 过后,然后进行不停地迭代,迭代过后,然后收敛,收敛后就会得到这个 $v^*$ 。当我们有了 $v^*$ 过后,一个问题是如何进一步推算出它的最佳策略。 +* 提取最佳策略的话,我们可以直接用 arg max。就先把它的 Q 函数重构出来,重构出来过后,每一个列对应的最大的那个 action 就是它现在的最佳策略。这样就可以从最佳价值函数里面提取出最佳策略。 +* 我们只是在解决一个 planning 的问题,而不是强化学习的问题,因为我们知道环境如何变化。 + +![](img/2.52.png) + +* value function 做的工作类似于 value 的反向传播,每次迭代做一步传播,所以中间过程的 policy 和 value function 是没有意义的。不像是 policy iteration,它每一次迭代的结果都是有意义的,都是一个完整的 policy。 +* 上图是一个可视化的过程,在一个 gridworld 中,我们设定了一个终点(goal),也就是左上角的点。不管你在哪一个位置开始,我们都希望能够到终点(实际上这个终点是在迭代过程中不必要的,只是为了更好的演示)。Value iteration 的迭代过程像是一个从某一个状态(这里是我们的 goal)反向传播其他各个状态的过程。因为每次迭代只能影响到与之直接相关的状态。 +* 让我们回忆下 `Principle of Optimality Theorem`:当你这次迭代求解的某个状态 s 的 value function $v_{k+1}(s)$ 是最优解,它的前提是能够从该状态到达的所有状态 s' 此时都已经得到了最优解;如果不是的话,它做的事情只是一个类似传递 value function 的过程。 +* 以上图为例,实际上,对于每一个状态,我们都可以看成一个终点。迭代由每一个终点开始,每次都根据 Bellman optimality equation 重新计算 value。如果它的相邻节点 value 发生变化,变得更好,那么它也会变得更好,一直到相邻节点都不变了。因此,**在我们迭代到** $v_7$ **之前,也就是还没将每个终点的最优的 value 传递给其他的所有状态之前,中间的几个 value function 只是一种暂存的不完整的数据,它不能代表每一个 state 的 value function,所以生成的 policy 是一个没有意义的 policy**。 +* 因为它是一个迭代过程,这里可视化了从 $v_1$ 到 $v_7$ 每一个状态的值的变化,它的这个值逐渐在变化。而且因为它每走一步,就会得到一个负的值,所以它需要尽快地到达左上角,可以发现离它越远的,那个值就越小。 +* $v_7$ 收敛过后,右下角那个值是 -6,相当于它要走六步,才能到达最上面那个值。而且离目的地越近,它的价值越大。 + +### Difference between Policy Iteration and Value Iteration + +![](img/2.53.png) + +![](img/2.54.png ':size=550') + +**我们来看一个 MDP control 的 Demo。** + +* 首先来看 policy iteration。之前的例子在每个状态都是采取固定的随机策略,就每个状态都是 0.25 的概率往上往下往左往右,没有策略的改变。 +* 但是我们现在想做 policy iteration,就是每个状态的策略都进行改变。Policy iteration 的过程是一个迭代过程。 + +![](img/2.55.png ':size=550') + +我们先在这个状态里面 run 一遍 policy evaluation,就得到了一个 value function,每个状态都有一个 value function。 + +![](img/2.56.png ':size=550') + +* **现在进行 policy improvement,点一下 policy update。**点一下 policy update 过后,你可以发现有些格子里面的 policy 已经产生变化。 +* 比如说对于中间这个 -1 的这个状态,它的最佳策略是往下走。当你到达这个状态后,你应该往下,这样就会得到最佳的这个值。 +* 绿色右边的这个方块的策略也改变了,它现在选取的最佳策略是往左走,也就是说在这个状态的时候,最佳策略应该是往左走。 + +![](img/2.57.png ':size=550') + +我们再 run 下一轮的 policy evaluation,你发现它的值又被改变了,很多次过后,它会收敛。 + +![](img/2.58.png ':size=550') + +我们再 run policy update,你发现每个状态里面的值基本都改变,它不再是上下左右随机在变了,它会选取一个最佳的策略。 + +![](img/2.59.png ':size=550') + +我们再 run 这个 policy evaluation,它的值又在不停地变化,变化之后又收敛了。 + +![](img/2.60.png ':size=550') + + +我们再来 run 一遍 policy update。现在它的值又会有变化,就在每一个状态,它的这个最佳策略也会产生一些改变。 + +![](img/2.61.png ':size=550') + +再来在这个状态下面进行改变,现在你看基本没有什么变化,就说明整个 MDP 已经收敛了。所以现在它每个状态的值就是它当前最佳的 value function 的值以及它当前状态对应的这个 policy 就是最佳的 policy。 + +比如说现在我们在右上角 0.38 的这个位置,然后它说现在应该往下走,我们往下走一步。它又说往下走,然后再往下走。现在我们有两个选择:往左走和往下走。我们现在往下走,随着这个箭头的指示,我们就会到达中间 1.20 的一个状态。如果能达到这个状态,我们就会得到很多 reward 。 + +这个 Demo 说明了 policy iteration 可以把 gridworld 解决掉。解决掉的意思是说,不管在哪个状态,都可以顺着状态对应的最佳的策略来到达可以获得最多奖励的一个状态。 + +![](img/2.62.png ':size=550') + +**我们再用 value iteration 来解 MDP,点 Toggle value iteration。** + +* 当它的这个值确定下来过后,它会产生它的最佳状态,这个最佳状态提取的策略跟 policy iteration 得出来的最佳策略是一致的。 +* 在每个状态,我们跟着这个最佳策略走,就会到达可以得到最多奖励的一个状态。 + +我们给出一个[ Demo](https://github.com/cuhkrlcourse/RLexample/tree/master/MDP),这个 Demo 是为了解一个叫 `FrozenLake` 的例子,这个例子是 OpenAI Gym 里的一个环境,跟 gridworld 很像,不过它每一个状态转移是一个概率。 + +**我们再来对比下 policy iteration 和 value iteration,这两个算法都可以解 MDP 的控制问题。** + +* Policy Iteration 分两步,首先进行 policy evaluation,即对当前已经搜索到的策略函数进行一个估值。得到估值过后,进行 policy improvement,即把 Q 函数算出来,我们进一步进行改进。不断重复这两步,直到策略收敛。 +* Value iteration 直接把 Bellman Optimality Equation 拿进来,然后去寻找最佳的 value function,没有 policy function 在这里面。当算出 optimal value function 过后,我们再来提取最佳策略。 + +### Summary for Prediction and Control in MDP + +![](img/2.65.png) + +总结如上表所示,就对于 MDP 里面的 prediction 和 control 都是用动态规划来解,我们其实采取了不同的 Bellman Equation。 + +* 如果是一个 prediction 的问题,即 policy evaluation 的问题,直接就是不停地 run 这个 Bellman Expectation Equation,这样我们就可以去估计出给定的这个策略,然后得到价值函数。 +* 对于 control, + * 如果采取的算法是 policy iteration,那这里用的是 Bellman Expectation Equation 。把它分成两步,先上它的这个价值函数,再去优化它的策略,然后不停迭代。这里用到的只是 Bellman Expectation Equation。 + * 如果采取的算法是 value iteration,那这里用到的 Bellman Equation 就是 Bellman Optimality Equation,通过 arg max 这个过程,不停地去 arg max 它,最后它就会达到最优的状态。 + +## References + +* [强化学习基础 David Silver 笔记](https://zhuanlan.zhihu.com/c_135909947) +* [Reinforcement Learning: An Introduction (second edition)](https://book.douban.com/subject/30323890/) +* [David Silver 强化学习公开课中文讲解及实践](https://zhuanlan.zhihu.com/reinforce) +* [UCL Course on RL(David Silver)](https://www.davidsilver.uk/teaching/) +* [Derivation of Bellman’s Equation](https://jmichaux.github.io/_notebook/2018-10-14-bellman/) +* [深入浅出强化学习:原理入门](https://book.douban.com/subject/27624485//) + diff --git a/docs/chapter2/chapter2_questions&keywords.md b/docs/chapter2/chapter2_questions&keywords.md new file mode 100644 index 0000000..236008e --- /dev/null +++ b/docs/chapter2/chapter2_questions&keywords.md @@ -0,0 +1,102 @@ +# Chapter2 马尔可夫决策过程(MDP) + +## 1 Keywords + +- **马尔可夫性质(Markov Property):** 如果某一个过程未来的转移跟过去是无关,只由现在的状态决定,那么其满足马尔可夫性质。换句话说,一个状态的下一个状态只取决于它当前状态,而跟它当前状态之前的状态都没有关系。 +- **马尔可夫链(Markov Chain):** 概率论和数理统计中具有马尔可夫性质(Markov property)且存在于离散的指数集(index set)和状态空间(state space)内的随机过程(stochastic process)。 +- **状态转移矩阵(State Transition Matrix):** 状态转移矩阵类似于一个 conditional probability,当我们知道当前我们在 $s_t$ 这个状态过后,到达下面所有状态的一个概念,它每一行其实描述了是从一个节点到达所有其它节点的概率。 +- **马尔可夫奖励过程(Markov Reward Process, MRP):** 即马尔可夫链再加上了一个奖励函数。在 MRP之中,转移矩阵跟它的这个状态都是跟马尔可夫链一样的,多了一个奖励函数(reward function)。奖励函数是一个期望,它说当你到达某一个状态的时候,可以获得多大的奖励。 +- **horizon:** 定义了同一个 episode 或者是整个一个轨迹的长度,它是由有限个步数决定的。 +- **return:** 把奖励进行折扣(discounted),然后获得的对应的收益。 +- **Bellman Equation(贝尔曼等式):** 定义了当前状态与未来状态的迭代关系,表示当前状态的值函数可以通过下个状态的值函数来计算。Bellman Equation 因其提出者、动态规划创始人 Richard Bellman 而得名 ,同时也被叫作“动态规划方程”。$V(s)=R(S)+ \gamma \sum_{s' \in S}P(s'|s)V(s')$ ,特别地,矩阵形式:$V=R+\gamma PV$。 +- **Monte Carlo Algorithm(蒙特卡罗方法):** 可用来计算价值函数的值。通俗的讲,我们当得到一个MRP过后,我们可以从某一个状态开始,然后让它让把这个小船放进去,让它随波逐流,这样就会产生一个轨迹。产生了一个轨迹过后,就会得到一个奖励,那么就直接把它的 Discounted 的奖励 $g$ 直接算出来。算出来过后就可以把它积累起来,当积累到一定的轨迹数量过后,然后直接除以这个轨迹,然后就会得到它的这个价值。 +- **Iterative Algorithm(动态规划方法):** 可用来计算价值函数的值。通过一直迭代对应的Bellman Equation,最后使其收敛。当这个最后更新的状态跟你上一个状态变化并不大的时候,这个更新就可以停止。 +- **Q函数 (action-value function):** 其定义的是某一个状态某一个行为,对应的它有可能得到的 return 的一个期望(over policy function)。 +- **MDP中的prediction(即policy evaluation问题):** 给定一个 MDP 以及一个 policy $\pi$ ,去计算它的 value function,即每个状态它的价值函数是多少。其可以通过动态规划方法(Iterative Algorithm)解决。 +- **MDP中的control问题:** 寻找一个最佳的一个策略,它的 input 就是MDP,输出是通过去寻找它的最佳策略,然后同时输出它的最佳价值函数(optimal value function)以及它的这个最佳策略(optimal policy)。其可以通过动态规划方法(Iterative Algorithm)解决。 +- **最佳价值函数(Optimal Value Function):** 我们去搜索一种 policy $\pi$ ,然后我们会得到每个状态它的状态值最大的一个情况,$v^*$ 就是到达每一个状态,它的值的极大化情况。在这种极大化情况上面,我们得到的策略就可以说它是最佳策略(optimal policy)。optimal policy 使得每个状态,它的状态函数都取得最大值。所以当我们说某一个 MDP 的环境被解了过后,就是说我们可以得到一个 optimal value function,然后我们就说它被解了。 + +## 2 Questions + +- 为什么在马尔可夫奖励过程(MRP)中需要有**discount factor**? + + 答: + + 1. 首先,是有些马尔可夫过程是**带环**的,它并没有终结,然后我们想**避免这个无穷的奖励**; + 2. 另外,我们是想把这个**不确定性**也表示出来,希望**尽可能快**地得到奖励,而不是在未来某一个点得到奖励; + 3. 接上面一点,如果这个奖励是它是有实际价值的了,我们可能是更希望立刻就得到奖励,而不是我们后面再得到奖励。 + 4. 还有在有些时候,这个系数也可以把它设为 0。比如说,当我们设为 0 过后,然后我们就只关注了它当前的奖励。我们也可以把它设为 1,设为 1 的话就是对未来并没有折扣,未来获得的奖励跟我们当前获得的奖励是一样的。 + + 所以,这个系数其实是应该可以作为强化学习 agent 的一个 hyperparameter 来进行调整,然后就会得到不同行为的 agent。 + +- 为什么矩阵形式的Bellman Equation的解析解比较难解? + + 答:通过矩阵求逆的过程,就可以把这个 V 的这个价值的解析解直接求出来。但是一个问题是这个矩阵求逆的过程的复杂度是 $O(N^3)$。所以就当我们状态非常多的时候,比如说从我们现在十个状态到一千个状态,到一百万个状态。那么当我们有一百万个状态的时候,这个转移矩阵就会是个一百万乘以一百万的一个矩阵。这样一个大矩阵的话求逆是非常困难的,所以这种通过解析解去解,只能对于很小量的MRP。 + +- 计算贝尔曼等式(Bellman Equation)的常见方法以及区别? + + 答: + + 1. **Monte Carlo Algorithm(蒙特卡罗方法):** 可用来计算价值函数的值。通俗的讲,我们当得到一个MRP过后,我们可以从某一个状态开始,然后让它让把这个小船放进去,让它随波逐流,这样就会产生一个轨迹。产生了一个轨迹过后,就会得到一个奖励,那么就直接把它的 Discounted 的奖励 $g$ 直接算出来。算出来过后就可以把它积累起来,当积累到一定的轨迹数量过后,然后直接除以这个轨迹,然后就会得到它的这个价值。 + 2. **Iterative Algorithm(动态规划方法):** 可用来计算价值函数的值。通过一直迭代对应的Bellman Equation,最后使其收敛。当这个最后更新的状态跟你上一个状态变化并不大的时候,通常是小于一个阈值 $\gamma$ ,这个更新就可以停止。 + 3. **以上两者的结合方法:** 另外我们也可以通过 Temporal-Difference Learning 的那个办法。这个 `Temporal-Difference Learning` 叫 `TD Leanring`,就是动态规划和蒙特卡罗的一个结合。 + +- 马尔可夫奖励过程(MRP)与马尔可夫决策过程 (MDP)的区别? + + 答:相对于 MRP,马尔可夫决策过程(Markov Decision Process)多了一个 decision,其它的定义跟 MRP 都是类似的。这里我们多了一个决策,多了一个 action ,那么这个状态转移也多了一个 condition,就是采取某一种行为,然后你未来的状态会不同。它不仅是依赖于你当前的状态,也依赖于在当前状态你这个 agent 它采取的这个行为会决定它未来的这个状态走向。对于这个价值函数,它也是多了一个条件,多了一个你当前的这个行为,就是说你当前的状态以及你采取的行为会决定你在当前可能得到的奖励多少。 + + 另外,两者之间是有转换关系的。具体来说,已知一个 MDP 以及一个 policy $\pi$ 的时候,我们可以把 MDP 转换成MRP。在 MDP 里面,转移函数 $P(s'|s,a)$ 是基于它当前状态以及它当前的 action,因为我们现在已知它 policy function,就是说在每一个状态,我们知道它可能采取的行为的概率,那么就可以直接把这个 action 进行加和,那我们就可以得到对于 MRP 的一个转移,这里就没有 action。同样地,对于奖励,我们也可以把 action 拿掉,这样就会得到一个类似于 MRP 的奖励。 + +- MDP 里面的状态转移跟 MRP 以及 MP 的结构或者计算方面的差异? + + 答: + + - 对于之前的马尔可夫链的过程,它的转移是直接就决定,就从你当前是 s,那么就直接通过这个转移概率就直接决定了你下一个状态会是什么。 + - 但是对于 MDP,它的中间多了一层这个行为 a ,就是说在你当前这个状态的时候,你首先要决定的是采取某一种行为。然后因为你有一定的不确定性,当你当前状态决定你当前采取的行为过后,你到未来的状态其实也是一个概率分布。所以你采取行为以及你决定,然后你可能有有多大的概率到达某一个未来状态,以及另外有多大概率到达另外一个状态。所以在这个当前状态跟未来状态转移过程中这里多了一层决策性,这是MDP跟之前的马尔可夫过程很不同的一个地方。在马尔科夫决策过程中,行为是由 agent 决定,所以多了一个 component,agent 会采取行为来决定未来的状态转移。 + +- 我们如何寻找最佳的policy,方法有哪些? + + 答:本质来说,当我们取得最佳的价值函数过后,我们可以通过对这个 Q 函数进行极大化,然后得到最佳的价值。然后,我们直接在这个Q函数上面取一个让这个action最大化的值,然后我们就可以直接提取出它的最佳的policy。 + + 具体方法: + + 1. **穷举法(一般不使用):**假设我们有有限多个状态、有限多个行为可能性,那么每个状态我们可以采取这个 A 种行为的策略,那么总共就是 $|A|^{|S|}$ 个可能的 policy。我们可以把这个穷举一遍,然后算出每种策略的 value function,然后对比一下可以得到最佳策略。但是效率极低。 + 2. **Policy iteration:** 一种迭代方法,有两部分组成,下面两个步骤一直在迭代进行,最终收敛:(有些类似于ML中EM算法(期望-最大化算法)) + - 第一个步骤是 **policy evaluation** ,即当前我们在优化这个 policy $\pi$ ,所以在优化过程中得到一个最新的这个 policy 。 + - 第二个步骤是 **policy improvement** ,即取得价值函数后,进一步推算出它的 Q 函数。得到 Q 函数过后,那我们就直接去取它的极大化。 + 3. **Value iteration:** 我们一直去迭代 Bellman Optimality Equation,到了最后,它能逐渐趋向于最佳的策略,这是 value iteration 算法的精髓,就是我们去为了得到最佳的 $v^*$ ,对于每个状态它的 $v^*$ 这个值,我们直接把这个 Bellman Optimality Equation 进行迭代,迭代了很多次之后它就会收敛到最佳的policy以及其对应的状态,这里面是没有policy function的。 + + +## 3 Something About Interview + +- 高冷的面试官: 请问马尔可夫过程是什么?马尔可夫决策过程又是什么?其中马尔可夫最重要的性质是什么呢? + + 答: 马尔可夫过程是是一个二元组 $ $ ,S为状态的集合,P为状态转移概率矩阵; + 而马尔可夫决策过程是一个五元组 $ $,其中 $R$ 表示为从 $S$ 到 $S'$ 能够获得的奖励期望, $\gamma$为折扣因子, $A$ 为动作集合. + 马尔可夫最重要的性质是下一个状态只与当前状态有关,与之前的状态无关,也就是 $P[S_{t+1} | S_t] = P[S_{t+1}|S_1,S_2,...,S_t]$ + +- 高冷的面试官: 请问我们一般怎么求解马尔可夫决策过程? + + 答: 我们直接求解马尔可夫决策过程可以直接求解贝尔曼等式(动态规划方程),即$V(s)=R(S)+ \gamma \sum_{s' \in S}P(s'|s)V(s')$ ,特别地,矩阵形式:$V=R+\gamma PV$.但是贝尔曼等式很难求解且计算复杂度较高,所以可以使用动态规划,蒙特卡洛,时间差分等方法求解. + +- 高冷的面试官: 请问如果数据流不满足马尔科夫性怎么办?应该如何处理? + + 答: 如果不满足马尔科夫性,即下一个状态与之前的状态也有关,若还仅仅用当前的状态来进行求解决策过程,势必导致决策的泛化能力变差。 为了解决这个问题,可以利用RNN对历史信息建模,获得包含历史信息的状态表征。表征过程可以 使用注意力机制等手段。最后在表征状态空间求解马尔可夫决策过程问题。 + +- 高冷的面试官: 请分别写出基于状态值函数的贝尔曼方程以及基于动作值的贝尔曼方程. + + 答: + + - 基于状态值函数的贝尔曼方程: $v_{\pi}(s) = \sum_{a}{\pi(a|s)}\sum_{s',r}{p(s',r|s,a)[r(s,a)+\gamma v_{\pi}(s')]}$ + - 基于动作值的贝尔曼方程: $q_{\pi}(s,a)=\sum_{s',r}p(s',r|s,a)[r(s',a)+\gamma v_{\pi}(s')]$ + +- 高冷的面试官: 请问最佳价值函数(optimal value function) $v^*$ 和最佳策略(optimal policy) $\pi^*$ 为什么等价呢? + + 答: 最佳价值函数的定义为: $v^* (s)=\max_{\pi} v^{\pi}(s)$ 即我们去搜索一种 policy $\pi$ 来让每个状态的价值最大。$v^*$ 就是到达每一个状态,它的值的极大化情况。在这种极大化情况上面,我们得到的策略就可以说它是最佳策略(optimal policy),即 $ \pi^{*}(s)=\underset{\pi}{\arg \max }~ v^{\pi}(s) $. Optimal policy 使得每个状态的价值函数都取得最大值。所以如果我们可以得到一个 optimal value function,就可以说某一个 MDP 的环境被解。在这种情况下,它的最佳的价值函数是一致的,就它达到的这个上限的值是一致的,但这里可能有多个最佳的 policy,就是说多个 policy 可以取得相同的最佳价值。 + +- 高冷的面试官:能不能手写一下第n步的值函数更新公式呀?另外,当n越来越大时,值函数的期望和方差分别变大还是变小呢? + + 答:$n$越大,方差越大,期望偏差越小。值函数的更新公式? 话不多说,公式如下: + $$ + Q\left(S, A\right) \leftarrow Q\left(S, A\right)+\alpha\left[\sum_{i=1}^{n} \gamma^{i-1} R_{t+i}+\gamma^{n} \max _{a} Q\left(S',a\right)-Q\left(S, A\right)\right] + $$ \ No newline at end of file diff --git a/docs/chapter2/img/2.1.png b/docs/chapter2/img/2.1.png new file mode 100644 index 0000000..d7ca24c Binary files /dev/null and b/docs/chapter2/img/2.1.png differ diff --git a/docs/chapter2/img/2.10.png b/docs/chapter2/img/2.10.png new file mode 100644 index 0000000..d4aaf67 Binary files /dev/null and b/docs/chapter2/img/2.10.png differ diff --git a/docs/chapter2/img/2.11.png b/docs/chapter2/img/2.11.png new file mode 100644 index 0000000..cb1ef21 Binary files /dev/null and b/docs/chapter2/img/2.11.png differ diff --git a/docs/chapter2/img/2.12.png b/docs/chapter2/img/2.12.png new file mode 100644 index 0000000..d2279d8 Binary files /dev/null and b/docs/chapter2/img/2.12.png differ diff --git a/docs/chapter2/img/2.13.png b/docs/chapter2/img/2.13.png new file mode 100644 index 0000000..5d6d52e Binary files /dev/null and b/docs/chapter2/img/2.13.png differ diff --git a/docs/chapter2/img/2.14.png b/docs/chapter2/img/2.14.png new file mode 100644 index 0000000..58072bb Binary files /dev/null and b/docs/chapter2/img/2.14.png differ diff --git a/docs/chapter2/img/2.15.png b/docs/chapter2/img/2.15.png new file mode 100644 index 0000000..38f27af Binary files /dev/null and b/docs/chapter2/img/2.15.png differ diff --git a/docs/chapter2/img/2.16.png b/docs/chapter2/img/2.16.png new file mode 100644 index 0000000..0b80fbf Binary files /dev/null and b/docs/chapter2/img/2.16.png differ diff --git a/docs/chapter2/img/2.17.png b/docs/chapter2/img/2.17.png new file mode 100644 index 0000000..2d6d83d Binary files /dev/null and b/docs/chapter2/img/2.17.png differ diff --git a/docs/chapter2/img/2.18.png b/docs/chapter2/img/2.18.png new file mode 100644 index 0000000..eb28dcb Binary files /dev/null and b/docs/chapter2/img/2.18.png differ diff --git a/docs/chapter2/img/2.19.png b/docs/chapter2/img/2.19.png new file mode 100644 index 0000000..640080c Binary files /dev/null and b/docs/chapter2/img/2.19.png differ diff --git a/docs/chapter2/img/2.2.png b/docs/chapter2/img/2.2.png new file mode 100644 index 0000000..53412f5 Binary files /dev/null and b/docs/chapter2/img/2.2.png differ diff --git a/docs/chapter2/img/2.20.png b/docs/chapter2/img/2.20.png new file mode 100644 index 0000000..b8141b5 Binary files /dev/null and b/docs/chapter2/img/2.20.png differ diff --git a/docs/chapter2/img/2.21.png b/docs/chapter2/img/2.21.png new file mode 100644 index 0000000..ca59dc3 Binary files /dev/null and b/docs/chapter2/img/2.21.png differ diff --git a/docs/chapter2/img/2.22.png b/docs/chapter2/img/2.22.png new file mode 100644 index 0000000..4714825 Binary files /dev/null and b/docs/chapter2/img/2.22.png differ diff --git a/docs/chapter2/img/2.23.png b/docs/chapter2/img/2.23.png new file mode 100644 index 0000000..cb960ca Binary files /dev/null and b/docs/chapter2/img/2.23.png differ diff --git a/docs/chapter2/img/2.24.png b/docs/chapter2/img/2.24.png new file mode 100644 index 0000000..0ee4bc1 Binary files /dev/null and b/docs/chapter2/img/2.24.png differ diff --git a/docs/chapter2/img/2.25.png b/docs/chapter2/img/2.25.png new file mode 100644 index 0000000..7a9be80 Binary files /dev/null and b/docs/chapter2/img/2.25.png differ diff --git a/docs/chapter2/img/2.26.png b/docs/chapter2/img/2.26.png new file mode 100644 index 0000000..a6526fc Binary files /dev/null and b/docs/chapter2/img/2.26.png differ diff --git a/docs/chapter2/img/2.27.png b/docs/chapter2/img/2.27.png new file mode 100644 index 0000000..86805c6 Binary files /dev/null and b/docs/chapter2/img/2.27.png differ diff --git a/docs/chapter2/img/2.28.png b/docs/chapter2/img/2.28.png new file mode 100644 index 0000000..a0d4329 Binary files /dev/null and b/docs/chapter2/img/2.28.png differ diff --git a/docs/chapter2/img/2.29.png b/docs/chapter2/img/2.29.png new file mode 100644 index 0000000..c6bda08 Binary files /dev/null and b/docs/chapter2/img/2.29.png differ diff --git a/docs/chapter2/img/2.3.png b/docs/chapter2/img/2.3.png new file mode 100644 index 0000000..6082bed Binary files /dev/null and b/docs/chapter2/img/2.3.png differ diff --git a/docs/chapter2/img/2.30.png b/docs/chapter2/img/2.30.png new file mode 100644 index 0000000..9dcb8a1 Binary files /dev/null and b/docs/chapter2/img/2.30.png differ diff --git a/docs/chapter2/img/2.31.png b/docs/chapter2/img/2.31.png new file mode 100644 index 0000000..eccc7a2 Binary files /dev/null and b/docs/chapter2/img/2.31.png differ diff --git a/docs/chapter2/img/2.32.png b/docs/chapter2/img/2.32.png new file mode 100644 index 0000000..e4bef9e Binary files /dev/null and b/docs/chapter2/img/2.32.png differ diff --git a/docs/chapter2/img/2.33.png b/docs/chapter2/img/2.33.png new file mode 100644 index 0000000..fda21ff Binary files /dev/null and b/docs/chapter2/img/2.33.png differ diff --git a/docs/chapter2/img/2.34.png b/docs/chapter2/img/2.34.png new file mode 100644 index 0000000..68328be Binary files /dev/null and b/docs/chapter2/img/2.34.png differ diff --git a/docs/chapter2/img/2.35.png b/docs/chapter2/img/2.35.png new file mode 100644 index 0000000..6bc5da2 Binary files /dev/null and b/docs/chapter2/img/2.35.png differ diff --git a/docs/chapter2/img/2.36.png b/docs/chapter2/img/2.36.png new file mode 100644 index 0000000..df03632 Binary files /dev/null and b/docs/chapter2/img/2.36.png differ diff --git a/docs/chapter2/img/2.37.png b/docs/chapter2/img/2.37.png new file mode 100644 index 0000000..fa3ff66 Binary files /dev/null and b/docs/chapter2/img/2.37.png differ diff --git a/docs/chapter2/img/2.38.png b/docs/chapter2/img/2.38.png new file mode 100644 index 0000000..f5e745e Binary files /dev/null and b/docs/chapter2/img/2.38.png differ diff --git a/docs/chapter2/img/2.39.png b/docs/chapter2/img/2.39.png new file mode 100644 index 0000000..baab26d Binary files /dev/null and b/docs/chapter2/img/2.39.png differ diff --git a/docs/chapter2/img/2.4.png b/docs/chapter2/img/2.4.png new file mode 100644 index 0000000..11a4acd Binary files /dev/null and b/docs/chapter2/img/2.4.png differ diff --git a/docs/chapter2/img/2.40.png b/docs/chapter2/img/2.40.png new file mode 100644 index 0000000..ff551e2 Binary files /dev/null and b/docs/chapter2/img/2.40.png differ diff --git a/docs/chapter2/img/2.41.png b/docs/chapter2/img/2.41.png new file mode 100644 index 0000000..cc8be84 Binary files /dev/null and b/docs/chapter2/img/2.41.png differ diff --git a/docs/chapter2/img/2.42.png b/docs/chapter2/img/2.42.png new file mode 100644 index 0000000..b9c678b Binary files /dev/null and b/docs/chapter2/img/2.42.png differ diff --git a/docs/chapter2/img/2.43.png b/docs/chapter2/img/2.43.png new file mode 100644 index 0000000..a1b445b Binary files /dev/null and b/docs/chapter2/img/2.43.png differ diff --git a/docs/chapter2/img/2.44.png b/docs/chapter2/img/2.44.png new file mode 100644 index 0000000..4a176d6 Binary files /dev/null and b/docs/chapter2/img/2.44.png differ diff --git a/docs/chapter2/img/2.45.png b/docs/chapter2/img/2.45.png new file mode 100644 index 0000000..e4b2145 Binary files /dev/null and b/docs/chapter2/img/2.45.png differ diff --git a/docs/chapter2/img/2.46.png b/docs/chapter2/img/2.46.png new file mode 100644 index 0000000..e365a9c Binary files /dev/null and b/docs/chapter2/img/2.46.png differ diff --git a/docs/chapter2/img/2.47.png b/docs/chapter2/img/2.47.png new file mode 100644 index 0000000..1378e2c Binary files /dev/null and b/docs/chapter2/img/2.47.png differ diff --git a/docs/chapter2/img/2.48.png b/docs/chapter2/img/2.48.png new file mode 100644 index 0000000..ac675b2 Binary files /dev/null and b/docs/chapter2/img/2.48.png differ diff --git a/docs/chapter2/img/2.49.png b/docs/chapter2/img/2.49.png new file mode 100644 index 0000000..04fe9b3 Binary files /dev/null and b/docs/chapter2/img/2.49.png differ diff --git a/docs/chapter2/img/2.5.png b/docs/chapter2/img/2.5.png new file mode 100644 index 0000000..9c375b3 Binary files /dev/null and b/docs/chapter2/img/2.5.png differ diff --git a/docs/chapter2/img/2.50.png b/docs/chapter2/img/2.50.png new file mode 100644 index 0000000..0c98189 Binary files /dev/null and b/docs/chapter2/img/2.50.png differ diff --git a/docs/chapter2/img/2.51.png b/docs/chapter2/img/2.51.png new file mode 100644 index 0000000..05fad9f Binary files /dev/null and b/docs/chapter2/img/2.51.png differ diff --git a/docs/chapter2/img/2.52.png b/docs/chapter2/img/2.52.png new file mode 100644 index 0000000..05e348d Binary files /dev/null and b/docs/chapter2/img/2.52.png differ diff --git a/docs/chapter2/img/2.53.png b/docs/chapter2/img/2.53.png new file mode 100644 index 0000000..2edc639 Binary files /dev/null and b/docs/chapter2/img/2.53.png differ diff --git a/docs/chapter2/img/2.54.png b/docs/chapter2/img/2.54.png new file mode 100644 index 0000000..ba8135b Binary files /dev/null and b/docs/chapter2/img/2.54.png differ diff --git a/docs/chapter2/img/2.55.png b/docs/chapter2/img/2.55.png new file mode 100644 index 0000000..cfbcbe7 Binary files /dev/null and b/docs/chapter2/img/2.55.png differ diff --git a/docs/chapter2/img/2.56.png b/docs/chapter2/img/2.56.png new file mode 100644 index 0000000..c72a0ce Binary files /dev/null and b/docs/chapter2/img/2.56.png differ diff --git a/docs/chapter2/img/2.57.png b/docs/chapter2/img/2.57.png new file mode 100644 index 0000000..9127088 Binary files /dev/null and b/docs/chapter2/img/2.57.png differ diff --git a/docs/chapter2/img/2.58.png b/docs/chapter2/img/2.58.png new file mode 100644 index 0000000..9f265e4 Binary files /dev/null and b/docs/chapter2/img/2.58.png differ diff --git a/docs/chapter2/img/2.59.png b/docs/chapter2/img/2.59.png new file mode 100644 index 0000000..9a6b53d Binary files /dev/null and b/docs/chapter2/img/2.59.png differ diff --git a/docs/chapter2/img/2.6.png b/docs/chapter2/img/2.6.png new file mode 100644 index 0000000..38706c0 Binary files /dev/null and b/docs/chapter2/img/2.6.png differ diff --git a/docs/chapter2/img/2.60.png b/docs/chapter2/img/2.60.png new file mode 100644 index 0000000..72bffe8 Binary files /dev/null and b/docs/chapter2/img/2.60.png differ diff --git a/docs/chapter2/img/2.61.png b/docs/chapter2/img/2.61.png new file mode 100644 index 0000000..e36091c Binary files /dev/null and b/docs/chapter2/img/2.61.png differ diff --git a/docs/chapter2/img/2.62.png b/docs/chapter2/img/2.62.png new file mode 100644 index 0000000..4d237e2 Binary files /dev/null and b/docs/chapter2/img/2.62.png differ diff --git a/docs/chapter2/img/2.63.png b/docs/chapter2/img/2.63.png new file mode 100644 index 0000000..72a8f5d Binary files /dev/null and b/docs/chapter2/img/2.63.png differ diff --git a/docs/chapter2/img/2.64.png b/docs/chapter2/img/2.64.png new file mode 100644 index 0000000..3a9eda9 Binary files /dev/null and b/docs/chapter2/img/2.64.png differ diff --git a/docs/chapter2/img/2.65.png b/docs/chapter2/img/2.65.png new file mode 100644 index 0000000..2b47190 Binary files /dev/null and b/docs/chapter2/img/2.65.png differ diff --git a/docs/chapter2/img/2.7.png b/docs/chapter2/img/2.7.png new file mode 100644 index 0000000..67fc79b Binary files /dev/null and b/docs/chapter2/img/2.7.png differ diff --git a/docs/chapter2/img/2.8.png b/docs/chapter2/img/2.8.png new file mode 100644 index 0000000..d59e958 Binary files /dev/null and b/docs/chapter2/img/2.8.png differ diff --git a/docs/chapter2/img/2.9.png b/docs/chapter2/img/2.9.png new file mode 100644 index 0000000..8dd6000 Binary files /dev/null and b/docs/chapter2/img/2.9.png differ diff --git a/docs/chapter2/img/control_example.png b/docs/chapter2/img/control_example.png new file mode 100644 index 0000000..edf9037 Binary files /dev/null and b/docs/chapter2/img/control_example.png differ diff --git a/docs/chapter2/img/prediction_example.png b/docs/chapter2/img/prediction_example.png new file mode 100644 index 0000000..bcd5d16 Binary files /dev/null and b/docs/chapter2/img/prediction_example.png differ diff --git a/docs/chapter2/img/q_function_backup.png b/docs/chapter2/img/q_function_backup.png new file mode 100644 index 0000000..b8f488d Binary files /dev/null and b/docs/chapter2/img/q_function_backup.png differ diff --git a/docs/chapter2/img/state_value_function_backup.png b/docs/chapter2/img/state_value_function_backup.png new file mode 100644 index 0000000..389d7f2 Binary files /dev/null and b/docs/chapter2/img/state_value_function_backup.png differ diff --git a/docs/chapter3/assets/Qlearning_1.png b/docs/chapter3/assets/Qlearning_1.png new file mode 100644 index 0000000..08e3bbc Binary files /dev/null and b/docs/chapter3/assets/Qlearning_1.png differ diff --git a/docs/chapter3/assets/cliffwalking_1.png b/docs/chapter3/assets/cliffwalking_1.png new file mode 100644 index 0000000..ae5b0f8 Binary files /dev/null and b/docs/chapter3/assets/cliffwalking_1.png differ diff --git a/docs/chapter3/assets/eval_rewards_curve_cn.png b/docs/chapter3/assets/eval_rewards_curve_cn.png new file mode 100644 index 0000000..766251e Binary files /dev/null and b/docs/chapter3/assets/eval_rewards_curve_cn.png differ diff --git a/docs/chapter3/assets/moving_average_rewards.png b/docs/chapter3/assets/moving_average_rewards.png new file mode 100644 index 0000000..2b28aa5 Binary files /dev/null and b/docs/chapter3/assets/moving_average_rewards.png differ diff --git a/docs/chapter3/assets/rewards.png b/docs/chapter3/assets/rewards.png new file mode 100644 index 0000000..b82663d Binary files /dev/null and b/docs/chapter3/assets/rewards.png differ diff --git a/docs/chapter3/assets/train_rewards_curve_cn.png b/docs/chapter3/assets/train_rewards_curve_cn.png new file mode 100644 index 0000000..e9f4b83 Binary files /dev/null and b/docs/chapter3/assets/train_rewards_curve_cn.png differ diff --git a/docs/chapter3/chapter3.md b/docs/chapter3/chapter3.md new file mode 100644 index 0000000..a2e85a0 --- /dev/null +++ b/docs/chapter3/chapter3.md @@ -0,0 +1,576 @@ +# Tabular Methods + +本章我们通过最简单的`表格型的方法(tabular methods)`来讲解如何使用 value-based 方法去求解强化学习。 + +## MDP + +![](img/3.1.png) + +**强化学习的三个重要的要素:状态、动作和奖励。**强化学习智能体跟环境是一步一步交互的,就是我先观察一下状态,然后再输入动作。再观察一下状态,再输出动作,拿到这些 reward 。它是一个跟时间相关的序列决策的问题。 + +举个例子,在 $t-1$ 时刻,我看到了熊对我招手,那我下意识的可能输出的动作就是赶紧跑路。熊看到了有人跑了,可能就觉得发现猎物,开始发动攻击。而在 $t$ 时刻的话,我如果选择装死的动作,可能熊咬了咬我,摔了几下就发现就觉得挺无趣的,可能会走开。这个时候,我再跑路的话可能就跑路成功了,就是这样子的一个序列决策的过程。 + +当然在输出每一个动作之前,你可以选择不同的动作。比如说在 $t$ 时刻,我选择跑路的时候,熊已经追上来了,如果说 $t$ 时刻,我没有选择装死,而我是选择跑路的话,这个时候熊已经追上了,那这个时候,其实我有两种情况转移到不同的状态去,就我有一定的概率可以逃跑成功,也有很大的概率我会逃跑失败。那我们就用状态转移概率 $p\left[s_{t+1}, r_{t} \mid s_{t}, a_{t}\right]$ 来表述说在 $s_t$ 的状态选择了 $a_t$ 的动作的时候,转移到 $s_{t+1}$ ,而且拿到 $r_t$ 的概率是多少。 + +这样子的一个状态转移概率是具有`马尔可夫性质`的(系统下一时刻的状态仅由当前时刻的状态决定,不依赖于以往任何状态)。因为这个状态转移概率,它是下一时刻的状态是取决于当前的状态,它和之前的 $s_{t-1}$ 和 $s_{t-2}$ 都没有什么关系。然后再加上这个过程也取决于智能体跟环境交互的这个 $a_t$ ,所以有一个决策的一个过程在里面。我们就称这样的一个过程为`马尔可夫决策过程(Markov Decision Process, MDP)`。 + +MDP 就是序列决策这样一个经典的表达方式。MDP 也是强化学习里面一个非常基本的学习框架。状态、动作、状态转移概率和奖励 $(S,A,P,R)$,这四个合集就构成了强化学习 MDP 的四元组,后面也可能会再加个衰减因子构成五元组。 + +### Model-based + + +![](img/3.2.png) + + + + +如上图所示,我们把这些可能的动作和可能的状态转移的关系画成一个树状图。它们之间的关系就是从 $s_t$ 到 $a_t$ ,再到 $s_{t+1}$ ,再到 $a_{t+1}$,再到 $s_{t+2}$ 这样子的一个过程。 + +我们去跟环境交互,只能走完整的一条通路。这里面产生了一系列的一个决策的过程,就是我们跟环境交互产生了一个经验。**我们会使用 `概率函数(probability function)`和 `奖励函数(reward function)`来去描述环境。**概率函数就是状态转移的概率,概率函数实际上反映的是环境的一个随机性。 + +当我们知道概率函数和奖励函数时,我们就说这个 MDP 是已知的,可以通过 policy iteration 和 value iteration 来找最佳的策略。 + +比如,在熊发怒的情况下,我如果选择装死,假设熊看到人装死就一定会走的话,我们就称在这里面的状态转移概率就是 100%。但如果说在熊发怒的情况下,我选择跑路而导致可能跑成功以及跑失败,出现这两种情况。那我们就可以用概率去表达一下说转移到其中一种情况的概率大概 10%,另外一种情况的概率大概是 90% 会跑失败。 + +**如果知道这些状态转移概率和奖励函数的话,我们就说这个环境是已知的,因为我们是用这两个函数去描述环境的。**如果是已知的话,我们其实可以用动态规划去计算说,如果要逃脱熊,那么能够逃脱熊概率最大的最优策略是什么。很多强化学习的经典算法都是 model-free 的,就是环境是未知的。 + +### Model-free + +![](img/3.3.png) +因为现实世界中人类第一次遇到熊之前,我们根本不知道能不能跑得过熊,所以刚刚那个 10%、90% 的概率也就是虚构出来的概率。熊到底在什么时候会往什么方向去转变的话,我们经常是不知道的。 + +**我们是处在一个未知的环境里的,也就是这一系列的决策的概率函数和奖励函数是未知的,这就是 model-based 跟 model-free 的一个最大的区别。** + +强化学习就是可以用来解决用完全未知的和随机的环境。强化学习要像人类一样去学习,人类学习的话就是一条路一条路地去尝试一下,先走一条路,看看结果到底是什么。多试几次,只要能活命的。我们可以慢慢地了解哪个状态会更好, + +* 我们用价值函数 $V(s)$ 来代表这个状态是好的还是坏的。 +* 用 Q 函数来判断说在什么状态下做什么动作能够拿到最大奖励,用 Q 函数来表示这个状态-动作值。 + +### Model-based vs. Model-free + +![](img/model_free_1.png) + +* Policy iteration 和 value iteration 都需要得到环境的转移和奖励函数,所以在这个过程中,agent 没有跟环境进行交互。 +* 在很多实际的问题中,MDP 的模型有可能是未知的,也有可能模型太大了,不能进行迭代的计算。比如 Atari 游戏、围棋、控制直升飞机、股票交易等问题,这些问题的状态转移太复杂了。 + +![](img/model_free_2.png) + +* 在这种情况下,我们使用 model-free 强化学习的方法来解。 +* Model-free 没有获取环境的状态转移和奖励函数,我们让 agent 跟环境进行交互,采集到很多的轨迹数据,agent 从轨迹中获取信息来改进策略,从而获得更多的奖励。 + +## Q-table + +![](img/3.4.png) + +接下来介绍下 Q 函数。在多次尝试和熊打交道之后,人类就可以对熊的不同的状态去做出判断,我们可以用状态动作价值来表达说在某个状态下,为什么动作 1 会比动作 2 好,因为动作 1 的价值比动作 2 要高,这个价值就叫 `Q 函数`。 + +**如果 `Q 表格`是一张已经训练好的表格的话,那这一张表格就像是一本生活手册。**我们就知道在熊发怒的时候,装死的价值会高一点。在熊离开的时候,我们可能偷偷逃跑的会比较容易获救。 + +这张表格里面 Q 函数的意义就是我选择了这个动作之后,最后面能不能成功,就是我需要去计算在这个状态下,我选择了这个动作,后续能够一共拿到多少总收益。如果可以预估未来的总收益的大小,我们当然知道在当前的这个状态下选择哪个动作,价值更高。我选择某个动作是因为我未来可以拿到的那个价值会更高一点。所以强化学习的目标导向性很强,环境给出的奖励是一个非常重要的反馈,它就是根据环境的奖励来去做选择。 + +![](img/3.5.png)Q: 为什么可以用未来的总收益来评价当前这个动作是好是坏? + +A: 举个例子,假设一辆车在路上,当前是红灯,我们直接走的收益就很低,因为违反交通规则,这就是当前的单步收益。可是如果我们这是一辆救护车,我们正在运送病人,把病人快速送达医院的收益非常的高,而且越快你的收益越大。在这种情况下,我们很可能应该要闯红灯,因为未来的远期收益太高了。这也是为什么强化学习需要去学习远期的收益,因为在现实世界中奖励往往是延迟的。所以我们一般会从当前状态开始,把后续有可能会收到所有收益加起来计算当前动作的 Q 的价值,让 Q 的价值可以真正地代表当前这个状态下,动作的真正的价值。 + +![](img/3.6.png) + +但有的时候把目光放得太长远不好,因为如果事情很快就结束的话,你考虑到最后一步的收益无可厚非。如果是一个持续的没有尽头的任务,即`持续式任务(Continuing Task)`,你把未来的收益全部相加,作为当前的状态价值就很不合理。 + +股票的例子就很典型了,我们要关注的是累积的收益。可是如果说十年之后才有一次大涨大跌,你显然不会把十年后的收益也作为当前动作的考虑因素。那我们会怎么办呢,有句俗话说得好,对远一点的东西,我们就当做近视,就不需要看得太清楚,我们可以引入这个衰减因子 $\gamma$ 来去计算这个未来总收益,$\gamma \in [0,1]$,越往后 $\gamma^n$ 就会越小,也就是说越后面的收益对当前价值的影响就会越小。 + +![](img/3.7.png) + + +举个例子来看看计算出来的是什么效果。这是一个悬崖问题,这个问题是需要智能体从出发点 S 出发,到达目的地 G,同时避免掉进悬崖(cliff),掉进悬崖的话就会有 -100 分的惩罚,但游戏不会结束,它会被直接拖回起点,游戏继续。为了到达目的地,我们可以沿着蓝线和红线走。 + +![](img/3.8.png) + +在这个环境当中,我们怎么去计算状态动作价值(未来的总收益)。 + +* 如果 $\gamma = 0$, 假设我走一条路,并从这个状态出发,在这里选择是向上,这里选择向右。如果 $\gamma = 0$,用这个公式去计算的话,它相当于考虑的就是一个单步的收益。我们可以认为它是一个目光短浅的计算的方法。 + +* 如果 $\gamma = 1$,那就等于是说把后续所有的收益都全部加起来。在这里悬崖问题,你每走一步都会拿到一个 -1 分的 reward,只有到了终点之后,它才会停止。如果 $\gamma =1 $ 的话,我们用这个公式去计算,就这里是 -1。然后这里的话,未来的总收益就是 $-1+-1=-2$ 。 + +* 如果 $\gamma = 0.6$,就是目光没有放得那么的长远,计算出来是这个样子的。利用 $G_{t}=R_{t+1}+\gamma G_{t+1}$ 这个公式从后往前推。 + +$$ +\begin{array}{l} +G_{7}=R+\gamma G_{8}=-1+0.6 *(-2.176)=-2.3056 \approx-2.3 \\ +G_{8}=R+\gamma G_{9}=-1+0.6 *(-1.96)=-2.176 \approx-2.18 \\ +G_{9}=R+\gamma G_{10}=-1+0.6 *(-1.6)=-1.96 \\ +G_{10}=R+\gamma G_{11}=-1+0.6 *(-1)=-1.6 \\ +G_{12}=R+\gamma G_{13}=-1+0.6 * 0=-1 \\ +G_{13}=0 +\end{array} +$$ + + +这里的计算是我们选择了一条路,计算出这条路径上每一个状态动作的价值。我们可以看一下右下角这个图,如果说我走的不是红色的路,而是蓝色的路,那我算出来的 Q 值可能是这样。那我们就知道,当小乌龟在 -12 这个点的时候,往右边走是 -11,往上走是 -15,它自然就知道往右走的价值更大,小乌龟就会往右走。 + +![](img/3.9.png) +类似于上图,最后我们要求解的就是一张 Q 表格, + +* 它的行数是所有的状态数量,一般可以用坐标来表示表示格子的状态,也可以用 1、2、3、4、5、6、7 来表示不同的位置。 +* Q 表格的列表示上下左右四个动作。 + +最开始这张 Q 表格会全部初始化为零,然后 agent 会不断地去和环境交互得到不同的轨迹,当交互的次数足够多的时候,我们就可以估算出每一个状态下,每个行动的平均总收益去更新这个 Q 表格。怎么去更新 Q 表格就是接下来要引入的强化概念。 + +**`强化`就是我们可以用下一个状态的价值来更新当前状态的价值,其实就是强化学习里面 bootstrapping 的概念。**在强化学习里面,你可以每走一步更新一下 Q 表格,然后用下一个状态的 Q 值来更新这个状态的 Q 值,这种单步更新的方法叫做`时序差分`。 + +## Model-free Prediction + +在没法获取 MDP 的模型情况下,我们可以通过以下两种方法来估计某个给定策略的价值: + +* Monte Carlo policy evaluation +* Temporal Difference(TD) learning + +### Monte-Carlo Policy Evaluation + +![](img/MC_1.png) + +* `蒙特卡罗(Monte-Carlo,MC)`方法是基于采样的方法,我们让 agent 跟环境进行交互,就会得到很多轨迹。每个轨迹都有对应的 return: + +$$ +G_{t}=R_{t+1}+\gamma R_{t+2}+\gamma^{2} R_{t+3}+\ldots +$$ + +* 我们把每个轨迹的 return 进行平均,就可以知道某一个策略下面对应状态的价值。 + +* MC 是用 `经验平均回报(empirical mean return)` 的方法来估计。 + +* MC 方法不需要 MDP 的转移函数和奖励函数,并且不需要像动态规划那样用 bootstrapping 的方法。 + +* MC 的局限性:只能用在有终止的 MDP 。 + +![](img/MC_2.png) + +* 上图是 MC 算法的概括。 +* 为了得到评估 $v(s)$,我们进行了如下的步骤: + * 在每个回合中,如果在时间步 t 状态 s 被访问了,那么 + * 状态 s 的访问数 $N(s)$ 增加 1, + * 状态 s 的总的回报 $S(s)$ 增加 $G_t$。 + * 状态 s 的价值可以通过 return 的平均来估计,即 $v(s)=S(s)/N(s)$。 + +* 根据大数定律,只要我们得到足够多的轨迹,就可以趋近这个策略对应的价值函数。 + +假设现在有样本 $x_1,x_2,\cdots$,我们可以把经验均值(empirical mean)转换成 `增量均值(incremental mean)` 的形式,如下式所示: +$$ +\begin{aligned} +\mu_{t} &=\frac{1}{t} \sum_{j=1}^{t} x_{j} \\ +&=\frac{1}{t}\left(x_{t}+\sum_{j=1}^{t-1} x_{j}\right) \\ +&=\frac{1}{t}\left(x_{t}+(t-1) \mu_{t-1}\right) \\ +&=\frac{1}{t}\left(x_{t}+t \mu_{t-1}-\mu_{t-1}\right) \\ +&=\mu_{t-1}+\frac{1}{t}\left(x_{t}-\mu_{t-1}\right) +\end{aligned} +$$ +通过这种转换,我们就可以把上一时刻的平均值跟现在时刻的平均值建立联系,即: +$$ +\mu_t = \mu_{t-1}+\frac{1}{t}(x_t-\mu_{t-1}) +$$ +其中: + +* $x_t- \mu_{t-1}$ 是残差 +* $\frac{1}{t}$ 类似于学习率(learning rate) + +当我们得到 $x_t$,就可以用上一时刻的值来更新现在的值。 + +![](img/MC_3.png) + +我们可以把 Monte-Carlo 更新的方法写成 incremental MC 的方法: + +* 我们采集数据,得到一个新的轨迹。 +* 对于这个轨迹,我们采用增量的方法进行更新,如下式所示: + +$$ +\begin{array}{l} +N\left(S_{t}\right) \leftarrow N\left(S_{t}\right)+1 \\ +v\left(S_{t}\right) \leftarrow v\left(S_{t}\right)+\frac{1}{N\left(S_{t}\right)}\left(G_{t}-v\left(S_{t}\right)\right) +\end{array} +$$ + +* 我们可以直接把 $\frac{1}{N(S_t)}$ 变成 $\alpha$ (学习率),$\alpha$ 代表着更新的速率有多快,我们可以进行设置。 + +![](img/MC_4.png) + +**我们再来看一下 DP 和 MC 方法的差异。** + +* 动态规划也是常用的估计价值函数的方法。在动态规划里面,我们使用了 bootstrapping 的思想。bootstrapping 的意思就是我们基于之前估计的量来估计一个量。 + +* DP 就是用 Bellman expectation backup,就是通过上一时刻的值 $v_{i-1}(s')$ 来更新当前时刻 $v_i(s)$ 这个值,不停迭代,最后可以收敛。Bellman expectation backup 就有两层加和,内部加和和外部加和,算了两次 expectation,得到了一个更新。 + +![](img/MC_5.png) + +MC 是通过 empirical mean return (实际得到的收益)来更新它,对应树上面蓝色的轨迹,我们得到是一个实际的轨迹,实际的轨迹上的状态已经是决定的,采取的行为都是决定的。MC 得到的是一条轨迹,这条轨迹表现出来就是这个蓝色的从起始到最后终止状态的轨迹。现在只是更新这个轨迹上的所有状态,跟这个轨迹没有关系的状态都没有更新。 + +![](img/MC_6.png) + +* MC 可以在不知道环境的情况下 work,而 DP 是 model-based。 +* MC 只需要更新一条轨迹的状态,而 DP 则是需要更新所有的状态。状态数量很多的时候(比如一百万个,两百万个),DP 这样去迭代的话,速度是非常慢的。这也是 sample-based 的方法 MC 相对于 DP 的优势。 + +### Temporal Difference + +![](img/3.10.png) + +为了让大家更好地理解`时序差分(Temporal Difference,TD)`这种更新方法,这边给出它的物理意义。我们先理解一下巴普洛夫的条件反射实验,这个实验讲的是小狗会对盆里面的食物无条件产生刺激,分泌唾液。一开始小狗对于铃声这种中性刺激是没有反应的,可是我们把这个铃声和食物结合起来,每次先给它响一下铃,再给它喂食物,多次重复之后,当铃声响起的时候,小狗也会开始流口水。盆里的肉可以认为是强化学习里面那个延迟的 reward,声音的刺激可以认为是有 reward 的那个状态之前的一个状态。多次重复实验之后,最后的这个 reward 会强化小狗对于这个声音的条件反射,它会让小狗知道这个声音代表着有食物,这个声音对于小狗来说也就有了价值,它听到这个声音也会流口水。 + +![](img/3.11.png) + +巴普洛夫效应揭示的是中性刺激(铃声)跟无条件刺激(食物)紧紧挨着反复出现的时候,中性刺激也可以引起无条件刺激引起的唾液分泌,然后形成条件刺激。 + +**这种中性刺激跟无条件刺激在时间上面的结合,我们就称之为强化。** 强化的次数越多,条件反射就会越巩固。小狗本来不觉得铃声有价值的,经过强化之后,小狗就会慢慢地意识到铃声也是有价值的,它可能带来食物。更重要是一种条件反射巩固之后,我们再用另外一种新的刺激和条件反射去结合,还可以形成第二级条件反射,同样地还可以形成第三级条件反射。 + +在人的身上是可以建立多级的条件反射的,举个例子,比如说一般我们遇到熊都是这样一个顺序:看到树上有熊爪,然后看到熊之后,突然熊发怒,扑过来了。经历这个过程之后,我们可能最开始看到熊才会瑟瑟发抖,后面就是看到树上有熊爪就已经有害怕的感觉了。也就说在不断的重复试验之后,下一个状态的价值,它是可以不断地去强化影响上一个状态的价值的。 + +为了让大家更加直观感受下一个状态影响上一个状态(状态价值迭代),我们推荐这个网站:[Temporal Difference Learning Gridworld Demo](https://cs.stanford.edu/people/karpathy/reinforcejs/gridworld_td.html)。 + +![](img/3.13.png ':size=500') + +* 我们先初始化一下,然后开始时序差分的更新过程。 +* 在训练的过程中,这个小黄球在不断地试错,在探索当中会先迅速地发现有奖励的地方。最开始的时候,只是这些有奖励的格子才有价值。当不断地重复走这些路线的时候,这些有价值的格子可以去慢慢地影响它附近的格子的价值。 +* 反复训练之后,这些有奖励的格子周围的格子的状态就会慢慢地被强化。强化就是当它收敛到最后一个最优的状态了,这些价值最终收敛到一个最优的情况之后,那个小黄球就会自动地知道,就是我一直往价值高的地方走,就能够走到能够拿到奖励的地方。 + +**下面开始正式介绍 TD 方法。** + +* TD 是介于 MC 和 DP 之间的方法。 +* TD 是 model-free 的,不需要 MDP 的转移矩阵和奖励函数。 +* TD 可以从**不完整的** episode 中学习,结合了 bootstrapping 的思想。 + +![](img/TD_2.png) + +* 上图是 TD 算法的框架。 + +* 目的:对于某个给定的策略,在线(online)地算出它的价值函数,即一步一步地(step-by-step)算。 + +* 最简单的算法是 `TD(0)`,每往前走一步,就做一步 bootstrapping,用得到的估计回报(estimated return)来更新上一时刻的值。 + +* 估计回报 $R_{t+1}+\gamma v(S_{t+1})$ 被称为 `TD target`,TD target 是带衰减的未来收益的总和。TD target 由两部分组成: + * 走了某一步后得到的实际奖励:$R_{t+1}$, + * 我们利用了 bootstrapping 的方法,通过之前的估计来估计 $v(S_{t+1})$ ,然后加了一个折扣系数,即 $\gamma v(S_{t+1})$,具体过程如下式所示: + + $$ + \begin{aligned} + v(s)&=\mathbb{E}\left[G_{t} \mid s_{t}=s\right] \\ + &=\mathbb{E}\left[R_{t+1}+\gamma R_{t+2}+\gamma^{2} R_{t+3}+\ldots \mid s_{t}=s\right] \\ + &=\mathbb{E}\left[R_{t+1}|s_t=s\right] +\gamma \mathbb{E}\left[R_{t+2}+\gamma R_{t+3}+\gamma^{2} R_{t+4}+\ldots \mid s_{t}=s\right]\\ + &=R(s)+\gamma \mathbb{E}[G_{t+1}|s_t=s] \\ + &=R(s)+\gamma \mathbb{E}[v(s_{t+1})|s_t=s]\\ + \end{aligned} + $$ + +* TD目标是估计有两个原因:它对期望值进行采样,并且使用当前估计 V 而不是真实 $v_{\pi}$。 + +* `TD error(误差)` $\delta=R_{t+1}+\gamma v(S_{t+1})-v(S_t)$。 + +* 可以类比于 Incremental Monte-Carlo 的方法,写出如下的更新方法: + +$$ +v\left(S_{t}\right) \leftarrow v\left(S_{t}\right)+\alpha\left(R_{t+1}+\gamma v\left(S_{t+1}\right)-v\left(S_{t}\right)\right) +$$ + +> 上式体现了强化这个概念。 + +* 我们对比下 MC 和 TD: + * 在 MC 里面 $G_{i,t}$ 是实际得到的值(可以看成 target),因为它已经把一条轨迹跑完了,可以算每个状态实际的 return。 + * TD 没有等轨迹结束,往前走了一步,就可以更新价值函数。 + +![](img/TD_3.png) + +* TD 只执行了一步,状态的值就更新。 +* MC 全部走完了之后,到了终止状态之后,再更新它的值。 + +接下来,进一步比较下 TD 和 MC。 + +* TD 可以在线学习(online learning),每走一步就可以更新,效率高。 +* MC 必须等游戏结束才可以学习。 + +* TD 可以从不完整序列上进行学习。 +* MC 只能从完整的序列上进行学习。 + +* TD 可以在连续的环境下(没有终止)进行学习。 +* MC 只能在有终止的情况下学习。 + +* TD 利用了马尔可夫性质,在马尔可夫环境下有更高的学习效率。 +* MC 没有假设环境具有马尔可夫性质,利用采样的价值来估计某一个状态的价值,在不是马尔可夫的环境下更加有效。 + +**举个例子来解释 TD 和 MC 的区别,** + +* TD 是指在不清楚马尔可夫状态转移概率的情况下,以采样的方式得到不完整的状态序列,估计某状态在该状态序列完整后可能得到的收益,并通过不断地采样持续更新价值。 +* MC 则需要经历完整的状态序列后,再来更新状态的真实价值。 + +例如,你想获得开车去公司的时间,每天上班开车的经历就是一次采样。假设今天在路口 A 遇到了堵车, + +* TD 会在路口 A 就开始更新预计到达路口 B、路口 C $\cdots \cdots$,以及到达公司的时间; +* 而 MC 并不会立即更新时间,而是在到达公司后,再修改到达每个路口和公司的时间。 + +**TD 能够在知道结果之前就开始学习,相比 MC,其更快速、灵活。** + +![](img/TD_5.png) + +* 我们可以把 TD 进行进一步的推广。之前是只往前走一步,即 one-step TD,TD(0)。 + +* 我们可以调整步数,变成 `n-step TD`。比如 `TD(2)`,即往前走两步,然后利用两步得到的 return,使用 bootstrapping 来更新状态的价值。 + +* 这样就可以通过 step 来调整这个算法需要多少的实际奖励和 bootstrapping。 + +![](img/TD_6.png) + +* 通过调整步数,可以进行一个 MC 和 TD 之间的 trade-off,如果 $n=\infty$, 即整个游戏结束过后,再进行更新,TD 就变成了 MC。 +* n-step 的 TD target 如下式所示: + +$$ +G_{t}^{n}=R_{t+1}+\gamma R_{t+2}+\ldots+\gamma^{n-1} R_{t+n}+\gamma^{n} v\left(S_{t+n}\right) +$$ + +* 得到 TD target 之后,我们用增量学习(incremental learning)的方法来更新状态的价值: + +$$ +v\left(S_{t}\right) \leftarrow v\left(S_{t}\right)+\alpha\left(G_{t}^{n}-v\left(S_{t}\right)\right) +$$ + +### Bootstrapping and Sampling for DP,MC and TD + +* Bootstrapping:更新时使用了估计: + * MC 没用 bootstrapping,因为它是根据实际的 return 来更新。 + * DP 用了 bootstrapping。 + * TD 用了 bootstrapping。 + +* Sampling:更新时通过采样得到一个期望: + * MC 是纯 sampling 的方法。 + * DP 没有用 sampling,它是直接用 Bellman expectation equation 来更新状态价值的。 + * TD 用了 sampling。TD target 由两部分组成,一部分是 sampling,一部分是 bootstrapping。 + +![](img/comparison_2.png) + +DP 是直接算 expectation,把它所有相关的状态都进行加和。 + +![](img/comparison_3.png) + +MC 在当前状态下,采一个支路,在一个path 上进行更新,更新这个 path 上的所有状态。 + +![](img/comparison_4.png) + +TD 是从当前状态开始,往前走了一步,关注的是非常局部的步骤。 + +![](img/comparison_5.png) + +* 如果 TD 需要更广度的 update,就变成了 DP(因为 DP 是把所有状态都考虑进去来进行更新)。 +* 如果 TD 需要更深度的 update,就变成了 MC。 +* 右下角是穷举的方法(exhaustive search),穷举的方法既需要很深度的信息,又需要很广度的信息。 + +## Model-free Control + +Q: 当我们不知道 MDP 模型情况下,如何优化价值函数,得到最佳的策略? + +A: 我们可以把 policy iteration 进行一个广义的推广,使它能够兼容 MC 和 TD 的方法,即 `Generalized Policy Iteration(GPI) with MC and TD`。 + +![](img/model_free_control_1.png) + +Policy iteration 由两个步骤组成: + +1. 根据给定的当前的 policy $\pi$ 来估计价值函数; +2. 得到估计的价值函数后,通过 greedy 的方法来改进它的算法。 + +这两个步骤是一个互相迭代的过程。 + +![](img/model_free_control_2.png) + +得到一个价值函数过后,我们并不知道它的奖励函数和状态转移,所以就没法估计它的 Q 函数。所以这里有一个问题:当我们不知道奖励函数和状态转移时,如何进行策略的优化。 + +![](img/model_free_control_3.png) + +针对上述情况,我们引入了广义的 policy iteration 的方法。 + +我们对 policy evaluation 部分进行修改:用 MC 的方法代替 DP 的方法去估计 Q 函数。 + +当得到 Q 函数后,就可以通过 greedy 的方法去改进它。 + +![](img/model_free_control_4.png) + +上图是用 MC 估计 Q 函数的算法。 + +* 假设每一个 episode 都有一个 `exploring start`,exploring start 保证所有的状态和动作都在无限步的执行后能被采样到,这样才能很好地去估计。 +* 算法通过 MC 的方法产生了很多的轨迹,每个轨迹都可以算出它的价值。然后,我们可以通过 average 的方法去估计 Q 函数。Q 函数可以看成一个 Q-table,通过采样的方法把表格的每个单元的值都填上,然后我们使用 policy improvement 来选取更好的策略。 +* 算法核心:如何用 MC 方法来填 Q-table。 + +![](img/model_free_control_5.png) + +为了确保 MC 方法能够有足够的探索,我们使用了 $\varepsilon$-greedy exploration。 + +$\varepsilon\text{-greedy}$ 的意思是说,我们有 $1-\varepsilon$ 的概率会按照 Q-function 来决定 action,通常 $\varepsilon$ 就设一个很小的值, $1-\varepsilon$ 可能是 90%,也就是 90% 的概率会按照 Q-function 来决定 action,但是你有 10% 的机率是随机的。通常在实现上 $\varepsilon$ 会随着时间递减。在最开始的时候。因为还不知道那个 action 是比较好的,所以你会花比较大的力气在做 exploration。接下来随着训练的次数越来越多。已经比较确定说哪一个 Q 是比较好的。你就会减少你的 exploration,你会把 $\varepsilon$ 的值变小,主要根据 Q-function 来决定你的 action,比较少做 random,这是 $\varepsilon\text{-greedy}$。 + +![](img/model_free_control_6.png) + +当我们使用 MC 和 $\varepsilon$-greedy 探索这个形式的时候,我们可以确保价值函数是单调的,改进的。 + +![](img/model_free_control_7.png)上图是带 $\varepsilon$-greedy 探索的 MC 算法的伪代码。 + +与 MC 相比,TD 有如下几个优势: + +* 低方差。 +* 能够在线学习。 +* 能够从不完整的序列学习。 + +所以我们可以把 TD 也放到 control loop 里面去估计 Q-table,再采取这个 $\varepsilon$-greedy policy improvement。这样就可以在 episode 没结束的时候来更新已经采集到的状态价值。 + +![](img/bias_variance.png ':size=450') + +>* **偏差(bias):**描述的是预测值(估计值)的期望与真实值之间的差距。偏差越大,越偏离真实数据,如上图第二行所示。 +>* **方差(variance):**描述的是预测值的变化范围,离散程度,也就是离其期望值的距离。方差越大,数据的分布越分散,如上图右列所示。 + +### Sarsa: On-policy TD Control + +![](img/model_free_control_9.png) + +TD 是给定了一个策略,然后我们去估计它的价值函数。接着我们要考虑怎么用 TD 这个框架来估计 Q-function。 + +![](img/3.14.png)Sarsa 所作出的改变很简单,就是将原本我们 TD 更新 V 的过程,变成了更新 Q,如下式所示: + +$$ +Q\left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma Q\left(S_{t+1}, A_{t+1}\right)-Q\left(S_{t}, A_{t}\right)\right] +$$ +这个公式就是说可以拿下一步的 Q 值 $Q(S_{t+_1},A_{t+1})$ 来更新我这一步的 Q 值 $Q(S_t,A_t)$ 。 + +Sarsa 是直接估计 Q-table,得到 Q-table 后,就可以更新策略。 + +为了理解这个公式,如上图所示,我们先把 $R_{t+1}+\gamma Q\left(S_{t+1}, A_{t+1}\right.)$ 当作是一个目标值,就是 $Q(S_t,A_t)$ 想要去逼近的一个目标值。$R_{t+1}+\gamma Q\left(S_{t+1}, A_{t+1}\right.)$ 就是 TD target。 + +我们想要计算的就是 $Q(S_t,A_t)$ 。因为最开始 Q 值都是随机初始化或者是初始化为零,它需要不断地去逼近它理想中真实的 Q 值(TD target),$R_{t+1}+\gamma Q\left(S_{t+1}, A_{t+1}\right)-Q\left(S_{t}, A_{t}\right)$ 就是 TD 误差。 + +也就是说,我们拿 $Q(S_t,A_t)$ 来逼近 $G_t$,那 $Q(S_{t+1},A_{t+1})$ 其实就是近似 $G_{t+1}$。我就可以用 $Q(S_{t+1},A_{t+1})$ 近似 $G_{t+1}$,然后把 $R_{t+1}+Q(S_{t+1},A_{t+1})$ 当成目标值。 + +$Q(S_t,A_t)$ 就是要逼近这个目标值,我们用软更新的方式来逼近。软更新的方式就是每次我只更新一点点,$\alpha$ 类似于学习率。最终的话,Q 值都是可以慢慢地逼近到真实的 target 值。这样我们的更新公式只需要用到当前时刻的 $S_{t},A_t$,还有拿到的 $R_{t+1}, S_{t+1},A_{t+1}$ 。 + +**该算法由于每次更新值函数需要知道当前的状态(state)、当前的动作(action)、奖励(reward)、下一步的状态(state)、下一步的动作(action),即 $(S_{t}, A_{t}, R_{t+1}, S_{t+1}, A_{t+1})$ 这几个值 ,由此得名 `Sarsa` 算法**。它走了一步之后,拿到了 $(S_{t}, A_{t}, R_{t+1}, S_{t+1}, A_{t+1})$ 之后,就可以做一次更新。 + +![](img/3.15.png) + +我们直接看这个框框里面的更新公式, 和之前的公式是一样的。$S'$ 就是 $S_{t+1}$ 。我们就是拿下一步的 Q 值 $Q(S',A')$ 来更新这一步的 Q 值 $Q(S,A)$,不断地强化每一个 Q。 + +![](img/n-step_sarsa.png)Sarsa 属于单步更新法,也就是说每执行一个动作,就会更新一次价值和策略。如果不进行单步更新,而是采取 $n$ 步更新或者回合更新,即在执行 $n$ 步之后再来更新价值和策略,这样就得到了 `n 步 Sarsa(n-step Sarsa)`。 + +比如 2-step Sarsa,就是执行两步后再来更新 Q 的值。 + +具体来说,对于 Sarsa,在 $t$ 时刻其价值的计算公式为 +$$ +q_{t}=R_{t+1}+\gamma Q\left(S_{t+1}, A_{t+1}\right) +$$ +而对于 $n$ 步 Sarsa,它的 $n$ 步 Q 收获为 +$$ +q_{t}^{(n)}=R_{t+1}+\gamma R_{t+2}+\ldots+\gamma^{n-1} R_{t+n}+\gamma^{n} Q\left(S_{t+n}, A_{t+n}\right) +$$ + +如果给 $q_t^{(n)}$ 加上衰减因子 $\lambda$ 并进行求和,即可得到 Sarsa($\lambda$) 的 Q 收获: +$$ +q_{t}^{\lambda}=(1-\lambda) \sum_{n=1}^{\infty} \lambda^{n-1} q_{t}^{(n)} +$$ +因此,$n$ 步 Sarsa($\lambda$)的更新策略可以表示为 +$$ +Q\left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\alpha\left(q_{t}^{\lambda}-Q\left(S_{t}, A_{t}\right)\right) +$$ +总的来说,Sarsa 和 Sarsa($\lambda$) 的差别主要体现在价值的更新上。 + +![](img/3.16.png) + +我们看看用代码去怎么去实现。了解单步更新的一个基本公式之后,代码实现就很简单了。右边是环境,左边是 agent 。我们每次跟环境交互一次之后呢,就可以 learn 一下,向环境输出 action,然后从环境当中拿到 state 和 reward。Agent 主要实现两个方法: + +* 一个就是根据 Q 表格去选择动作,输出 action。 +* 另外一个就是拿到 $(S_{t}, A_{t}, R_{t+1}, S_{t+1}, A_{t+1})$ 这几个值去更新我们的 Q 表格。 + +### Q-learning: Off-policy TD Control + +![](img/3.17.png) + +Sarsa 是一种 on-policy 策略。Sarsa 优化的是它实际执行的策略,它直接拿下一步会执行的 action 来去优化 Q 表格,所以 on-policy 在学习的过程中,只存在一种策略,它用一种策略去做 action 的选取,也用一种策略去做优化。所以 Sarsa 知道它下一步的动作有可能会跑到悬崖那边去,所以它就会在优化它自己的策略的时候,会尽可能的离悬崖远一点。这样子就会保证说,它下一步哪怕是有随机动作,它也还是在安全区域内。 + +而 off-policy 在学习的过程中,有两种不同的策略: + +* 第一个策略是我们需要去学习的策略,即`target policy(目标策略)`,一般用 $\pi$ 来表示,Target policy 就像是在后方指挥战术的一个军师,它可以根据自己的经验来学习最优的策略,不需要去和环境交互。 +* 另外一个策略是探索环境的策略,即`behavior policy(行为策略)`,一般用 $\mu$ 来表示。$\mu$ 可以大胆地去探索到所有可能的轨迹,采集轨迹,采集数据,然后把采集到的数据喂给 target policy 去学习。而且喂给目标策略的数据中并不需要 $A_{t+1}$ ,而 Sarsa 是要有 $A_{t+1}$ 的。Behavior policy 像是一个战士,可以在环境里面探索所有的动作、轨迹和经验,然后把这些经验交给目标策略去学习。比如目标策略优化的时候,Q-learning 不会管你下一步去往哪里探索,它就只选收益最大的策略。 + +![](img/off_policy_learning.png) + +再举个例子,如上图所示,比如环境是一个波涛汹涌的大海,但 learning policy 很胆小,没法直接跟环境去学习,所以我们有了 exploratory policy,exploratory policy 是一个不畏风浪的海盗,他非常激进,可以在环境中探索。他有很多经验,可以把这些经验写成稿子,然后喂给这个 learning policy。Learning policy 可以通过这个稿子来进行学习。 + +在 off-policy learning 的过程中,我们这些轨迹都是 behavior policy 跟环境交互产生的,产生这些轨迹后,我们使用这些轨迹来更新 target policy $\pi$。 + +**Off-policy learning 有很多好处:** + +* 我们可以利用 exploratory policy 来学到一个最佳的策略,学习效率高; +* 可以让我们学习其他 agent 的行为,模仿学习,学习人或者其他 agent 产生的轨迹; +* 重用老的策略产生的轨迹。探索过程需要很多计算资源,这样的话,可以节省资源。 + +Q-learning 有两种 policy:behavior policy 和 target policy。 + +Target policy $\pi$ 直接在 Q-table 上取 greedy,就取它下一步能得到的所有状态,如下式所示: +$$ +\pi\left(S_{t+1}\right)=\underset{a^{\prime}}{\arg \max}~ Q\left(S_{t+1}, a^{\prime}\right) +$$ +Behavior policy $\mu$ 可以是一个随机的 policy,但我们采取 $\varepsilon\text{-greedy}$,让 behavior policy 不至于是完全随机的,它是基于 Q-table 逐渐改进的。 + +我们可以构造 Q-learning target,Q-learning 的 next action 都是通过 arg max 操作来选出来的,于是我们可以代入 arg max 操作,可以得到下式: +$$ +\begin{aligned} +R_{t+1}+\gamma Q\left(S_{t+1}, A^{\prime}\right) &=R_{t+1}+\gamma Q\left(S_{t+1},\arg \max ~Q\left(S_{t+1}, a^{\prime}\right)\right) \\ +&=R_{t+1}+\gamma \max _{a^{\prime}} Q\left(S_{t+1}, a^{\prime}\right) +\end{aligned} +$$ +接着我们可以把 Q-learning 更新写成增量学习的形式,TD target 就变成 max 的值,即 +$$ +Q\left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\alpha\left[R_{t+1}+\gamma \max _{a} Q\left(S_{t+1}, a\right)-Q\left(S_{t}, A_{t}\right)\right] +$$ +![](img/3.18.png) + + **我们再通过对比的方式来进一步理解 `Q-learning`。Q-learning 是 off-policy 的时序差分学习方法,Sarsa 是 on-policy 的时序差分学习方法。** + +* Sarsa 在更新 Q 表格的时候,它用到的 A' 。我要获取下一个 Q 值的时候,A' 是下一个 step 一定会执行的 action。这个 action 有可能是 $\varepsilon$-greedy 方法采样出来的值,也有可能是 max Q 对应的 action,也有可能是随机动作,但这是它实际执行的那个动作。 +* 但是 Q-learning 在更新 Q 表格的时候,它用到这个的 Q 值 $Q(S',a)$ 对应的那个 action ,它不一定是下一个 step 会执行的实际的 action,因为你下一个实际会执行的那个 action 可能会探索。 +* Q-learning 默认的 next action 不是通过 behavior policy 来选取的,Q-learning 直接看 Q-table,取它的 max 的这个值,它是默认 A' 为最优策略选的动作,所以 Q-learning 在学习的时候,不需要传入 A',即 $A_{t+1}$ 的值。 + +> 事实上,Q-learning 算法被提出的时间更早,Sarsa 算法是 Q-learning 算法的改进。 + + +![](img/3.19.png) + +**Sarsa 和 Q-learning 的更新公式都是一样的,区别只在 target 计算的这一部分,** + +* Sarsa 是 $R_{t+1}+\gamma Q(S_{t+1}, A_{t+1})$ ; +* Q-learning 是 $R_{t+1}+\gamma \underset{a}{\max} Q\left(S_{t+1}, a\right)$ 。 + +Sarsa 是用自己的策略产生了 S,A,R,S',A' 这一条轨迹。然后拿着 $Q(S_{t+1},A_{t+1})$ 去更新原本的 Q 值 $Q(S_t,A_t)$。 + +但是 Q-learning 并不需要知道我实际上选择哪一个 action ,它默认下一个动作就是 Q 最大的那个动作。Q-learning 知道实际上 behavior policy 可能会有 10% 的概率去选择别的动作,但 Q-learning 并不担心受到探索的影响,它默认了就按照最优的策略来去优化目标策略,所以它可以更大胆地去寻找最优的路径,它会表现得比 Sarsa 大胆非常多。 + +对 Q-learning 进行逐步地拆解的话,跟 Sarsa 唯一一点不一样就是并不需要提前知道 $A_2$ ,我就能更新 $Q(S_1,A_1)$ 。在训练一个 episode 这个流程图当中,Q-learning 在 learn 之前它也不需要去拿到 next action $A'$,它只需要前面四个 $ (S,A,R,S')$ ,这跟 Sarsa 很不一样。 +## On-policy vs. Off-policy + +**总结一下 on-policy 和 off-policy 的区别。** + +* Sarsa 是一个典型的 on-policy 策略,它只用了一个 policy $\pi$,它不仅使用策略 $\pi$ 学习,还使用策略 $\pi$ 与环境交互产生经验。如果 policy 采用 $\varepsilon$-greedy 算法的话,它需要兼顾探索,为了兼顾探索和利用,它训练的时候会显得有点胆小。它在解决悬崖问题的时候,会尽可能地离悬崖边上远远的,确保说哪怕自己不小心探索了一点,也还是在安全区域内。此外,因为采用的是 $\varepsilon$-greedy 算法,策略会不断改变($\varepsilon$ 会不断变小),所以策略不稳定。 +* Q-learning 是一个典型的 off-policy 的策略,它有两种策略:target policy 和 behavior policy。它分离了目标策略跟行为策略。Q-learning 就可以大胆地用 behavior policy 去探索得到的经验轨迹来去优化目标策略,从而更有可能去探索到最优的策略。Behavior policy 可以采用 $\varepsilon$-greedy 算法,但 target policy 采用的是 greedy 算法,直接根据 behavior policy 采集到的数据来采用最优策略,所以 Q-learning 不需要兼顾探索。 +* 比较 Q-learning 和 Sarsa 的更新公式可以发现,Sarsa 并没有选取最大值的 max 操作,因此, + * Q-learning 是一个非常激进的算法,希望每一步都获得最大的利益; + * 而 Sarsa 则相对非常保守,会选择一条相对安全的迭代路线。 + + + +## Summary +![](img/3.21.png) + +总结如上图所示。 + +## References + +* [百度强化学习](https://aistudio.baidu.com/aistudio/education/lessonvideo/460292) + +* [强化学习基础 David Silver 笔记](https://zhuanlan.zhihu.com/c_135909947) +* [Intro to Reinforcement Learning (强化学习纲要)](https://github.com/zhoubolei/introRL) +* [Reinforcement Learning: An Introduction (second edition)](https://book.douban.com/subject/30323890/) +* [百面深度学习](https://book.douban.com/subject/35043939/) +* [神经网络与深度学习](https://nndl.github.io/) +* [机器学习](https://book.douban.com/subject/26708119//) +* [Understanding the Bias-Variance Tradeoff](http://scott.fortmann-roe.com/docs/BiasVariance.html) + + + + + diff --git a/docs/chapter3/chapter3_questions&keywords.md b/docs/chapter3/chapter3_questions&keywords.md new file mode 100644 index 0000000..75e226a --- /dev/null +++ b/docs/chapter3/chapter3_questions&keywords.md @@ -0,0 +1,108 @@ +# Chapter3 表格型方法 + +## 1 Keywords + +- **P函数和R函数:** P函数反应的是状态转移的概率,即反应的环境的随机性,R函数就是Reward function。但是我们通常处于一个未知的环境(即P函数和R函数是未知的)。 +- **Q表格型表示方法:** 表示形式是一种表格形式,其中横坐标为 action(agent)的行为,纵坐标是环境的state,其对应着每一个时刻agent和环境的情况,并通过对应的reward反馈去做选择。一般情况下,Q表格是一个已经训练好的表格,不过,我们也可以每进行一步,就更新一下Q表格,然后用下一个状态的Q值来更新这个状态的Q值(即时序差分方法)。 +- **时序差分(Temporal Difference):** 一种Q函数(Q值)的更新方式,也就是可以拿下一步的 Q 值 $Q(S_{t+_1},A_{t+1})$ 来更新我这一步的 Q 值 $Q(S_t,A_t)$ 。完整的计算公式如下:$Q(S_t,A_t) \larr Q(S_t,A_t) + \alpha [R_{t+1}+\gamma Q(S_{t+1},A_{t+1})-Q(S_t,A_t)]$ +- **SARSA算法:** 一种更新前一时刻状态的单步更新的强化学习算法,也是一种on-policy策略。该算法由于每次更新值函数需要知道前一步的状态(state),前一步的动作(action)、奖励(reward)、当前状态(state)、将要执行的动作(action),即 $(S_{t}, A_{t}, R_{t+1}, S_{t+1}, A_{t+1})$ 这几个值,所以被称为SARSA算法。agent每进行一次循环,都会用 $(S_{t}, A_{t}, R_{t+1}, S_{t+1}, A_{t+1})$ 对于前一步的Q值(函数)进行一次更新。 + +## 2 Questions + +- 构成强化学习MDP的四元组有哪些变量? + + 答:状态、动作、状态转移概率和奖励,分别对应(S,A,P,R),后面有可能会加上个衰减因子构成五元组。 + +- 基于以上的描述所构成的强化学习的“学习”流程。 + + 答:强化学习要像人类一样去学习了,人类学习的话就是一条路一条路的去尝试一下,先走一条路,我看看结果到底是什么。多试几次,只要能一直走下去的,我们其实可以慢慢的了解哪个状态会更好。我们用价值函数 $V(s)$ 来代表这个状态是好的还是坏的。然后用这个 Q 函数来判断说在什么状态下做什么动作能够拿到最大奖励,我们用 Q 函数来表示这个状态-动作值。 + +- 基于SARSA算法的agent的学习过程。 + + 答:我们现在有环境,有agent。每交互一次以后,我们的agent会向环境输出action,接着环境会反馈给agent当前时刻的state和reward。那么agent此时会实现两个方法: + + 1.使用已经训练好的Q表格,对应环境反馈的state和reward选取对应的action进行输出。 + + 2.我们已经拥有了$(S_{t}, A_{t}, R_{t+1}, S_{t+1}, A_{t+1})$ 这几个值,并直接使用 $A_{t+1}$ 去更新我们的Q表格。 + +- Q-learning和Sarsa的区别? + + 答:Sarsa算法是Q-learning算法的改进。(这句话出自「神经网络与深度学习」的第 342 页)(可参考SARSA「on-line q-learning using connectionist systems」的 abstract 部分) + + 1. 首先,Q-learning 是 off-policy 的时序差分学习方法,Sarsa 是 on-policy 的时序差分学习方法。 + + 2. 其次,Sarsa 在更新 Q 表格的时候,它用到的 A' 。我要获取下一个 Q 值的时候,A' 是下一个 step 一定会执行的 action 。这个 action 有可能是 $\varepsilon$-greddy 方法 sample 出来的值,也有可能是 max Q 对应的 action,也有可能是随机动作。但是就是它实实在在执行了的那个动作。 + + 3. 但是 Q-learning 在更新 Q 表格的时候,它用到这个的 Q 值 $Q(S',a')$ 对应的那个 action ,它不一定是下一个 step 会执行的实际的 action,因为你下一个实际会执行的那个 action 可能会探索。Q-learning 默认的 action 不是通过 behavior policy 来选取的,它是默认 A' 为最优策略选的动作,所以 Q-learning 在学习的时候,不需要传入 A',即 $a_{t+1}$ 的值。 + + 4. 更新公式的对比(区别只在target计算这一部分): + + - Sarsa的公式: $R_{t+1}+\gamma Q(S_{t+1}, A_{t+1})$ ; + - Q-learning的公式:$R_{t+1}+\gamma \underset{a}{\max} Q\left(S_{t+1}, a\right)$ + + Sarsa 实际上都是用自己的策略产生了 S,A,R,S',A' 这一条轨迹。然后拿着 $Q(S_{t+1},A_{t+1})$ 去更新原本的 Q 值 $Q(S_t,A_t)$。 但是 Q-learning 并不需要知道,我实际上选择哪一个 action ,它默认下一个动作就是 Q 最大的那个动作。所以基于此,Sarsa的action通常会更加“保守”、“胆小”,而对应的Q-Learning的action会更加“莽撞”、“激进”。 + +- On-policy和 off-policy 的区别? + + 答: + + 1. Sarsa 就是一个典型的 on-policy 策略,它只用一个 $\pi$ ,为了兼顾探索和利用,所以它训练的时候会显得有点胆小怕事。它在解决悬崖问题的时候,会尽可能地离悬崖边上远远的,确保说哪怕自己不小心探索了一点了,也还是在安全区域内不不至于跳进悬崖。 + 2. Q-learning 是一个比较典型的 off-policy 的策略,它有目标策略 target policy,一般用 $\pi$ 来表示。然后还有行为策略 behavior policy,用 $\mu$ 来表示。它分离了目标策略跟行为策略。Q-learning 就可以大胆地用 behavior policy 去探索得到的经验轨迹来去优化我的目标策略。这样子我更有可能去探索到最优的策略。 + 3. 比较 Q-learning 和 Sarsa 的更新公式可以发现,Sarsa 并没有选取最大值的 max 操作。因此,Q-learning 是一个非常激进的算法,希望每一步都获得最大的利益;而 Sarsa 则相对非常保守,会选择一条相对安全的迭代路线。 + + +## 3 Something About Interview + +- 高冷的面试官:同学,你能否简述on-policy和off-policy的区别? + + 答: off-policy和on-policy的根本区别在于生成样本的policy和参数更新时的policy是否相同。对于on-policy,行为策略和要优化的策略是一个策略,更新了策略后,就用该策略的最新版本对于数据进行采样;对于off-policy,使用任意的一个行为策略来对于数据进行采样,并利用其更新目标策略。如果举例来说,Q-learning在计算下一状态的预期收益时使用了max操作,直接选择最优动作,而当前policy并不一定能选择到最优的action,因此这里生成样本的policy和学习时的policy不同,所以Q-learning为off-policy算法;相对应的SARAS则是基于当前的policy直接执行一次动作选择,然后用这个样本更新当前的policy,因此生成样本的policy和学习时的policy相同,所以SARAS算法为on-policy算法。 + +- 高冷的面试官:小同学,能否讲一下Q-Learning,最好可以写出其 $Q(s,a)$ 的更新公式。另外,它是on-policy还是off-policy,为什么? + + 答: Q-learning是通过计算最优动作值函数来求策略的一种时序差分的学习方法,其更新公式为: + $$ + Q(s, a) \larr Q(s, a) + \alpha [r(s,a) + \gamma \max_{a'} Q(s', a') - Q(s, a)] + $$ + 其是off-policy的,由于是Q更新使用了下一个时刻的最大值,所以我们只关心哪个动作使得 $Q(s_{t+1}, a)$ 取得最大值,而实际到底采取了哪个动作(行为策略),并不关心。这表明优化策略并没有用到行为策略的数据,所以说它是 off-policy 的。 + +- 高冷的面试官:小朋友,能否讲一下SARSA,最好可以写出其Q(s,a)的更新公式。另外,它是on-policy还是off-policy,为什么? + + 答:SARSA可以算是Q-learning的改进(这句话出自「神经网络与深度学习」的第 342 页)(可参考SARSA「on-line q-learning using connectionist systems」的 abstract 部分),其更新公式为: + $$ + Q(s, a) \larr Q(s, a) + \alpha [r(s,a) + \gamma Q(s', a') - Q(s, a)] + $$ + 其为on-policy的,SARSA必须执行两次动作得到 $(s,a,r,s',a') $才可以更新一次;而且 $a'$ 是在特定策略 $\pi$ 的指导下执行的动作,因此估计出来的 $Q(s,a)$ 是在该策略 $\pi$ 之下的Q-value,样本生成用的 $\pi$ 和估计的 $\pi$ 是同一个,因此是on-policy。 + +- 高冷的面试官:请问value-based和policy-based的区别是什么? + + 答: + + 1. 生成policy上的差异:前者确定,后者随机。Value-Base中的 action-value估计值最终会收敛到对应的true values(通常是不同的有限数,可以转化为0到1之间的概率),因此通常会获得一个确定的策略(deterministic policy);而Policy-Based不会收敛到一个确定性的值,另外他们会趋向于生成optimal stochastic policy。如果optimal policy是deterministic的,那么optimal action对应的性能函数将远大于suboptimal actions对应的性能函数,性能函数的大小代表了概率的大小。 + 2. 动作空间是否连续,前者离散,后者连续。Value-Base,对于连续动作空间问题,虽然可以将动作空间离散化处理,但离散间距的选取不易确定。过大的离散间距会导致算法取不到最优action,会在这附近徘徊,过小的离散间距会使得action的维度增大,会和高维度动作空间一样导致维度灾难,影响算法的速度;而Policy-Based适用于连续的动作空间,在连续的动作空间中,可以不用计算每个动作的概率,而是通过Gaussian distribution (正态分布)选择action。 + 3. value-based,例如Q-learning,是通过求解最优值函数间接的求解最优策略;policy-based,例如REINFORCE,Monte-Carlo Policy Gradient,等方法直接将策略参数化,通过策略搜索,策略梯度或者进化方法来更新策略的参数以最大化回报。基于值函数的方法不易扩展到连续动作空间,并且当同时采用非线性近似、自举和离策略时会有收敛性问题。策略梯度具有良好的收敛性证明。 + 4. 补充:对于值迭代和策略迭代:策略迭代。它有两个循环,一个是在策略估计的时候,为了求当前策略的值函数需要迭代很多次。另外一个是外面的大循环,就是策略评估,策略提升这个循环。值迭代算法则是一步到位,直接估计最优值函数,因此没有策略提升环节。 + +- 高冷的面试官:请简述以下时序差分(Temporal Difference,TD)算法。 + + 答:TD算法是使用广义策略迭代来更新Q函数的方法,核心使用了自举(bootstrapping),即值函数的更新使用了下一个状态的值函数来估计当前状态的值。也就是使用下一步的 $Q$ 值 $Q(S_{t+1},A_{t+1})$ 来更新我这一步的 Q 值 $Q(S_t,A_t) $。完整的计算公式如下: + $$ + Q(S_t,A_t) \larr Q(S_t,A_t) + \alpha [R_{t+1}+\gamma Q(S_{t+1},A_{t+1})] + $$ + +- 高冷的面试官:请问蒙特卡洛方法(Monte Carlo Algorithm,MC)和时序差分(Temporal Difference,TD)算法是无偏估计吗?另外谁的方法更大呢?为什么呢? + + 答:蒙特卡洛方法(MC)是无偏估计,时序差分(TD)是有偏估计;MC的方差较大,TD的方差较小,原因在于TD中使用了自举(bootstrapping)的方法,实现了基于平滑的效果,导致估计的值函数的方差更小。 + +- 高冷的面试官:能否简单说下动态规划、蒙特卡洛和时序差分的异同点? + + 答: + + - 相同点:都用于进行值函数的描述与更新,并且所有方法都是基于对未来事件的展望来计算一个回溯值。 + + - 不同点:蒙特卡洛和TD算法隶属于model-free,而动态规划属于model-based;TD算法和蒙特卡洛的方法,因为都是基于model-free的方法,因而对于后续状态的获知也都是基于试验的方法;TD算法和动态规划的策略评估,都能基于当前状态的下一步预测情况来得到对于当前状态的值函数的更新。 + + 另外,TD算法不需要等到实验结束后才能进行当前状态的值函数的计算与更新,而蒙特卡洛的方法需要试验交互,产生一整条的马尔科夫链并直到最终状态才能进行更新。TD算法和动态规划的策略评估不同之处为model-free和model-based 这一点,动态规划可以凭借已知转移概率就能推断出来后续的状态情况,而TD只能借助试验才能知道。 + + 蒙特卡洛方法和TD方法的不同在于,蒙特卡洛方法进行完整的采样来获取了长期的回报值,因而在价值估计上会有着更小的偏差,但是也正因为收集了完整的信息,所以价值的方差会更大,原因在于毕竟基于试验的采样得到,和真实的分布还是有差距,不充足的交互导致的较大方差。而TD算法与其相反,因为只考虑了前一步的回报值 其他都是基于之前的估计值,因而相对来说,其估计值具有偏差大方差小的特点。 + + - 三者的联系:对于$TD(\lambda)$方法,如果 $ \lambda = 0$ ,那么此时等价于TD,即只考虑下一个状态;如果$ \lambda = 1$,等价于MC,即考虑 $T-1$ 个后续状态即到整个episode序列结束。 diff --git a/docs/chapter3/img/3.1.png b/docs/chapter3/img/3.1.png new file mode 100644 index 0000000..eb28b9c Binary files /dev/null and b/docs/chapter3/img/3.1.png differ diff --git a/docs/chapter3/img/3.10.png b/docs/chapter3/img/3.10.png new file mode 100644 index 0000000..6e666f9 Binary files /dev/null and b/docs/chapter3/img/3.10.png differ diff --git a/docs/chapter3/img/3.11.png b/docs/chapter3/img/3.11.png new file mode 100644 index 0000000..e39171c Binary files /dev/null and b/docs/chapter3/img/3.11.png differ diff --git a/docs/chapter3/img/3.12.png b/docs/chapter3/img/3.12.png new file mode 100644 index 0000000..1936a65 Binary files /dev/null and b/docs/chapter3/img/3.12.png differ diff --git a/docs/chapter3/img/3.13.png b/docs/chapter3/img/3.13.png new file mode 100644 index 0000000..c451a23 Binary files /dev/null and b/docs/chapter3/img/3.13.png differ diff --git a/docs/chapter3/img/3.14.png b/docs/chapter3/img/3.14.png new file mode 100644 index 0000000..63d6e04 Binary files /dev/null and b/docs/chapter3/img/3.14.png differ diff --git a/docs/chapter3/img/3.15.png b/docs/chapter3/img/3.15.png new file mode 100644 index 0000000..6ca2f3f Binary files /dev/null and b/docs/chapter3/img/3.15.png differ diff --git a/docs/chapter3/img/3.16.png b/docs/chapter3/img/3.16.png new file mode 100644 index 0000000..03d2c76 Binary files /dev/null and b/docs/chapter3/img/3.16.png differ diff --git a/docs/chapter3/img/3.17.png b/docs/chapter3/img/3.17.png new file mode 100644 index 0000000..6c48225 Binary files /dev/null and b/docs/chapter3/img/3.17.png differ diff --git a/docs/chapter3/img/3.18.png b/docs/chapter3/img/3.18.png new file mode 100644 index 0000000..cd0c6b7 Binary files /dev/null and b/docs/chapter3/img/3.18.png differ diff --git a/docs/chapter3/img/3.19.png b/docs/chapter3/img/3.19.png new file mode 100644 index 0000000..cbd192b Binary files /dev/null and b/docs/chapter3/img/3.19.png differ diff --git a/docs/chapter3/img/3.2.png b/docs/chapter3/img/3.2.png new file mode 100644 index 0000000..5597140 Binary files /dev/null and b/docs/chapter3/img/3.2.png differ diff --git a/docs/chapter3/img/3.20.png b/docs/chapter3/img/3.20.png new file mode 100644 index 0000000..c56a0ab Binary files /dev/null and b/docs/chapter3/img/3.20.png differ diff --git a/docs/chapter3/img/3.21.png b/docs/chapter3/img/3.21.png new file mode 100644 index 0000000..ecc724f Binary files /dev/null and b/docs/chapter3/img/3.21.png differ diff --git a/docs/chapter3/img/3.3.png b/docs/chapter3/img/3.3.png new file mode 100644 index 0000000..ba0f7a7 Binary files /dev/null and b/docs/chapter3/img/3.3.png differ diff --git a/docs/chapter3/img/3.4.png b/docs/chapter3/img/3.4.png new file mode 100644 index 0000000..de654a2 Binary files /dev/null and b/docs/chapter3/img/3.4.png differ diff --git a/docs/chapter3/img/3.5.png b/docs/chapter3/img/3.5.png new file mode 100644 index 0000000..5877863 Binary files /dev/null and b/docs/chapter3/img/3.5.png differ diff --git a/docs/chapter3/img/3.6.png b/docs/chapter3/img/3.6.png new file mode 100644 index 0000000..402a395 Binary files /dev/null and b/docs/chapter3/img/3.6.png differ diff --git a/docs/chapter3/img/3.7.png b/docs/chapter3/img/3.7.png new file mode 100644 index 0000000..b6c4b12 Binary files /dev/null and b/docs/chapter3/img/3.7.png differ diff --git a/docs/chapter3/img/3.8.png b/docs/chapter3/img/3.8.png new file mode 100644 index 0000000..d3127c5 Binary files /dev/null and b/docs/chapter3/img/3.8.png differ diff --git a/docs/chapter3/img/3.9.png b/docs/chapter3/img/3.9.png new file mode 100644 index 0000000..9710b0f Binary files /dev/null and b/docs/chapter3/img/3.9.png differ diff --git a/docs/chapter3/img/MC_1.png b/docs/chapter3/img/MC_1.png new file mode 100644 index 0000000..7ab3930 Binary files /dev/null and b/docs/chapter3/img/MC_1.png differ diff --git a/docs/chapter3/img/MC_2.png b/docs/chapter3/img/MC_2.png new file mode 100644 index 0000000..90bdbd0 Binary files /dev/null and b/docs/chapter3/img/MC_2.png differ diff --git a/docs/chapter3/img/MC_3.png b/docs/chapter3/img/MC_3.png new file mode 100644 index 0000000..fea40c8 Binary files /dev/null and b/docs/chapter3/img/MC_3.png differ diff --git a/docs/chapter3/img/MC_4.png b/docs/chapter3/img/MC_4.png new file mode 100644 index 0000000..f07be4b Binary files /dev/null and b/docs/chapter3/img/MC_4.png differ diff --git a/docs/chapter3/img/MC_5.png b/docs/chapter3/img/MC_5.png new file mode 100644 index 0000000..46eb3c7 Binary files /dev/null and b/docs/chapter3/img/MC_5.png differ diff --git a/docs/chapter3/img/MC_6.png b/docs/chapter3/img/MC_6.png new file mode 100644 index 0000000..dc9fbe8 Binary files /dev/null and b/docs/chapter3/img/MC_6.png differ diff --git a/docs/chapter3/img/Q-learning.png b/docs/chapter3/img/Q-learning.png new file mode 100644 index 0000000..5da93bb Binary files /dev/null and b/docs/chapter3/img/Q-learning.png differ diff --git a/docs/chapter3/img/TD_1.png b/docs/chapter3/img/TD_1.png new file mode 100644 index 0000000..38d5933 Binary files /dev/null and b/docs/chapter3/img/TD_1.png differ diff --git a/docs/chapter3/img/TD_2.png b/docs/chapter3/img/TD_2.png new file mode 100644 index 0000000..eef0dc4 Binary files /dev/null and b/docs/chapter3/img/TD_2.png differ diff --git a/docs/chapter3/img/TD_3.png b/docs/chapter3/img/TD_3.png new file mode 100644 index 0000000..2f8f2b9 Binary files /dev/null and b/docs/chapter3/img/TD_3.png differ diff --git a/docs/chapter3/img/TD_4.png b/docs/chapter3/img/TD_4.png new file mode 100644 index 0000000..d56130f Binary files /dev/null and b/docs/chapter3/img/TD_4.png differ diff --git a/docs/chapter3/img/TD_5.png b/docs/chapter3/img/TD_5.png new file mode 100644 index 0000000..e7b2640 Binary files /dev/null and b/docs/chapter3/img/TD_5.png differ diff --git a/docs/chapter3/img/TD_6.png b/docs/chapter3/img/TD_6.png new file mode 100644 index 0000000..8b79464 Binary files /dev/null and b/docs/chapter3/img/TD_6.png differ diff --git a/docs/chapter3/img/bias_variance.png b/docs/chapter3/img/bias_variance.png new file mode 100644 index 0000000..1014685 Binary files /dev/null and b/docs/chapter3/img/bias_variance.png differ diff --git a/docs/chapter3/img/comparison_1.png b/docs/chapter3/img/comparison_1.png new file mode 100644 index 0000000..aba2ff7 Binary files /dev/null and b/docs/chapter3/img/comparison_1.png differ diff --git a/docs/chapter3/img/comparison_2.png b/docs/chapter3/img/comparison_2.png new file mode 100644 index 0000000..d183db7 Binary files /dev/null and b/docs/chapter3/img/comparison_2.png differ diff --git a/docs/chapter3/img/comparison_3.png b/docs/chapter3/img/comparison_3.png new file mode 100644 index 0000000..11a4683 Binary files /dev/null and b/docs/chapter3/img/comparison_3.png differ diff --git a/docs/chapter3/img/comparison_4.png b/docs/chapter3/img/comparison_4.png new file mode 100644 index 0000000..26d6d92 Binary files /dev/null and b/docs/chapter3/img/comparison_4.png differ diff --git a/docs/chapter3/img/comparison_5.png b/docs/chapter3/img/comparison_5.png new file mode 100644 index 0000000..25edaf9 Binary files /dev/null and b/docs/chapter3/img/comparison_5.png differ diff --git a/docs/chapter3/img/model_free_1.png b/docs/chapter3/img/model_free_1.png new file mode 100644 index 0000000..1edcc26 Binary files /dev/null and b/docs/chapter3/img/model_free_1.png differ diff --git a/docs/chapter3/img/model_free_2.png b/docs/chapter3/img/model_free_2.png new file mode 100644 index 0000000..ad64308 Binary files /dev/null and b/docs/chapter3/img/model_free_2.png differ diff --git a/docs/chapter3/img/model_free_control_1.png b/docs/chapter3/img/model_free_control_1.png new file mode 100644 index 0000000..1955e48 Binary files /dev/null and b/docs/chapter3/img/model_free_control_1.png differ diff --git a/docs/chapter3/img/model_free_control_2.png b/docs/chapter3/img/model_free_control_2.png new file mode 100644 index 0000000..cdf1738 Binary files /dev/null and b/docs/chapter3/img/model_free_control_2.png differ diff --git a/docs/chapter3/img/model_free_control_3.png b/docs/chapter3/img/model_free_control_3.png new file mode 100644 index 0000000..223af51 Binary files /dev/null and b/docs/chapter3/img/model_free_control_3.png differ diff --git a/docs/chapter3/img/model_free_control_4.png b/docs/chapter3/img/model_free_control_4.png new file mode 100644 index 0000000..ca9e51f Binary files /dev/null and b/docs/chapter3/img/model_free_control_4.png differ diff --git a/docs/chapter3/img/model_free_control_5.png b/docs/chapter3/img/model_free_control_5.png new file mode 100644 index 0000000..aef34c4 Binary files /dev/null and b/docs/chapter3/img/model_free_control_5.png differ diff --git a/docs/chapter3/img/model_free_control_6.png b/docs/chapter3/img/model_free_control_6.png new file mode 100644 index 0000000..77c280b Binary files /dev/null and b/docs/chapter3/img/model_free_control_6.png differ diff --git a/docs/chapter3/img/model_free_control_7.png b/docs/chapter3/img/model_free_control_7.png new file mode 100644 index 0000000..af5b746 Binary files /dev/null and b/docs/chapter3/img/model_free_control_7.png differ diff --git a/docs/chapter3/img/model_free_control_8.png b/docs/chapter3/img/model_free_control_8.png new file mode 100644 index 0000000..0a4e306 Binary files /dev/null and b/docs/chapter3/img/model_free_control_8.png differ diff --git a/docs/chapter3/img/model_free_control_9.png b/docs/chapter3/img/model_free_control_9.png new file mode 100644 index 0000000..428058e Binary files /dev/null and b/docs/chapter3/img/model_free_control_9.png differ diff --git a/docs/chapter3/img/n-step_sarsa.png b/docs/chapter3/img/n-step_sarsa.png new file mode 100644 index 0000000..d6d2f51 Binary files /dev/null and b/docs/chapter3/img/n-step_sarsa.png differ diff --git a/docs/chapter3/img/off_policy_learning.png b/docs/chapter3/img/off_policy_learning.png new file mode 100644 index 0000000..1b3e246 Binary files /dev/null and b/docs/chapter3/img/off_policy_learning.png differ diff --git a/docs/chapter3/project1.md b/docs/chapter3/project1.md new file mode 100644 index 0000000..50e648e --- /dev/null +++ b/docs/chapter3/project1.md @@ -0,0 +1,86 @@ +# 使用Q-learning解决悬崖寻路问题 + +强化学习在运动规划方面也有很大的应用前景,具体包括路径规划与决策,智能派单等等,本次项目就将单体运动规划抽象并简化,让大家初步认识到强化学习在这方面的应用。在运动规划方面,其实已有很多适用于强化学习的仿真环境,小到迷宫,大到贴近真实的自动驾驶环境[CARLA](http://carla.org/),对这块感兴趣的童鞋可以再多搜集一点。本项目采用gym开发的```CliffWalking-v0```环境,在上面实现一个简单的Q-learning入门demo。 + +## CliffWalking-v0环境简介 + +首先对该环境做一个简介,该环境中文名称叫悬崖寻路问题(CliffWalking),是指在一个4 x 12的网格中,智能体以网格的左下角位置为起点,以网格的下角位置为终点,目标是移动智能体到达终点位置,智能体每次可以在上、下、左、右这4个方向中移动一步,每移动一步会得到-1单位的奖励。 + +![](assets/cliffwalking_1.png) + +如图,红色部分表示悬崖,数字代表智能体能够观测到的位置信息,即observation,总共会有0-47等48个不同的值,智能体再移动中会有以下限制: + +* 智能体不能移出网格,如果智能体想执行某个动作移出网格,那么这一步智能体不会移动,但是这个操作依然会得到-1单位的奖励 + +* 如果智能体“掉入悬崖” ,会立即回到起点位置,并得到-100单位的奖励 + +* 当智能体移动到终点时,该回合结束,该回合总奖励为各步奖励之和 + +实际的仿真界面如下: + +![](assets/cliffwalking_2.png) + +**由于从起点到终点最少需要13步,每步得到-1的reward,因此最佳训练算法下,每个episode下reward总和应该为-13**。所以我们的目标也是要通过RL训练出一个模型,使得该模型能在测试中一个episode的reward能够接近于-13左右。 + +## RL基本训练接口 + +以下是强化学习算法的基本接口,也就是一个完整的上层训练模式,首先是初始化环境和智能体,然后每个episode中,首先agent选择action给到环境,然后环境反馈出下一个状态和reward,然后agent开始更新或者学习,如此多个episode之后agent开始收敛并保存模型。其中可以通过可视化reward随每个episode的变化来查看训练的效果。另外由于强化学习的不稳定性,在收敛的状态下也可能会有起伏的情况,此时可以使用滑动平均的reward让曲线更加平滑便于分析。 + +```python + '''初始化环境''' +env = gym.make("CliffWalking-v0") # 0 up, 1 right, 2 down, 3 left +env = CliffWalkingWapper(env) +agent = QLearning( + state_dim=env.observation_space.n, + action_dim=env.action_space.n, + learning_rate=cfg.policy_lr, + gamma=cfg.gamma, +rewards = [] +ma_rewards = [] # moving average reward +for i_ep in range(cfg.train_eps): # train_eps: 训练的最大episodes数 + ep_reward = 0 # 记录每个episode的reward + state = env.reset() # 重置环境, 重新开一局(即开始新的一个episode) + while True: + action = agent.choose_action(state) # 根据算法选择一个动作 + next_state, reward, done, _ = env.step(action) # 与环境进行一次动作交互 + agent.update(state, action, reward, next_state, done) # Q-learning算法更新 + state = next_state # 存储上一个观察值 + ep_reward += reward + if done: + break + rewards.append(ep_reward) + if ma_rewards: + ma_rewards.append(ma_rewards[-1]*0.9+ep_reward*0.1) + else: + ma_rewards.append(ep_reward) + print("Episode:{}/{}: reward:{:.1f}".format(i_ep+1, cfg.train_eps,ep_reward)) +``` + +## 任务要求 + +基于以上的目标,本次任务即使训练并绘制reward以及滑动平均后的reward随episode的变化曲线图并记录超参数写成报告,示例如下: + +![rewards](assets/rewards.png) + +![moving_average_rewards](assets/moving_average_rewards.png) + +## 主要代码清单 + +**main.py** 或 **task_train.py**:保存强化学习基本接口,以及相应的超参数 + +**agent.py**: 保存算法模型,主要包含choose_action(预测动作)和update两个函数,有时会多一个predict_action函数,此时choose_action使用了epsilon-greedy策略便于训练的探索,而测试时用predict_action单纯贪心地选择网络的值输出动作 + +**model.py**:保存神经网络,比如全连接网络等等,对于一些算法,分为Actor和Critic两个类 + +**memory.py**:保存replay buffer,根据算法的不同,replay buffer功能有所不同,因此会改写 + +**plot.py**:保存相关绘制函数 + +[参考代码](https://github.com/datawhalechina/easy-rl/tree/master/codes/QLearning) + +## 备注 + +* 注意 $\varepsilon$-greedy 策略的使用,以及相应的参数$\varepsilon$如何衰减 +* 训练模型和测试模型的时候选择动作有一些不同,训练时采取 $\varepsilon$-greedy策略,而测试时直接选取Q值最大对应的动作,所以算法在动作选择的时候会包括sample(训练时的动作采样)和predict(测试时的动作选择) + +* Q值最大对应的动作可能不止一个,此时可以随机选择一个输出结果 diff --git a/docs/chapter4/chapter4.md b/docs/chapter4/chapter4.md new file mode 100644 index 0000000..61349ce --- /dev/null +++ b/docs/chapter4/chapter4.md @@ -0,0 +1,349 @@ +# Policy Gradient +## Policy Gradient + +![](img/4.1.png) + +在强化学习中有 3 个组成部分:`演员(actor)`、`环境(environment)` 和 `奖励函数(reward function)`。 + +让机器玩视频游戏时, + +* 演员做的事情就是去操控游戏的摇杆, 比如说向左、向右、开火等操作; +* 环境就是游戏的主机, 负责控制游戏的画面,负责控制怪物要怎么移动, 你现在要看到什么画面等等; +* 奖励函数就是当你做什么事情,发生什么状况的时候,你可以得到多少分数, 比如说杀一只怪兽得到 20 分等等。 + +同样的概念用在围棋上也是一样的, + +* 演员就是 Alpha Go,它要决定下哪一个位置; +* 环境就是对手; +* 奖励函数就是按照围棋的规则, 赢就是得一分,输就是负一分。 + +在强化学习里面,环境跟奖励函数不是你可以控制的,环境跟奖励函数是在开始学习之前,就已经事先给定的。你唯一能做的事情是调整演员里面的策略(policy),使得演员可以得到最大的奖励。演员里面会有一个策略,这个策略决定了演员的行为。给定一个外界的输入,策略会输出演员现在应该要执行的行为。 + +![](img/4.2.png) + +* 策略一般写成 $\pi$。假设你是用深度学习的技术来做强化学习的话,**策略就是一个网络**。网络里面就有一堆参数,我们用 $\theta$ 来代表 $\pi$ 的参数。 + +* **网络的输入就是现在机器看到的东西**,如果让机器打电玩的话,机器看到的东西就是游戏的画面。机器看到什么东西,会影响你现在训练到底好不好训练。举例来说,在玩游戏的时候, 也许你觉得游戏的画面前后是相关的,也许你觉得你应该让你的策略,看从游戏初始到现在这个时间点,所有画面的总和。你可能会觉得你要用到 RNN 来处理它,不过这样子会比较难处理。要让你的机器,你的策略看到什么样的画面,这个是你自己决定的。让你知道说给机器看到什么样的游戏画面,可能是比较有效的。 +* **输出的就是机器要采取什么样的行为。** + +* 上图就是具体的例子, + * 策略就是一个网络; + * 输入 就是游戏的画面,它通常是由像素(pixels)所组成的; + * 输出就是看看说有哪些选项是你可以去执行的,输出层就有几个神经元。 + * 假设你现在可以做的行为有 3 个,输出层就是有 3 个神经元。每个神经元对应到一个可以采取的行为。 + * 输入一个东西后,网络就会给每一个可以采取的行为一个分数。你可以把这个分数当作是概率。演员就是看这个概率的分布,根据这个概率的分布来决定它要采取的行为。比如说 70% 会向左走,20% 向右走,10% 开火等等。概率分布不同,演员采取的行为就会不一样。 + +![](img/4.3.png) +**接下来用一个例子来说明演员是怎么样跟环境互动的。** + +首先演员会看到一个游戏画面,我们用 $s_1$ 来表示游戏初始的画面。接下来演员看到这个游戏的初始画面以后,根据它内部的网络,根据它内部的策略来决定一个动作。假设它现在决定的动作 是向右,它决定完动作 以后,它就会得到一个奖励,代表它采取这个动作以后得到的分数。 + +我们把一开始的初始画面记作 $s_1$, 把第一次执行的动作记作 $a_1$,把第一次执行动作完以后得到的奖励记作 $r_1$。不同的书会有不同的定义,有人会觉得说这边应该要叫做 $r_2$,这个都可以,你自己看得懂就好。演员决定一个行为以后,就会看到一个新的游戏画面,这边是 $s_2$。然后把这个 $s_2$ 输入给演员,这个演员决定要开火,然后它可能杀了一只怪,就得到五分。这个过程就反复地持续下去,直到今天走到某一个时间点执行某一个动作,得到奖励之后,这个环境决定这个游戏结束了。比如说,如果在这个游戏里面,你是控制绿色的船去杀怪,如果你被杀死的话,游戏就结束,或是你把所有的怪都清空,游戏就结束了。 + +![](img/4.4.png) + +* 一场游戏叫做一个 `回合(episode)` 或者 `试验(trial)`。 +* 把这场游戏里面所有得到的奖励都加起来,就是 `总奖励(total reward)`,我们称其为`回报(return)`,用 R 来表示它。 +* 演员要想办法去最大化它可以得到的奖励。 + +![](img/4.5.png) +首先,`环境` 是一个`函数`,游戏的主机也可以把它看作是一个函数,虽然它不一定是神经网络,可能是基于规则的(rule-based)规则,但你可以把它看作是一个函数。这个函数一开始就先吐出一个状态,也就是游戏的画面,接下来你的演员看到这个游戏画面 $s_1$ 以后,它吐出 $a_1$,然后环境把 $a_1$ 当作它的输入,然后它再吐出 $s_2$,吐出新的游戏画面。演员看到新的游戏画面,再采取新的行为 $a_2$,然后 环境再看到 $a_2$,再吐出 $s_3$。这个过程会一直持续下去,直到环境觉得说应该要停止为止。 + +在一场游戏里面,我们把环境输出的 $s$ 跟演员输出的行为 $a$,把 $s$ 跟 $a$ 全部串起来, 叫做一个 `Trajectory(轨迹)`,如下式所示。 +$$ +\text { Trajectory } \tau=\left\{s_{1}, a_{1}, s_{2}, a_{2}, \cdots, s_{t}, a_{t}\right\} +$$ + +你可以计算每一个轨迹发生的概率。假设现在演员的参数已经被给定了话,就是 $\theta$。根据 $\theta$,你其实可以计算某一个轨迹发生的概率,你可以计算某一个回合里面发生这样子状况的概率。 + +$$ +\begin{aligned} +p_{\theta}(\tau) +&=p\left(s_{1}\right) p_{\theta}\left(a_{1} | s_{1}\right) p\left(s_{2} | s_{1}, a_{1}\right) p_{\theta}\left(a_{2} | s_{2}\right) p\left(s_{3} | s_{2}, a_{2}\right) \cdots \\ +&=p\left(s_{1}\right) \prod_{t=1}^{T} p_{\theta}\left(a_{t} | s_{t}\right) p\left(s_{t+1} | s_{t}, a_{t}\right) +\end{aligned} +$$ + +怎么算呢,如上式所示。在假设演员的参数就是 $\theta$ 的情况下,某一个轨迹 $\tau$ 的概率就是这样算的,你先算环境输出 $s_1$ 的概率,再计算根据 $s_1$ 执行 $a_1$ 的概率,这是由你策略里面的网络参数 $\theta$ 所决定的, 它是一个概率,因为你的策略的网络的输出是一个分布,演员是根据这个分布去做采样,决定现在实际上要采取的动作是哪一个。接下来环境根据 $a_1$ 跟 $s_1$ 产生 $s_2$,因为 $s_2$ 跟 $s_1$ 还是有关系的,下一个游戏画面跟前一个游戏画面通常还是有关系的,至少要是连续的, 所以给定前一个游戏画面 $s_1$ 和现在演员采取的行为 $a_1$,就会产生 $s_2$。 + +这件事情可能是概率,也可能不是概率,这个取决于环境,就是主机它内部设定是怎样。看今天这个主机在决定,要输出什么样的游戏画面的时候,有没有概率。因为如果没有概率的话,这个游戏的每次的行为都一样,你只要找到一条路径就可以过关了,这样感觉是蛮无聊的 。所以游戏里面通常还是有一些概率的,你做同样的行为,给同样的前一个画面, 下次产生的画面不见得是一样的。过程就反复继续下去,你就可以计算一个轨迹 $s_1$,$a_1$, $s_2$ , $a_2$ 出现的概率有多大。 + +**这个概率取决于两部分:环境的行为和 agent 的行为**, + +* `环境的行为` 。环境的函数内部的参数或内部的规则长什么样子。 $p(s_{t+1}|s_t,a_t)$这一项代表的是环境,环境这一项通常你是无法控制它的,因为那个是人家写好的,你不能控制它。 +* `agent 的行为`。你能控制的是 $p_\theta(a_t|s_t)$。给定一个 $s_t$,演员要采取什么样的 $a_t$ 会取决于演员的参数 $\theta$, 所以这部分是演员可以自己控制的。随着演员的行为不同,每个同样的轨迹, 它就会有不同的出现的概率。 + + +![](img/4.6.png) + +在强化学习里面,除了环境跟演员以外, 还有`奖励函数(reward function)`。 + +奖励函数根据在某一个状态采取的某一个动作决定说现在这个行为可以得到多少的分数。 它是一个函数,给它 $s_1$,$a_1$,它告诉你得到 $r_1$。给它 $s_2$ ,$a_2$,它告诉你得到 $r_2$。 把所有的 $r$ 都加起来,我们就得到了 $R(\tau)$ ,代表某一个轨迹 $\tau$ 的奖励。 + +在某一场游戏里面, 某一个回合里面,我们会得到 R。**我们要做的事情就是调整演员内部的参数 $\theta$, 使得 R 的值越大越好。** 但实际上奖励并不只是一个标量,奖励其实是一个随机变量。R 其实是一个随机变量,因为演员在给定同样的状态会做什么样的行为,这件事情是有随机性的。环境在给定同样的观测要采取什么样的动作,要产生什么样的观测,本身也是有随机性的,所以 R 是一个随机变量。你能够计算的是 R 的期望值。你能够计算的是说,在给定某一组参数 $\theta$ 的情况下,我们会得到的 $R_{\theta}$ 的期望值是多少。 +$$ +\bar{R}_{\theta}=\sum_{\tau} R(\tau) p_{\theta}(\tau) +$$ +这个期望值的算法如上式所示。我们要穷举所有可能的轨迹 $\tau$, 每一个轨迹 $\tau$ 都有一个概率。 + +比如 $\theta$ 是一个很强的模型,它都不会死。因为 $\theta$ 很强,所以: + +* 如果有一个回合 $\theta$ 很快就死掉了,因为这种情况很少会发生,所以该回合对应的轨迹 $\tau$ 的概率就很小; +* 如果有一个回合 $\theta$ 都一直没有死,因为这种情况很可能发生,所以该回合对应的轨迹 $\tau$ 的概率就很大。 + +你可以根据 $\theta$ 算出某一个轨迹 $\tau$ 出现的概率,接下来计算这个 $\tau$ 的总奖励是多少。总奖励使用这个 $\tau$ 出现的概率进行加权,对所有的 $\tau$ 进行求和,就是期望值。给定一个参数,你会得到的期望值。 +$$ +\bar{R}_{\theta}=\sum_{\tau} R(\tau) p_{\theta}(\tau)=E_{\tau \sim p_{\theta}(\tau)}[R(\tau)] +$$ +我们还可以写成上式那样,从 $p_{\theta}(\tau)$ 这个分布采样一个轨迹 $\tau$,然后计算 $R(\tau)$ 的期望值,就是你的期望的奖励。 我们要做的事情就是最大化期望奖励。 + +![](img/4.7.png) + +怎么最大化期望奖励呢?我们用的是 `梯度上升(gradient ascent)`,因为要让它越大越好,所以是梯度上升。梯度上升在更新参数的时候要加。要进行梯度上升,我们先要计算期望的奖励(expected reward) $\bar{R}$ 的梯度。我们对 $\bar{R}$ 取一个梯度,这里面只有 $p_{\theta}(\tau)$ 是跟 $\theta$ 有关,所以梯度就放在 $p_{\theta}(\tau)$ 这个地方。$R(\tau)$ 这个奖励函数不需要是可微分的(differentiable),这个不影响我们解接下来的问题。举例来说,如果是在 GAN 里面,$R(\tau)$ 其实是一个 discriminator,它就算是没有办法微分,也无所谓,你还是可以做接下来的运算。 + +取梯度之后,我们背一个公式: +$$ +\nabla f(x)=f(x)\nabla \log f(x) +$$ +我们可以对 $\nabla p_{\theta}(\tau)$ 使用这个公式,然后会得到 $\nabla p_{\theta}(\tau)=p_{\theta}(\tau) \nabla \log p_{\theta}(\tau)$,进一步地,我们可以得到下式: + +$$ +\frac{\nabla p_{\theta}(\tau)}{p_{\theta}(\tau)}=\nabla \log p_{\theta}(\tau) +$$ + +如下式所示,对 $\tau$ 进行求和,把 $R(\tau)$ 和 $\log p_{\theta}(\tau)$ 这两项使用 $p_{\theta}(\tau)$ 进行加权, 既然使用 $p_{\theta}(\tau)$ 进行加权 ,它们就可以被写成期望的形式。也就是你从 $p_{\theta}(\tau)$ 这个分布里面采样 $\tau$ 出来, 去计算 $R(\tau)$ 乘上 $\nabla\log p_{\theta}(\tau)$,然后把它对所有可能的 $\tau$ 进行求和,就是这个期望的值(expected value)。 +$$ +\begin{aligned} +\nabla \bar{R}_{\theta}&=\sum_{\tau} R(\tau) \nabla p_{\theta}(\tau)\\&=\sum_{\tau} R(\tau) p_{\theta}(\tau) \frac{\nabla p_{\theta}(\tau)}{p_{\theta}(\tau)} \\&= +\sum_{\tau} R(\tau) p_{\theta}(\tau) \nabla \log p_{\theta}(\tau) \\ +&=E_{\tau \sim p_{\theta}(\tau)}\left[R(\tau) \nabla \log p_{\theta}(\tau)\right] +\end{aligned} +$$ + +实际上这个期望值没有办法算,所以你是用采样的方式来采样一大堆的 $\tau$。你采样 $N$ 笔 $\tau$, 然后你去计算每一笔的这些值,然后把它全部加起来,就可以得到梯度。你就可以去更新参数,你就可以去更新你的 agent,如下式所示: +$$ +\begin{aligned} +E_{\tau \sim p_{\theta}(\tau)}\left[R(\tau) \nabla \log p_{\theta}(\tau)\right] &\approx \frac{1}{N} \sum_{n=1}^{N} R\left(\tau^{n}\right) \nabla \log p_{\theta}\left(\tau^{n}\right) \\ +&=\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}} R\left(\tau^{n}\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) +\end{aligned} +$$ +下面给出 $\nabla \log p_{\theta}(\tau)$ 的具体计算过程,如下式所示。 +$$ +\begin{aligned} +\nabla \log p_{\theta}(\tau) &= \nabla \left(\log p(s_1)+\sum_{t=1}^{T}\log p_{\theta}(a_t|s_t)+ \sum_{t=1}^{T}\log p(s_{t+1}|s_t,a_t) \right) \\ +&= \nabla \log p(s_1)+ \nabla \sum_{t=1}^{T}\log p_{\theta}(a_t|s_t)+ \nabla \sum_{t=1}^{T}\log p(s_{t+1}|s_t,a_t) \\ +&=\nabla \sum_{t=1}^{T}\log p_{\theta}(a_t|s_t)\\ +&=\sum_{t=1}^{T} \nabla\log p_{\theta}(a_t|s_t) +\end{aligned} +$$ + +注意, $p(s_1)$ 和 $p(s_{t+1}|s_t,a_t)$ 来自于环境,$p_\theta(a_t|s_t)$ 是来自于 agent。$p(s_1)$ 和 $p(s_{t+1}|s_t,a_t)$ 由环境决定,所以与 $\theta$ 无关,因此 $\nabla \log p(s_1)=0$ ,$\nabla \sum_{t=1}^{T}\log p(s_{t+1}|s_t,a_t)=0$。 + + +$$ +\begin{aligned} +\nabla \bar{R}_{\theta}&=\sum_{\tau} R(\tau) \nabla p_{\theta}(\tau)\\&=\sum_{\tau} R(\tau) p_{\theta}(\tau) \frac{\nabla p_{\theta}(\tau)}{p_{\theta}(\tau)} \\&= +\sum_{\tau} R(\tau) p_{\theta}(\tau) \nabla \log p_{\theta}(\tau) \\ +&=E_{\tau \sim p_{\theta}(\tau)}\left[R(\tau) \nabla \log p_{\theta}(\tau)\right]\\ +&\approx \frac{1}{N} \sum_{n=1}^{N} R\left(\tau^{n}\right) \nabla \log p_{\theta}\left(\tau^{n}\right) \\ +&=\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}} R\left(\tau^{n}\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) +\end{aligned} +$$ + +我们可以直观地来理解上面这个式子,也就是在你采样到的数据里面, 你采样到在某一个状态 $s_t$ 要执行某一个动作 $a_t$, 这个 $s_t$ 跟 $a_t$ 它是在整个轨迹 $\tau$ 的里面的某一个状态和动作的对。 + +* 假设你在 $s_t$ 执行 $a_t$,最后发现 $\tau$ 的奖励是正的, 那你就要增加这一项的概率,你就要增加在 $s_t$ 执行 $a_t$ 的概率。 +* 反之,在 $s_t$ 执行 $a_t$ 会导致 $\tau$ 的奖励变成负的, 你就要减少这一项的概率。 + +![](img/4.8.png) +这个怎么实现呢? 你用梯度上升来更新你的参数,你原来有一个参数 $\theta$ ,把你的 $\theta$ 加上你的梯度这一项,那当然前面要有个学习率,学习率也是要调整的,你可用 Adam、RMSProp 等方法对其进行调整。 + +我们可以套下面这个公式来把梯度计算出来: +$$ +\nabla \bar{R}_{\theta}=\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}} R\left(\tau^{n}\right) \nabla \log p_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right) +$$ +实际上,要套上面这个公式, 首先你要先收集一大堆的 s 跟 a 的对(pair),你还要知道这些 s 跟 a 在跟环境互动的时候,你会得到多少的奖励。 这些资料怎么收集呢?你要拿你的 agent,它的参数是 $\theta$,去跟环境做互动, 也就是拿你已经训练好的 agent 先去跟环境玩一下,先去跟那个游戏互动一下, 互动完以后,你就会得到一大堆游戏的纪录,你会记录说,今天先玩了第一场,在第一场游戏里面,我们在状态 $s_1$ 采取动作 $a_1$,在状态$s_2$ 采取动作 $a_2$ 。 + +玩游戏的时候是有随机性的,所以 agent 本身是有随机性的,在同样状态$s_1$,不是每次都会采取 $a_1$,所以你要记录下来。在状态 $s_1^1$ 采取 $a_1^1$,在状态 $s_2^1$ 采取 $a_2^1$。整场游戏结束以后,得到的分数是 $R(\tau^1)$。你会采样到另外一笔数据,也就是另外一场游戏。在另外一场游戏里面,你在状态 $s_1^2$ 采取 $a_1^2$,在状态 $s_2^2$ 采取 $a_2^2$,然后你采样到的就是 $\tau^2$,得到的奖励是 $R(\tau^2)$。 + +你就可以把采样到的东西代到这个梯度的式子里面,把梯度算出来。也就是把这边的每一个 s 跟 a 的对拿进来,算一下它的对数概率(log probability)。你计算一下在某一个状态采取某一个动作的对数概率,然后对它取梯度,然后这个梯度前面会乘一个权重,权重就是这场游戏的奖励。 有了这些以后,你就会去更新你的模型。 + +更新完你的模型以后。你要重新去收集数据,再更新模型。注意,一般 `policy gradient(PG) `采样的数据就只会用一次。你把这些数据采样起来,然后拿去更新参数,这些数据就丢掉了。接着再重新采样数据,才能够去更新参数,等一下我们会解决这个问题。 + +![](img/4.9.png) + +**接下来讲一些实现细节。** + +我们可以把它想成一个分类的问题,在分类里面就是输入一个图像,然后输出决定说是 10 个类里面的哪一个。在做分类时,我们要收集一堆训练数据,要有输入跟输出的对。 + +在实现的时候,你就把状态当作是分类器的输入。 你就当在做图像分类的问题,只是现在的类不是说图像里面有什么东西,而是说看到这张图像我们要采取什么样的行为,每一个行为就是一个类。比如说第一个类叫做向左,第二个类叫做向右,第三个类叫做开火。 + +在做分类的问题时,要有输入和正确的输出,要有训练数据。而这些训练数据是从采样的过程来的。假设在采样的过程里面,在某一个状态,你采样到你要采取动作 a, 你就把这个动作 a 当作是你的 ground truth。你在这个状态,你采样到要向左。 本来向左这件事概率不一定是最高, 因为你是采样,它不一定概率最高。假设你采样到向左,在训练的时候,你告诉机器说,调整网络的参数, 如果看到这个状态,你就向左。在一般的分类问题里面,其实你在实现分类的时候,你的目标函数都会写成最小化交叉熵(cross entropy),其实最小化交叉熵就是最大化对数似然(log likelihood)。 + + +![](img/4.10.png) + +做分类的时候,目标函数就是最大化或最小化的对象, 因为我们现在是最大化似然(likelihood),所以其实是最大化, 你要最大化的对象,如下式所示: +$$ +\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}} \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) +$$ +像这种损失函数,你可在 TensorFlow 里调用现成的函数,它就会自动帮你算,然后你就可以把梯度计算出来。这是一般的分类问题,RL 唯一不同的地方是 loss 前面乘上一个权重:整场游戏得到的总奖励 R,它并不是在状态 s 采取动作 a 的时候得到的奖励,如下式所示: +$$ +\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}} R\left(\tau^{n}\right) \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) +$$ +你要把你的每一笔训练数据,都使用这个 R 进行加权。然后你用 TensorFlow 或 PyTorch 去帮你算梯度就结束了,跟一般分类差不多。 + +## Tips +这边有一些在实现的时候,你也许用得上的 tip。 +### Tip 1: 添加基线 + +![](img/4.11.png) + +**第一个 tip 是添加基线(baseline)。** 如果给定状态 s 采取动作 a 会给你整场游戏正的奖励,就要增加它的概率。如果状态 s 执行动作 a,整场游戏得到负的奖励,就要减少这一项的概率。 + +但在很多游戏里面,奖励总是正的,就是说最低都是 0。比如说打乒乓球游戏, 你的分数就是介于 0 到 21 分之间,所以 R 总是正的。假设你直接套用这个式子, 在训练的时候告诉模型说,不管是什么动作你都应该要把它的概率提升。 在理想上,这么做并不一定会有问题。因为虽然说 R 总是正的,但它正的量总是有大有小,你在玩乒乓球那个游戏里面,得到的奖励总是正的,但它是介于 0~21分之间,有时候你采取某些动作可能是得到 0 分,采取某些动作可能是得到 20 分。 + +![](img/4.12.png) + +假设你在某一个状态有 3 个动作 a/b/c可以执行。根据下式, +$$ +\nabla \bar{R}_{\theta} \approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}} R\left(\tau^{n}\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) +$$ +你要把这 3 项的概率,对数概率都拉高。 但是它们前面权重的 R 是不一样的。 R 是有大有小的,权重小的,它上升的就少,权重多的,它上升的就大一点。 因为这个对数概率是一个概率,所以动作 a、b、c 的对数概率的和要是 0。 所以上升少的,在做完归一化(normalize)以后, 它其实就是下降的,上升的多的,才会上升。 + + + ![1](img/4.13.png) + + +这是一个理想上的状况,但是实际上,我们是在做采样就本来这边应该是一个期望(expectation),对所有可能的 s 跟 a 的对进行求和。 但你真正在学的时候不可能是这么做的,你只是采样了少量的 s 跟 a 的对而已。 因为我们做的是采样,有一些动作可能从来都没有采样到。在某一个状态,虽然可以执行的动作有 a/b/c,但你可能只采样到动作 b,你可能只采样到动作 c,你没有采样到动作 a。但现在所有动作的奖励都是正的,所以根据这个式子,它的每一项的概率都应该要上升。你会遇到的问题是,因为 a 没有被采样到,其它动作的概率如果都要上升,a 的概率就下降。 所以 a 不一定是一个不好的动作, 它只是没被采样到。但只是因为它没被采样到, 它的概率就会下降,这个显然是有问题的,要怎么解决这个问题呢?你会希望你的奖励不要总是正的。 + +![1.](img/4.14.png) + +为了解决奖励总是正的这个问题,你可以把奖励减掉一项叫做 b,这项 b 叫做基线。你减掉这项 b 以后,就可以让 $R(\tau^n)-b$ 这一项有正有负。 所以如果得到的总奖励 $R(\tau^n)$ 大于 b 的话,就让它的概率上升。如果这个总奖励小于 b,就算它是正的,正的很小也是不好的,你就要让这一项的概率下降。 如果$R(\tau^n) 独热编码(one-hot Encoding)通常用于处理类别间不具有大小关系的特征。 例如血型,一共有4个取值(A型、B型、AB型、O型),独热编码会把血型变成一个4维稀疏向量,A型血表示为(1,0,0,0),B型血表示为(0,1,0,0),AB型会表示为(0,0,1,0),O型血表示为(0,0,0,1)。 + +![](img/4.23.png) + +* 手写数字识别是一个经典的多分类问题,输入是一张手写数字的图片,经过神经网络输出的是各个类别的一个概率。 +* 目的是希望输出的这个概率的分布尽可能地去贴近真实值的概率分布。 +* 因为真实值只有一个数字 9,你用这个 one-hot 向量的形式去给它编码的话,也可以把这个真实值理解为一个概率分布,9 的概率就是1,其他的概率就是 0。 + +* 神经的网络输出一开始可能会比较平均,通过不断地迭代,训练优化之后,我会希望 9 输出的概率可以远高于其他数字输出的概率。 + +![](img/4.24.png) + +如上图所示,就是提高 9 对应的概率,降低其他数字对应的概率,让神经网络输出的概率能够更贴近这个真实值的概率分布。我们可以用`交叉熵`来去表示两个概率分布之间的差距。 + +![](img/4.25.png) + +我们看一下它的优化流程,就是怎么让这个输出去逼近这个真实值。 + +* 它的优化流程就是将图片作为输入传给神经网络,神经网络会判断这个图片属于哪一类数字,输出所有数字可能的概率,然后再计算这个交叉熵,就是神经网络的输出 $Y_i$ 和真实的标签值 $Y_i'$ 之间的距离 $-\sum Y_{i}^{\prime} \cdot \log \left(Y_{i}\right)$。 +* 我们希望尽可能地缩小这两个概率分布之间的差距,计算出来的交叉熵可以作为这个损失函数传给神经网络里面的优化器去优化,去自动去做神经网络的参数更新。 + +![](img/4.26.png) + +* 类似地,policy gradient 预测每一个状态下面应该要输出的这个行动的概率,就是输入状态 $s_t$,然后输出动作的概率,比如 0.02,0.08,0.9。实际上输出给环境的动作是随机选了一个动作,比如说我选了右这个动作,它的 one-hot 向量就是 0,0,1。 + +* 我们把神经网络的输出和实际动作带入交叉熵的公式就可以求出输出的概率和实际的动作之间的差距。 +* 但这个实际的动作 $a_t$ 只是我们输出的真实的动作,它并不一定是正确的动作,它不能像手写数字识别一样作为一个正确的标签来去指导神经网络朝着正确的方向去更新,所以我们需要乘以一个奖励回报 $G_t$。这个奖励回报相当于是对这个真实动作 的评价。 + * 如果 $G_t$ 越大,未来总收益越大,那就说明当前输出的这个真实的动作就越好,这个 loss 就越需要重视。 + * 如果 $G_t$ 越小,那就说明做这个动作 $a_t$ 并没有那么的好,loss 的权重就要小一点,优化力度就小一点。 +* 通过这个和那个手写输入识别的一个对比,我们就知道为什么 loss 会构造成这个样子。 + +![](img/4.27.png) + +实际上我们在计算这个 loss 的时候,我们要拿到那个 $\ln \pi(A_t|S_t,\theta)$。我就拿实际执行的这个动作,先取个 one-hot 向量,然后再拿到神经网络预测的动作概率,这两个一相乘,我就可以拿到算法里面的那个 $\ln \pi(A_t|S_t,\theta)$。这个就是我们要构造的 loss。因为我们会拿到整个回合的所有的轨迹,所以我们可以对这一条整条轨迹里面的每个动作都去计算一个 loss。把所有的 loss 加起来之后,我们再扔给 adam 的优化器去自动更新参数就好了。 + +![](img/4.28.png) + +上图是 REINFORCE 的流程图。首先我们需要一个 policy model 来输出动作概率,输出动作概率后,我们 `sample()` 函数去得到一个具体的动作,然后跟环境交互过后,我们可以得到一整个回合的数据。拿到回合数据之后,我再去执行一下 `learn()` 函数,在 `learn()` 函数里面,我就可以拿这些数据去构造损失函数,扔给这个优化器去优化,去更新我的 policy model。 + +## References + +* [Intro to Reinforcement Learning (强化学习纲要)](https://github.com/zhoubolei/introRL) +* [神经网络与深度学习](https://nndl.github.io/) +* [百面深度学习](https://book.douban.com/subject/35043939/) + diff --git a/docs/chapter4/chapter4_questions&keywords.md b/docs/chapter4/chapter4_questions&keywords.md new file mode 100644 index 0000000..e1d5786 --- /dev/null +++ b/docs/chapter4/chapter4_questions&keywords.md @@ -0,0 +1,119 @@ +# Chapter4 梯度策略 + +## 1 Keywords + +- **policy(策略):** 每一个actor中会有对应的策略,这个策略决定了actor的行为。具体来说,Policy 就是给一个外界的输入,然后它会输出 actor 现在应该要执行的行为。**一般地,我们将policy写成 $\pi$ 。** +- **Return(回报):** 一个回合(Episode)或者试验(Trial)所得到的所有的reward的总和,也被人们称为Total reward。**一般地,我们用 $R$ 来表示它。** +- **Trajectory:** 一个试验中我们将environment 输出的 $s$ 跟 actor 输出的行为 $a$,把这个 $s$ 跟 $a$ 全部串起来形成的集合,我们称为Trajectory,即 $\text { Trajectory } \tau=\left\{s_{1}, a_{1}, s_{2}, a_{2}, \cdots, s_{t}, a_{t}\right\}$。 +- **Reward function:** 根据在某一个 state 采取的某一个 action 决定说现在这个行为可以得到多少的分数,它是一个 function。也就是给一个 $s_1$,$a_1$,它告诉你得到 $r_1$。给它 $s_2$ ,$a_2$,它告诉你得到 $r_2$。 把所有的 $r$ 都加起来,我们就得到了 $R(\tau)$ ,代表某一个 trajectory $\tau$ 的 reward。 +- **Expected reward:** $\bar{R}_{\theta}=\sum_{\tau} R(\tau) p_{\theta}(\tau)=E_{\tau \sim p_{\theta}(\tau)}[R(\tau)]$。 +- **REINFORCE:** 基于策略梯度的强化学习的经典算法,其采用回合更新的模式。 + +## 2 Questions + +- 如果我们想让机器人自己玩video game, 那么强化学习中三个组成(actor、environment、reward function)部分具体分别是什么? + + 答:actor 做的事情就是去操控游戏的摇杆, 比如说向左、向右、开火等操作;environment 就是游戏的主机, 负责控制游戏的画面负责控制说,怪物要怎么移动, 你现在要看到什么画面等等;reward function 就是当你做什么事情,发生什么状况的时候,你可以得到多少分数, 比如说杀一只怪兽得到 20 分等等。 + +- 在一个process中,一个具体的trajectory $s_1$,$a_1$, $s_2$ , $a_2$ 出现的概率取决于什么? + + 答: + + 1. 一部分是 **environment 的行为**, environment 的 function 它内部的参数或内部的规则长什么样子。 $p(s_{t+1}|s_t,a_t)$这一项代表的是 environment, environment 这一项通常你是无法控制它的,因为那个是人家写好的,或者已经客观存在的。 + + 2. 另一部分是 **agent 的行为**,你能控制的是 $p_\theta(a_t|s_t)$。给定一个 $s_t$, actor 要采取什么样的 $a_t$ 会取决于你 actor 的参数 $\theta$, 所以这部分是 actor 可以自己控制的。随着 actor 的行为不同,每个同样的 trajectory, 它就会有不同的出现的概率。 + +- 当我们在计算 maximize expected reward时,应该使用什么方法? + + 答: **gradient ascent(梯度上升)**,因为要让它越大越好,所以是 gradient ascent。Gradient ascent 在 update 参数的时候要加。要进行 gradient ascent,我们先要计算 expected reward $\bar{R}$ 的 gradient 。我们对 $\bar{R}$ 取一个 gradient,这里面只有 $p_{\theta}(\tau)$ 是跟 $\theta$ 有关,所以 gradient 就放在 $p_{\theta}(\tau)$ 这个地方。 + +- 我们应该如何理解梯度策略的公式呢? + + 答: + $$ + \begin{aligned} + E_{\tau \sim p_{\theta}(\tau)}\left[R(\tau) \nabla \log p_{\theta}(\tau)\right] &\approx \frac{1}{N} \sum_{n=1}^{N} R\left(\tau^{n}\right) \nabla \log p_{\theta}\left(\tau^{n}\right) \\ + &=\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}} R\left(\tau^{n}\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) + \end{aligned} + $$ + $p_{\theta}(\tau)$ 里面有两项,$p(s_{t+1}|s_t,a_t)$ 来自于 environment,$p_\theta(a_t|s_t)$ 是来自于 agent。 $p(s_{t+1}|s_t,a_t)$ 由环境决定从而与 $\theta$ 无关,因此 $\nabla \log p(s_{t+1}|s_t,a_t) =0 $。因此 $\nabla p_{\theta}(\tau)= + \nabla \log p_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right)$。 公式的具体推导可见我们的教程。 + + 具体来说: + + * 假设你在 $s_t$ 执行 $a_t$,最后发现 $\tau$ 的 reward 是正的, 那你就要增加这一项的概率,即增加在 $s_t$ 执行 $a_t$ 的概率。 + * 反之,在 $s_t$ 执行 $a_t$ 会导致$\tau$ 的 reward 变成负的, 你就要减少这一项的概率。 + +- 我们可以使用哪些方法来进行gradient ascent的计算? + + 答:用 gradient ascent 来 update 参数,对于原来的参数 $\theta$ ,可以将原始的 $\theta$ 加上更新的 gradient 这一项,再乘以一个 learning rate,learning rate 其实也是要调的,和神经网络一样,我们可以使用 Adam、RMSProp 等优化器对其进行调整。 + +- 我们进行基于梯度策略的优化时的小技巧有哪些? + + 答: + + 1. **Add a baseline:**为了防止所有的reward都大于0,从而导致每一个stage和action的变换,会使得每一项的概率都会上升。所以通常为了解决这个问题,我们把reward 减掉一项叫做 b,这项 b 叫做 baseline。你减掉这项 b 以后,就可以让 $R(\tau^n)-b$ 这一项, 有正有负。 所以如果得到的 total reward $R(\tau^n)$ 大于 b 的话,就让它的概率上升。如果这个 total reward 小于 b,就算它是正的,正的很小也是不好的,你就要让这一项的概率下降。 如果$R(\tau^n) 对于ー个随机变量,通常用概率密度函数来刻画该变量的概率分布特性。具体来说,给定随机变量的一个取值,可以根据概率密度函数来计算该值对应的概率(密度)。反过来,也可以根据概率密度函数提供的概率分布信息来生成随机变量的一个取值,这就是采样。因此,从某种意义上来说,采样是概率密度函数的逆向应用。与根据概率密度函数计算样本点对应的概率值不同,采样过程往往没有那么直接,通常需要根据待采样分布的具体特点来选择合适的采样策略。 + +假设你有一个函数 $f(x)$,你要计算从 p 这个分布采样 $x$,再把 $x$ 带到 $f$ 里面,得到 $f(x)$。你要该怎么计算这个 $f(x)$ 的期望值?假设你不能对 p 这个分布做积分的话,那你可以从 p 这个分布去采样一些数据 $x^i$。把 $x^i$ 代到 $f(x)$ 里面,然后取它的平均值,就可以近似 $f(x)$ 的期望值。 + +现在有另外一个问题,我们没有办法从 p 这个分布里面采样数据。假设我们不能从 p 采样数据,只能从另外一个分布 q 去采样数据,q 可以是任何分布。我们不能够从 p 去采样数据,但可以从 q 去采样 $x$。我们从 q 去采样 $x^i$ 的话就不能直接套下面的式子: +$$ +E_{x \sim p}[f(x)] \approx \frac{1}{N} \sum_{i=1}^N f(x^i) +$$ +因为上式是假设你的 $x$ 都是从 p 采样出来的。 + +所以做一个修正,修正是这样子的。期望值 $E_{x \sim p}[f(x)]$ 其实就是 $\int f(x) p(x) dx$,我们对其做如下的变换: +$$ +\int f(x) p(x) d x=\int f(x) \frac{p(x)}{q(x)} q(x) d x=E_{x \sim q}[f(x){\frac{p(x)}{q(x)}}] +$$ +我们就可以写成对 q 里面所采样出来的 $x$ 取期望值。我们从 q 里面采样 $x$,然后再去计算 $f(x) \frac{p(x)}{q(x)}$,再去取期望值。所以就算我们不能从 p 里面去采样数据,只要能够从 q 里面去采样数据,然后代入上式,你就可以计算从 p 这个分布采样 $x$ 代入 $f$ 以后所算出来的期望值。 + +这边是从 q 做采样,所以从 q 里采样出来的每一笔数据,你需要乘上一个`重要性权重(importance weight)` $\frac{p(x)}{q(x)}$ 来修正这两个分布的差异。$q(x)$ 可以是任何分布,唯一的限制情况就是 $q(x)$ 的概率是 0 的时候,$p(x)$ 的概率不为 0,这样会没有定义。假设 $q(x)$ 的概率是 0 的时候,$p(x)$ 的概率也都是 0 的话,那这样 $p(x)$ 除以 $q(x)$是有定义的。所以这个时候你就可以使用重要性采样这个技巧。你就可以从 p 做采样换成从 q 做采样。 + +**重要性采样有一些问题。**虽然理论上你可以把 p 换成任何的 q。但是在实现上,p 和 q 不能差太多。差太多的话,会有一些问题。什么样的问题呢? +$$ +E_{x \sim p}[f(x)]=E_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right] +$$ +虽然上式成立(上式左边是 $f(x)$ 的期望值,它的分布是 p,上式右边是 $f(x) \frac{p(x)}{q(x)}$ 的期望值,它的分布是 q),但如果不是算期望值,而是算方差的话,$\operatorname{Var}_{x \sim p}[f(x)]$ 和 $\operatorname{Var}_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right]$ 是不一样的。两个随机变量的平均值一样,并不代表它的方差一样。 + +我们可以代一下方差的公式 $\operatorname{Var}[X]=E\left[X^{2}\right]-(E[X])^{2}$,然后得到下式: +$$ +\operatorname{Var}_{x \sim p}[f(x)]=E_{x \sim p}\left[f(x)^{2}\right]-\left(E_{x \sim p}[f(x)]\right)^{2} +$$ + +$$ +\begin{aligned} +\operatorname{Var}_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right] &=E_{x \sim q}\left[\left(f(x) \frac{p(x)}{q(x)}\right)^{2}\right]-\left(E_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right]\right)^{2} \\ +&=E_{x \sim p}\left[f(x)^{2} \frac{p(x)}{q(x)}\right]-\left(E_{x \sim p}[f(x)]\right)^{2} +\end{aligned} +$$ + +$\operatorname{Var}_{x \sim p}[f(x)]$ 和 $\operatorname{Var}_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right]$ 的差别在第一项是不同的, $\operatorname{Var}_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right]$ 的第一项多乘了$\frac{p(x)}{q(x)}$,如果 $\frac{p(x)}{q(x)}$ 差距很大的话,$f(x)\frac{p(x)}{q(x)}$ 的方差就会很大。所以理论上它们的期望值一样,也就是说,你只要对 p 这个分布采样够多次,q 这个分布采样够多,你得到的结果会是一样的。但是如果你采样的次数不够多,因为它们的方差差距是很大的,所以你就有可能得到非常大的差别。 + +![](img/5.4.png) + +举个例子,当 $p(x)$ 和 $q(x)$ 差距很大的时候,会发生什么样的问题。 + +假设蓝线是 $p(x)$ 的分布,绿线是 $q(x)$ 的分布,红线是 $f(x)$。如果我们要计算 $f(x)$的期望值,从 $p(x)$ 这个分布做采样的话,那显然 $E_{x \sim p}[f(x)]$ 是负的,因为左边那块区域 $p(x)$ 的概率很高,所以要采样的话,都会采样到这个地方,而 $f(x)$ 在这个区域是负的, 所以理论上这一项算出来会是负。 + +接下来我们改成从 $q(x)$ 这边做采样,因为 $q(x)$ 在右边这边的概率比较高,所以如果你采样的点不够的话,那你可能都只采样到右侧。如果你都只采样到右侧的话,你会发现说,算 $E_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right]$这一项,搞不好还应该是正的。你这边采样到这些点,然后你去计算它们的 $f(x) \frac{p(x)}{q(x)}$ 都是正的。你采样到这些点都是正的。 你取期望值以后也都是正的,这是因为你采样的次数不够多。假设你采样次数很少,你只能采样到右边这边。左边虽然概率很低,但也不是没有可能被采样到。假设你今天好不容易采样到左边的点,因为左边的点,$p(x)$ 和 $q(x)$ 是差很多的, 这边 $p(x)$ 很大,$q(x)$ 很小。今天 $f(x)$ 好不容易终于采样到一个负的,这个负的就会被乘上一个非常大的权重,这样就可以平衡掉刚才那边一直采样到正的值的情况。最终你算出这一项的期望值,终究还是负的。但前提是你要采样够多次,这件事情才会发生。**但有可能采样次数不够多,$E_{x \sim p}[f(x)]$ 跟 $E_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right]$ 就有可能有很大的差距。这就是重要性采样的问题。** + +![](img/5.5.png) + +现在要做的事情就是把重要性采样用在 off-policy 的情况,把 on-policy 训练的算法改成 off-policy 训练的算法。 + +怎么改呢,之前我们是拿 $\theta$ 这个 policy 去跟环境做互动,采样出轨迹 $\tau$,然后计算 $R(\tau) \nabla \log p_{\theta}(\tau)$。现在我们不用 $\theta$ 去跟环境做互动,假设有另外一个 policy $\theta'$,它就是另外一个 actor。它的工作是去做示范(demonstration)。$\theta'$ 的工作是要去示范给 $\theta$ 看。它去跟环境做互动,告诉 $\theta$ 说,它跟环境做互动会发生什么事,借此来训练 $\theta$。我们要训练的是 $\theta$ ,$\theta'$ 只是负责做示范,跟环境做互动。 + +我们现在的 $\tau$ 是从 $\theta'$ 采样出来的,是拿 $\theta'$ 去跟环境做互动。所以采样出来的 $\tau$ 是从 $\theta'$ 采样出来的,这两个分布不一样。但没有关系,假设你本来是从 p 做采样,但你发现你不能从 p 做采样,所以我们不拿 $\theta$ 去跟环境做互动。你可以把 p 换 q,然后在后面补上一个重要性权重。现在的状况就是一样,把 $\theta$ 换成 $\theta'$ 后,要补上一个重要性权重 $\frac{p_{\theta}(\tau)}{p_{\theta^{\prime}}(\tau)}$。这个重要性权重就是某一个轨迹 $\tau$ 用 $\theta$ 算出来的概率除以这个轨迹 $\tau$ 用 $\theta'$ 算出来的概率。这一项是很重要的,因为你要学习的是 actor $\theta$ 和 $\theta'$ 是不太一样的,$\theta'$ 会见到的情形跟 $\theta$ 见到的情形不见得是一样的,所以中间要做一个修正的项。 + +Q: 现在的数据是从 $\theta'$ 采样出来的,从 $\theta$ 换成 $\theta'$ 有什么好处? + +A: 因为现在跟环境做互动是 $\theta'$ 而不是 $\theta$。所以采样出来的东西跟 $\theta$ 本身是没有关系的。所以你就可以让 $\theta'$ 做互动采样一大堆的数据,$\theta$ 可以更新参数很多次,一直到 $\theta$ 训练到一定的程度,更新很多次以后,$\theta'$ 再重新去做采样,这就是 on-policy 换成 off-policy 的妙用。 + +![](img/5.6.png) + +实际在做 policy gradient 的时候,我们并不是给整个轨迹 $\tau$ 都一样的分数,而是每一个状态-动作的对(pair)会分开来计算。实际上更新梯度的时候,如下式所示。 +$$ +=E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta}}\left[A^{\theta}\left(s_{t}, a_{t}\right) \nabla \log p_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right)\right] +$$ + +我们用 $\theta$ 这个 actor 去采样出 $s_t$ 跟 $a_t$,采样出状态跟动作的对,我们会计算这个状态跟动作对的 advantage $A^{\theta}\left(s_{t}, a_{t}\right)$, 就是它有多好。 + +$A^{\theta}\left(s_{t}, a_{t}\right)$ 就是累积奖励减掉 bias,这一项就是估测出来的。它要估测的是,在状态 $s_t$ 采取动作 $a_t$ 是好的还是不好的。接下来后面会乘上 $\nabla \log p_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right)$,也就是说如果 $A^{\theta}\left(s_{t}, a_{t}\right)$ 是正的,就要增加概率, 如果是负的,就要减少概率。 + +我们通过重要性采样把 on-policy 变成 off-policy,从 $\theta$ 变成 $\theta'$。所以现在 $s_t$、$a_t$ 是 $\theta'$ 跟环境互动以后所采样到的数据。 但是拿来训练要调整参数是模型 $\theta$。因为 $\theta'$ 跟 $\theta$ 是不同的模型,所以你要做一个修正的项。这项修正的项,就是用重要性采样的技术,把 $s_t$、$a_t$ 用 $\theta$ 采样出来的概率除掉 $s_t$、$a_t$ 用 $\theta'$ 采样出来的概率。 + +$$ +=E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(s_{t}, a_{t}\right)}{p_{\theta^{\prime}}\left(s_{t}, a_{t}\right)} A^{\theta}\left(s_{t}, a_{t}\right) \nabla \log p_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right)\right] +$$ + +$A^{\theta}(s_t,a_t)$ 有一个上标 $\theta$,$\theta$ 代表说这个是 actor $\theta$ 跟环境互动的时候所计算出来的 A。但是实际上从 $\theta$ 换到 $\theta'$ 的时候,$A^{\theta}(s_t,a_t)$ 应该改成 $A^{\theta'}(s_t,a_t)$,为什么?A 这一项是想要估测说现在在某一个状态采取某一个动作,接下来会得到累积奖励的值减掉 baseline 。你怎么估 A 这一项,你就会看在状态 $s_t$,采取动作 $a_t$,接下来会得到的奖励的总和,再减掉 baseline。之前是 $\theta$ 在跟环境做互动,所以你观察到的是 $\theta$ 可以得到的奖励。但现在是 $\theta'$ 在跟环境做互动,所以你得到的这个 advantage, 其实是根据 $\theta'$ 所估计出来的 advantage。但我们现在先不要管那么多,我们就假设这两项可能是差不多的。 + +接下来,我们可以拆解 $p_{\theta}\left(s_{t}, a_{t}\right)$ 和 $p_{\theta'}\left(s_{t}, a_{t}\right)$,即 +$$ +\begin{aligned} +p_{\theta}\left(s_{t}, a_{t}\right)&=p_{\theta}\left(a_{t}|s_{t}\right) p_{\theta}(s_t) \\ +p_{\theta'}\left(s_{t}, a_{t}\right)&=p_{\theta'}\left(a_{t}|s_{t}\right) p_{\theta'}(s_t) +\end{aligned} +$$ +于是我们得到下式: +$$ +=E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} | s_{t}\right)} \frac{p_{\theta}\left(s_{t}\right)}{p_{\theta^{\prime}}\left(s_{t}\right)} A^{\theta^{\prime}}\left(s_{t}, a_{t}\right) \nabla \log p_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right)\right] +$$ + + +这边需要做一件事情是,假设模型是 $\theta$ 的时候,你看到 $s_t$ 的概率,跟模型是 $\theta'$ 的时候,你看到 $s_t$ 的概率是差不多的,即 $p_{\theta}(s_t)=p_{\theta'}(s_t)$。因为它们是一样的,所以你可以把它删掉,即 +$$ +=E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} | s_{t}\right)} A^{\theta^{\prime}}\left(s_{t}, a_{t}\right) \nabla \log p_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right)\right] \tag{1} +$$ + +Q: 为什么可以假设 $p_{\theta}(s_t)$ 和 $p_{\theta'}(s_t)$ 是差不多的? + +A: 因为你会看到什么状态往往跟你会采取什么样的动作是没有太大的关系的。比如说你玩不同的 Atari 的游戏,其实你看到的游戏画面都是差不多的,所以也许不同的 $\theta$ 对 $s_t$ 是没有影响的。但更直觉的理由就是 $p_{\theta}(s_t)$ 很难算,想想看这项要怎么算,这一项你还要说我有一个参数 $\theta$,然后拿 $\theta$ 去跟环境做互动,算 $s_t$ 出现的概率,这个你很难算。尤其如果输入是图片的话, 同样的 $s_t$ 根本就不会出现第二次。你根本没有办法估这一项, 所以干脆就无视这个问题。 + +但是 $p_{\theta}(a_t|s_t)$很好算。你手上有 $\theta$ 这个参数,它就是个网络。你就把 $s_t$ 带进去,$s_t$ 就是游戏画面,你把游戏画面带进去,它就会告诉你某一个状态的 $a_t$ 概率是多少。我们有个 policy 的网络,把 $s_t$ 带进去,它会告诉我们每一个 $a_t$ 的概率是多少。所以 $\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} | s_{t}\right)}$ 这一项,你只要知道 $\theta$ 和 $\theta'$ 的参数就可以算。 + +现在我们得到一个新的目标函数。 + +$$ +J^{\theta^{\prime}}(\theta)=E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} | s_{t}\right)} A^{\theta^{\prime}}\left(s_{t}, a_{t}\right)\right] +$$ + + +式(1)是梯度,其实我们可以从梯度去反推原来的目标函数,我们可以用如下的公式来反推目标函数: + +$$ +\nabla f(x)=f(x) \nabla \log f(x) +$$ + +要注意一点,对 $\theta$ 求梯度时,$p_{\theta^{\prime}}(a_{t} | s_{t})$ 和 $A^{\theta^{\prime}}\left(s_{t}, a_{t}\right)$ 都是常数。 + + +所以实际上,当我们使用重要性采样的时候,要去优化的那一个目标函数就长这样子,我们把它写作 $J^{\theta^{\prime}}(\theta)$。为什么写成 $J^{\theta^{\prime}}(\theta)$ 呢,这个括号里面那个 $\theta$ 代表我们要去优化的那个参数。$\theta'$ 是说我们拿 $\theta'$ 去做示范,就是现在真正在跟环境互动的是 $\theta'$。因为 $\theta$ 不跟环境做互动,是 $\theta'$ 在跟环境互动。 + +然后你用 $\theta'$ 去跟环境做互动,采样出 $s_t$、$a_t$ 以后,你要去计算 $s_t$ 跟 $a_t$ 的 advantage,然后你再去把它乘上 $\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} | s_{t}\right)}$。$\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} | s_{t}\right)}$ 是好算的,$A^{\theta^{\prime}}\left(s_{t}, a_{t}\right)$ 可以从这个采样的结果里面去估测出来的,所以 $J^{\theta^{\prime}}(\theta)$ 是可以算的。实际上在更新参数的时候,就是按照式(1) 来更新参数。 + +## PPO + +![](img/5.7.png) + +我们可以通过重要性采样把 on-policy 换成 off-policy,但重要性采样有一个问题:如果 $p_{\theta}\left(a_{t} | s_{t}\right)$ 跟 $p_{\theta'}\left(a_{t} | s_{t}\right)$ 这两个分布差太多的话,重要性采样的结果就会不好。怎么避免它差太多呢?这个就是 `Proximal Policy Optimization (PPO) ` 在做的事情。**注意,由于在 PPO 中 $\theta'$ 是 $\theta_{\text{old}}$,即 behavior policy 也是 $\theta$,所以 PPO 是 on-policy 的算法**。 + +PPO 实际上做的事情就是这样,在 off-policy 的方法里要优化的是 $J^{\theta^{\prime}}(\theta)$。但是这个目标函数又牵涉到重要性采样。在做重要性采样的时候,$p_{\theta}\left(a_{t} | s_{t}\right)$ 不能跟 $p_{\theta'}\left(a_{t} | s_{t}\right)$差太多。你做示范的模型不能够跟真正的模型差太多,差太多的话,重要性采样的结果就会不好。我们在训练的时候,多加一个约束(constrain)。这个约束是 $\theta$ 跟 $\theta'$ 输出的动作的 KL 散度(KL divergence),简单来说,这一项的意思就是要衡量说 $\theta$ 跟 $\theta'$ 有多像。 + +然后我们希望在训练的过程中,学习出来的 $\theta$ 跟 $\theta'$ 越像越好。因为如果 $\theta$ 跟 $\theta'$ 不像的话,最后的结果就会不好。所以在 PPO 里面有两个式子,一方面是优化本来要优化的东西,但再加一个约束。这个约束就好像正则化(regularization) 的项(term) 一样,在做机器学习的时候不是有 L1/L2 的正则化。这一项也很像正则化,这样正则化做的事情就是希望最后学习出来的 $\theta$ 不要跟 $\theta'$ 太不一样。 + +PPO 有一个前身叫做`信任区域策略优化(Trust Region Policy Optimization,TRPO)`,TRPO 的式子如下式所示: + +$$ +\begin{aligned} +J_{T R P O}^{\theta^{\prime}}(\theta)=E_{\left(s_{t}, a_{t}\right) \sim \pi_{\theta^{\prime}}}\left[\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{\prime}}\left(a_{t} | s_{t}\right)} A^{\theta^{\prime}}\left(s_{t}, a_{t}\right)\right] \\ \\ + \mathrm{KL}\left(\theta, \theta^{\prime}\right)<\delta +\end{aligned} +$$ + +它与 PPO 不一样的地方是约束摆的位置不一样,PPO 是直接把约束放到你要优化的那个式子里面,然后你就可以用梯度上升的方法去最大化这个式子。但 TRPO 是把 KL 散度当作约束,它希望 $\theta$ 跟 $\theta'$ 的 KL 散度小于一个 $\delta$。如果你使用的是基于梯度的优化时,有约束是很难处理的。 + +TRPO 是很难处理的,因为它把 KL 散度约束当做一个额外的约束,没有放目标(objective)里面,所以它很难算,所以一般就用 PPO 而不是 TRPO。看文献上的结果是,PPO 跟 TRPO 性能差不多,但 PPO 在实现上比 TRPO 容易的多。 + +Q: KL 散度到底指的是什么? + +A: + +这边我是直接把 KL 散度当做一个函数,输入是 $\theta$ 跟 $\theta'$,但我的意思并不是说把 $\theta$ 或 $\theta'$ 当做一个分布,算这两个分布之间的距离。所谓的 $\theta$ 跟 $\theta'$ 的距离并不是参数上的距离,而是行为(behavior)上的距离。 + +假设你有两个 actor,它们的参数分别为 $\theta$ 和 $\theta'$,所谓参数上的距离就是你算这两组参数有多像。这里讲的不是参数上的距离, 而是它们行为上的距离。你先代进去一个状态 s,它会对这个动作的空间输出一个分布。假设你有 3 个动作,3 个可能的动作就输出 3 个值。今天所指的距离是行为距离(behavior distance),也就是说,给定同样的状态,输出动作之间的差距。这两个动作的分布都是一个概率分布,所以就可以计算这两个概率分布的 KL 散度。把不同的状态输出的这两个分布的 KL 散度平均起来才是我这边所指的两个 actor 间的 KL 散度。 + +Q: 为什么不直接算 $\theta$ 和 $\theta'$ 之间的距离?算这个距离的话,甚至不要用 KL 散度算,L1 跟 L2 的范数(norm)也可以保证 $\theta$ 跟 $\theta'$ 很接近。 + +A: 在做强化学习的时候,之所以我们考虑的不是参数上的距离,而是动作上的距离,是因为很有可能对 actor 来说,参数的变化跟动作的变化不一定是完全一致的。有时候你参数小小变了一下,它可能输出的行为就差很多。或者是参数变很多,但输出的行为可能没什么改变。**所以我们真正在意的是这个 actor 的行为上的差距,而不是它们参数上的差距。**所以在做 PPO 的时候,所谓的 KL 散度并不是参数的距离,而是动作的距离。 + +### PPO-Penalty + +![](img/5.8.png) + +**PPO 算法有两个主要的变种:PPO-Penalty 和 PPO-Clip。** + +我们来看一下 `PPO1` 的算法,即 `PPO-Penalty`。它先初始化一个 policy 的参数 $\theta^0$。然后在每一个迭代里面,你要用参数 $\theta^k$,$\theta^k$ 就是你在前一个训练的迭代得到的 actor 的参数,你用 $\theta^k$ 去跟环境做互动,采样到一大堆状态-动作的对。 + +然后你根据 $\theta^k$ 互动的结果,估测一下 $A^{\theta^{k}}\left(s_{t}, a_{t}\right)$。然后你就使用 PPO 的优化的公式。但跟原来的 policy gradient 不一样,原来的 policy gradient 只能更新一次参数,更新完以后,你就要重新采样数据。但是现在不用,你拿 $\theta^k$ 去跟环境做互动,采样到这组数据以后,你可以让 $\theta$ 更新很多次,想办法去最大化目标函数。这边 $\theta$ 更新很多次没有关系,因为我们已经有做重要性采样,所以这些经验,这些状态-动作的对是从 $\theta^k$ 采样出来的没有关系。$\theta$ 可以更新很多次,它跟 $\theta^k$ 变得不太一样也没有关系,你还是可以照样训练 $\theta$。 + +![](img/5.9.png) + +在 PPO 的论文里面还有一个 `adaptive KL divergence`。这边会遇到一个问题就是 $\beta$ 要设多少,它就跟正则化一样。正则化前面也要乘一个权重,所以这个 KL 散度前面也要乘一个权重,但 $\beta$ 要设多少呢?所以有个动态调整 $\beta$ 的方法。 + +* 在这个方法里面,你先设一个你可以接受的 KL 散度的最大值。假设优化完这个式子以后,你发现 KL 散度的项太大,那就代表说后面这个惩罚的项没有发挥作用,那就把 $\beta$ 调大。 +* 另外,你设一个 KL 散度的最小值。如果优化完上面这个式子以后,你发现 KL 散度比最小值还要小,那代表后面这一项的效果太强了,你怕他只弄后面这一项,那 $\theta$ 跟 $\theta^k$ 都一样,这不是你要的,所以你要减少 $\beta$。 + +所以 $\beta$ 是可以动态调整的。这个叫做 `adaptive KL penalty`。 + +### PPO-Clip + +![](img/5.10.png) + +如果你觉得算 KL 散度很复杂,有一个`PPO2`,PPO2 即 `PPO-Clip`。PPO2 要去最大化的目标函数如下式所示,它的式子里面就没有 KL 散度 。 +$$ +\begin{aligned} +J_{P P O 2}^{\theta^{k}}(\theta) \approx \sum_{\left(s_{t}, a_{t}\right)} \min &\left(\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)} A^{\theta^{k}}\left(s_{t}, a_{t}\right),\right.\\ +&\left.\operatorname{clip}\left(\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)}, 1-\varepsilon, 1+\varepsilon\right) A^{\theta^{k}}\left(s_{t}, a_{t}\right)\right) +\end{aligned} +$$ +这个式子看起来有点复杂,但实际实现就很简单。我们来看一下这个式子到底是什么意思。 + +* Min 这个操作符(operator)做的事情是第一项跟第二项里面选比较小的那个。 +* 第二项前面有个 clip 函数,clip 函数的意思是说, + * 在括号里面有三项,如果第一项小于第二项的话,那就输出 $1-\varepsilon$ 。 + * 第一项如果大于第三项的话,那就输出 $1+\varepsilon$。 +* $\varepsilon$ 是一个超参数,你要 tune 的,你可以设成 0.1 或 设 0.2 。 + +假设这边设 0.2 的话,如下式所示 +$$ +\operatorname{clip}\left(\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)}, 0.8, 1.2\right) +$$ + +如果 $\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)}$ 算出来小于 0.8,那就当作 0.8。如果算出来大于 1.2,那就当作1.2。 + +我们先看看下面这项这个算出来到底是什么东西: +$$ +\operatorname{clip}\left(\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)}, 1-\varepsilon, 1+\varepsilon\right) +$$ + +![](img/5.11.png ':size=450') + +上图的横轴是 $\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)}$,纵轴是 clip 函数的输出。 + +* 如果 $\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)}$ 大于$1+\varepsilon$,输出就是 $1+\varepsilon$。 +* 如果小于 $1-\varepsilon$, 它输出就是 $1-\varepsilon$。 +* 如果介于 $1+\varepsilon$ 跟 $1-\varepsilon$ 之间, 就是输入等于输出。 + +![](img/5.12.png ':size=450') + +* $\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)}$ 是绿色的线; +* $\operatorname{clip}\left(\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)}, 1-\varepsilon, 1+\varepsilon\right)$ 是蓝色的线; +* 在绿色的线跟蓝色的线中间,我们要取一个最小的。假设前面乘上的这个项 A,它是大于 0 的话,取最小的结果,就是红色的这一条线。 + +![](img/5.13.png ':size=450') + +如果 A 小于 0 的话,取最小的以后,就得到红色的这一条线。 + +![](img/5.14.png ':size=500') + +虽然这个式子看起来有点复杂,实现起来是蛮简单的,**因为这个式子想要做的事情就是希望 $p_{\theta}(a_{t} | s_{t})$ 跟 $p_{\theta^k}(a_{t} | s_{t})$,也就是你拿来做示范的模型跟你实际上学习的模型,在优化以后不要差距太大。** + +**怎么让它做到不要差距太大呢?** + +* 如果 A > 0,也就是某一个状态-动作的对是好的,那我们希望增加这个状态-动作对的概率。也就是说,我们想要让 $p_{\theta}(a_{t} | s_{t})$ 越大越好,但它跟 $p_{\theta^k}(a_{t} | s_{t})$ 的比值不可以超过 $1+\varepsilon$。如果超过 $1+\varepsilon$ 的话,就没有 benefit 了。红色的线就是我们的目标函数,我们希望目标越大越好,我们希望 $p_{\theta}(a_{t} | s_{t})$ 越大越好。但是 $\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)}$ 只要大过 $1+\varepsilon$,就没有 benefit 了。所以今天在训练的时候,当 $p_{\theta}(a_{t} | s_{t})$ 被训练到 $\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)}>1+\varepsilon$ 时,它就会停止。假设 $p_{\theta}(a_{t} | s_{t})$ 比 $p_{\theta^k}(a_{t} | s_{t})$ 还要小,并且这个 advantage 是正的。因为这个动作是好的,我们当然希望这个动作被采取的概率越大越好,我们希望 $p_{\theta}(a_{t} | s_{t})$ 越大越好。所以假设 $p_{\theta}(a_{t} | s_{t})$ 还比 $p_{\theta^k}(a_{t} | s_{t})$ 小,那就尽量把它挪大,但只要大到 $1+\varepsilon$ 就好。 +* 如果 A < 0,也就是某一个状态-动作对是不好的,我们希望把 $p_{\theta}(a_{t} | s_{t})$ 减小。如果 $p_{\theta}(a_{t} | s_{t})$ 比 $p_{\theta^k}(a_{t} | s_{t})$ 还大,那你就尽量把它压小,压到 $\frac{p_{\theta}\left(a_{t} | s_{t}\right)}{p_{\theta^{k}}\left(a_{t} | s_{t}\right)}$ 是 $1-\epsilon$ 的时候就停了,就不要再压得更小。 + +这样的好处就是,你不会让 $p_{\theta}(a_{t} | s_{t})$ 跟 $p_{\theta^k}(a_{t} | s_{t})$ 差距太大。要实现这个东西,很简单。 + +![](img/5.15.png) +上图是 PPO 跟其它方法的比较。Actor-Critic 和 A2C+Trust Region 方法是基于 actor-critic 的方法。PPO 是紫色线的方法,这边每张图就是某一个 RL 的任务,你会发现说在多数的情况(cases)里面,PPO 都是不错的,不是最好的,就是第二好的。 + +## References + +* [OpenAI Spinning Up ](https://spinningup.openai.com/en/latest/spinningup/rl_intro.html#) +* [百面机器学习](https://book.douban.com/subject/30285146/) + + + diff --git a/docs/chapter5/chapter5_questions&keywords.md b/docs/chapter5/chapter5_questions&keywords.md new file mode 100644 index 0000000..f260a6d --- /dev/null +++ b/docs/chapter5/chapter5_questions&keywords.md @@ -0,0 +1,51 @@ +# Chapter5 Proximal Policy Optimization(PPO) + +## 1 Keywords + +- **on-policy(同策略):** 要learn的agent和环境互动的agent是同一个时,对应的policy。 +- **off-policy(异策略):** 要learn的agent和环境互动的agent不是同一个时,对应的policy。 +- **important sampling(重要性采样):** 使用另外一种数据分布,来逼近所求分布的一种方法,在强化学习中通常和蒙特卡罗方法结合使用,公式如下:$\int f(x) p(x) d x=\int f(x) \frac{p(x)}{q(x)} q(x) d x=E_{x \sim q}[f(x){\frac{p(x)}{q(x)}}]=E_{x \sim p}[f(x)]$ 我们在已知 $q$ 的分布后,可以使用上述公式计算出从 $p$ 这个distribution sample x 代入 $f$ 以后所算出来的期望值。 +- **Proximal Policy Optimization (PPO):** 避免在使用important sampling时由于在 $\theta$ 下的 $p_{\theta}\left(a_{t} | s_{t}\right)$ 跟 在 $\theta '$ 下的 $p_{\theta'}\left(a_{t} | s_{t}\right)$ 差太多,导致important sampling结果偏差较大而采取的算法。具体来说就是在training的过程中增加一个constrain,这个constrain对应着 $\theta$ 跟 $\theta'$ output 的 action 的 KL divergence,来衡量 $\theta$ 与 $\theta'$ 的相似程度。 + +## 2 Questions + +- 基于on-policy的policy gradient有什么可改进之处?或者说其效率较低的原因在于? + + 答: + + - 经典policy gradient的大部分时间花在sample data处,即当我们的agent与环境做了交互后,我们就要进行policy model的更新。但是对于一个回合我们仅能更新policy model一次,更新完后我们就要花时间去重新collect data,然后才能再次进行如上的更新。 + + - 所以我们的可以自然而然地想到,使用off-policy方法使用另一个不同的policy和actor,与环境进行互动并用collect data进行原先的policy的更新。这样等价于使用同一组data,在同一个回合,我们对于整个的policy model更新了多次,这样会更加有效率。 + +- 使用important sampling时需要注意的问题有哪些。 + + 答:我们可以在important sampling中将 $p$ 替换为任意的 $q$,但是本质上需要要求两者的分布不能差的太多,即使我们补偿了不同数据分布的权重 $\frac{p(x)}{q(x)}$ 。 $E_{x \sim p}[f(x)]=E_{x \sim q}\left[f(x) \frac{p(x)}{q(x)}\right]$ 当我们对于两者的采样次数都比较多时,最终的结果时一样的,没有影响的。但是通常我们不会取理想的数量的sample data,所以如果两者的分布相差较大,最后结果的variance差距(平方级)将会很大。 + +- 基于off-policy的importance sampling中的 data 是从 $\theta'$ sample 出来的,从 $\theta$ 换成 $\theta'$ 有什么优势? + + 答:使用off-policy的importance sampling后,我们不用 $\theta$ 去跟环境做互动,假设有另外一个 policy $\theta'$,它就是另外一个actor。它的工作是他要去做demonstration,$\theta'$ 的工作是要去示范给 $\theta$ 看。它去跟环境做互动,告诉 $\theta$ 说,它跟环境做互动会发生什么事。然后,借此来训练$\theta$。我们要训练的是 $\theta$ ,$\theta'$ 只是负责做 demo,负责跟环境做互动,所以 sample 出来的东西跟 $\theta$ 本身是没有关系的。所以你就可以让 $\theta'$ 做互动 sample 一大堆的data,$\theta$ 可以update 参数很多次。然后一直到 $\theta$ train 到一定的程度,update 很多次以后,$\theta'$ 再重新去做 sample,这就是 on-policy 换成 off-policy 的妙用。 + +- 在本节中PPO中的KL divergence指的是什么? + + 答:本质来说,KL divergence是一个function,其度量的是两个action (对应的参数分别为$\theta$ 和 $\theta'$ )间的行为上的差距,而不是参数上的差距。这里行为上的差距(behavior distance)可以理解为在相同的state的情况下,输出的action的差距(他们的概率分布上的差距),这里的概率分布即为KL divergence。 + + +## 3 Something About Interview + +- 高冷的面试官:请问什么是重要性采样呀? + + 答:使用另外一种数据分布,来逼近所求分布的一种方法,算是一种期望修正的方法,公式是: + $$\begin{aligned} + \int f(x) p(x) d x &= \int f(x) \frac{p(x)}{q(x)} q(x) d x \\ + &= E_{x \sim q}[f(x){\frac{p(x)}{q(x)}}] \\ + &= E_{x \sim p}[f(x)] + \end{aligned}$$ + 我们在已知 $q$ 的分布后,可以使用上述公式计算出从 $p$ 分布的期望值。也就可以使用 $q$ 来对于 $p$ 进行采样了,即为重要性采样。 + +- 高冷的面试官:请问on-policy跟off-policy的区别是什么? + + 答:用一句话概括两者的区别,生成样本的policy(value-funciton)和网络参数更新时的policy(value-funciton)是否相同。具体来说,on-policy:生成样本的policy(value function)跟网络更新参数时使用的policy(value function)相同。SARAS算法就是on-policy的,基于当前的policy直接执行一次action,然后用这个样本更新当前的policy,因此生成样本的policy和学习时的policy相同,算法为on-policy算法。该方法会遭遇探索-利用的矛盾,仅利用目前已知的最优选择,可能学不到最优解,收敛到局部最优,而加入探索又降低了学习效率。epsilon-greedy 算法是这种矛盾下的折衷。优点是直接了当,速度快,劣势是不一定找到最优策略。off-policy:生成样本的policy(value function)跟网络更新参数时使用的policy(value function)不同。例如,Q-learning在计算下一状态的预期收益时使用了max操作,直接选择最优动作,而当前policy并不一定能选择到最优动作,因此这里生成样本的policy和学习时的policy不同,即为off-policy算法。 + +- 高冷的面试官:请简述下PPO算法。其与TRPO算法有何关系呢? + + 答:PPO算法的提出:旨在借鉴TRPO算法,使用一阶优化,在采样效率、算法表现,以及实现和调试的复杂度之间取得了新的平衡。这是因为PPO会在每一次迭代中尝试计算新的策略,让损失函数最小化,并且保证每一次新计算出的策略能够和原策略相差不大。具体来说,在避免使用important sampling时由于在 $\theta$ 下的 $p_{\theta}\left(a_{t} | s_{t}\right)$ 跟 在 $\theta'$ 下的 $ p_{\theta'}\left(a_{t} | s_{t}\right) $ 差太多,导致important sampling结果偏差较大而采取的算法。 diff --git a/docs/chapter5/img/5.1.png b/docs/chapter5/img/5.1.png new file mode 100644 index 0000000..341489a Binary files /dev/null and b/docs/chapter5/img/5.1.png differ diff --git a/docs/chapter5/img/5.10.png b/docs/chapter5/img/5.10.png new file mode 100644 index 0000000..b34d9aa Binary files /dev/null and b/docs/chapter5/img/5.10.png differ diff --git a/docs/chapter5/img/5.11.png b/docs/chapter5/img/5.11.png new file mode 100644 index 0000000..13c06d7 Binary files /dev/null and b/docs/chapter5/img/5.11.png differ diff --git a/docs/chapter5/img/5.12.png b/docs/chapter5/img/5.12.png new file mode 100644 index 0000000..bd502df Binary files /dev/null and b/docs/chapter5/img/5.12.png differ diff --git a/docs/chapter5/img/5.13.png b/docs/chapter5/img/5.13.png new file mode 100644 index 0000000..4f32003 Binary files /dev/null and b/docs/chapter5/img/5.13.png differ diff --git a/docs/chapter5/img/5.14.png b/docs/chapter5/img/5.14.png new file mode 100644 index 0000000..4ba9b3e Binary files /dev/null and b/docs/chapter5/img/5.14.png differ diff --git a/docs/chapter5/img/5.15.png b/docs/chapter5/img/5.15.png new file mode 100644 index 0000000..a11a36e Binary files /dev/null and b/docs/chapter5/img/5.15.png differ diff --git a/docs/chapter5/img/5.2.png b/docs/chapter5/img/5.2.png new file mode 100644 index 0000000..0bfcffa Binary files /dev/null and b/docs/chapter5/img/5.2.png differ diff --git a/docs/chapter5/img/5.3.png b/docs/chapter5/img/5.3.png new file mode 100644 index 0000000..8ada283 Binary files /dev/null and b/docs/chapter5/img/5.3.png differ diff --git a/docs/chapter5/img/5.4.png b/docs/chapter5/img/5.4.png new file mode 100644 index 0000000..ae8562d Binary files /dev/null and b/docs/chapter5/img/5.4.png differ diff --git a/docs/chapter5/img/5.5.png b/docs/chapter5/img/5.5.png new file mode 100644 index 0000000..90e3a1c Binary files /dev/null and b/docs/chapter5/img/5.5.png differ diff --git a/docs/chapter5/img/5.6.png b/docs/chapter5/img/5.6.png new file mode 100644 index 0000000..8158e0f Binary files /dev/null and b/docs/chapter5/img/5.6.png differ diff --git a/docs/chapter5/img/5.7.png b/docs/chapter5/img/5.7.png new file mode 100644 index 0000000..6eef590 Binary files /dev/null and b/docs/chapter5/img/5.7.png differ diff --git a/docs/chapter5/img/5.8.png b/docs/chapter5/img/5.8.png new file mode 100644 index 0000000..6f85510 Binary files /dev/null and b/docs/chapter5/img/5.8.png differ diff --git a/docs/chapter5/img/5.9.png b/docs/chapter5/img/5.9.png new file mode 100644 index 0000000..97e02b4 Binary files /dev/null and b/docs/chapter5/img/5.9.png differ diff --git a/docs/chapter6/chapter6.md b/docs/chapter6/chapter6.md new file mode 100644 index 0000000..bf9a6b1 --- /dev/null +++ b/docs/chapter6/chapter6.md @@ -0,0 +1,384 @@ +# DQN +传统的强化学习算法会使用表格的形式存储状态值函数 $V(s)$ 或状态动作值函数 $Q(s,a)$,但是这样的方法存在很大的局限性。例如:现实中的强化学习任务所面临的状态空间往往是连续的,存在无穷多个状态,在这种情况下,就不能再使用表格对值函数进行存储。值函数近似利用函数直接拟合状态值函数或状态动作值函数,减少了对存储空间的要求,有效地解决了这个问题。 + +为了在连续的状态和动作空间中计算值函数 $Q^{\pi}(s,a)$,我们可以用一个函数 $Q_{\phi}(\boldsymbol{s},\boldsymbol{a})$ 来表示近似计算,称为`价值函数近似(Value Function Approximation)`。 +$$ +Q_{\phi}(\boldsymbol{s}, \boldsymbol{a}) \approx Q^{\pi}(s, a) +$$ + +其中 +* $\boldsymbol{s},\boldsymbol{a}$ 分别是状态 $s$ 和动作 $a$ 的向量表示, +* 函数 $Q_{\phi}(\boldsymbol{s}, \boldsymbol{a})$ 通常是一个参数为 $\phi$ 的函数,比如`神经网络`,输出为一个实数,称为`Q 网络(Q-network)`。 + +## State Value Function + +**Q-learning 是 `value-based` 的方法。在 value-based 的方法里面,我们学习的不是策略,我们要学习的是一个 `critic(评论家)`。**评论家要做的事情是评价现在的行为有多好或是有多不好。假设有一个演员(actor) $\pi$ ,评论家就是来评价这个演员的策略 $\pi$ 好还是不好,即 `Policy Evaluation(策略评估)`。 + +> 注:「李宏毅深度强化学习」课程提到的 Q-learning,其实是 DQN(Deep Q-network)。 +> +> DQN 是指基于深度学习的 Q-learning 算法,主要结合了`价值函数近似(Value Function Approximation)`与神经网络技术,并采用了目标网络和经历回放的方法进行网络的训练。 +> +> 在 Q-learning 中,我们使用表格来存储每个状态 s 下采取动作 a 获得的奖励,即状态-动作值函数 $Q(s,a)$。然而,这种方法在状态量巨大甚至是连续的任务中,会遇到维度灾难问题,往往是不可行的。因此,DQN 采用了价值函数近似的表示方法。 + +举例来说,有一种评论家叫做 `state value function(状态价值函数)`。状态价值函数的意思就是说,假设演员叫做 $\pi$,拿 $\pi$ 跟环境去做互动。假设 $\pi$ 看到了某一个状态 s,如果在玩 Atari 游戏的话,状态 s 是某一个画面,看到某一个画面的时候,接下来一直玩到游戏结束,期望的累积奖励有多大。所以 $V^{\pi}$ 是一个函数,这个函数输入一个状态,然后它会输出一个标量( scalar)。这个标量代表说,$\pi$ 这个演员看到状态 s 的时候,接下来预期到游戏结束的时候,它可以得到多大的值。 + +![](img/6.1.png ':size=550') + +举个例子,假设你是玩 space invader 的话, + +* 左边这个状态 s,这个游戏画面,$V^{\pi}(s)$ 也许会很大,因为还有很多的怪可以杀, 所以你会得到很大的分数。一直到游戏结束的时候,你仍然有很多的分数可以吃。 +* 右边这种情况得到的 $V^{\pi}(s)$ 可能就很小,因为剩下的怪也不多了,并且红色的防护罩已经消失了,所以可能很快就会死掉。所以接下来得到预期的奖励,就不会太大。 + +这边需要强调的一个点是说,评论家都是绑一个演员的,评论家没有办法去凭空去评价一个状态的好坏,它所评价的东西是在给定某一个状态的时候, 假设接下来互动的演员是 $\pi$,那我会得到多少奖励。因为就算是给同样的状态,你接下来的 $\pi$ 不一样,你得到的奖励也是不一样的。 + +举例来说,在左边的情况,假设是一个正常的 $\pi$,它可以杀很多怪,那假设它是一个很弱的 $\pi$,它就站在原地不动,然后马上就被射死了,那你得到的 $V^\pi(s)$ 还是很小。所以评论家的输出值取决于状态和演员。所以评论家其实都要绑一个演员,它是在衡量某一个演员的好坏,而不是衡量一个状态的好坏。这边要强调一下,评论家的输出是跟演员有关的,状态的价值其实取决于你的演员,当演员变的时候,状态价值函数的输出其实也是会跟着改变的。 + +### State Value Function Estimation + +**怎么衡量这个状态价值函数 $V^{\pi}(s)$ 呢?**有两种不同的做法:MC-based 的方法和 TD-based 的方法。 + +` Monte-Carlo(MC)-based`的方法就是让演员去跟环境做互动,要看演员好不好,我们就让演员去跟环境做互动,给评论家看。然后,评论家就统计说, + +* 演员如果看到状态 $s_a$,接下来的累积奖励会有多大。 +* 如果它看到状态 $s_b$,接下来的累积奖励会有多大。 + +但是实际上,我们不可能把所有的状态通通都扫过。如果是玩 Atari 游戏的话,状态是图像,你没有办法把所有的状态通通扫过。所以实际上 $V^{\pi}(s)$ 是一个网络。对一个网络来说,就算输入状态是从来都没有看过的,它也可以想办法估测一个值。 + +![](img/6.2.png ':size=350') + +怎么训练这个网络呢?因为如果在状态 $s_a$,接下来的累积奖励就是 $G_a$。也就是说,对这个价值函数来说,如果输入是状态 $s_a$,正确的输出应该是 $G_a$。如果输入状态 $s_b$,正确的输出应该是值 $G_b$。**所以在训练的时候, 它就是一个 `回归问题(regression problem)`。**网络的输出就是一个值,你希望在输入 $s_a$ 的时候,输出的值跟 $G_a$ 越近越好,输入 $s_b$ 的时候,输出的值跟 $G_b$ 越近越好。接下来把网络训练下去,就结束了。这是 MC-based 的方法。 + +![](img/6.3.png ':size=550') + +**第二个方法是`Temporal-difference(时序差分)` 的方法, `即 TD-based ` 的方法。** + +在 MC-based 的方法中,每次我们都要算累积奖励,也就是从某一个状态 $s_a$ 一直玩到游戏结束的时候,得到的所有奖励的总和。所以你要使用 MC-based 的方法,你必须至少把这个游戏玩到结束。但有些游戏非常长,你要玩到游戏结束才能够更新网络,花的时间太长了,因此我们会采用 TD-based 的方法。 + +TD-based 的方法不需要把游戏玩到底,只要在游戏的某一个情况,某一个状态 $s_t$ 的时候,采取动作 $a_t$ 得到奖励$r_t$ ,跳到状态 $s_{t+1}$,就可以使用 TD 的方法。 + +怎么使用 TD 的方法呢?这边是基于以下这个式子: +$$ +V^{\pi}\left(s_{t}\right)=V^{\pi}\left(s_{t+1}\right)+r_{t} +$$ + +假设我们现在用的是某一个策略 $\pi$,在状态 $s_t$,它会采取动作 $a_t$,给我们奖励 $r_t$ ,接下来进入 $s_{t+1}$ 。状态 $s_{t+1}$ 的值跟状态 $s_t$ 的值,它们的中间差了一项 $r_t$。因为你把 $s_{t+1}$ 得到的值加上得到的奖励 $r_t$ 就会等于 $s_t$ 得到的值。有了这个式子以后,你在训练的时候,你并不是直接去估测 V,而是希望你得到的结果 V 可以满足这个式子。 + +也就是说我们会是这样训练的,我们把 $s_t$ 丢到网络里面,因为 $s_t$ 丢到网络里面会得到 $V^{\pi}(s_t)$,把 $s_{t+1}$ 丢到你的值网络里面会得到 $V^{\pi}(s_{t+1})$,这个式子告诉我们,$V^{\pi}(s_t)$ 减 $V^{\pi}(s_{t+1})$ 的值应该是 $r_t$。然后希望它们两个相减的 loss 跟 $r_t$ 越接近,训练下去,更新 V 的参数,你就可以把 V 函数学习出来。 + +![](img/6.4.png ':size=500') + +**MC 跟 TD 有什么样的差别呢?** + +**MC 最大的问题就是方差很大。**因为我们在玩游戏的时候,它本身是有随机性的。所以你可以把 $G_a$ 看成一个随机变量。因为你每次同样走到 $s_a$ 的时候,最后你得到的 $G_a$ 其实是不一样的。你看到同样的状态 $s_a$,最后玩到游戏结束的时候,因为游戏本身是有随机性的,玩游戏的模型搞不好也有随机性,所以你每次得到的 $G_a$ 是不一样的,每一次得到 $G_a$ 的差别其实会很大。为什么它会很大呢?因为 $G_a$ 其实是很多个不同的步骤的奖励的和。假设你每一个步骤都会得到一个奖励,$G_a$ 是从状态 $s_a$ 开始,一直玩到游戏结束,每一个步骤的奖励的和。 +$$ +\operatorname{Var}[k X]=k^{2} \operatorname{Var}[X] +$$ +> Var 是指 variance。 + +为了方便说明问题,我们简化一下,假设从状态 $s_a$ 开始,一直玩到游戏结束,各个步骤的奖励相同,均为 $r_a$, $G_a=kr_a$。根据上式,则可得 +$$ +\operatorname{Var}[G_a]=\operatorname{Var}[kr_a]=k^{2} \operatorname{Var}[r_a] +$$ + +因此 $G_a$ 的方差相较于某一个状态的奖励是比较大的。 + +如果用 TD 的话,你是要去最小化这样的一个式子: + +![](img/6.5.png ':size=550') + +在这中间会有随机性的是 r。因为计算你在 $s_t$ 采取同一个动作,你得到的奖励也不一定是一样的,所以 r 是一个随机变量。但这个随机变量的方差会比 $G_a$ 还要小,因为 $G_a$ 是很多 r 合起来,这边只是某一个 r 而已。$G_a$ 的方差会比较大,r 的方差会比较小。但是这边你会遇到的**一个问题是你这个 V 不一定估得准**。假设你的这个 V 估得是不准的,那你使用这个式子学习出来的结果,其实也会是不准的。所以 MC 跟 TD 各有优劣。**今天其实 TD 的方法是比较常见的,MC 的方法其实是比较少用的。** + +![](img/6.6.png ':size=550') + +**上图是讲 TD 跟 MC 的差异。**假设有某一个评论家,它去观察某一个策略 $\pi$ 跟环境互动的 8 个 episode 的结果。有一个演员 $\pi$ 跟环境互动了8 次,得到了8 次玩游戏的结果。接下来这个评论家去估测状态的值。 + +**我们先计算 $s_b$ 的值。** 状态 $s_b$ 在 8 场游戏里面都有经历过,其中有 6 场得到奖励 1,有 2 场得到奖励 0。所以如果你是要算期望值的话,就算看到状态 $s_b$ 以后得到的奖励,一直到游戏结束的时候得到的累积奖励期望值是 3/4,计算过程如下式所示: +$$ +\frac{6 \times 1 + 2 \times 0}{8}=\frac{6}{8}=\frac{3}{4} +$$ +**但 $s_a$ 期望的奖励到底应该是多少呢?**这边其实有两个可能的答案:一个是 0,一个是 3/4。为什么有两个可能的答案呢?这取决于你用 MC 还是TD。用 MC 跟用 TD 算出来的结果是不一样的。 + +假如用 MC 的话,你会发现这个 $s_a$ 就出现一次,看到 $s_a$ 这个状态,接下来累积奖励就是 0,所以 $s_a$ 期望奖励就是 0。 + +但 TD 在计算的时候,它要更新下面这个式子: +$$ +V^{\pi}\left(s_{a}\right)=V^{\pi}\left(s_{b}\right)+r +$$ + +因为我们在状态 $s_a$ 得到奖励 r=0 以后,跳到状态 $s_b$。所以状态 $s_b$ 的奖励会等于状态 $s_b$ 的奖励加上在状态 $s_a$ 跳到状态 $s_b$ 的时候可能得到的奖励 r。而这个得到的奖励 r 的值是 0,$s_b$ 期望奖励是 3/4,那 $s_a$ 的奖励应该是 3/4。 + +用 MC 跟 TD 估出来的结果很有可能是不一样的。就算评论家观察到一样的训练数据,它最后估出来的结果也不一定是一样的。为什么会这样呢?你可能问说,哪一个结果比较对呢?其实就都对。 + +因为在第一个轨迹, $s_a$ 得到奖励 0 以后,再跳到 $s_b$ 也得到奖励 0。这边有两个可能。 + +* 一个可能是: $s_a$ 是一个标志性的状态,只要看到 $s_a$ 以后,$s_b$ 就会拿不到奖励,$s_a$ 可能影响了 $s_b$。如果是用 MC 的算法的话,它会把 $s_a$ 影响 $s_b$ 这件事考虑进去。所以看到 $s_a$ 以后,接下来 $s_b$ 就得不到奖励,$s_b$ 期望的奖励是 0。 + +* 另一个可能是:看到 $s_a$ 以后,$s_b$ 的奖励是 0 这件事只是一个巧合,并不是 $s_a$ 所造成,而是因为说 $s_b$ 有时候就是会得到奖励 0,这只是单纯运气的问题。其实平常 $s_b$ 会得到奖励期望值是 3/4,跟 $s_a$ 是完全没有关系的。所以假设 $s_a$ 之后会跳到 $s_b$,那其实得到的奖励按照 TD 来算应该是 3/4。 + +**所以不同的方法考虑了不同的假设,运算结果不同。** + +## State-action Value Function(Q-function) + +还有另外一种评论家叫做 `Q-function`。它又叫做`state-action value function(状态-动作价值函数)`。 + +* 状态价值函数的输入是一个状态,它是根据状态去计算出,看到这个状态以后的期望的累积奖励( expected accumulated reward)是多少。 +* 状态-动作价值函数的输入是一个状态、动作对,它的意思是说,在某一个状态采取某一个动作,假设我们都使用演员 $\pi$ ,得到的累积奖励的期望值有多大。 + +Q-function 有一个需要注意的问题是,这个演员 $\pi$,在看到状态 s 的时候,它采取的动作不一定是 a。Q-function 假设在状态 s 强制采取动作 a。不管你现在考虑的这个演员 $\pi$, 它会不会采取动作 a,这不重要。在状态 s 强制采取动作 a。接下来都用演员 $\pi$ 继续玩下去,就只有在状态 s,我们才强制一定要采取动作 a,接下来就进入自动模式,让演员 $\pi$ 继续玩下去,得到的期望奖励才是 $Q^{\pi}(s,a)$ 。 + +![](img/6.7.png ':size=550') + +Q-function 有两种写法: + +* 输入是状态跟动作,输出就是一个标量; +* 输入是一个状态,输出就是好几个值。 + +假设动作是离散的,动作就只有 3 个可能:往左往右或是开火。那这个 Q-function 输出的 3 个值就分别代表 a 是向左的时候的 Q 值,a 是向右的时候的 Q 值,还有 a 是开火的时候的 Q 值。 + +要注意的事情是,上图右边的函数只有离散动作才能够使用。如果动作是无法穷举的,你只能够用上图左边这个式子,不能够用右边这个式子。 + +![](img/6.8.png ':size=550') + +上图是文献上的结果,你去估计 Q-function 的话,看到的结果可能如上图所示。假设我们有 3 个动作:原地不动、向上、向下。 + +* 假设是在第一个状态,不管是采取哪个动作,最后到游戏结束的时候,得到的期望奖励其实都差不多。因为球在这个地方,就算是你向下,接下来你应该还可以急救。所以不管采取哪个动作,都差不了太多。 + +* 假设在第二个状态,这个乒乓球它已经反弹到很接近边缘的地方,这个时候你采取向上,你才能得到正的奖励,才接的到球。如果你是站在原地不动或向下的话,接下来你都会错过这个球。你得到的奖励就会是负的。 + +* 假设在第三个状态,球很近了,所以就要向上。 + +* 假设在第四个状态,球被反弹回去,这时候采取哪个动作就都没有差了。 + +这是状态-动作价值的一个例子。 + +![](img/6.9.png ':size=550') + +虽然表面上我们学习一个 Q-function,它只能拿来评估某一个演员$\pi$ 的好坏,但只要有了这个 Q-function,我们就可以做强化学习。有了这个 Q-function,我们就可以决定要采取哪一个动作,我们就可以进行`策略改进(Policy Improvement)`。 + +它的大原则是这样,假设你有一个初始的演员,也许一开始很烂,随机的也没有关系。初始的演员叫做 $\pi$,这个 $\pi$ 跟环境互动,会收集数据。接下来你学习一个 $\pi$ 这个演员的 Q 值,你去衡量一下 $\pi$ 在某一个状态强制采取某一个动作,接下来用 $\pi$ 这个策略 会得到的期望奖励,用 TD 或 MC 都是可以的。你学习出一个 Q-function 以后,就保证你可以找到一个新的策略 $\pi'$ ,policy $\pi'$ 一定会比原来的策略 $\pi$ 还要好。那等一下会定义说,什么叫做好。所以假设你有一个 Q-function 和某一个策略 $\pi$,你根据策略 $\pi$ 学习出策略 $\pi$ 的 Q-function,接下来保证你可以找到一个新的策略 $\pi'$ ,它一定会比 $\pi$ 还要好,然后你用 $\pi'$ 取代 $\pi$,再去找它的 Q-function,得到新的以后,再去找一个更好的策略。**这样一直循环下去,policy 就会越来越好。** + +首先要定义的是什么叫做比较好?我们说 $\pi'$ 一定会比 $\pi$ 还要好,这边好是说,对所有可能的状态 s 而言,$V^{\pi^{\prime}}(s) \geq V^{\pi}(s)$。也就是说我们走到同一个状态 s 的时候,如果拿 $\pi$ 继续跟环境互动下去,我们得到的奖励一定会小于等于用 $\pi'$ 跟环境互动下去得到的奖励。所以不管在哪一个状态,你用 $\pi'$ 去做交互,得到的期望奖励一定会比较大。所以 $\pi'$ 是比 $\pi$ 还要好的一个策略。 + +有了 Q-function 以后,怎么找这个 $\pi'$ 呢?如果你根据以下的这个式子去决定你的动作, +$$ +\pi^{\prime}(s)=\arg \max _{a} Q^{\pi}(s, a) +$$ + +根据上式去决定你的动作的步骤叫做 $\pi'$ 的话,那 $\pi'$ 一定会比 $\pi$ 还要好。假设你已经学习出 $\pi$ 的 Q-function,今天在某一个状态 s,你把所有可能的动作 a 都一一带入这个 Q-function,看看哪一个 a 可以让 Q-function 的值最大,那这个动作就是 $\pi'$ 会采取的动作。 + +这边要注意一下,给定这个状态 s,你的策略 $\pi$ 并不一定会采取动作a,我们是给定某一个状态 s 强制采取动作 a,用 $\pi$ 继续互动下去得到的期望奖励,这个才是 Q-function 的定义。所以在状态 s 里面不一定会采取动作 a。用 $\pi'$ 在状态 s 采取动作 a 跟 $\pi$ 采取的动作是不一定会一样的,$\pi'$ 所采取的动作会让它得到比较大的奖励。 + +* 所以这个 $\pi'$ 是用 Q-function 推出来的,没有另外一个网络决定 $\pi'$ 怎么交互,有 Q-function 就可以找出 $\pi'$。 +* 但是这边有另外一个问题就是,在这边要解一个 arg max 的问题,所以 a 如果是连续的就会有问题。如果是离散的,a 只有 3 个选项,一个一个带进去, 看谁的 Q 最大,没有问题。但如果 a 是连续的,要解 arg max 问题,你就会有问题。 + +**接下来讲一下为什么用 $Q^{\pi}(s,a)$ 决定出来的 $\pi'$ 一定会比 $\pi$ 好。** + +假设有一个策略叫做 $\pi'$,它是由 $Q^{\pi}$ 决定的。我们要证对所有的状态 s 而言,$V^{\pi^{\prime}}(s) \geq V^{\pi}(s)$。 + +怎么证呢?我们先把 $V^{\pi}(s)$ 写出来: +$$ +V^{\pi}(s)=Q^{\pi}(s, \pi(s)) +$$ +假设在状态 s follow $\pi$ 这个演员,它会采取的动作就是 $\pi(s)$,那你算出来的 $Q^{\pi}(s, \pi(s))$ 会等于 $V^{\pi}(s)$。一般而言,$Q^{\pi}(s, \pi(s))$ 不一定等于 $V^{\pi}(s)$ ,因为动作不一定是 $\pi(s)$。但如果这个动作是 $\pi(s)$ 的话,$Q^{\pi}(s, \pi(s))$ 是等于 $V^{\pi}(s)$ 的。 + + +$Q^{\pi}(s, \pi(s))$ 还满足如下的关系: +$$ +Q^{\pi}(s, \pi(s)) \le \max _{a} Q^{\pi}(s, a) +$$ + +因为 a 是所有动作里面可以让 Q 最大的那个动作,所以今天这一项一定会比它大。这一项就是 $Q^{\pi}(s, a)$,$a$ 就是 $\pi'(s)$。因为 $\pi'(s)$ 输出的 $a$ 就是可以让 $Q^\pi(s,a)$ 最大的那一个,所以我们得到了下面的式子: +$$ +\max _{a} Q^{\pi}(s, a)=Q^{\pi}\left(s, \pi^{\prime}(s)\right) +$$ + +于是: +$$ +V^{\pi}(s) \leq Q^{\pi}\left(s, \pi^{\prime}(s)\right) +$$ +也就是说某一个状态,如果按照策略 $\pi$ 一直做下去,你得到的奖励一定会小于等于,在这个状态 s 你故意不按照 $\pi$ 所给你指示的方向,而是按照 $\pi'$ 的方向走一步,但只有第一步是按照 $\pi'$ 的方向走,只有在状态 s 这个地方,你才按照 $\pi'$ 的指示走,接下来你就按照 $\pi$ 的指示走。虽然只有一步之差, 但是从上面这个式子可知,虽然只有一步之差,但你得到的奖励一定会比完全 follow $\pi$ 得到的奖励还要大。 + +接下来要证下面的式子: +$$ +Q^{\pi}\left(s, \pi^{\prime}(s) \right) \le V^{\pi'}(s) +$$ + +也就是说,只有一步之差,你会得到比较大的奖励。**但假设每步都是不一样的,每步都是 follow $\pi'$ 而不是 $\pi$ 的话,那你得到的奖励一定会更大。**如果你要用数学式把它写出来的话,你可以写成 $Q^{\pi}\left(s, \pi^{\prime}(s)\right)$ ,它的意思就是说,我们在状态 $s_t$ 采取动作 $a_t$,得到奖励 $r_{t}$,然后跳到状态 $s_{t+1}$,即如下式所示: + +$$ +Q^{\pi}\left(s, \pi^{\prime}(s)\right)=E\left[r_t+V^{\pi}\left(s_{t+1}\right) \mid s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] +$$ +> 在文献上有时也会说:在状态 $s_t$ 采取动作 $a_t$ 得到奖励 $r_{t+1}$, 有人会写成 $r_t$,但意思其实都是一样的。 + +在状态 $s$ 按照 $\pi'$ 采取某一个动作 $a_t$ ,得到奖励 $r_{t}$,然后跳到状态 $s_{t+1}$,$V^{\pi}\left(s_{t+1}\right)$ 是状态 $s_{t+1}$ 根据 $\pi$ 这个演员所估出来的值。因为在同样的状态采取同样的动作,你得到的奖励和会跳到的状态不一定一样, 所以这边需要取一个期望值。 + +因为 $V^{\pi}(s) \leq Q^{\pi}\left(s, \pi^{\prime}(s)\right)$,也就是 $V^{\pi}(s_{t+1}) \leq Q^{\pi}\left(s_{t+1}, \pi^{\prime}(s_{t+1})\right)$,所以我们得到下式: +$$ +\begin{array}{l} +E\left[r_{t}+V^{\pi}\left(s_{t+1}\right) | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +\leq E\left[r_{t}+Q^{\pi}\left(s_{t+1}, \pi^{\prime}\left(s_{t+1}\right)\right) | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] +\end{array} +$$ + +因为 $Q^{\pi}\left(s_{t+1}, \pi^{\prime}\left(s_{t+1}\right)\right) = r_{t+1}+V^{\pi}\left(s_{t+2}\right)$,所以我们得到下式: +$$ +\begin{array}{l} +E\left[r_{t}+Q^{\pi}\left(s_{t+1}, \pi^{\prime}\left(s_{t+1}\right)\right) | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +=E\left[r_{t}+r_{t+1}+V^{\pi}\left(s_{t+2}\right) | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] +\end{array} +$$ + +然后你再代入 $V^{\pi}(s) \leq Q^{\pi}\left(s, \pi^{\prime}(s)\right)$,一直算到回合结束,即: +$$ +\begin{aligned} +V^{\pi}(s) &\le Q^{\pi}(s,\pi'(s)) \\ +&=E\left[r_{t}+V^{\pi}\left(s_{t+1}\right) | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right]\\ +&\le E\left[r_{t}+Q^{\pi}\left(s_{t+1}, \pi^{\prime}\left(s_{t+1}\right)\right) | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +&=E\left[r_{t}+r_{t+1}+V^{\pi}\left(s_{t+2}\right) |s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +& \le E\left[r_{t}+r_{t+1}+Q^{\pi}\left(s_{t+2},\pi'(s_{t+2}\right) | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +& = E\left[r_{t}+r_{t+1}+r_{t+2}+V^{\pi}\left(s_{t+3}\right) |s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +& \le \cdots\\ +& \le E\left[r_{t}+r_{t+1}+r_{t+2}+\cdots | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +& = V^{\pi'}(s) +\end{aligned} +$$ + + +因此: +$$ +V^{\pi}(s)\le V^{\pi'}(s) +$$ + +**从这边我们可以知道,你可以估计某一个策略的 Q-function,接下来你就可以找到另外一个策略 $\pi'$ 比原来的策略还要更好。** + +## Target Network + +![](img/6.12.png ':size=550') + +接下来讲一下在 DQN 里一定会用到的 tip。第一个是 `目标网络(target network)`,什么意思呢?我们在学习 Q-function 的时候,也会用到 TD 的概念。那怎么用 TD?你现在收集到一个数据, 是说在状态 $s_t$,你采取动作 $a_t$ 以后,你得到奖励 $r_t$ ,然后跳到状态 $s_{t+1}$。然后根据这个 Q-function,你会知道说 +$$ +\mathrm{Q}^{\pi}\left(s_{t}, a_{t}\right) +=r_{t}+\mathrm{Q}^{\pi}\left(s_{t+1}, \pi\left(s_{t+1}\right)\right) +$$ + +所以在学习的时候,你会说我们有 Q-function,输入 $s_t$, $a_t$ 得到的值,跟输入 $s_{t+1}$, $\pi (s_{t+1})$ 得到的值中间,我们希望它差了一个 $r_t$, 这跟刚才讲的 TD 的概念是一样的。 + +但是实际上这样的一个输入并不好学习,因为假设这是一个回归问题,$\mathrm{Q}^{\pi}\left(s_{t}, a_{t}\right) $ 是网络的输出,$r_{t}+\mathrm{Q}^{\pi}\left(s_{t+1}, \pi\left(s_{t+1}\right)\right)$ 是目标,你会发现目标是会动的。当然你要实现这样的训练,其实也没有问题,就是你在做反向传播的时候, $Q^{\pi}$ 的参数会被更新,你会把两个更新的结果加在一起。因为它们是同一个模型 $Q^{\pi}$, 所以两个更新的结果会加在一起。但这样会导致训练变得不太稳定,因为假设你把 $\mathrm{Q}^{\pi}\left(s_{t}, a_{t}\right) $ 当作你模型的输出,$r_{t}+\mathrm{Q}^{\pi}\left(s_{t+1}, \pi\left(s_{t+1}\right)\right)$ 当作目标的话,你要去拟合的目标是一直在变的,这种一直在变的目标的训练是不太好训练的。 + +所以你会把其中一个 Q 网络,通常是你会把右边这个 Q 网络固定住。也就是说你在训练的时候,你只更新左边的 Q 网络的参数,而右边的 Q 网络的参数会被固定住。因为右边的 Q 网络负责产生目标,所以叫 `目标网络`。因为目标网络是固定的,所以你现在得到的目标 $r_{t}+\mathrm{Q}^{\pi}\left(s_{t+1}, \pi\left(s_{t+1}\right)\right)$ 的值也是固定的。因为目标网络是固定的,我们只调左边网络的参数,它就变成是一个回归问题。我们希望模型的输出的值跟目标越接近越好,你会最小化它的均方误差(mean square error)。 + +在实现的时候,你会把左边的 Q 网络更新好几次以后,再去用更新过的 Q 网络替换这个目标网络。但它们两个不要一起动,它们两个一起动的话,结果会很容易坏掉。 + +一开始这两个网络是一样的,然后在训练的时候,你会把右边的 Q 网络固定住。你在做梯度下降的时候,只调左边这个网络的参数,那你可能更新 100 次以后才把这个参数复制到右边的网络去,把它盖过去。把它盖过去以后,你这个目标值就变了。就好像说你本来在做一个回归问题,那你训练 后把这个回归问题的 loss 压下去以后,接下来你把这边的参数把它复制过去以后,你的目标就变掉了,接下来就要重新再训练。 + +### Intuition + +![](img/6.13.png ':size=550') + +我们可以通过猫追老鼠的例子来直观地理解为什么要 fix target network。猫是 `Q estimation`,老鼠是 `Q target`。一开始的话,猫离老鼠很远,所以我们想让这个猫追上老鼠。 + +![](img/6.14.png ':size=550') + +因为 Q target 也是跟模型参数相关的,所以每次优化后,Q target 也会动。这就导致一个问题,猫和老鼠都在动。 + +![](img/6.15.png ':size=550') + +然后它们就会在优化空间里面到处乱动,就会产生非常奇怪的优化轨迹,这就使得训练过程十分不稳定。所以我们可以固定 Q target,让老鼠动得不是那么频繁,可能让它每 5 步动一次,猫则是每一步都在动。如果老鼠每 5 次动一步的话,猫就有足够的时间来接近老鼠。然后它们之间的距离会随着优化过程越来越小,最后它们就可以拟合,拟合过后就可以得到一个最好的Q 网络。 + + +## Exploration + +![](img/6.16.png ':size=550') + +**第二个 tip 是`探索(Exploration)`。**当我们使用 Q-function 的时候,policy 完全取决于 Q-function。给定某一个状态,你就穷举所有的 a, 看哪个 a 可以让 Q 值最大,它就是采取的动作。这个跟策略梯度不一样,在做策略梯度的时候,输出其实是随机的。我们输出一个动作的分布,根据这个动作的分布去做采样, 所以在策略梯度里面,你每次采取的动作是不一样的,是有随机性的。 + +像这种 Q-function, 如果你采取的动作总是固定的,会有什么问题呢?你会遇到的问题就是这不是一个好的收集数据的方式。因为假设我们今天真的要估某一个状态,你可以采取动作 $a_{1}$, $a_{2}$, $a_{3}$。你要估测在某一个状态采取某一个动作会得到的 Q 值,你一定要在那一个状态采取过那一个动作,才估得出它的值。如果你没有在那个状态采取过那个动作,你其实估不出那个值的。如果是用深的网络,就你的 Q-function 是一个网络,这种情形可能会没有那么严重。但是一般而言,假设 Q-function 是一个表格,没有看过的 state-action pair,它就是估不出值来。网络也是会有一样的问题,只是没有那么严重。所以今天假设你在某一个状态,动作 $a_{1}$, $a_{2}$, $a_{3}$ 你都没有采取过,那你估出来的 $Q(s,a_{1})$, $Q(s,a_{2})$, $Q(s,a_{3})$ 的值可能都是一样的,就都是一个初始值,比如说 0,即 +$$ +\begin{array}{l} +Q(s, a_1)=0 \\ +Q(s, a_2)=0 \\ +Q(s, a_3)=0 +\end{array} +$$ + +但是假设你在状态 s,你采样过某一个动作 $a_{2}$ ,它得到的值是正的奖励。那 $Q(s, a_2)$ 就会比其他的动作都要好。在采取动作的时候, 就看说谁的 Q 值最大就采取谁,所以之后你永远都只会采样到 $a_{2}$,其他的动作就再也不会被做了,所以就会有问题。就好像说你进去一个餐厅吃饭,其实你都很难选。你今天点了某一个东西以后,假说点了某一样东西, 比如说椒麻鸡,你觉得还可以。接下来你每次去就都会点椒麻鸡,再也不会点别的东西了,那你就不知道说别的东西是不是会比椒麻鸡好吃,这个是一样的问题。 + +如果你没有好的探索的话,你在训练的时候就会遇到这种问题。举个例子, 假设你用 DQN 来玩`slither.io`。 你会有一个蛇,它在环境里面就走来走去,吃到星星,它就加分。假设这个游戏一开始,它往上走,然后就吃到那个星星,它就得到分数,它就知道说往上走可以得到奖励。接下来它就再也不会采取往上走以外的动作了,所以接下来就会变成每次游戏一开始,它就往上冲,然后就死掉。所以需要有探索的机制,让机器知道说,虽然根据之前采样的结果,$a_2$ 好像是不错的,但你至少偶尔也试一下 $a_{1}$ 跟 $a_{3}$,说不定它们更好。 + +这个问题其实就是`探索-利用窘境(Exploration-Exploitation dilemma)`问题。 + +有两个方法解这个问题,一个是 `Epsilon Greedy`。Epsilon Greedy($\varepsilon\text{-greedy}$) 的意思是说,我们有 $1-\varepsilon$ 的概率会按照 Q-function 来决定 动作,通常 $\varepsilon$ 就设一个很小的值, $1-\varepsilon$ 可能是 90%,也就是 90% 的概率会按照 Q-function 来决定 动作,但是你有 10% 的机率是随机的。通常在实现上 $\varepsilon$ 会随着时间递减。在最开始的时候。因为还不知道那个动作是比较好的,所以你会花比较大的力气在做探索。接下来随着训练的次数越来越多。已经比较确定说哪一个 Q 是比较好的。你就会减少你的探索,你会把 $\varepsilon$ 的值变小,主要根据 Q-function 来决定你的动作,比较少随机决定动作,这是 Epsilon Greedy。 + +还有一个方法叫做 `Boltzmann Exploration`,这个方法就比较像是策略梯度。在策略梯度里面,网络的输出是一个期望的动作空间上面的一个的概率分布,再根据概率分布去做采样。那其实你也可以根据 Q 值 去定一个概率分布,假设某一个动作的 Q 值越大,代表它越好,我们采取这个动作的机率就越高。但是某一个动作的 Q 值小,不代表我们不能尝试。 + +Q: 我们有时候也要尝试那些 Q 值比较差的动作,怎么做呢? + +A: 因为 Q 值是有正有负的,所以可以它弄成一个概率,你先取指数,再做归一化。然后把 $\exp(Q(s,a))$ 做归一化的这个概率当作是你在决定动作的时候采样的概率。在实现上,Q 是一个网络,所以你有点难知道, 在一开始的时候网络的输出到底会长怎么样子。假设你一开始没有任何的训练数据,参数是随机的,那给定某一个状态 s,不同的 a 输出的值可能就是差不多的,所以一开始 $Q(s,a)$ 应该会倾向于是均匀的。也就是在一开始的时候,你这个概率分布算出来,它可能是比较均匀的。 + +## Experience Replay + +![](img/6.17.png ':size=550') + +**第三个 tip 是 `Experience Replay(经验回放)`。** Experience Replay 会构建一个 `Replay Buffer`,Replay Buffer 又被称为 `Replay Memory`。Replay Buffer 是说现在会有某一个策略$\pi$ 去跟环境做互动,然后它会去收集数据。我们会把所有的数据放到一个 buffer 里面,buffer 里面就存了很多数据。比如说 buffer 是 5 万,这样它里面可以存 5 万笔资料,每一笔资料就是记得说,我们之前在某一个状态 $s_t$,采取某一个动作 $a_t$,得到了奖励 $r_t$。然后跳到状态 $s_{t+1}$。那你用 $\pi$ 去跟环境互动很多次,把收集到的资料都放到这个 replay buffer 里面。 + +这边要注意是 replay buffer 里面的经验可能是来自于不同的策略,你每次拿 $\pi$ 去跟环境互动的时候,你可能只互动 10000 次,然后接下来就更新你的 $\pi$ 了。但是这个 buffer 里面可以放 5 万笔资料,所以 5 万笔资料可能是来自于不同的策略。Buffer 只有在它装满的时候,才会把旧的资料丢掉。所以这个 buffer 里面它其实装了很多不同的策略的经验。 + +![](img/6.18.png ':size=550') + +有了 buffer 以后,怎么训练 Q 的模型,怎么估 Q-function?你的做法是这样:迭代地去训练这个 Q-function,在每次迭代里面,从这个 buffer 里面随机挑一个 batch 出来,就跟一般的网络训练一样,从那个训练集里面,去挑一个 batch 出来。你去采样一个 batch 出来,里面有一把的经验,根据这把经验去更新你的 Q-function。就跟 TD learning 要有一个目标网络是一样的。你去采样一堆 batch,采样一个 batch 的数据,采样一堆经验,然后再去更新你的 Q-function。 + +当我们这么做的时候, 它变成了一个 `off-policy` 的做法。因为本来我们的 Q 是要观察 $\pi$ 的经验,但实际上 replay buffer 里面的这些经验不是通通来自于 $\pi$,有些是过去其他的 $\pi$ 所遗留下来的经验。因为你不会拿某一个 $\pi$ 就把整个 buffer 装满,然后拿去测 Q-function,这个 $\pi$ 只是采样一些数据塞到那个 buffer 里面去,然后接下来就让 Q 去训练。所以 Q 在采样的时候, 它会采样到过去的一些资料。 + +这么做有两个好处: + +* 其实在做强化学习的时候, 往往最花时间的步骤是在跟环境做互动,训练网络反而是比较快的。因为用 GPU 训练其实很快, 真正花时间的往往是在跟环境做互动。用 replay buffer 可以减少跟环境做互动的次数,因为在做训练的时候,你的经验不需要通通来自于某一个策略。一些过去的策略所得到的经验可以放在 buffer 里面被使用很多次,被反复的再利用,这样让采样到经验的利用是比较高效的。 + +* 在训练网络的时候,其实我们希望一个 batch 里面的数据越多样(diverse)越好。如果 batch 里面的数据都是同样性质的,训练下去是容易坏掉的。如果 batch 里面都是一样的数据,训练的时候,performance 会比较差。我们希望 batch 的数据越多样越好。那如果 buffer 里面的那些经验通通来自于不同的策略,那采样到的一个 batch 里面的数据会是比较多样的。 + +Q:我们明明是要观察 $\pi$ 的值,里面混杂了一些不是 $\pi$ 的经验,这有没有关系? + +A:没关系。这并不是因为过去的 $\pi$ 跟现在的 $\pi$ 很像, 就算过去的 $\pi$ 没有很像,其实也是没有关系的。主要的原因是因为, 我们并不是去采样一个轨迹,我们只采样了一笔经验,所以跟是不是 off-policy 这件事是没有关系的。就算是 off-policy,就算是这些经验不是来自于 $\pi$,我们其实还是可以拿这些经验来估测 $Q^{\pi}(s,a)$。这件事有点难解释,不过你就记得说 Experience Replay 在理论上也是没有问题的。 + +## DQN + +![](img/dqn.png ':size=550') + +DQN 使用深度卷积神经网络近似拟合状态动作值函数 $Q(s,a)$,其网络结构如上图所示。DQN 模型的输入是距离当前时刻最近的 4 帧图像,该输入经过 3 个卷积层和 2 个全连接层的非线性变化后,最终在输出层输出每个动作对应的 Q 值。 + +![](img/6.19.png ':size=550') + + +上图就是一般的 `Deep Q-network(DQN)` 的算法。 + +这个算法是这样的。初始化的时候,你初始化 2 个网络:Q 和 $\hat{Q}$,其实 $\hat{Q}$ 就等于 Q。一开始这个目标 Q 网络,跟你原来的 Q 网络是一样的。在每一个 episode,你拿你的演员去跟环境做互动,在每一次互动的过程中,你都会得到一个状态 $s_t$,那你会采取某一个动作 $a_t$。怎么知道采取哪一个动作 $a_t$ 呢?你就根据你现在的 Q-function。但是你要有探索的机制。比如说你用 Boltzmann 探索或是 Epsilon Greedy 的探索。那接下来你得到奖励 $r_t$,然后跳到状态 $s_{t+1}$。所以现在收集到一笔数据,这笔数据是 ($s_t$, $a_t$ ,$r_t$, $s_{t+1}$)。这笔数据就塞到你的 buffer 里面去。如果 buffer 满的话, 你就再把一些旧的资料丢掉。接下来你就从你的 buffer 里面去采样数据,那你采样到的是 $(s_{i}, a_{i}, r_{i}, s_{i+1})$。这笔数据跟你刚放进去的不一定是同一笔,你可能抽到一个旧的。要注意的是,其实你采样出来不是一笔数据,你采样出来的是一个 batch 的数据,你采样一个 batch 出来,采样一把经验出来。接下来就是计算你的目标。假设采样出这么一笔数据。根据这笔数据去算你的目标。你的目标是什么呢?目标记得要用目标网络 $\hat{Q}$ 来算。目标是: + +$$ +y=r_{i}+\max _{a} \hat{Q}\left(s_{i+1}, a\right) +$$ +其中 a 就是让 $\hat{Q}$ 的值最大的 a。因为我们在状态 $s_{i+1}$会采取的动作 a,其实就是那个可以让 Q 值最大的那一个 a。接下来我们要更新 Q 的值,那就把它当作一个回归问题。希望 $Q(s_i,a_i)$ 跟你的目标越接近越好。然后假设已经更新了某一个数量的次,比如说 C 次,设 C = 100, 那你就把 $\hat{Q}$ 设成 Q,这就是 DQN。 + +Q: DQN 和 Q-learning 有什么不同? + +A: 整体来说,DQN 与 Q-learning 的目标价值以及价值的更新方式都非常相似,主要的不同点在于: + +* DQN 将 Q-learning 与深度学习结合,用深度网络来近似动作价值函数,而 Q-learning 则是采用表格存储; +* DQN 采用了经验回放的训练方法,从历史数据中随机采样,而 Q-learning 直接采用下一个状态的数据进行学习。 + +## 一些技巧 + +下面我们介绍下 DQN 的基本技巧: + +* 在 Atari 游戏里面,一般 mini-batch 设置为 32。 +* Experience replay 用在新问题上一般为 $10^6$。 + +## References + +* [Intro to Reinforcement Learning (强化学习纲要)](https://github.com/zhoubolei/introRL) +* [神经网络与深度学习](https://nndl.github.io/) +* [强化学习基础 David Silver 笔记](https://zhuanlan.zhihu.com/c_135909947) +* [百面深度学习](https://book.douban.com/subject/35043939/) +* [机器学习(北理工)](https://www.icourse163.org/course/BIT-1449601164) +* 苗光辉. 面向部分可观测环境的值迭代深度网络模型研究[D].北京理工大学,2018. + + + diff --git a/docs/chapter6/chapter6_questions&keywords.md b/docs/chapter6/chapter6_questions&keywords.md new file mode 100644 index 0000000..336114b --- /dev/null +++ b/docs/chapter6/chapter6_questions&keywords.md @@ -0,0 +1,114 @@ +# Chapter6 Q-learning-State Value Function + +## 1 Keywords + +- **DQN(Deep Q-Network):** 基于深度学习的Q-learninyang算法,其结合了 Value Function Approximation(价值函数近似)与神经网络技术,并采用了目标网络(Target Network)和经验回放(Experience Replay)等方法进行网络的训练。 +- **State-value Function:** 本质是一种critic。其输入为actor某一时刻的state,对应的输出为一个标量,即当actor在对应的state时,预期的到过程结束时间段中获得的value的数值。 +- **State-value Function Bellman Equation:** 基于state-value function的Bellman Equation,它表示在状态 $s_t$ 下带来的累积奖励 $G_t$ 的期望。 +- **Q-function:** 其也被称为state-action value function。其input 是一个 state 跟 action 的 pair,即在某一个 state 采取某一个action,假设我们都使用 actor $\pi$ ,得到的 accumulated reward 的期望值有多大。 +- **Target Network:** 为了解决在基于TD的Network的问题时,优化目标 $\mathrm{Q}^{\pi}\left(s_{t}, a_{t}\right) + =r_{t}+\mathrm{Q}^{\pi}\left(s_{t+1}, \pi\left(s_{t+1}\right)\right)$ 左右两侧会同时变化使得训练过程不稳定,从而增大regression的难度。target network选择将上式的右部分即 $r_{t}+\mathrm{Q}^{\pi}\left(s_{t+1}, \pi\left(s_{t+1}\right)\right)$ 固定,通过改变上式左部分的network的参数,进行regression,这也是一个DQN中比较重要的tip。 +- **Exploration:** 在我们使用Q-function的时候,我们的policy完全取决于Q-function,有可能导致出现对应的action是固定的某几个数值的情况,而不像policy gradient中的output为随机的,我们再从随机的distribution中sample选择action。这样会导致我们继续训练的input的值一样,从而“加重”output的固定性,导致整个模型的表达能力的急剧下降,这也就是`探索-利用窘境难题(Exploration-Exploitation dilemma)`。所以我们使用`Epsilon Greedy`和 `Boltzmann Exploration`等Exploration方法进行优化。 +- **Experience Replay(经验回放):** 其会构建一个Replay Buffer(Replay Memory),用来保存许多data,每一个data的形式如下:在某一个 state $s_t$,采取某一个action $a_t$,得到了 reward $r_t$,然后跳到 state $s_{t+1}$。我们使用 $\pi$ 去跟环境互动很多次,把收集到的数据都放到这个 replay buffer 中。当我们的buffer”装满“后,就会自动删去最早进入buffer的data。在训练时,对于每一轮迭代都有相对应的batch(与我们训练普通的Network一样通过sample得到),然后用这个batch中的data去update我们的Q-function。综上,Q-function再sample和训练的时候,会用到过去的经验数据,所以这里称这个方法为Experience Replay,其也是DQN中比较重要的tip。 + +## 2 Questions + +- 为什么在DQN中采用价值函数近似(Value Function Approximation)的表示方法? + + 答:首先DQN为基于深度学习的Q-learning算法,而在Q-learning中,我们使用表格来存储每一个state下action的reward,即我们前面所讲的状态-动作值函数 $Q(s,a)$ 。但是在我们的实际任务中,状态量通常数量巨大并且在连续的任务中,会遇到维度灾难的问题,所以使用真正的Value Function通常是不切实际的,所以使用了价值函数近似(Value Function Approximation)的表示方法。 + +- critic output通常与哪几个值直接相关? + + 答:critic output与state和actor有关。我们在讨论output时通常是对于一个actor下来衡量一个state的好坏,也就是state value本质上来说是依赖于actor。不同的actor在相同的state下也会有不同的output。 + +- 我们通常怎么衡量state value function $V^{\pi}(s)$ ?分别的优势和劣势有哪些? + + 答: + + - **基于Monte-Carlo(MC)的方法** :本质上就是让actor与environment做互动。critic根据”统计“的结果,将actor和state对应起来,即当actor如果看到某一state $s_a$ ,将预测接下来的accumulated reward有多大如果它看到 state $s_b$,接下来accumulated reward 会有多大。 但是因为其普适性不好,其需要把所有的state都匹配到,如果我们我们是做一个简单的贪吃蛇游戏等state有限的问题,还可以进行。但是如果我们做的是一个图片型的任务,我们几乎不可能将所有的state(对应每一帧的图像)的都”记录“下来。总之,其不能对于未出现过的input state进行对应的value的输出。 + - **基于MC的Network方法:** 为了解决上面描述的Monte-Carlo(MC)方法的不足,我们将其中的state value function $V^{\pi}(s)$ 定义为一个Network,其可以对于从未出现过的input state,根据network的泛化和拟合能力,也可以”估测“出一个value output。 + - **基于Temporal-difference(时序差分)的Network方法,即TD based Network:** 与我们再前4章介绍的MC与TD的区别一样,这里两者的区别也相同。在 MC based 的方法中,每次我们都要算 accumulated reward,也就是从某一个 state $s_a$ 一直玩到游戏结束的时候,得到的所有 reward 的总和。所以要应用 MC based 方法时,我们必须至少把这个游戏玩到结束。但有些游戏非常的长,你要玩到游戏结束才能够 update network,花的时间太长了。因此我们会采用 TD based 的方法。TD based 的方法不需要把游戏玩到底,只要在游戏的某一个情况,某一个 state $s_t$ 的时候,采取 action $a_t$ 得到 reward $r_t$ ,跳到 state $s_{t+1}$,就可以应用 TD 的方法。公式与之前介绍的TD方法类似,即:$V^{\pi}\left(s_{t}\right)=V^{\pi}\left(s_{t+1}\right)+r_{t}$。 + - **基于MC和基于TD的区别在于:** MC本身具有很大的随机性,我们可以将其 $G_a$ 堪称一个random的变量,所以其最终的variance很大。而对于TD,其具有随机性的变量为 $r$ ,因为计算 $s_t$ 我们采取同一个 action,你得到的 reward 也不一定是一样的,所以对于TD来说,$r$ 是一个 random 变量。但是相对于MC的 $G_a$ 的随机程度来说, $r$ 的随机性非常小,这是因为本身 $G_a$ 就是由很多的 $r$ 组合而成的。但另一个角度来说, 在TD中,我们的前提是 $r_t=V^{\pi}\left(s_{t+1}\right)-V^{\pi}\left(s_{t}\right)$ ,但是我们通常无法保证 $V^{\pi}\left(s_{t+1}\right)、V^{\pi}\left(s_{t}\right)$ 计算的误差为零。所以当 $V^{\pi}\left(s_{t+1}\right)、V^{\pi}\left(s_{t}\right)$ 计算的不准确的话,那应用上式得到的结果,其实也会是不准的。所以 MC 跟 TD各有优劣。 + - **目前, TD 的方法是比较常见的,MC 的方法其实是比较少用的。** + +- 基于我们上面说的network(基于MC)的方法,我们怎么训练这个网络呢?或者我们应该将其看做ML中什么类型的问题呢? + + 答:理想状态,我们期望对于一个input state输出其无误差的reward value。也就是说这个 value function 来说,如果 input 是 state $s_a$,正确的 output 应该是$G_a$。如果 input state $s_b$,正确的output 应该是value $G_b$。所以在训练的时候,其就是一个典型的ML中的回归问题(regression problem)。所以我们实际中需要输出的仅仅是一个非精确值,即你希望在 input $s_a$ 的时候,output value 跟 $G_a$ 越近越好,input $s_b$ 的时候,output value 跟 $G_b$ 越近越好。其训练方法,和我们在训练CNN、DNN时的方法类似,就不再一一赘述。 + +- 基于上面介绍的基于TD的network方法,具体地,我们应该怎么训练模型呢? + + 答:核心的函数为 $V^{\pi}\left(s_{t}\right)=V^{\pi}\left(s_{t+1}\right)+r_{t}$。我们将state $s_t$ 作为input输入network 里,因为 $s_t$ 丢到 network 里面会得到output $V^{\pi}(s_t)$,同样将 $s_{t+1}$ 作为input输入 network 里面会得到$V^{\pi}(s_{t+1})$。同时核心函数:$V^{\pi}\left(s_{t}\right)=V^{\pi}\left(s_{t+1}\right)+r_{t}$ 告诉我们, $V^{\pi}(s_t)$ 减 $V^{\pi}(s_{t+1})$ 的值应该是 $r_t$。然后希望它们两个相减的 loss 跟 $r_t$ 尽可能地接近。这也就是我们这个network的优化目标或者说loss function。 + +- state-action value function(Q-function)和 state value function的有什么区别和联系? + + 答: + + - state value function 的 input 是一个 state,它是根据 state 去计算出,看到这个state 以后的 expected accumulated reward 是多少。 + - state-action value function 的 input 是一个 state 跟 action 的 pair,即在某一个 state 采取某一个action,假设我们都使用 actor $\pi$ ,得到的 accumulated reward 的期望值有多大。 + +- Q-function的两种表示方法? + + 答: + + - 当input 是 state和action的pair时,output 就是一个 scalar。 + - 当input 仅是一个 state时,output 就是好几个 value。 + +- 当我们有了Q-function后,我们怎么找到更好的策略 $\pi'$ 呢?或者说这个 $\pi'$ 本质来说是什么? + + 答:首先, $\pi'$ 是由 $\pi^{\prime}(s)=\arg \max _{a} Q^{\pi}(s, a)$ 计算而得,其表示假设你已经 learn 出 $\pi$ 的Q-function,今天在某一个 state s,把所有可能的 action a 都一一带入这个 Q-function,看看说那一个 a 可以让 Q-function 的 value 最大,那这一个 action,就是 $\pi'$ 会采取的 action。所以根据上式决定的actoin的步骤一定比原来的 $\pi$ 要好,即$V^{\pi^{\prime}}(s) \geq V^{\pi}(s)$。 + +- 解决`探索-利用窘境(Exploration-Exploitation dilemma)`问题的Exploration的方法有哪些?他们具体的方法是怎样的? + + 答: + + 1. **Epsilon Greedy:** 我们有$1-\varepsilon$ 的机率,通常 $\varepsilon$ 很小,完全按照Q-function 来决定action。但是有 $\varepsilon$ 的机率是随机的。通常在实现上 $\varepsilon$ 会随着时间递减。也就是在最开始的时候。因为还不知道那个action 是比较好的,所以你会花比较大的力气在做 exploration。接下来随着training 的次数越来越多。已经比较确定说哪一个Q 是比较好的。你就会减少你的exploration,你会把 $\varepsilon$ 的值变小,主要根据Q-function 来决定你的action,比较少做random,这是**Epsilon Greedy**。 + 2. **Boltzmann Exploration:** 这个方法就比较像是 policy gradient。在 policy gradient 里面network 的output 是一个 expected action space 上面的一个的 probability distribution。再根据 probability distribution 去做 sample。所以也可以根据Q value 去定一个 probability distribution,假设某一个 action 的 Q value 越大,代表它越好,我们采取这个 action 的机率就越高。这是**Boltzmann Exploration**。 + +- 我们使用Experience Replay(经验回放)有什么好处? + + 答: + + 1. 首先,在强化学习的整个过程中, 最花时间的 step 是在跟环境做互动,使用GPU乃至TPU加速来训练 network 相对来说是比较快的。而用 replay buffer 可以减少跟环境做互动的次数,因为在训练的时候,我们的 experience 不需要通通来自于某一个policy(或者当前时刻的policy)。一些过去的 policy 所得到的 experience 可以放在 buffer 里面被使用很多次,被反复的再利用,这样让你的 sample 到 experience 的利用是高效的。 + 2. 另外,在训练网络的时候,其实我们希望一个 batch 里面的 data 越 diverse 越好。如果你的 batch 里面的 data 都是同样性质的,我们的训练出的模型拟合能力不会很乐观。如果 batch 里面都是一样的 data,你 train 的时候,performance 会比较差。我们希望 batch data 越 diverse 越好。那如果 buffer 里面的那些 experience 通通来自于不同的 policy ,那你 sample 到的一个 batch 里面的 data 会是比较 diverse 。这样可以保证我们模型的性能至少不会很差。 + +- 在Experience Replay中我们是要观察 $\pi$ 的 value,里面混杂了一些不是 $\pi$ 的 experience ,这会有影响吗? + + 答:没关系。这并不是因为过去的 $\pi$ 跟现在的 $\pi$ 很像, 就算过去的$\pi$ 没有很像,其实也是没有关系的。主要的原因是我们并不是去sample 一个trajectory,我们只sample 了一个experience,所以跟是不是 off-policy 这件事是没有关系的。就算是off-policy,就算是这些 experience 不是来自于 $\pi$,我们其实还是可以拿这些 experience 来估测 $Q^{\pi}(s,a)$。 + +- DQN(Deep Q-learning)和Q-learning有什么异同点? + + 答:整体来说,从名称就可以看出,两者的目标价值以及价值的update方式基本相同,另外一方面,不同点在于: + + - 首先,DQN 将 Q-learning 与深度学习结合,用深度网络来近似动作价值函数,而 Q-learning 则是采用表格存储。 + - DQN 采用了我们前面所描述的经验回放(Experience Replay)训练方法,从历史数据中随机采样,而 Q-learning 直接采用下一个状态的数据进行学习。 + + +## 3 Something About Interview + +- 高冷的面试官:请问DQN(Deep Q-Network)是什么?其两个关键性的技巧分别是什么? + + 答:Deep Q-Network是基于深度学习的Q-learning算法,其结合了 Value Function Approximation(价值函数近似)与神经网络技术,并采用了目标网络(Target Network)和经验回放(Experience Replay)的方法进行网络的训练。 + +- 高冷的面试官:接上题,DQN中的两个trick:目标网络和experience replay的具体作用是什么呢? + + 答:在DQN中某个动作值函数的更新依赖于其他动作值函数。如果我们一直更新值网络的参数,会导致 + 更新目标不断变化,也就是我们在追逐一个不断变化的目标,这样势必会不太稳定。 为了解决在基于TD的Network的问题时,优化目标 $\mathrm{Q}^{\pi}\left(s_{t}, a_{t}\right) =r_{t}+\mathrm{Q}^{\pi}\left(s_{t+1}, \pi\left(s_{t+1}\right)\right)$ 左右两侧会同时变化使得训练过程不稳定,从而增大regression的难度。target network选择将上式的右部分即 $r_{t}+\mathrm{Q}^{\pi}\left(s_{t+1}, \pi\left(s_{t+1}\right)\right)$ 固定,通过改变上式左部分的network的参数,进行regression。对于经验回放,其会构建一个Replay Buffer(Replay Memory),用来保存许多data,每一个data的形式如下:在某一个 state $s_t$,采取某一个action $a_t$,得到了 reward $r_t$,然后跳到 state $s_{t+1}$。我们使用 $\pi$ 去跟环境互动很多次,把收集到的数据都放到这个 replay buffer 中。当我们的buffer”装满“后,就会自动删去最早进入buffer的data。在训练时,对于每一轮迭代都有相对应的batch(与我们训练普通的Network一样通过sample得到),然后用这个batch中的data去update我们的Q-function。也就是,Q-function再sample和训练的时候,会用到过去的经验数据,也可以消除样本之间的相关性。 + +- 高冷的面试官:DQN(Deep Q-learning)和Q-learning有什么异同点? + + 答:整体来说,从名称就可以看出,两者的目标价值以及价值的update方式基本相同,另外一方面,不同点在于: + + - 首先,DQN 将 Q-learning 与深度学习结合,用深度网络来近似动作价值函数,而 Q-learning 则是采用表格存储。 + - DQN 采用了我们前面所描述的经验回放(Experience Replay)训练方法,从历史数据中随机采样,而 Q-learning 直接采用下一个状态的数据进行学习。 + +- 高冷的面试官:请问,随机性策略和确定性策略有什么区别吗? + + 答:随机策略表示为某个状态下动作取值的分布,确定性策略在每个状态只有一个确定的动作可以选。 + 从熵的角度来说,确定性策略的熵为0,没有任何随机性。随机策略有利于我们进行适度的探索,确定 + 性策略的探索问题更为严峻。 + +- 高冷的面试官:请问不打破数据相关性,神经网络的训练效果为什么就不好? + + 答:在神经网络中通常使用随机梯度下降法。随机的意思是我们随机选择一些样本来增量式的估计梯度,比如常用的 + 采用batch训练。如果样本是相关的,那就意味着前后两个batch的很可能也是相关的,那么估计的梯度也会呈现 + 出某种相关性。如果不幸的情况下,后面的梯度估计可能会抵消掉前面的梯度量。从而使得训练难以收敛。 diff --git a/docs/chapter6/img/6.1.png b/docs/chapter6/img/6.1.png new file mode 100644 index 0000000..5f7913e Binary files /dev/null and b/docs/chapter6/img/6.1.png differ diff --git a/docs/chapter6/img/6.10.png b/docs/chapter6/img/6.10.png new file mode 100644 index 0000000..524757f Binary files /dev/null and b/docs/chapter6/img/6.10.png differ diff --git a/docs/chapter6/img/6.11.png b/docs/chapter6/img/6.11.png new file mode 100644 index 0000000..5a29a67 Binary files /dev/null and b/docs/chapter6/img/6.11.png differ diff --git a/docs/chapter6/img/6.12.png b/docs/chapter6/img/6.12.png new file mode 100644 index 0000000..33baec0 Binary files /dev/null and b/docs/chapter6/img/6.12.png differ diff --git a/docs/chapter6/img/6.13.png b/docs/chapter6/img/6.13.png new file mode 100644 index 0000000..5580d0e Binary files /dev/null and b/docs/chapter6/img/6.13.png differ diff --git a/docs/chapter6/img/6.14.png b/docs/chapter6/img/6.14.png new file mode 100644 index 0000000..04f1e18 Binary files /dev/null and b/docs/chapter6/img/6.14.png differ diff --git a/docs/chapter6/img/6.15.png b/docs/chapter6/img/6.15.png new file mode 100644 index 0000000..f85ecfe Binary files /dev/null and b/docs/chapter6/img/6.15.png differ diff --git a/docs/chapter6/img/6.16.png b/docs/chapter6/img/6.16.png new file mode 100644 index 0000000..6dbed99 Binary files /dev/null and b/docs/chapter6/img/6.16.png differ diff --git a/docs/chapter6/img/6.17.png b/docs/chapter6/img/6.17.png new file mode 100644 index 0000000..2d3de49 Binary files /dev/null and b/docs/chapter6/img/6.17.png differ diff --git a/docs/chapter6/img/6.18.png b/docs/chapter6/img/6.18.png new file mode 100644 index 0000000..5afe2d9 Binary files /dev/null and b/docs/chapter6/img/6.18.png differ diff --git a/docs/chapter6/img/6.19.png b/docs/chapter6/img/6.19.png new file mode 100644 index 0000000..4a9e18b Binary files /dev/null and b/docs/chapter6/img/6.19.png differ diff --git a/docs/chapter6/img/6.2.png b/docs/chapter6/img/6.2.png new file mode 100644 index 0000000..8a1a9eb Binary files /dev/null and b/docs/chapter6/img/6.2.png differ diff --git a/docs/chapter6/img/6.3.png b/docs/chapter6/img/6.3.png new file mode 100644 index 0000000..44e8aa7 Binary files /dev/null and b/docs/chapter6/img/6.3.png differ diff --git a/docs/chapter6/img/6.4.png b/docs/chapter6/img/6.4.png new file mode 100644 index 0000000..f4d4efc Binary files /dev/null and b/docs/chapter6/img/6.4.png differ diff --git a/docs/chapter6/img/6.5.png b/docs/chapter6/img/6.5.png new file mode 100644 index 0000000..fed7cc6 Binary files /dev/null and b/docs/chapter6/img/6.5.png differ diff --git a/docs/chapter6/img/6.6.png b/docs/chapter6/img/6.6.png new file mode 100644 index 0000000..4bb8720 Binary files /dev/null and b/docs/chapter6/img/6.6.png differ diff --git a/docs/chapter6/img/6.7.png b/docs/chapter6/img/6.7.png new file mode 100644 index 0000000..44939f7 Binary files /dev/null and b/docs/chapter6/img/6.7.png differ diff --git a/docs/chapter6/img/6.8.png b/docs/chapter6/img/6.8.png new file mode 100644 index 0000000..c80f5af Binary files /dev/null and b/docs/chapter6/img/6.8.png differ diff --git a/docs/chapter6/img/6.9.png b/docs/chapter6/img/6.9.png new file mode 100644 index 0000000..8f152d7 Binary files /dev/null and b/docs/chapter6/img/6.9.png differ diff --git a/docs/chapter6/img/dqn.png b/docs/chapter6/img/dqn.png new file mode 100644 index 0000000..6aa4e9d Binary files /dev/null and b/docs/chapter6/img/dqn.png differ diff --git a/docs/chapter7/assets/image-20201015221032985.png b/docs/chapter7/assets/image-20201015221032985.png new file mode 100644 index 0000000..1f443a4 Binary files /dev/null and b/docs/chapter7/assets/image-20201015221032985.png differ diff --git a/docs/chapter7/assets/moving_average_rewards_eval.png b/docs/chapter7/assets/moving_average_rewards_eval.png new file mode 100644 index 0000000..c2ba80b Binary files /dev/null and b/docs/chapter7/assets/moving_average_rewards_eval.png differ diff --git a/docs/chapter7/assets/moving_average_rewards_train.png b/docs/chapter7/assets/moving_average_rewards_train.png new file mode 100644 index 0000000..34af087 Binary files /dev/null and b/docs/chapter7/assets/moving_average_rewards_train.png differ diff --git a/docs/chapter7/assets/rewards_eval.png b/docs/chapter7/assets/rewards_eval.png new file mode 100644 index 0000000..735fa2b Binary files /dev/null and b/docs/chapter7/assets/rewards_eval.png differ diff --git a/docs/chapter7/assets/rewards_train.png b/docs/chapter7/assets/rewards_train.png new file mode 100644 index 0000000..471ecff Binary files /dev/null and b/docs/chapter7/assets/rewards_train.png differ diff --git a/docs/chapter7/assets/steps_eval.png b/docs/chapter7/assets/steps_eval.png new file mode 100644 index 0000000..c3864ee Binary files /dev/null and b/docs/chapter7/assets/steps_eval.png differ diff --git a/docs/chapter7/assets/steps_train.png b/docs/chapter7/assets/steps_train.png new file mode 100644 index 0000000..3ba5e60 Binary files /dev/null and b/docs/chapter7/assets/steps_train.png differ diff --git a/docs/chapter7/chapter7.md b/docs/chapter7/chapter7.md new file mode 100644 index 0000000..b4cd92c --- /dev/null +++ b/docs/chapter7/chapter7.md @@ -0,0 +1,149 @@ +# Tips of Q-learning +## Double DQN +![](img/7.1.png) + +接下来要讲的是训练 Q-learning 的一些 tips。第一个 tip 是做 `Double DQN`。为什么要有 Double DQN 呢?因为在实现上,你会发现 Q 值往往是被高估的。上图来自于 Double DQN 的原始 paper,它想要显示的结果就是 Q 值往往是被高估的。 + +这边有 4 个不同的小游戏,横轴是训练的时间,红色锯齿状一直在变的线就是 Q-function 对不同的状态估计出来的平均 Q 值,有很多不同的状态,每个状态你都 sample 一下,然后算它们的 Q 值,把它们平均起来。 + +这条红色锯齿状的线在训练的过程中会改变,但它是不断上升的。因为 Q-function 是取决于你的策略的。学习的过程中你的策略越来越强,你得到的 Q 值会越来越大。在同一个状态, 你得到 reward 的期望会越来越大,所以一般而言,这个值都是上升的,但这是 Q-network 估测出来的值。 + +接下来你真地去算它,怎么真地去算?你有策略,然后真的去玩那个游戏,就玩很多次,玩个一百万次。然后就去真地算说,在某一个状态, 你会得到的 Q 值到底有多少。你会得到在某一个状态采取某一个动作。你接下来会得到累积奖励(accumulated reward)是多少。你会发现估测出来的值远比实际的值大,在每一个游戏都是这样,都大很多。所以今天要提出 Double DQN 的方法,它可以让估测的值跟实际的值是比较接近的。 + +我们先看它的结果,蓝色的锯齿状的线是 Double DQN 的 Q-network 所估测出来的 Q 值,蓝色的无锯齿状的线是真正的 Q 值,你会发现它们是比较接近的。 用网络估测出来的就不用管它,比较没有参考价值。用 Double DQN 得出来真正的累积奖励,在这 3 种情况下都是比原来的 DQN 高的,代表 Double DQN 学习出来的那个策略比较强。所以它实际上得到的 reward 是比较大的。虽然一般的 DQN 的 Q-network 高估了自己会得到的 reward,但实际上它得到的 reward 是比较低的。 + +![](img/7.2.png) + +Q: 为什么 Q 值总是被高估了呢? + +A:因为实际上在做的时候,是要让左边这个式子跟右边这个目标越接近越好。你会发现目标的值很容易一不小心就被设得太高。因为在算这个目标的时候,我们实际上在做的事情是,看哪一个 a 可以得到最大的 Q 值,就把它加上去,就变成我们的目标。所以假设有某一个动作得到的值是被高估的。 + +举例来说, 现在有 4 个动作,本来它们得到的值都是差不多的,它们得到的 reward 都是差不多的。但是在估计的时候,网络是有误差的。 + +* 假设是第一个动作被高估了,假设绿色的东西代表是被高估的量,它被高估了,那这个目标就会选这个动作,然后就会选这个高估的 Q 值来加上 $r_t$,来当作你的目标。 +* 如果第四个动作被高估了,那就会选第四个动作来加上 $r_t$ 来当作你的目标值。所以你总是会选那个 Q 值被高估的,你总是会选那个 reward 被高估的动作当作这个 max 的结果去加上 $r_t$ 当作你的目标,所以你的目标总是太大。 + +![](img/7.3.png) +Q: 怎么解决目标值总是太大的问题呢? + +A: 在 Double DQN 里面,选动作的 Q-function 跟算值的 Q-function 不是同一个。在原来的 DQN 里面,你穷举所有的 a,把每一个 a 都带进去, 看哪一个 a 可以给你的 Q 值最高,那你就把那个 Q 值加上 $r_t$。但是在 Double DQN 里面,你有两个 Q-network: + +* 第一个 Q-network Q 决定哪一个动作的 Q 值最大(你把所有的 a 带入 Q 中,看看哪一个 Q 值最大)。 +* 你决定你的动作以后,你的 Q 值是用 $Q'$ 算出来的。 + +假设我们有两个 Q-function, + +* 假设第一个 Q-function 高估了它现在选出来的动作 a,只要第二个 Q-function $Q'$ 没有高估这个动作 a 的值,那你算出来的就还是正常的值。 +* 假设 $Q'$ 高估了某一个动作的值,那也没差,因为只要前面这个 Q 不要选那个动作出来就没事了,这个就是 Double DQN 神奇的地方。 + +Q: 哪来 Q 跟 $Q'$ 呢?哪来两个网络呢? + +A: 在实现上,你有两个 Q-network:目标的 Q-network 和你会更新的 Q-network。所以在 Double DQN 里面,你会拿你会更新参数的那个 Q-network 去选动作,然后你拿目标网络(固定住不动的网络)去算值。 + +Double DQN 相较于原来的 DQN 的更改是最少的,它几乎没有增加任何的运算量,连新的网络都不用,因为原来就有两个网络了。你唯一要做的事情只有,本来你在找 Q 值最大的 a 的时候,你是用 $Q'$ 来算,你是用目标网络来算,现在改成用另外一个会更新的 Q-network 来算。 + +假如你今天只选一个 tip 的话,正常人都是实现 Double DQN,因为很容易实现。 + +## Dueling DQN +![](img/7.4.png) +第二个 tip 是 `Dueling DQN`。其实 Dueling DQN 也蛮好做的,相较于原来的 DQN,它唯一的差别是改了网络的架构。Q-network 就是输入状态,输出就是每一个动作的 Q 值。Dueling DQN 唯一做的事情是改了网络的架构,其它的算法都不需要动。 + +Q: Dueling DQN 是怎么改了网络的架构呢? + +A: 本来的 DQN 就是直接输出 Q 值的值。现在这个 dueling 的 DQN,就是下面这个网络的架构。它不直接输出 Q 值的值,它分成两条路径去运算: + +* 第一条路径会输出一个 scalar,这个 scalar 叫做 $V(s)$。因为它跟输入 s 是有关系,所以叫做 $V(s)$,$V(s)$ 是一个 scalar。 +* 第二条路径会输出一个 vector,这个 vector 叫做 $A(s,a)$。下面这个 vector,它是每一个动作都有一个值。 + +你再把这两个东西加起来就可以得到你的 Q 值。 + +![](img/7.5.png) + +Q: 这么改有什么好处? + +A : 那我们假设说,原来的 $Q(s,a)$ 就是一个表格。我们假设状态是离散的,实际上状态不是离散的。为了说明方便,我们假设就是只有 4 个不同的状态,只有 3 个不同的动作,所以 $Q(s,a)$ 你可以看作是一个表格。 + +我们知道: +$$ +Q(s,a) = V(s) + A(s,a) +$$ + +其中 + +* $V(s)$ 是对不同的状态 它都有一个值。 +* $A(s,a)$ 它是对不同的状态,不同的动作都有一个值。 + +你把这个 V 的值加到 A 的每一列就会得到 Q 的值。把 2+1,2+(-1),2+0,就得到 3,1,2,以此类推。 + +如上图所示,假设说你在训练网络的时候,目标是希望这一个值变成 4,这一个值变成 0。但是你实际上能更改的并不是 Q 的值,你的网络更改的是 V 跟 A 的值。根据网络的参数,V 跟 A 的值输出以后,就直接把它们加起来,所以其实不是更动 Q 的值。 + +然后在学习网络的时候,假设你希望这边的值,这个 3 增加 1 变成 4,这个 -1 增加 1 变成 0。最后你在训练网络的时候,网络 可能会说,我们就不要动这个 A 的值,就动 V 的值,把 V 的值从 0 变成 1。把 0 变成 1 有什么好处呢?你会发现说,本来你只想动这两个东西的值,那你会发现说,这个第三个值也动了,-2 变成 -1。所以有可能说你在某一个状态,你明明只 sample 到这 2 个动作,你没 sample 到第三个动作,但是你其实也可以更改第三个动作的 Q 值。这样的好处就是你不需要把所有的 state-action pair 都 sample 过,你可以用比较高效的方式去估计 Q 值出来。因为有时候你更新的时候,不一定是更新下面这个表格。而是只更新了 $V(s)$,但更新 $V(s)$ 的时候,只要一改所有的值就会跟着改。这是一个比较有效率的方法,去使用你的数据,这个是 Dueling DQN 可以带给我们的好处。 + +那可是接下来有人就会问说会不会最后 学习出来的结果是说,反正 machine 就学到 V 永远都是 0,然后反正 A 就等于 Q,那你就没有得到任何 Dueling DQN 可以带给你的好处, 就变成跟原来的 DQN 一模一样。为了避免这个问题,实际上你要给 A 一些约束,让 更新 A 其实比较麻烦,让网络倾向于会想要去用 V 来解问题。 + +举例来说,你可以看原始的文献,它有不同的约束 。一个最直觉的约束是你必须要让这个 A 的每一列的和都是 0,所以看我这边举的例子,列的和都是 0。如果这边列的和都是 0,这边这个 V 的值,你就可以想成是上面 Q 的每一列的平均值。这个平均值,加上这些值才会变成是 Q 的 值。所以今天假设你发现说你在更新参数的时候,你是要让整个列一起被更新。你就不会想要更新这边,因为你不会想要更新 A 这个矩阵。因为 A 这个矩阵的每一列的和都要是 0,所以你没有办法说,让这边的值,通通都 +1,这件事是做不到的。因为它的约束就是你的和永远都是要 0。所以不可以都 +1,这时候就会强迫网络去更新 V 的值,然后让你可以用比较有效率的方法,去使用你的数据。 + +![](img/7.6.png) + +实现时,你要给这个 A 一个约束。举个例子,假设你有 3 个动作,然后在这边输出的 vector 是 $[7,3,2]^{\mathrm{T}}$,你在把这个 A 跟这个 V 加起来之前,先加一个归一化(normalization),就好像做那个层归一化( layer normalization)一样。加一个归一化,这个归一化做的事情就是把 7+3+2 加起来等于 12,12/3 = 4。然后把这边通通减掉 4,变成 3, -1, 2。再把 3, -1, 2 加上 1.0,得到最后的 Q 值。这个归一化的步骤就是网络的其中一部分,在训练的时候,你从这边也是一路 back propagate 回来的,只是归一化是没有参数的,它只是一个归一化的操作。把它可以放到网络里面,跟网络的其他部分 jointly trained,这样 A 就会有比较大的约束。这样网络就会给它一些好处,倾向于去更新 V 的值,这个是 Dueling DQN。 + + +## Prioritized Experience Replay + +![](img/7.7.png) +有一个技巧叫做 `Prioritized Experience Replay`。Prioritized Experience Replay 是什么意思呢? + +我们原来在 sample 数据去训练你的 Q-network 的时候,你是均匀地从 experience buffer 里面去 sample 数据。那这样不见得是最好的, 因为也许有一些数据比较重要。假设有一些数据,你之前有 sample 过。你发现这些数据的 TD error 特别大(TD error 就是网络的输出跟目标之间的差距),那这些数据代表说你在训练网络的时候, 你是比较训练不好的。那既然比较训练不好, 那你就应该给它比较大的概率被 sample 到,即给它 `priority`。这样在训练的时候才会多考虑那些训练不好的训练数据。实际上在做 prioritized experience replay 的时候,你不仅会更改 sampling 的 process,你还会因为更改了 sampling 的过程,更改更新参数的方法。所以 prioritized experience replay 不仅改变了 sample 数据的分布,还改变了训练过程。 +## Balance between MC and TD + +![](img/7.8.png) +**另外一个可以做的方法是 balance MC 跟 TD。**MC 跟 TD 的方法各自有各自的优劣,怎么在 MC 跟 TD 里面取得一个平衡呢?我们的做法是这样,在 TD 里面,在某一个状态 $s_t$ 采取某一个动作$a_t$ 得到 reward $r_t$,接下来跳到那一个状态 $s_{t+1}$。但是我们可以不要只存一个步骤的数据,我们存 N 个步骤的数据。 + +我们记录在 $s_t$ 采取 $a_t$,得到 $r_t$,会跳到什么样 $s_t$。一直纪录到在第 N 个步骤以后,在 $s_{t+N}$采取 $a_{t+N}$ 得到 reward $r_{t+N}$,跳到 $s_{t+N+1}$ 的这个经验,通通把它存下来。实际上你今天在做更新的时候, 在做 Q-network learning 的时候,你的 learning 的方法会是这样,你 learning 的时候,要让 $Q(s_t,a_t)$ 跟你的目标值越接近越好。$\hat{Q}$ 所计算的不是 $s_{t+1}$,而是 $s_{t+N+1}$的。你会把 N 个步骤以后的状态 丢进来,去计算 N 个步骤以后,你会得到的 reward。要算目标值的话,要再加上多步(multi-step) 的 reward $\sum_{t^{\prime}=t}^{t+N} r_{t^{\prime}}$ ,多步的 reward 是从时间 t 一直到 t+N 的 N 个 reward 的和。然后希望你的 $Q(s_t,a_t)$ 和目标值越接近越好。 + +你会发现说这个方法就是 MC 跟 TD 的结合。因此它就有 MC 的好处跟坏处,也有 TD 的好处跟坏处。如果看它的这个好处的话,因为我们现在 sample 了比较多的步骤,之前是只 sample 了一个步骤, 所以某一个步骤得到的数据是 real 的,接下来都是 Q 值估测出来的。现在 sample 比较多步骤,sample N 个步骤才估测值,所以估测的部分所造成的影响就会比小。当然它的坏处就跟 MC 的坏处一样,因为你的 r 比较多项,你把 N 项的 r 加起来,方差就会比较大。但是你可以去调这个 N 的值,去在 方差 跟不精确的 Q 之间取得一个平衡。N 就是一个 hyperparameter,你要调这个 N 到底是多少,你是要多 sample 三步,还是多 sample 五步。 + +## Noisy Net +![](img/7.9.png) +我们还可以改进探索。Epsilon Greedy 这样的探索是在动作的空间上面加噪声,**但是有一个更好的方法叫做`Noisy Net`,它是在参数的空间上面加噪声。** + +Noisy Net 的意思是说,每一次在一个 episode 开始的时候,在你要跟环境互动的时候,你就把你的 Q-function 拿出来,Q-function 里面其实就是一个网络,就变成你把那个网络拿出来,在网络的每一个参数上面加上一个高斯噪声(Gaussian noise),那你就把原来的 Q-function 变成 $\tilde{Q}$ 。因为 $\hat{Q}$ 已经用过,$\hat{Q}$ 是那个目标网络,我们用 $\tilde{Q}$ 来代表一个 `Noisy Q-function`。我们把每一个参数都加上一个高斯噪声,就得到一个新的网络叫做 $\tilde{Q}$。 + +这边要注意在每个 episode 开始的时候,开始跟环境互动之前,我们就 sample 网络。接下来你就会用这个固定住的 noisy网络去玩这个游戏,直到游戏结束,你才重新再去 sample 新的噪声。OpenAI 跟 DeepMind 又在同时间提出了一模一样的方法,通通都发表在 ICLR 2018,两篇 paper 的方法就是一样的。不一样的地方是,他们用不同的方法,去加噪声。OpenAI 加的方法好像比较简单,他就直接加一个高斯噪声就结束了,就你把每一个参数,每一个 weight 都加一个高斯噪声就结束了。DeepMind 做比较复杂,他们的噪声是由一组参数控制的,也就是说网络可以自己决定说它那个噪声要加多大,但是概念就是一样的。总之就是把你的 Q-function 的里面的那个网络加上一些噪声,把它变得有点不一样,跟原来的 Q-function 不一样,然后拿去跟环境做互动。两篇 paper 里面都有强调说,你这个参数虽然会加噪声,但在同一个 episode 里面你的参数就是固定的,你是在换 episode, 玩第二场新的游戏的时候,你才会重新 sample 噪声,在同一场游戏里面就是同一个 noisy Q-network 在玩那一场游戏,这件事非常重要。为什么这件事非常重要呢?因为这是导致了 Noisy Net 跟原来的 Epsilon Greedy 或其它在动作做 sample 方法的本质上的差异。 + +![](img/7.10.png) + +有什么样本质上的差异呢?在原来 sample 的方法,比如说 Epsilon Greedy 里面,就算是给同样的状态,你的 agent 采取的动作也不一定是一样的。因为你是用 sample 决定的,给定同一个状态,要根据 Q-function 的网络,你会得到一个动作,你 sample 到 random,你会采取另外一个 动作。所以给定同样的状态,如果你今天是用 Epsilon Greedy 的方法,它得到的动作是不一样的。但实际上你的策略并不是这样运作的啊。在一个真实世界的策略,给同样的状态,他应该会有同样的回应。而不是给同样的状态,它其实有时候吃 Q-function,然后有时候又是随机的,所以这是一个不正常的动作,是在真实的情况下不会出现的动作。但是如果你是在 Q-function 上面去加噪声的话, 就不会有这个情形。因为如果你今天在 Q-function 上加噪声,在 Q-function 的网络的参数上加噪声,那在整个互动的过程中,在同一个 episode 里面,它的网络的参数总是固定的,所以看到同样的状态,或是相似的状态,就会采取同样的动作,那这个是比较正常的。在 paper 里面有说,这个叫做 `state-dependent exploration`,也就是说你虽然会做探索这件事, 但是你的探索是跟状态有关系的,看到同样的状态, 你就会采取同样的探索的方式,而 noisy 的动作只是随机乱试。但如果你是在参数下加噪声,那在同一个 episode 里面,里面你的参数是固定的。那你就是有系统地在尝试,每次会试说,在某一个状态,我都向左试试看。然后再下一次在玩这个同样游戏的时候,看到同样的状态,你就说我再向右试试看,你是有系统地在探索这个环境。 + +## Distributional Q-function +![](img/7.11.png) + +还有一个技巧叫做 `Distributional Q-function`。我们不讲它的细节,只告诉你大致的概念。Distributional Q-function 还蛮有道理的, 但是它没有红起来。你就发现说没有太多人真的在实现的时候用这个技术,可能一个原因就是它不好实现。Q-function 是累积奖励的期望值,所以我们算出来的这个 Q 值其实是一个期望值。因为环境是有随机性的,在某一个状态 采取某一个动作的时候,我们把所有的 reward 玩到游戏结束的时候所有的 reward 进行一个统计,你其实得到的是一个分布。也许在 reward 得到 0 的机率很高,在 -10 的概率比较低,在 +10 的概率比较低,但是它是一个分布。我们对这一个分布算它的平均值才是这个 Q 值,我们算出来是累积奖励的期望。所以累积奖励是一个分布,对它取期望,对它取平均值,你得到了 Q 值。但不同的分布,它们其实可以有同样的平均值。也许真正的分布是右边的分布,它算出来的平均值跟左边的分布 算出来的平均值 其实是一样的,但它们背后所代表的分布其实是不一样的。假设我们只用 Q 值的期望来代表整个 reward 的话,其实可能会丢失一些信息,你没有办法 model reward 的分布。 + +![](img/7.12.png) + +Distributional Q-function 它想要做的事情是对分布(distribution)建模,怎么做呢?在原来的 Q-function 里面,假设你只能够采取 $a_1$, $a_2$, $a_3$ 3 个动作,那你就是输入一个状态,输出 3 个值。3 个值分别代表 3 个动作的 Q 值,但是这个 Q 值是一个分布的期望值。所以 Distributional Q-function 的想法就是何不直接输出那个分布。但是要直接输出一个分布也不知道怎么做。 + +实际上的做法是说, 假设分布的值就分布在某一个 range 里面,比如说 -10 到 10,那把 -10 到 10 中间拆成一个一个的 bin,拆成一个一个的长条图。举例来说,在这个例子里面,每一个动作的 reward 的空间就拆成 5 个 bin。假设 reward 可以拆成 5 个 bin 的话,今天你的 Q-function 的输出是要预测说,你在某一个状态,采取某一个动作,你得到的 reward,落在某一个 bin 里面的概率。 + +所以其实这边的概率的和,这些绿色的 bar 的和应该是 1,它的高度代表说,在某一个状态 采取某一个动作的时候,它落在某一个 bin 的机率。这边绿色的代表动作 1,红色的代表动作 2,蓝色的代表动作 3。所以今天你就可以真的用 Q-function 去估计 $a_1$ 的分布,$a_2$ 的分布,$a_3$ 的分布。那实际上在做测试的时候, 我们还是要选某一个动作去执行,那选哪一个动作呢?实际上在做的时候,还是选这个平均值最大的那个动作去执行。 + +但假设我们可以对 distribution 建模的话,除了选平均值最大的以外,也许在未来你可以有更多其他的运用。举例来说,你可以考虑它的分布长什么样子。如果分布方差很大,代表说采取这个动作虽然平均值可能平均而言很不错,但也许风险很高,你可以训练一个网络它是可以规避风险的。就在 2 个动作平均值都差不多的情况下,也许可以选一个风险比较小的动作来执行,这是 Distributional Q-function 的好处。关于怎么训练这样的 Q-network 的细节,我们就不讲,你只要记得说 Q-network 有办法输出一个分布就对了。我们可以不只是估测得到的期望 reward 平均值的值。我们其实是可以估测一个分布的。 + +## Rainbow + +![](img/7.13.png) + +**最后一个技巧叫做 rainbow,把刚才所有的方法都综合起来就变成 rainbow 。**因为刚才每一个方法,就是有一种自己的颜色,把所有的颜色通通都合起来,就变成 rainbow,它把原来的 DQN 也算是一种方法,故有 7 色。 + +那我们来看看这些不同的方法。横轴是训练过程,纵轴是玩了 10 几个 Atari 小游戏的平均的分数的和,但它取的是中位数的分数,为什么是取中位数不是直接取平均呢?因为它说每一个小游戏的分数,其实差很多。如果你取平均的话,到时候某几个游戏就控制了你的结果,所以它取中位数的值。 + +如果你是一般的 DQN,就灰色这一条线,就没有很强。那如果是你换 Noisy DQN,就强很多。如果这边每一个单一颜色的线是代表说只用某一个方法,那紫色这一条线是 DDQN(Double DQN),DDQN 还蛮有效的。然后 Prioritized DDQN、Dueling DDQN 和 Distributional DQN 都蛮强的,它们都差不多很强的。A3C 其实是 Actor-Critic 的方法。单纯的 A3C 看起来是比 DQN 强的。这边怎么没有多步的方法,多步的方法就是平衡 TD 跟 MC,我猜是因为 A3C 本身内部就有做多步的方法,所以他可能觉得说有实现 A3C 就算是有实现多步的方法。所以可以把这个 A3C 的结果想成是多步方法的结果。其实这些方法他们本身之间是没有冲突的,所以全部都用上去就变成七彩的一个方法,就叫做 rainbow,然后它很高。 + +![](img/7.14.png) + +上图是说,在 rainbow 这个方法里面, 如果我们每次拿掉其中一个技术,到底差多少。因为现在是把所有的方法都加在一起,发现说进步很多,但会不会有些方法其实是没用的。所以看看说, 每一个方法哪些方法特别有用,哪些方法特别没用。这边的虚线就是拿掉某一种方法以后的结果,你会发现说,黄色的虚线,拿掉多步掉很多。Rainbow 是彩色这一条,拿掉多步会掉下来。拿掉 Prioritized Experience Replay 后也马上就掉下来。拿掉分布,它也掉下来。 + +这边有一个有趣的地方是说,在开始的时候,分布的训练的方法跟其他方法速度差不多。但是如果你拿掉分布的时候,你的训练不会变慢,但是性能(performance)最后会收敛在比较差的地方。拿掉 Noisy Net 后性能也是差一点。拿掉 Dueling 也是差一点。拿掉 Double 没什么差,所以看来全部合在一起的时候,Double 是比较没有影响的。其实在 paper 里面有给一个 make sense 的解释,其实当你有用 Distributional DQN 的时候,本质上就不会高估你的 reward。我们是为了避免高估 reward 才加了 Double DQN。那在 paper 里面有讲说,如果有做 Distributional DQN,就比较不会有高估的结果。 事实上他有真的算了一下发现说,其实多数的状况是低估 reward 的,所以变成 Double DQN 没有用。 + +为什么做 Distributional DQN,不会高估 reward,反而会低估 reward 呢?因为这个 Distributional DQN 的输出的是一个分布的范围,输出的范围不可能是无限宽的,你一定是设一个范围, 比如说最大输出范围就是从 -10 到 10。假设今天得到的 reward 超过 10 怎么办?是 100 怎么办,就当作没看到这件事。所以 reward 很极端的值,很大的值其实是会被丢掉的, 所以用 Distributional DQN 的时候,你不会有高估的现象,反而会低估。 \ No newline at end of file diff --git a/docs/chapter7/chapter7_questions&keywords.md b/docs/chapter7/chapter7_questions&keywords.md new file mode 100644 index 0000000..daa4a2e --- /dev/null +++ b/docs/chapter7/chapter7_questions&keywords.md @@ -0,0 +1,57 @@ +# Chapter7 Q-learning-Double DQN + +## 1 Keywords + +- **Double DQN:** 在Double DQN中存在有两个 Q-network,首先,第一个 Q-network,决定的是哪一个 action 的 Q value 最大,从而决定了你的action。另一方面, Q value 是用 $Q'$ 算出来的,这样就可以避免 over estimate 的问题。具体来说,假设我们有两个 Q-function,假设第一个Q-function 它高估了它现在选出来的action a,那没关系,只要第二个Q-function $Q'$ 没有高估这个action a 的值,那你算出来的,就还是正常的值。 +- **Dueling DQN:** 将原来的DQN的计算过程分为**两个path**。对于第一个path,会计算一个于input state有关的一个标量 $V(s)$;对于第二个path,会计算出一个vector $A(s,a)$ ,其对应每一个action。最后的网络是将两个path的结果相加,得到我们最终需要的Q value。用一个公式表示也就是 $Q(s,a)=V(s)+A(s,a)$ 。 +- **Prioritized Experience Replay (优先经验回放):** 这个方法是为了解决我们在chapter6中提出的**Experience Replay(经验回放)**方法不足进一步优化提出的。我们在使用Experience Replay时是uniformly取出的experience buffer中的sample data,这里并没有考虑数据间的权重大小。例如,我们应该将那些train的效果不好的data对应的权重加大,即其应该有更大的概率被sample到。综上, prioritized experience replay 不仅改变了 sample data 的 distribution,还改变了 training process。 +- **Noisy Net:** 其在每一个episode 开始的时候,即要和环境互动的时候,将原来的Q-function 的每一个参数上面加上一个Gaussian noise。那你就把原来的Q-function 变成$\tilde{Q}$ ,即**Noisy Q-function**。同样的我们把每一个network的权重等参数都加上一个Gaussian noise,就得到一个新的network $\tilde{Q}$。我们会使用这个新的network从与环境互动开始到互动结束。 +- **Distributional Q-function:** 对于DQN进行model distribution。将最终的网络的output的每一类别的action再进行distribution。 +- **Rainbow:** 也就是将我们这两节内容所有的七个tips综合起来的方法,7个方法分别包括:DQN、DDQN、Prioritized DDQN、Dueling DDQN、A3C、Distributional DQN、Noisy DQN,进而考察每一个方法的贡献度或者是否对于与环境的交互式正反馈的。 + +## 2 Questions + +- 为什么传统的DQN的效果并不好?参考公式 $Q(s_t ,a_t)=r_t+\max_{a}Q(s_{t+1},a)$ + + 答:因为实际上在做的时候,是要让左边这个式子跟右边这个 target 越接近越好。比较容易可以发现target 的值很容易一不小心就被设得太高。因为在算这个 target 的时候,我们实际上在做的事情是看哪一个a 可以得到最大的Q value,就把它加上去,就变成我们的target。 + + 举例来说,现在有 4 个 actions,本来其实它们得到的值都是差不多的,它们得到的reward 都是差不多的。但是在estimate 的时候,那毕竟是个network。所以estimate 的时候是有误差的。所以假设今天是第一个action它被高估了,假设绿色的东西代表是被高估的量,它被高估了,那这个target 就会选这个action。然后就会选这个高估的Q value来加上$r_t$,来当作你的target。如果第4 个action 被高估了,那就会选第4 个action 来加上$r_t$ 来当作你的target value。所以你总是会选那个Q value 被高估的,你总是会选那个reward 被高估的action 当作这个max 的结果去加上$r_t$ 当作你的target。所以你的target 总是太大。 + +- 接着上个思考题,我们应该怎么解决target 总是太大的问题呢? + + 答: 我们可以使用Double DQN解决这个问题。首先,在 Double DQN 里面,选 action 的 Q-function 跟算 value 的 Q-function不同。在原来的DQN 里面,你穷举所有的 a,把每一个a 都带进去, 看哪一个 a 可以给你的 Q value 最高,那你就把那个 Q value 加上$r_t$。但是在 Double DQN 里面,你**有两个 Q-network**,第一个 Q-network,决定哪一个 action 的 Q value 最大,你用第一个 Q-network 去带入所有的 a,去看看哪一个Q value 最大。然后你决定你的action 以后,你的 Q value 是用 $Q'$ 算出来的,这样子有什么好处呢?为什么这样就可以避免 over estimate 的问题呢?因为今天假设我们有两个 Q-function,假设第一个Q-function 它高估了它现在选出来的action a,那没关系,只要第二个Q-function $Q'$ 没有高估这个action a 的值,那你算出来的,就还是正常的值。假设反过来是 $Q'$ 高估了某一个action 的值,那也没差, 因为反正只要前面这个Q 不要选那个action 出来就没事了。 + +- 哪来 Q 跟 $Q'$ 呢?哪来两个 network 呢? + + 答:在实现上,你有两个 Q-network, 一个是 target 的 Q-network,一个是真正你会 update 的 Q-network。所以在 Double DQN 里面,你的实现方法会是拿你会 update 参数的那个 Q-network 去选action,然后你拿target 的network,那个固定住不动的network 去算value。而 Double DQN 相较于原来的 DQN 的更改是最少的,它几乎没有增加任何的运算量,连新的network 都不用,因为你原来就有两个network 了。你唯一要做的事情只有,本来你在找最大的a 的时候,你在决定这个a 要放哪一个的时候,你是用$Q'$ 来算,你是用target network 来算,现在改成用另外一个会 update 的 Q-network 来算。 + +- 如何理解Dueling DQN的模型变化带来的好处? + + 答:对于我们的 $Q(s,a)$ 其对应的state由于为table的形式,所以是离散的,而实际中的state不是离散的。对于 $Q(s,a)$ 的计算公式, $Q(s,a)=V(s)+A(s,a)$ 。其中的 $V(s)$ 是对于不同的state都有值,对于 $A(s,a)$ 对于不同的state都有不同的action对应的值。所以本质上来说,我们最终的矩阵 $Q(s,a)$ 的结果是将每一个 $V(s)$ 加到矩阵 $A(s,a)$ 中得到的。从模型的角度考虑,我们的network直接改变的 $Q(s,a)$ 而是 更改的 $V、A$ 。但是有时我们update时不一定会将 $V(s)$ 和 $Q(s,a)$ 都更新。我们将其分成两个path后,我们就不需要将所有的state-action pair都sample一遍,我们可以使用更高效的estimate Q value方法将最终的 $Q(s,a)$ 计算出来。 + +- 使用MC和TD平衡方法的优劣分别有哪些? + + 答: + + - 优势:因为我们现在 sample 了比较多的step,之前是只sample 了一个step, 所以某一个step 得到的data 是真实值,接下来都是Q value 估测出来的。现在sample 比较多step,sample N 个step 才估测value,所以估测的部分所造成的影响就会比小。 + - 劣势:因为我们的 reward 比较多,当我们把 N 步的 reward 加起来,对应的 variance 就会比较大。但是我们可以选择通过调整 N 值,去在variance 跟不精确的 Q 之间取得一个平衡。这里介绍的参数 N 就是一个hyper parameter,你要调这个N 到底是多少,你是要多 sample 三步,还是多 sample 五步。 + + + +## 3 Something About Interview + +- 高冷的面试官:DQN都有哪些变种?引入状态奖励的是哪种? + + 答:DQN三个经典的变种:Double DQN、Dueling DQN、Prioritized Replay Buffer。 + + - Double-DQN:将动作选择和价值估计分开,避免价值过高估计。 + - Dueling-DQN:将Q值分解为状态价值和优势函数,得到更多有用信息。 + - Prioritized Replay Buffer:将经验池中的经验按照优先级进行采样。 + +- 简述double DQN原理? + + 答:DQN由于总是选择当前值函数最大的动作值函数来更新当前的动作值函数,因此存在着过估计问题(估计的值函数大于真实的值函数)。为了解耦这两个过程,double DQN 使用了两个值网络,一个网络用来执行动作选择,然后用另一个值函数对一个的动作值更新当前网络。 + +- 高冷的面试官:请问Dueling DQN模型有什么优势呢? + + 答:对于我们的 $Q(s,a)$ 其对应的state由于为table的形式,所以是离散的,而实际中的state不是离散的。对于 $Q(s,a)$ 的计算公式, $Q(s,a)=V(s)+A(s,a)$ 。其中的 $V(s)$ 是对于不同的state都有值,对于 $A(s,a)$ 对于不同的state都有不同的action对应的值。所以本质上来说,我们最终的矩阵 $Q(s,a)$ 的结果是将每一个 $V(s)$ 加到矩阵 $A(s,a)$ 中得到的。从模型的角度考虑,我们的network直接改变的 $Q(s,a)$ 而是更改的 $V、A$ 。但是有时我们update时不一定会将 $V(s)$ 和 $Q(s,a)$ 都更新。我们将其分成两个path后,我们就不需要将所有的state-action pair都sample一遍,我们可以使用更高效的estimate Q value方法将最终的 $Q(s,a)$ 计算出来。 diff --git a/docs/chapter7/img/7.1.png b/docs/chapter7/img/7.1.png new file mode 100644 index 0000000..f2e4c77 Binary files /dev/null and b/docs/chapter7/img/7.1.png differ diff --git a/docs/chapter7/img/7.10.png b/docs/chapter7/img/7.10.png new file mode 100644 index 0000000..bbe4247 Binary files /dev/null and b/docs/chapter7/img/7.10.png differ diff --git a/docs/chapter7/img/7.11.png b/docs/chapter7/img/7.11.png new file mode 100644 index 0000000..da79108 Binary files /dev/null and b/docs/chapter7/img/7.11.png differ diff --git a/docs/chapter7/img/7.12.png b/docs/chapter7/img/7.12.png new file mode 100644 index 0000000..b2ac0a0 Binary files /dev/null and b/docs/chapter7/img/7.12.png differ diff --git a/docs/chapter7/img/7.13.png b/docs/chapter7/img/7.13.png new file mode 100644 index 0000000..8957ca5 Binary files /dev/null and b/docs/chapter7/img/7.13.png differ diff --git a/docs/chapter7/img/7.14.png b/docs/chapter7/img/7.14.png new file mode 100644 index 0000000..8ecd196 Binary files /dev/null and b/docs/chapter7/img/7.14.png differ diff --git a/docs/chapter7/img/7.2.png b/docs/chapter7/img/7.2.png new file mode 100644 index 0000000..02c9d86 Binary files /dev/null and b/docs/chapter7/img/7.2.png differ diff --git a/docs/chapter7/img/7.3.png b/docs/chapter7/img/7.3.png new file mode 100644 index 0000000..0cdff9d Binary files /dev/null and b/docs/chapter7/img/7.3.png differ diff --git a/docs/chapter7/img/7.4.png b/docs/chapter7/img/7.4.png new file mode 100644 index 0000000..a312b48 Binary files /dev/null and b/docs/chapter7/img/7.4.png differ diff --git a/docs/chapter7/img/7.5.png b/docs/chapter7/img/7.5.png new file mode 100644 index 0000000..68a78e0 Binary files /dev/null and b/docs/chapter7/img/7.5.png differ diff --git a/docs/chapter7/img/7.6.png b/docs/chapter7/img/7.6.png new file mode 100644 index 0000000..7197cf6 Binary files /dev/null and b/docs/chapter7/img/7.6.png differ diff --git a/docs/chapter7/img/7.7.png b/docs/chapter7/img/7.7.png new file mode 100644 index 0000000..aa3cf84 Binary files /dev/null and b/docs/chapter7/img/7.7.png differ diff --git a/docs/chapter7/img/7.8.png b/docs/chapter7/img/7.8.png new file mode 100644 index 0000000..d776fb0 Binary files /dev/null and b/docs/chapter7/img/7.8.png differ diff --git a/docs/chapter7/img/7.9.png b/docs/chapter7/img/7.9.png new file mode 100644 index 0000000..49f17b7 Binary files /dev/null and b/docs/chapter7/img/7.9.png differ diff --git a/docs/chapter7/img/p1.png b/docs/chapter7/img/p1.png new file mode 100644 index 0000000..1197da0 Binary files /dev/null and b/docs/chapter7/img/p1.png differ diff --git a/docs/chapter7/img/p2.png b/docs/chapter7/img/p2.png new file mode 100644 index 0000000..59a8671 Binary files /dev/null and b/docs/chapter7/img/p2.png differ diff --git a/docs/chapter7/project2.md b/docs/chapter7/project2.md new file mode 100644 index 0000000..70f8347 --- /dev/null +++ b/docs/chapter7/project2.md @@ -0,0 +1,89 @@ +# 使用DQN实现CartPole-v0 + +推荐使用Double-DQN去解决,即建立两个初始参数相同的全连接网络target_net和policy_net。 + +## CartPole-v0 + +CartPole-v0是OpenAI gym中的一个经典环境,通过向左(action=0)或向右(action=1)推车能够实现平衡,所以动作空间由两个动作组成。每进行一个step就会给一个+1的reward,如果无法保持平衡那么done等于true,本次episode失败。 + +**理想状态下,每个episode至少能进行200个step,也就是说每个episode的reward总和至少为200,step数目至少为200**。 + +![p1](img/p1.png) + +环境建立如下: + +```python +env = gym.make('CartPole-v0') +env.seed(1) # 设置env随机种子 +n_states = env.observation_space.shape[0] # 获取总的状态数 +n_actions = env.action_space.n # 获取总的动作数 +``` + +## 强化学习基本接口 + +```python +rewards = [] # 记录总的rewards +moving_average_rewards = [] # 记录总的经滑动平均处理后的rewards +ep_steps = [] +for i_episode in range(1, cfg.max_episodes+1): # cfg.max_episodes为最大训练的episode数 + state = env.reset() # reset环境状态 + ep_reward = 0 + for i_step in range(1, cfg.max_steps+1): # cfg.max_steps为每个episode的补偿 + action = agent.select_action(state) # 根据当前环境state选择action + next_state, reward, done, _ = env.step(action) # 更新环境参数 + ep_reward += reward + agent.memory.push(state, action, reward, next_state, done) # 将state等这些transition存入memory + state = next_state # 跳转到下一个状态 + agent.update() # 每步更新网络 + if done: + break + # 更新target network,复制DQN中的所有weights and biases + if i_episode % cfg.target_update == 0: # cfg.target_update为target_net的更新频率 + agent.target_net.load_state_dict(agent.policy_net.state_dict()) + print('Episode:', i_episode, ' Reward: %i' % + int(ep_reward), 'n_steps:', i_step, 'done: ', done,' Explore: %.2f' % agent.epsilon) + ep_steps.append(i_step) + rewards.append(ep_reward) + # 计算滑动窗口的reward + if i_episode == 1: + moving_average_rewards.append(ep_reward) + else: + moving_average_rewards.append( + 0.9*moving_average_rewards[-1]+0.1*ep_reward) +``` + +## 任务要求 + +训练并绘制reward以及滑动平均后的reward随episode的变化曲线图并记录超参数写成报告,图示如下: + +![rewards_train](assets/rewards_train.png) + +![moving_average_rewards_train](assets/moving_average_rewards_train.png) + +![steps_train](assets/steps_train.png) + +同时也可以绘制测试(eval)模型时的曲线: + +![rewards_eval](assets/rewards_eval.png) + +![moving_average_rewards_eval](assets/moving_average_rewards_eval.png) + +![steps_eval](assets/steps_eval.png) + +也可以[tensorboard](https://pytorch.org/docs/stable/tensorboard.html)查看结果,如下: + +![image-20201015221032985](assets/image-20201015221032985.png) + +### 代码清单 + +**main.py**:保存强化学习基本接口,以及相应的超参数,可使用argparse + +**model.py**:保存神经网络,比如全链接网络 + +**dqn.py**: 保存算法模型,主要包含select_action和update两个函数 + +**memory.py**:保存Replay Buffer + +**plot.py**:保存相关绘制函数,可选 + +[参考代码](https://github.com/datawhalechina/easy-rl/tree/master/codes/DQN) \ No newline at end of file diff --git a/docs/chapter8/chapter8.md b/docs/chapter8/chapter8.md new file mode 100644 index 0000000..c7154e0 --- /dev/null +++ b/docs/chapter8/chapter8.md @@ -0,0 +1,54 @@ +# 针对连续动作的 DQN + +## 方案 1 & 方案 2 +跟基于策略梯度的方法比起来,DQN 是比较稳的。策略梯度是没有太多游戏是玩得起来的,策略梯度比较不稳,在没有 近端策略优化 之前,我们很难用策略梯度做什么事情。DQN 相对而言是比较稳的。最早 DeepMind 的论文拿深度强化学习来玩雅达利的游戏,用的就是 DQN。DQN 比较容易训练的一个理由是:在 DQN 里面,你只要能够估计出Q函数,就保证你一定可以找到一个比较好的策略。也就是你只要能够估计出Q函数,就保证你可以改进策略。而估计Q函数这件事情,是比较容易的,因为它就是一个回归问题。在回归问题里面, 你可以轻易地知道模型学习得是不是越来越好,只要看那个回归的损失有没有下降,你就知道说模型学习得好不好,所以估计Q函数相较于学习一个策略是比较容易的。你只要估计Q函数,就可以保证说现在一定会得到比较好的策略。所以一般而言 DQN 比较容易操作。 + +DQN 其实存在一些问题,最大的问题是它不太容易处理连续动作。很多时候动作是连续的,比如我们玩雅达利的游戏,智能体只需要决定比如说上下左右,这种动作是离散的。那很多时候动作是连续的。举例来说假设智能体要做的事情是开自驾车,它要决定说它方向盘要左转几度, 右转几度,这是连续的。假设智能体是一个机器人,它身上有 50 个 关节,它的每一个动作就对应到它身上的这 50 个关节的角度。而那些角度也是连续的。所以很多时候动作并不是一个离散的东西,它是一个向量。在这个向量里面,它的每一个维度都有一个对应的值,都是实数,它是连续的。假设动作是连续的,做 DQN 就会有困难。因为在做 DQN 里面一个很重要的一步是你要能够解这个优化问题。估计出 Q函数$Q(s,a)$ 以后,必须要找到一个 $a$,它可以让 $Q(s,a)$ 最大,如下式所示。 + +$$ + a=\arg \max _{a} Q(s, a) +$$ + +假设$a$是离散的,即$a$的可能性都是有限的。举例来说,雅达利的小游戏里面,$a$ 就是上下左右跟开火,它是有限的,我们可以把每一个可能的动作都带到 Q 里面算它的 Q 值。但假如$a$是连续的,你无法穷举所有可能的连续动作,试试看哪一个连续动作可以让 Q 的值最大。 + +怎么解这个问题呢?就有各种不同的方案。 + +第一个方案是假设你不知道怎么解这个问题,因为$a$是没有办法穷举的,怎么办?我们可以采样出 $N$ 个可能的 $a$:$\left\{a_{1}, a_{2}, \cdots, a_{N}\right\}$ ,一个一个带到 Q函数里面,看谁最大。这个方法其实也不会太不高效, 因为你在运算的时候会使用 GPU,一次会把 $N$ 个连续动作都丢到 Q函数里面,一次得到 $N$ 个 Q 值,然后看谁最大。当然这不是一个非常精确的做法,因为你没有办法做太多的采样, 所以你估计出来的 Q 值,最后决定的动作可能不是非常的精确,这是第一个方案。 + +第二个方案是什么呢?既然要解的是一个优化问题(optimization problem),其实是要最大化目标函数(objective function),要最大化一个东西, 就可以用梯度上升。我们就把$a$当作是参数,然后要找一组$a$去最大化Q函数,就用梯度上升去更新 $a$ 的值,最后看看能不能找到一个$a$去最大化Q函数,也就是目标函数。当然这样子你会遇到全局最大值(global maximum)的问题, 就不见得能够真的找到最优的结果,而且这个运算量显然很大, 因为你要迭代地更新 $a$。我们训练一个网络就很花时间了。如果你用梯度上升的方法来处理连续的问题, 等于是你每次要决定采取哪一个动作的时候,都还要做一次训练网络的过程,显然运算量是很大的。这是第二个方案。 + + +## 方案 3:设计网络 + +第三个方案是特别设计一个网络的架构,特别设计Q函数,使得解 arg max 的问题变得非常容易。也就是这边的Q函数不是一个一般的Q函数,特别设计一下它的样子,让你要找让这个Q函数最大的 $a$ 的时候非常容易。 + +下图是一个例子,这边有Q函数,这个Q函数的做法是这样。 + 通常输入状态 $s$ 就是一个图像,可以用一个向量或一个矩阵来表示。 + 输入 $s$,Q函数会输出 3 个东西。它会输出 $\mu(s)$,这是一个向量。它会输出 $\Sigma(s)$ ,这是一个矩阵。它会输出 $V(s)$,这是一个标量。 + 输出这 3 个东西以后,我们知道Q函数其实是吃一个$s$跟 $a$,然后决定一个值。Q函数意思是说在某一个状态,采取某一个动作的时候,你期望的奖励有多大。到目前为止这个Q函数只吃 $s$,它还没有吃$a$进来,$a$ 在哪里呢?当这个Q函数吐出 $\mu$、 $\Sigma$ 跟 $V$ 的时候,我们才把$a$引入,用$a$跟 $\mu(s)、\Sigma(s)、V$ 互相作用一下,你才算出最终的 Q 值。 + +![](img/8.2.png) + + $a$怎么和这 3 个东西互相作用呢?实际上 $Q(s,a)$,Q函数的运作方式是先输入 $s$,让你得到 $\mu,\Sigma$ 跟 $V$。然后再输入 $a$,然后接下来把$a$跟 $\mu$ 相减。注意一下$a$现在是连续的动作,所以它也是一个向量。假设你现在是要操作机器人的话,这个向量的每一个维度,可能就对应到机器人的某一个关节,它的数值就是关节的角度,所以$a$是一个向量。把向量 $a$ 减掉向量 $\mu$,取转置,所以它是一个横的向量。$\Sigma$ 是一个矩阵。然后$a$减掉 $\mu(s)$ ,$a$ 和 $\mu(s)$ 都是向量,减掉以后还是一个竖的向量。所以 $-(a-\mu(s))^{T} \Sigma(s)(a-\mu(s))+V(s)$ 是一个标量,这个数值就是 Q 值 $Q(s,a)$。 + + 假设 $Q(s,a)$ 定义成这个样子,我们要怎么找到一个$a$去最大化这个 Q 值呢?这个方案非常简单。因为 $(a-\mu(s))^{T} \Sigma(s)(a-\mu(s))$ 一定是正的,它前面乘上一个负号,所以第一项就假设我们不看这个负号的话,第一项的值越小,最终的 Q 值就越大。因为我们是把 $V(s)$ 减掉第一项,所以第一项的值越小,最后的 Q 值就越大。怎么让第一项的值最小呢?你直接把$a$代入 $\mu$ 的值,让它变成 0,就会让第一项的值最小。 + + $\Sigma$ 一定是正定的。因为这个东西就像是高斯分布(Gaussian distribution),所以 $\mu$ 就是高斯分布的均值,$\Sigma$ 就是高斯分布的方差。但方差是一个正定(positive definite)的矩阵,怎么样让这个 $\Sigma$ 一定是正定的矩阵呢?其实在 $Q^{\pi}$ 里面,它不是直接输出 $\Sigma$,如果直接输出 一个 $\Sigma$, 它不一定是正定的矩阵。它其实是输出 一个矩阵,然后再把那个矩阵跟另外一个矩阵做转置相乘, 然后可以确保 $\Sigma$ 是正定的。这边要强调的点就是说,实际上它不是直接输出一个矩阵。你再去那个论文里面查看一下它的技巧,它可以保证说 $\Sigma$ 是正定的。 + + 你把$a$代入 $\mu(s)$ 以后,你可以让 Q 的值最大。所以假设要你 arg max Q 函数,如下式所示。 +$$ +\mu(s)=\arg \max _{a} Q(s, a) +$$ + +虽然一般而言,若 Q 是一个一般的函数, 你很难算,但是我们这边设计了 Q 这个函数,$a$ 只要设 $\mu(s)$,我们就得到最大值。你在解这个 arg max 的问题的时候就变得非常容易。所以 DQN 也可以用在连续的情况,只是有一些局限,就是函数不能够随便乱设,它必须有一些限制。 + +## 方案 4:不使用DQN +第 4 招就是不要用 DQN。用 DQN 处理连续动作还是比较麻烦。 +基于策略的方法 PPO 和基于价值的方法 DQN,这两者其实是可以结合在一起的,如下图所示,也就是演员-评论员的方法。 + +![](img/8.3.png) + + + + + diff --git a/docs/chapter8/chapter8_questions&keywords.md b/docs/chapter8/chapter8_questions&keywords.md new file mode 100644 index 0000000..9645c9c --- /dev/null +++ b/docs/chapter8/chapter8_questions&keywords.md @@ -0,0 +1,20 @@ +# Chapter8 Q-learning for Continuous Actions + +## Questions + +- Q-learning相比于policy gradient based方法为什么训练起来效果更好,更平稳? + + 答:在 Q-learning 中,只要能够 estimate 出Q-function,就可以保证找到一个比较好的 policy,同样的只要能够 estimate 出 Q-function,就保证可以 improve 对应的 policy。而因为 estimate Q-function 作为一个回归问题,是比较容易的。在这个回归问题中, 我们可以时刻观察我们的模型训练的效果是不是越来越好,一般情况下我们只需要关注 regression 的 loss 有没有下降,你就知道你的 model learn 的好不好。所以 estimate Q-function 相较于 learn 一个 policy 是比较容易的。你只要 estimate Q-function,就可以保证说现在一定会得到比较好的 policy,同样其也比较容易操作。 + +- Q-learning在处理continuous action时存在什么样的问题呢? + + 答:在日常的问题中,我们的问题都是continuous action的,例如我们的 agent 要做的事情是开自驾车,它要决定说它方向盘要左转几度, 右转几度,这就是 continuous 的;假设我们的 agent 是一个机器人,假设它身上有 50 个关节,它的每一个 action 就对应到它身上的这 50 个关节的角度,而那些角度也是 continuous 的。 + + 然而在解决Q-learning问题时,很重要的一步是要求能够解对应的优化问题。当我们 estimate 出Q-function $Q(s,a)$ 以后,必须要找到一个 action,它可以让 $Q(s,a)$ 最大。假设 action 是 discrete 的,那 a 的可能性都是有限的。但如果action是continuous的情况下,我们就不能像离散的action一样,穷举所有可能的continuous action了。 + + 为了解决这个问题,有以下几种solutions: + + - 第一个解决方法:我们可以使用所谓的sample方法,即随机sample出N个可能的action,然后一个一个带到我们的Q-function中,计算对应的N个Q value比较哪一个的值最大。但是这个方法因为是sample所以不会非常的精确。 + - 第二个解决方法:我们将这个continuous action问题,看为一个优化问题,从而自然而然地想到了可以用gradient ascend去最大化我们的目标函数。具体地,我们将action看为我们的变量,使用gradient ascend方法去update action对应的Q-value。但是这个方法通常的时间花销比较大,因为是需要迭代运算的。 + - 第三个解决方法:设计一个特别的network架构,设计一个特别的Q-function,使得解我们 argmax Q-value的问题变得非常容易。也就是这边的 Q-function 不是一个 general 的 Q-function,特别设计一下它的样子,让你要找让这个 Q-function 最大的 a 的时候非常容易。但是这个方法的function不能随意乱设,其必须有一些额外的限制。具体的设计方法,可以我们的chapter8的详细教程。 + - 第四个解决方法:不用Q-learning,毕竟用其处理continuous的action比较麻烦。 diff --git a/docs/chapter8/img/8.2.png b/docs/chapter8/img/8.2.png new file mode 100644 index 0000000..a871c1a Binary files /dev/null and b/docs/chapter8/img/8.2.png differ diff --git a/docs/chapter8/img/8.3.png b/docs/chapter8/img/8.3.png new file mode 100644 index 0000000..03af460 Binary files /dev/null and b/docs/chapter8/img/8.3.png differ diff --git a/docs/chapter9/chapter9.md b/docs/chapter9/chapter9.md new file mode 100644 index 0000000..0b166eb --- /dev/null +++ b/docs/chapter9/chapter9.md @@ -0,0 +1,206 @@ +# Actor-Critic + +## Actor-Critic + +在 REINFORCE 算法中,每次需要根据一个策略采集一条完整的轨迹,并计算这条轨迹上的回报。这种采样方式的方差比较大,学习效率也比较低。我们可以借鉴时序差分学习的思想,使用动态规划方法来提高采样的效率,即从状态 $s$ 开始的总回报可以通过当前动作的即时奖励 $r(s,a,s')$ 和下一个状态 $s'$ 的值函数来近似估计。 + +`演员-评论家算法(Actor-Critic Algorithm)`是一种结合`策略梯度`和`时序差分学习`的强化学习方法,其中: + +* 演员(Actor)是指策略函数 $\pi_{\theta}(a|s)$,即学习一个策略来得到尽量高的回报。 +* 评论家(Critic)是指值函数 $V^{\pi}(s)$,对当前策略的值函数进行估计,即评估演员的好坏。 +* 借助于值函数,演员-评论家算法可以进行单步更新参数,不需要等到回合结束才进行更新。 + +在 Actor-Critic 算法 里面,最知名的方法就是 `A3C(Asynchronous Advantage Actor-Critic)`。 + +* 如果去掉 Asynchronous,只有 `Advantage Actor-Critic`,就叫做 `A2C`。 +* 如果加了 Asynchronous,变成 `Asynchronous Advantage Actor-Critic`,就变成 `A3C`。 + +### Review: Policy Gradient + +![](img/9.1.png) + +那我们复习一下 policy gradient,在 policy gradient,我们在更新 policy 的参数 $\theta$ 的时候,我们是用了下面这个式子来算出 gradient。 +$$ +\nabla \bar{R}_{\theta} \approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}}\left(\sum_{t^{\prime}=t}^{T_{n}} \gamma^{t^{\prime}-t} r_{t^{\prime}}^{n}-b\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) +$$ +这个式子是在说,我们先让 agent 去跟环境互动一下,那我们可以计算出在某一个状态 s,采取了某一个动作 a 的概率 $p_{\theta}(a_t|s_t)$。接下来,我们去计算在某一个状态 s 采取了某一个动作 a 之后,到游戏结束为止,累积奖励有多大。我们把这些奖励从时间 t 到时间 T 的奖励通通加起来,并且会在前面乘一个折扣因子,可能设 0.9 或 0.99。我们会减掉一个 baseline b,减掉这个值 b 的目的,是希望括号这里面这一项是有正有负的。如果括号里面这一项是正的,我们就要增加在这个状态采取这个动作的机率;如果括号里面是负的,我们就要减少在这个状态采取这个动作的机率。 + +我们把用 G 来表示累积奖励。但 G 这个值,其实是非常不稳定的。因为互动的过程本身是有随机性的,所以在某一个状态 s 采取某一个动作 a,然后计算累积奖励,每次算出来的结果都是不一样的,所以 G 其实是一个随机变量。给同样的状态 s,给同样的动作 a,G 可能有一个固定的分布。但我们是采取采样的方式,我们在某一个状态 s 采取某一个动作 a,然后玩到底,我们看看得到多少的奖励,我们就把这个东西当作 G。 + +把 G 想成是一个随机变量的话,我们实际上是对这个 G 做一些采样,然后拿这些采样的结果,去更新我们的参数。但实际上在某一个状态 s 采取某一个动作 a,接下来会发生什么事,它本身是有随机性的。虽然说有个固定的分布,但它本身是有随机性的,而这个随机变量的方差可能会非常大。你在同一个状态采取同一个动作,你最后得到的结果可能会是天差地远的。 + +假设我们可以采样足够的次数,在每次更新参数之前,我们都可以采样足够的次数,那其实没有什么问题。但问题就是我们每次做 policy gradient,每次更新参数之前都要做一些采样,这个采样的次数其实是不可能太多的,我们只能够做非常少量的采样。如果你正好采样到差的结果,比如说你采样到 G = 100,采样到 G = -10,那显然你的结果会是很差的。 + +### Review: Q-learning + +![](img/9.2.png) + +Q: 能不能让整个训练过程变得比较稳定一点,能不能够直接估测 G 这个随机变量的期望值? + +A: 我们在状态 s 采取动作 a 的时候,直接用一个网络去估测在状态 s 采取动作 a 的时候,G 的期望值。如果这件事情是可行的,那之后训练的时候,就用期望值来代替采样的值,这样会让训练变得比较稳定。 + +Q: 怎么拿期望值代替采样的值呢? + +A: 这边就需要引入基于价值的(value-based)的方法。基于价值的方法就是 Q-learning。Q-learning 有两种函数,有两种 critics。 + +* 第一种 critic 是 $V^{\pi}(s)$,它的意思是说,假设 actor 是 $\pi$,拿 $\pi$ 去跟环境做互动,当我们看到状态 s 的时候,接下来累积奖励 的期望值有多少。 +* 还有一个 critic 是 $Q^{\pi}(s,a)$。$Q^{\pi}(s,a)$ 把 s 跟 a 当作输入,它的意思是说,在状态 s 采取动作 a,接下来都用 actor $\pi$ 来跟环境进行互动,累积奖励的期望值是多少。 + +* $V^{\pi}$ 输入 s,输出一个标量。 + +* $Q^{\pi}$ 输入 s,然后它会给每一个 a 都分配一个 Q value。 + +* 你可以用 TD 或 MC 来估计。用 TD 比较稳,用 MC 比较精确。 + +### Actor-Critic + +![](img/9.3.png) + +随机变量 $G$ 的期望值正好就是 Q ,即 +$$ +E\left[G_{t}^{n}\right]=Q^{\pi_{\theta}} \left(s_{t}^{n}, a_{t}^{n}\right) +$$ + +因为这个就是 Q 的定义。Q-function 的定义就是在某一个状态 s,采取某一个动作 a,假设 policy 就是 $\pi$ 的情况下会得到的累积奖励的期望值有多大,而这个东西就是 G 的期望值。累积奖励的期望值就是 G 的期望值。 + +所以假设用 $E\left[G_{t}^{n}\right]$ 来代表 $\sum_{t^{\prime}=t}^{T_{n}} \gamma^{t^{\prime}-t} r_{t^{\prime}}^{n}$ 这一项的话,把 Q-function 套在这里就结束了,我们就可以把 Actor 跟 Critic 这两个方法结合起来。 + +有不同的方法来表示 baseline,但一个常见的做法是用价值函数 $V^{\pi_{\theta}}\left(s_{t}^{n}\right)$ 来表示 baseline。价值函数是说,假设 policy 是 $\pi$,在某一个状态 s 一直互动到游戏结束,期望奖励(expected reward)有多大。 $V^{\pi_{\theta}}\left(s_{t}^{n}\right)$ 没有涉及到动作,$Q^{\pi_{\theta}}\left(s_{t}^{n}, a_{t}^{n}\right)$ 涉及到动作。 + +其实 $V^{\pi_{\theta}}\left(s_{t}^{n}\right)$ 会是 $Q^{\pi_{\theta}}\left(s_{t}^{n}, a_{t}^{n}\right)$ 的期望值,所以 $Q^{\pi_{\theta}}\left(s_{t}^{n}, a_{t}^{n}\right)-V^{\pi_{\theta}}\left(s_{t}^{n}\right)$ 会有正有负,所以 $\sum_{t^{\prime}=t}^{T_{n}} \gamma^{t^{\prime}-t} r_{t^{\prime}}^{n}-b$ 这一项就会是有正有负的。 + +所以我们就把 policy gradient 里面 $\sum_{t^{\prime}=t}^{T_{n}} \gamma^{t^{\prime}-t} r_{t^{\prime}}^{n}-b$ 这一项换成了 $Q^{\pi_{\theta}}\left(s_{t}^{n}, a_{t}^{n}\right)-V^{\pi_{\theta}}\left(s_{t}^{n}\right)$。 + +### Advantage Actor-Critic + +![](img/9.4.png) + +如果你这么实现的话,有一个缺点是:你要估计 2 个 网络:Q-network 和 V-network,你估测不准的风险就变成两倍。所以我们何不只估测一个网络? + +事实上在这个 Actor-Critic 方法里面。你可以只估测 V 这个网络,你可以用 V 的值来表示 Q 的值,$Q^{\pi}\left(s_{t}^{n}, a_{t}^{n}\right)$ 可以写成 $ r_{t}^{n}+V^{\pi}\left(s_{t+1}^{n}\right)$ 的期望值,即 +$$ +Q^{\pi}\left(s_{t}^{n}, a_{t}^{n}\right)=E\left[r_{t}^{n}+V^{\pi}\left(s_{t+1}^{n}\right)\right] +$$ + +你在状态 s 采取动作 a,会得到奖励 r,然后跳到状态 $s_{t+1}$。但是你会得到什么样的奖励 r,跳到什么样的状态 $s_{t+1}$,它本身是有随机性的。所以要把右边这个式子,取期望值它才会等于 Q-function。但我们现在把期望值这件事情去掉,即 +$$ +Q^{\pi}\left(s_{t}^{n}, a_{t}^{n}\right)=r_{t}^{n}+V^{\pi}\left(s_{t+1}^{n}\right) +$$ + +我们就可以把 Q-function 用 r + V 取代掉,然后得到下式: +$$ +r_{t}^{n}+V^{\pi}\left(s_{t+1}^{n}\right)-V^{\pi}\left(s_{t}^{n}\right) +$$ +把这个期望值去掉的好处就是你不需要估计 Q 了,你只需要估计 V 就够了,你只要估计 一个网络就够了。但这样你就引入了一个随机的东西 r ,它是有随机性的,它是一个随机变量。但是这个随机变量,相较于累积奖励 G 可能还好,因为它是某一个步骤会得到的奖励,而 G 是所有未来会得到的奖励的总和。G 的方差比较大,r 虽然也有一些方差,但它的方差会比 G 要小。所以把原来方差比较大的 G 换成方差比较小的 r 也是合理的。 + +Q: 为什么可以直接把期望值拿掉? + +A: 原始的 A3C paper 试了各种方法,最后做出来就是这个最好。当然你可能说,搞不好估计 Q 和 V,也可以估计 很好,那我告诉你就是做实验的时候,最后结果就是这个最好,所以后来大家都用这个。 + +![](img/9.5.png) + +因为 $r_{t}^{n}+V^{\pi}\left(s_{t+1}^{n}\right)-V^{\pi}\left(s_{t}^{n}\right)$ 叫做 `Advantage function`。所以这整个方法就叫 `Advantage Actor-Critic`。 + +整个流程是这样子的。我们有一个 $\pi$,有个初始的 actor 去跟环境做互动,先收集资料。在 policy gradient 方法里面收集资料以后,你就要拿去更新 policy。但是在 actor-critic 方法里面,你不是直接拿那些资料去更新 policy。你先拿这些资料去估计价值函数,你可以用 TD 或 MC 来估计价值函数 。接下来,你再基于价值函数,套用下面这个式子去更新 $\pi$。 +$$ +\nabla \bar{R}_{\theta} \approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}}\left(r_{t}^{n}+V^{\pi}\left(s_{t+1}^{n}\right)-V^{\pi}\left(s_{t}^{n}\right)\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) +$$ +然后你有了新的 $\pi$ 以后,再去跟环境互动,再收集新的资料,去估计价值函数。然后再用新的价值函数 去更新 policy,去更新 actor。 + +整个 actor-critic 的算法就是这么运作的。 + +![](img/9.6.png) + +实现 Actor-Critic 的时候,有两个一定会用的 tip。 + +* 第一个 tip 是说,我们需要估计两个网络:V function 和 policy 的网络(也就是 actor)。 + * Critic 网络 $V^\pi(s)$ 输入一个状态,输出一个标量。 + * Actor 网络 $\pi(s)$ 输入一个状态, + * 如果动作是离散的,输出就是一个动作的分布。 + * 如果动作是连续的,输出就是一个连续的向量。 + * 上图是举的是离散的例子,但连续的情况也是一样的。输入一个状态,然后它决定你现在要采取哪一个动作。**这两个网络,actor 和 critic 的输入都是 s,所以它们前面几个层(layer),其实是可以共享的。** + * 尤其是假设你今天是玩 Atari 游戏,输入都是图像。输入的图像都非常复杂,图像很大,通常你前面都会用一些 CNN 来处理,把那些图像抽象成高级(high level)的信息。把像素级别的信息抽象成高级信息这件事情,其实对 actor 跟 critic 来说是可以共用的。所以通常你会让 actor 跟 critic 的共享前面几个层,你会让 actor 跟 critic 的前面几个层共用同一组参数,那这一组参数可能是 CNN 的参数。 + * 先把输入的像素变成比较高级的信息,然后再给 actor 去决定说它要采取什么样的行为,给这个 critic,给价值函数去计算期望奖励。 +* **第二个 tip 是我们一样需要探索(exploration)的机制。**在做 Actor-Critic 的时候,有一个常见的探索的方法是你会对你的 $\pi$ 的输出的分布下一个约束。这个约束是希望这个分布的熵(entropy)不要太小,希望这个分布的熵可以大一点,也就是希望不同的动作它的被采用的概率平均一点。这样在测试的时候,它才会多尝试各种不同的动作,才会把这个环境探索的比较好,才会得到比较好的结果。 + +这个就是 Advantage Actor-Critic。 + +## A3C +![](img/9.7.png) + +强化学习有一个问题就是它很慢,那怎么增加训练的速度呢?举个例子,火影忍者就是有一次鸣人说,他想要在一周之内打败晓,所以要加快修行的速度,他老师就教他一个方法:用影分身进行同样修行。两个一起修行的话,经验值累积的速度就会变成 2 倍,所以鸣人就开了 1000 个影分身来进行修行。这个其实就是 `Asynchronous(异步的) Advantage Actor-Critic`,也就是 A3C 这个方法的精神。 + +![](img/9.8.png) + +**A3C 这个方法就是同时开很多个 worker,那每一个 worker 其实就是一个影分身。那最后这些影分身会把所有的经验,通通集合在一起。**你如果没有很多个 CPU,可能也是不好实现的,你可以实现 A2C 就好。 + +Q: A3C 是怎么运作的? + +A: + +* A3C 一开始有一个 global network。那我们刚才有讲过,其实 policy network 跟 value network 是绑(tie)在一起的,它们的前几个层会被绑一起。我们有一个 global network,它们有包含 policy 的部分和 value 的部分。 + +* 假设 global network 的参数是 $\theta_1$,你会开很多个 worker。每一个 worker 就用一张 CPU 去跑。比如你就开 8 个 worker,那你至少 8 张 CPU。每一个 worker 工作前都会 global network 的参数复制过来。 +* 接下来你就去跟环境做互动,每一个 actor 去跟环境做互动的时候,要收集到比较多样性的数据。举例来说,如果是走迷宫的话,可能每一个 actor 起始的位置都会不一样,这样它们才能够收集到比较多样性的数据。 +* 每一个 actor 跟环境做互动,互动完之后,你就会计算出梯度。计算出梯度以后,你要拿梯度去更新你的参数。你就计算一下你的梯度,然后用你的梯度去更新 global network 的参数。就是这个 worker 算出梯度以后,就把梯度传回给中央的控制中心,然后中央的控制中心就会拿这个梯度去更新原来的参数。 +* 注意,所有的 actor 都是平行跑的,每一个 actor 就是各做各的,不管彼此。所以每个人都是去要了一个参数以后,做完就把参数传回去。所以当第一个 worker 做完想要把参数传回去的时候,本来它要的参数是 $\theta_1$,等它要把梯度传回去的时候。可能别人已经把原来的参数覆盖掉,变成 $\theta_2$了。但是没有关系,它一样会把这个梯度就覆盖过去就是了。Asynchronous actor-critic 就是这么做的,这个就是 A3C。 + + ## Pathwise Derivative Policy Gradient +![](img/9.9.png) + +讲完 A3C 之后,我们要讲另外一个方法叫做 `Pathwise Derivative Policy Gradient`。这个方法可以看成是 Q-learning 解连续动作的一种特别的方法,也可以看成是一种特别的 Actor-Critic 的方法。 + +用棋灵王来比喻的话,阿光是一个 actor,佐为是一个 critic。阿光落某一子以后, + +* 如果佐为是一般的 Actor-Critic,他会告诉阿光说这时候不应该下小马步飞,他会告诉你,你现在采取的这一步算出来的 value 到底是好还是不好,但这样就结束了,他只告诉你说好还是不好。因为一般的这个 Actor-Critic 里面那个 critic 就是输入状态或输入状态跟动作的对(pair),然后给你一个 value 就结束了。所以对 actor 来说,它只知道它做的这个行为到底是好还是不好。 +* 但如果是在 pathwise derivative policy gradient 里面,这个 critic 会直接告诉 actor 说采取什么样的动作才是好的。所以今天佐为不只是告诉阿光说,这个时候不要下小马步飞,同时还告诉阿光说这个时候应该要下大马步飞,所以这个就是 Pathwise Derivative Policy Gradient 中的 critic。critic 会直接告诉 actor 做什么样的动作才可以得到比较大的 value。 + +从 Q-learning 的观点来看,Q-learning 的一个问题是你在用 Q-learning 的时候,考虑 continuous vector 会比较麻烦,比较没有通用的解决方法(general solution),怎么解这个优化问题呢? + +我们用一个 actor 来解这个优化的问题。本来在 Q-learning 里面,如果是一个连续的动作,我们要解这个优化问题。但是现在这个优化问题由 actor 来解,假设 actor 就是一个 solver,这个 solver 的工作就是给定状态 s,然后它就去解,告诉我们说,哪一个动作可以给我们最大的 Q value,这是从另外一个观点来看 pathwise derivative policy gradient 这件事情。 + +在 GAN 中也有类似的说法。我们学习一个 discriminator 来评估东西好不好,要 discriminator 生成东西的话,非常困难,那怎么办?因为要解一个 arg max 的问题非常的困难,所以用 generator 来生成。 + +所以今天的概念其实是一样的,Q 就是那个 discriminator。要根据这个 discriminator 决定动作非常困难,怎么办?另外学习一个网络来解这个优化问题,这个东西就是 actor。 + +所以两个不同的观点是同一件事。从两个不同的观点来看, + +* 一个观点是说,我们可以对原来的 Q-learning 加以改进,我们学习一个 actor 来决定动作以解决 arg max 不好解的问题。 +* 另外一个观点是,原来的 actor-critic 的问题是 critic 并没有给 actor 足够的信息,它只告诉它好或不好,没有告诉它说什么样叫好,那现在有新的方法可以直接告诉 actor 说,什么样叫做好。 + +![](img/9.10.png) + +那我们讲一下它的算法。假设我们学习了一个 Q-function,Q-function 就是输入 s 跟 a,输出就是 $Q^{\pi}(s,a)$。那接下来,我们要学习一个 actor,这个 actor 的工作就是解这个 arg max 的问题。这个 actor 的工作就是输入一个状态 s,希望可以输出一个动作 a。这个动作 a 被丢到 Q-function 以后,它可以让 $Q^{\pi}(s,a)$ 的值越大越好。 + +那实际上在训练的时候,你其实就是把 Q 跟 actor 接起来变成一个比较大的网络。Q 是一个网络,输入 s 跟 a,输出一个 value。Actor 在训练的时候,它要做的事情就是输入 s,输出 a。把 a 丢到 Q 里面,希望输出的值越大越好。在训练的时候会把 Q 跟 actor 接起来,当作是一个大的网络。然后你会固定住 Q 的参数,只去调 actor 的参数,就用 gradient ascent 的方法去最大化 Q 的输出。这就是一个 GAN,这就是 conditional GAN。Q 就是 discriminator,但在强化学习就是 critic,actor 在 GAN 里面就是 generator,其实它们就是同一件事情。 + +![](img/9.11.png) + +我们来看一下 pathwise derivative policy gradient 的算法。一开始你会有一个 actor $\pi$,它去跟环境互动,然后,你可能会要它去估计 Q value。估计完 Q value 以后,你就把 Q value 固定,只去学习一个 actor。假设这个 Q 估得是很准的,它知道在某一个状态采取什么样的动作,会真的得到很大的 value。接下来就学习这个 actor,actor 在给定 s 的时候,它采取了 a,可以让最后 Q-function 算出来的 value 越大越好。你用这个 criteria 去更新你的 actor $\pi$。然后有新的 $\pi$ 再去跟环境做互动,再估计 Q,再得到新的 $\pi$ 去最大化 Q 的输出。本来在 Q-learning 里面,你用得上的技巧,在这边也几乎都用得上,比如说 replay buffer、exploration 等等。 + +![](img/9.12.png) + +上图是原来 Q-learning 的算法。你有一个 Q-function Q,你有另外一个目标的 Q-function 叫做 $\hat{Q}$。然后在每一次 训练,在每一个回合的每一个时间点里面,你会看到一个状态 $s_t$,你会采取某一个动作 $a_{t}$。至于采取哪一个动作是由 Q-function 所决定的,因为解一个 arg max 的问题。如果是离散的话没有问题,你就看说哪一个 a 可以让 Q 的 value 最大,就采取哪一个动作。那你需要加一些探索,这样表现才会好。你会得到奖励 $r_t$,跳到新的状态 $s_{t+1}$。你会把 $s_t$, $a_{t}$, $r_t$, $s_{t+1}$ 塞到你的 buffer 里面去。你会从你的 buffer 里面采样一个批量的数据,在这个批量数据里面,可能某一笔是 $s_i, a_i, r_i, s_{i+1}$。接下来你会算一个目标,这个目标叫做 $y$ ,$y=r_{i}+\max _{a} \hat{Q}\left(s_{i+1}, a\right)$。然后怎么学习你的 Q 呢?你希望 $Q(s_i,a_i)$ 跟 y 越接近越好,这是一个回归的问题,最后每 C 个步骤,你要把用 Q 替代 $\hat{Q}$ 。 + +![](img/9.13.png) + + 接下来我们把 Q-learning 改成 Pathwise Derivative Policy Gradient,这边需要做四个改变。 + +* 第一个改变是,你要把 Q 换成 $\pi$,本来是用 Q 来决定在状态 $s_t$ 产生那一个动作, $a_{t}$ 现在是直接用 $\pi$ 。我们不用再解 arg max 的问题了,我们直接学习了一个 actor。这个 actor 输入 $s_t$ 就会告诉我们应该采取哪一个 $a_{t}$。所以本来输入 $s_t$,采取哪一个 $a_t$,是 Q 决定的。在 Pathwise Derivative Policy Gradient 里面,我们会直接用 $\pi$ 来决定,这是第一个改变。 +* 第二个改变是,本来这个地方是要计算在 $s_{i+1}$,根据你的 policy 采取某一个动作 a 会得到多少的 Q value。那你会采取让 $\hat{Q}$ 最大的那个动作 a。那现在因为我们其实不好解这个 arg max 的问题,所以 arg max 问题,其实现在就是由 policy $\pi$ 来解了,所以我们就直接把 $s_{i+1}$ 代到 policy $\pi$ 里面,你就会知道说给定 $s_{i+1}$ ,哪一个动作会给我们最大的 Q value,那你在这边就会采取那一个动作。在 Q-function 里面,有两个 Q network,一个是真正的 Q network,另外一个是目标 Q network。那实际上你在实现这个算法 的时候,你也会有两个 actor,你会有一个真正要学习的 actor $\pi$,你会有一个目标 actor $\hat{\pi}$ 。这个原理就跟为什么要有目标 Q network 一样,我们在算目标 value 的时候,我们并不希望它一直的变动,所以我们会有一个目标的 actor 和一个目标的 Q-function,它们平常的参数就是固定住的,这样可以让你的这个目标的 value 不会一直地变化。所以本来到底是要用哪一个动作 a,你会看说哪一个动作 a 可以让 $\hat{Q}$ 最大。但现在因为哪一个动作 a 可以让 $\hat{Q}$ 最大这件事情已经用 policy 取代掉了,所以我们要知道哪一个动作 a 可以让 $\hat{Q}$ 最大,就直接把那个状态带到 $\hat{\pi}$ 里面,看它得到哪一个 a,那个 a 就是会让 $\hat{Q}(s,a)$ 的值最大的那个 a 。其实跟原来的这个 Q-learning 也是没什么不同,只是原来你要解 arg max 的地方,通通都用 policy 取代掉了,那这个是第二个不同。 +* 第三个不同就是之前只要学习 Q,现在你多学习一个 $\pi$,那学习 $\pi$ 的时候的方向是什么呢?学习 $\pi$ 的目的,就是为了最大化 Q-function,希望你得到的这个 actor,它可以让你的 Q-function 输出越大越好,这个跟学习 GAN 里面的 generator 的概念。其实是一样的。 +* 第四个步骤,就跟原来的 Q-function 一样。你要把目标的 Q network 取代掉,你现在也要把目标 policy 取代掉。 + +## Connection with GAN +![](img/9.14.png) + +其实 GAN 跟 Actor-Critic 的方法是非常类似的。这边就不细讲,你可以去找到一篇 paper 叫做 `Connecting Generative Adversarial Network and Actor-Critic Methods`。 + +Q: 知道 GAN 跟 Actor-Critic 非常像有什么帮助呢? + +A: 一个很大的帮助就是 GAN 跟 Actor-Critic 都是以难训练而闻名的。所以在文献上就会收集各式各样的方法,告诉你说怎么样可以把 GAN 训练起来。怎么样可以把 Actor-Critic 训练起来。但是因为做 GAN 跟 Actor-Critic 的人是两群人,所以这篇 paper 里面就列出说在 GAN 上面有哪些技术是有人做过的,在 Actor-Critic 上面,有哪些技术是有人做过的。也许在 GAN 上面有试过的技术,你可以试着应用在 Actor-Critic 上,在 Actor-Critic 上面做过的技术,你可以试着应用在 GAN 上面,看看是否 work。 + +## References + +* [神经网络与深度学习](https://nndl.github.io/) + diff --git a/docs/chapter9/chapter9_questions&keywords.md b/docs/chapter9/chapter9_questions&keywords.md new file mode 100644 index 0000000..979df36 --- /dev/null +++ b/docs/chapter9/chapter9_questions&keywords.md @@ -0,0 +1,89 @@ +# Chapter9 Actor-Critic + +## 1 Keywords + +- **A2C:** Advantage Actor-Critic的缩写,一种Actor-Critic方法。 + +- **A3C:** Asynchronous(异步的)Advantage Actor-Critic的缩写,一种改进的Actor-Critic方法,通过异步的操作,进行RL模型训练的加速。 +- **Pathwise Derivative Policy Gradient:** 其为使用 Q-learning 解 continuous action 的方法,也是一种 Actor-Critic 方法。其会对于actor提供value最大的action,而不仅仅是提供某一个action的好坏程度。 + +## 2 Questions + +- 整个Advantage actor-critic(A2C)算法的工作流程是怎样的? + + 答:在传统的方法中,我们有一个policy $\pi$ 以及一个初始的actor与environment去做互动,收集数据以及反馈。通过这些每一步得到的数据与反馈,我们就要进一步更新我们的policy $\pi$ ,通常我们所使用的方式是policy gradient。但是对于actor-critic方法,我们不是直接使用每一步得到的数据和反馈进行policy $\pi$ 的更新,而是使用这些数据进行 estimate value function,这里我们通常使用的算法包括前几个chapters重点介绍的TD和MC等算法以及他们的优化算法。接下来我们再基于value function来更新我们的policy,公式如下: + $$ + \nabla \bar{R}_{\theta} \approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}}\left(r_{t}^{n}+V^{\pi}\left(s_{t+1}^{n}\right)-V^{\pi}\left(s_{t}^{n}\right)\right) \nabla \log p_{\theta}\left(a_{t}^{n} \mid s_{t}^{n}\right) + $$ + 其中,上式中的 $r_{t}^{n}+V^{\pi}\left(s_{t+1}^{n}\right)-V^{\pi}\left(s_{t}^{n}\right)$ 我们称为Advantage function,我们通过上式得到新的policy后,再去与environment进行交互,然后再重复我们的estimate value function的操作,再用value function来更新我们的policy。以上的整个方法我们称为Advantage Actor-Critic。 + +- 在实现 Actor-Critic 的时候,有哪些我们用到的tips? + + 答:与我们上一章讲述的东西有关: + + 1. **estimate 两个 network:** 一个是estimate V function,另外一个是 policy 的 network,也就是你的 actor。 V-network的input 是一个 state,output 是一个 scalar。然后 actor 这个 network的input 是一个 state,output 是一个 action 的 distribution。这两个 network,actor 和 critic 的 input 都是 s,所以它们前面几个 layer,其实是可以 share 的。尤其是假设你今天是玩 Atari 游戏,input 都是 image。那 input 那个 image 都非常复杂,image 很大,通常前面都会用一些 CNN 来处理,把那些 image 抽象成 high level 的 information,所以对 actor 跟 critic 来说是可以共用的。我们可以让 actor 跟 critic 的前面几个 layer 共用同一组参数。那这一组参数可能是 CNN。先把 input 的 pixel 变成比较 high level 的信息,然后再给 actor 去决定说它要采取什么样的行为,给这个 critic,给 value function 去计算 expected reward。 + 2. **exploration 机制:** 其目的是对policy $\pi$ 的 output 的分布进行一个限制,从而使得 distribution 的 entropy 不要太小,即希望不同的 action 被采用的机率平均一点。这样在 testing 的时候,它才会多尝试各种不同的 action,才会把这个环境探索的比较好,才会得到比较好的结果。 + +- A3C(Asynchronous Advantage Actor-Critic)在训练是回有很多的worker进行异步的工作,最后再讲他们所获得的“结果”再集合到一起。那么其具体的如何运作的呢? + + 答:A3C一开始会有一个 global network。它们有包含 policy 的部分和 value 的部分,假设它的参数就是 $\theta_1$。对于每一个 worker 都用一张 CPU 训练(举例子说明),第一个 worker 就把 global network 的参数 copy 过来,每一个 worker 工作前都会global network 的参数 copy 过来。然后这个worker就要去跟environment进行交互,每一个 actor 去跟environment做互动后,就会计算出 gradient并且更新global network的参数。这里要注意的是,所有的 actor 都是平行跑的、之间没有交叉。所以每个worker都是在global network“要”了一个参数以后,做完就把参数传回去。所以当第一个 worker 做完想要把参数传回去的时候,本来它要的参数是 $\theta_1$,等它要把 gradient 传回去的时候。可能别人已经把原来的参数覆盖掉,变成 $\theta_2$了。但是没有关系,它一样会把这个 gradient 就覆盖过去就是了。 + +- 对比经典的Q-learning算法,我们的Pathwise Derivative Policy Gradient有哪些改进之处? + + 答: + + 1. 首先,把 $Q(s,a)$ 换成 了 $\pi$,之前是用 $Q(s,a)$ 来决定在 state $s_t$ 产生那一个 action, $a_{t}$ 现在是直接用 $\pi$ 。原先我们需要解 argmax 的问题,现在我们直接训练了一个 actor。这个 actor input $s_t$ 就会告诉我们应该采取哪一个 $a_{t}$。综上,本来 input $s_t$,采取哪一个 $a_t$,是 $Q(s,a)$ 决定的。在 Pathwise Derivative Policy Gradient 里面,我们会直接用 $\pi$ 来决定。 + 2. 另外,原本是要计算在 $s_{i+1}$ 时对应的 policy 采取的 action a 会得到多少的 Q value,那你会采取让 $\hat{Q}$ 最大的那个 action a。现在因为我们不需要再解argmax 的问题。所以现在我们就直接把 $s_{i+1}$ 代入到 policy $\pi$ 里面,直接就会得到在 $s_{i+1}$ 下,哪一个 action 会给我们最大的 Q value,那你在这边就会 take 那一个 action。在 Q-function 里面,有两个 Q network,一个是真正的 Q network,另外一个是 target Q network。那实际上你在 implement 这个 algorithm 的时候,你也会有两个 actor,你会有一个真正要 learn 的 actor $\pi$,你会有一个 target actor $\hat{\pi}$ 。但现在因为哪一个 action a 可以让 $\hat{Q}$ 最大这件事情已经被用那个 policy 取代掉了,所以我们要知道哪一个 action a 可以让 $\hat{Q}$ 最大,就直接把那个 state 带到 $\hat{\pi}$ 里面,看它得到哪一个 a,就用那一个 a,其也就是会让 $\hat{Q}(s,a)$ 的值最大的那个 a 。 + 3. 还有,之前只要 learn Q,现在你多 learn 一个 $\pi$,其目的在于maximize Q-function,希望你得到的这个 actor,它可以让你的 Q-function output 越大越好,这个跟 learn GAN 里面的 generator 的概念类似。 + 4. 最后,与原来的 Q-function 一样。我们要把 target 的 Q-network 取代掉,你现在也要把 target policy 取代掉。 + + +## 3 Something About Interview + +- 高冷的面试官:请简述一下A3C算法吧,另外A3C是on-policy还是off-policy呀? + + 答:A3C就是异步优势演员-评论家方法(Asynchronous Advantage Actor-Critic):评论家学习值函数,同时有多个actor并行训练并且不时与全局参数同步。A3C旨在用于并行训练,是 on-policy 的方法。 + +- 高冷的面试官:请问Actor - Critic有何优点呢? + + 答: + + - 相比以值函数为中心的算法,Actor - Critic应用了策略梯度的做法,这能让它在连续动作或者高维动作空间中选取合适的动作,而 Q-learning 做这件事会很困难甚至瘫痪。 + - 相比单纯策略梯度,Actor - Critic应用了Q-learning或其他策略评估的做法,使得Actor Critic能进行单步更新而不是回合更新,比单纯的Policy Gradient的效率要高。 + +- 高冷的面试官:请问A3C算法具体是如何异步更新的? + + 答:下面是算法大纲: + + - 定义全局参数 $\theta$ 和 $w$ 以及特定线程参数 $θ′$ 和 $w′$。 + - 初始化时间步 $t=1$。 + - 当 $T<=T_{max}$: + - 重置梯度:$dθ=0$ 并且 $dw=0$。 + - 将特定于线程的参数与全局参数同步:$θ′=θ$ 以及 $w′=w$。 + - 令 $t_{start} =t$ 并且随机采样一个初始状态 $s_t$。 + - 当 ($s_t!=$ 终止状态)并$t−t_{start}<=t_{max}$: + - 根据当前线程的策略选择当前执行的动作 $a_t∼π_{θ′}(a_t|s_t)$,执行动作后接收回报$r_t$然后转移到下一个状态st+1。 + - 更新 t 以及 T:t=t+1 并且 T=T+1。 + - 初始化保存累积回报估计值的变量 + - 对于 $i=t_1,…,t_{start}$: + - r←γr+ri;这里 r 是 Gi 的蒙特卡洛估计。 + - 累积关于参数 θ′的梯度:$dθ←dθ+∇θ′logπθ′(ai|si)(r−Vw′(si))$; + - 累积关于参数 w′ 的梯度:$dw←dw+2(r−Vw′(si))∇w′(r−Vw′(si))$. + - 分别使用 dθ以及 dw异步更新 θ以及 w。 + +- 高冷的面试官:Actor-Critic两者的区别是什么? + + 答:Actor是策略模块,输出动作;critic是判别器,用来计算值函数。 + +- 高冷的面试官:actor-critic框架中的critic起了什么作用? + + 答:critic表示了对于当前决策好坏的衡量。结合策略模块,当critic判别某个动作的选择时有益的,策略就更新参数以增大该动作出现的概率,反之降低动作出现的概率。 + +- 高冷的面试官:简述A3C的优势函数? + + 答:$A(s,a)=Q(s,a)-V(s)$是为了解决value-based方法具有高变异性。它代表着与该状态下采取的平均行动相比所取得的进步。 + + - 如果 A(s,a)>0: 梯度被推向了该方向 + - 如果 A(s,a)<0: (我们的action比该state下的平均值还差) 梯度被推向了反方 + + 但是这样就需要两套 value function,所以可以使用TD error 做估计:$A(s,a)=r+\gamma V(s')-V(s)$。 diff --git a/docs/chapter9/img/9.1.png b/docs/chapter9/img/9.1.png new file mode 100644 index 0000000..7bbc0ee Binary files /dev/null and b/docs/chapter9/img/9.1.png differ diff --git a/docs/chapter9/img/9.10.png b/docs/chapter9/img/9.10.png new file mode 100644 index 0000000..a10f9ae Binary files /dev/null and b/docs/chapter9/img/9.10.png differ diff --git a/docs/chapter9/img/9.11.png b/docs/chapter9/img/9.11.png new file mode 100644 index 0000000..fdf8471 Binary files /dev/null and b/docs/chapter9/img/9.11.png differ diff --git a/docs/chapter9/img/9.12.png b/docs/chapter9/img/9.12.png new file mode 100644 index 0000000..ea648c4 Binary files /dev/null and b/docs/chapter9/img/9.12.png differ diff --git a/docs/chapter9/img/9.13.png b/docs/chapter9/img/9.13.png new file mode 100644 index 0000000..edfcbaa Binary files /dev/null and b/docs/chapter9/img/9.13.png differ diff --git a/docs/chapter9/img/9.14.png b/docs/chapter9/img/9.14.png new file mode 100644 index 0000000..091e7fe Binary files /dev/null and b/docs/chapter9/img/9.14.png differ diff --git a/docs/chapter9/img/9.2.png b/docs/chapter9/img/9.2.png new file mode 100644 index 0000000..395629f Binary files /dev/null and b/docs/chapter9/img/9.2.png differ diff --git a/docs/chapter9/img/9.3.png b/docs/chapter9/img/9.3.png new file mode 100644 index 0000000..964d4ff Binary files /dev/null and b/docs/chapter9/img/9.3.png differ diff --git a/docs/chapter9/img/9.4.png b/docs/chapter9/img/9.4.png new file mode 100644 index 0000000..ab3b876 Binary files /dev/null and b/docs/chapter9/img/9.4.png differ diff --git a/docs/chapter9/img/9.5.png b/docs/chapter9/img/9.5.png new file mode 100644 index 0000000..c147a2f Binary files /dev/null and b/docs/chapter9/img/9.5.png differ diff --git a/docs/chapter9/img/9.6.png b/docs/chapter9/img/9.6.png new file mode 100644 index 0000000..d2e4f2a Binary files /dev/null and b/docs/chapter9/img/9.6.png differ diff --git a/docs/chapter9/img/9.7.png b/docs/chapter9/img/9.7.png new file mode 100644 index 0000000..c3ca4e5 Binary files /dev/null and b/docs/chapter9/img/9.7.png differ diff --git a/docs/chapter9/img/9.8.png b/docs/chapter9/img/9.8.png new file mode 100644 index 0000000..9ac19fe Binary files /dev/null and b/docs/chapter9/img/9.8.png differ diff --git a/docs/chapter9/img/9.9.png b/docs/chapter9/img/9.9.png new file mode 100644 index 0000000..4c2ad93 Binary files /dev/null and b/docs/chapter9/img/9.9.png differ diff --git a/docs/errata.md b/docs/errata.md new file mode 100644 index 0000000..ba48747 --- /dev/null +++ b/docs/errata.md @@ -0,0 +1,39 @@ +# 纸质版勘误表 + +如何使用勘误?首先找到你的书的印次,接下来对着下表索引印次,该印次之后所有的勘误都是你的书中所要注意的勘误,印次前的所有勘误在当印次和之后印次均已印刷修正。 + +## 第1版第1次印刷(2022.03) + +* 47页,2.3.5节的第3行:称为备份图(backup diagram) → 称为备份图(backup diagram)或回溯图 +* 76页,式(3.1) 中 $G$ 和 $r$ 后面的数字改为下标,即 + +$$ +\begin{array}{l} +G_{13}=0 \\ +G_{12}=r_{13}+\gamma G_{13}=-1+0.6 \times 0=-1 \\ +G_{11}=r_{12}+\gamma G_{12}=-1+0.6 \times(-1)=-1.6 \\ +G_{10}=r_{11}+\gamma G_{11}=-1+0.6 \times(-1.6)=-1.96 \\ +G_9=r_{10}+\gamma G_{10}=-1+0.6 \times(-1.96)=-2.176 \approx-2.18 \\ +G_8=r_9+\gamma G_9=-1+0.6 \times(-2.176)=-2.3056 \approx-2.3 +\end{array} +$$ + +* 149页,式(6.15) 改为 + +$$ +\begin{aligned} +V^{\pi}(s) &\le Q^{\pi}(s,\pi'(s)) \\ +&=E\left[r_{t}+V^{\pi}\left(s_{t+1}\right) | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right]\\ +&\le E\left[r_{t}+Q^{\pi}\left(s_{t+1}, \pi^{\prime}\left(s_{t+1}\right)\right) | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +&=E\left[r_{t}+r_{t+1}+V^{\pi}\left(s_{t+2}\right) |s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +& \le E\left[r_{t}+r_{t+1}+Q^{\pi}\left(s_{t+2},\pi'(s_{t+2}\right) | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +& = E\left[r_{t}+r_{t+1}+r_{t+2}+V^{\pi}\left(s_{t+3}\right) |s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +& \le \cdots\\ +& \le E\left[r_{t}+r_{t+1}+r_{t+2}+\cdots | s_{t}=s, a_{t}=\pi^{\prime}\left(s_{t}\right)\right] \\ +& = V^{\pi'}(s) +\end{aligned} +$$ + +* 229页,第2行:很强的序列 → 很长的序列 + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..2d912c2 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,73 @@ + + + + + EasyRL + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + --> + + + + + diff --git a/docs/res/mogu.png b/docs/res/mogu.png new file mode 100644 index 0000000..9da9b13 Binary files /dev/null and b/docs/res/mogu.png differ diff --git a/docs/res/qrcode.jpeg b/docs/res/qrcode.jpeg new file mode 100644 index 0000000..ab1affd Binary files /dev/null and b/docs/res/qrcode.jpeg differ diff --git a/docs/res/yanggao.png b/docs/res/yanggao.png new file mode 100644 index 0000000..2fb51c7 Binary files /dev/null and b/docs/res/yanggao.png differ