Repository: NovemberChopin/RL_Tutorial Branch: master Commit: e75041f1b0e8 Files: 71 Total size: 226.7 KB Directory structure: gitextract_da0h0f0p/ ├── .gitignore ├── Doc/ │ ├── 1-RL-MDP.md │ ├── 10-RL-Policy Gradient.md │ ├── 11-RL-REINFORCE Algo.md │ ├── 12-RL-AC Algo.md │ ├── 13-RL-DDPG.md │ ├── 14-RL-TD3.md │ ├── 15-RL-PPO.md │ ├── 2-RL-DP.md │ ├── 3-RL-Model-free MC.md │ ├── 4-RL-Model-free TD.md │ ├── 5-RL-SARSA-QLearning.md │ ├── 6-RL-VFA.md │ ├── 7-RL-DQN.md │ ├── 8-RL-DQN Code.md │ └── 9-RL-DQN-Improvement.md ├── LICENSE ├── README.md ├── code/ │ ├── AC_Continous.py │ ├── AC_Discrete.py │ ├── DDPG.py │ ├── DDQN.py │ ├── DQN.py │ ├── Dueling DQN.py │ ├── PG_Continous.py │ ├── PG_Discrete.py │ ├── PPO.py │ ├── Q-Learning.py │ ├── SAC.py │ ├── Sarsa.py │ └── TD3.py ├── code_pytorch/ │ ├── DQN.py │ ├── PG_Continue.py │ ├── PG_Discreate.py │ ├── buffer.py │ ├── network.py │ └── parameter.py ├── model/ │ ├── AC_CartPole-v1/ │ │ ├── model_actor.npz │ │ └── model_critic.npz │ ├── AC_Pendulum-v0/ │ │ ├── model_actor.npz │ │ └── model_critic.npz │ ├── DDPG_Pendulum-v0/ │ │ ├── actor.hdf5 │ │ ├── actor_target.hdf5 │ │ ├── critic.hdf5 │ │ └── critic_target.hdf5 │ ├── DDQN_CartPole-v1/ │ │ ├── model.hdf5 │ │ └── target_model.hdf5 │ ├── DQN_CartPole-v1/ │ │ ├── model.hdf5 │ │ └── target_model.hdf5 │ ├── DuelineDQN_CartPole-v1/ │ │ ├── model.hdf5 │ │ └── target_model.hdf5 │ ├── PG_CartPole-v1/ │ │ └── pg_policy.hdf5 │ ├── PPO_Pendulum-v0/ │ │ ├── actor.hdf5 │ │ ├── actor_old.hdf5 │ │ └── critic.hdf5 │ ├── SAC_Pendulum-v0/ │ │ ├── log_alpha.npy │ │ ├── model_policy_net.npz │ │ ├── model_q_net1.npz │ │ ├── model_q_net2.npz │ │ ├── model_target_q_net1.npz │ │ └── model_target_q_net2.npz │ ├── TD3_Pendulum-v0/ │ │ ├── model_policy_net.npz │ │ ├── model_q_net1.npz │ │ ├── model_q_net2.npz │ │ ├── model_target_policy_net.npz │ │ ├── model_target_q_net1.npz │ │ └── model_target_q_net2.npz │ ├── qlearning_table.npy │ └── sarsa_q_table.npy └── model_torch/ ├── PG-Continue_Pendulum-v1/ │ └── model.pth └── PG-Discreate_CartPole-v1/ └── model.pth ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class .idea # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ================================================ FILE: Doc/1-RL-MDP.md ================================================ # 强化学习 --- 马尔科夫决策过程(MDP) ## 1、强化学习介绍 > ​ 强化学习任务通常使用马尔可夫决策过程(Markov Decision Process,简称MDP)来描述,具体而言:机器处在一个环境中,每个状态为机器对当前环境的感知;机器只能通过动作来影响环境,当机器执行一个动作后,会使得环境按某种概率转移到另一个状态;同时,环境会根据潜在的奖赏函数反馈给机器一个奖赏。综合而言,强化学习主要包含四个要素:状态、动作、转移概率以及奖赏函数。 ![tbQcZj.png](https://s1.ax1x.com/2020/06/11/tbQcZj.png) ​ 根据上图,agent(智能体)在进行某个任务时,首先与environment进行交互,产生新的状态state,同时环境给出奖励reward,如此循环下去,agent和environment不断交互产生更多新的数据。强化学习算法就是通过一系列动作策略与环境交互,产生新的数据,再利用新的数据去修改自身的动作策略,经过数次迭代后,agent就会学习到完成任务所需要的动作策略。 ## 2、马尔科夫过程(Markov Process) ​ **马尔可夫性**当前状态包含了对未来预测所需要的有用信息,过去信息对未来预测不重要,该就满足了马尔科夫性,严格来说,就是某一状态信息包含了所有相关的历史,只要当前状态可知,所有的历史信息都不再需要,当前状态就可以决定未来,则认为该状态具有马尔科夫性。用公式描述为: $$ P(S_{t+1}|S_t) = p(S_{t+1}|S_1, S_2, \cdots , S_t) $$ ​ **马尔科夫过程**又叫做马尔科夫链`(Markov Chain)`,它是一个无记忆的随机过程,可以用一个元组``表示,其中 - `S`是有限数量的状态集 $S ={s_1, s_2, s_3, \cdots, s_t}$ - `P`是状态转移概率矩阵 $p(S_{t+1} = s'|s_t=s) \;$ 其中 $s'$ 表示下一时刻的状态,$s$ 表示当前状态 如下如所示:对于状态$s_1$来说,有0.1的概率保持不变,有0.2的概率转移到$s_2$状态,有0.7的概率转移到$s_4$状态。 tHJIg0.png 我们可以使用矩阵来表示: $$ P = \begin{pmatrix} P(s_1|s_1) & P(s_2|s_1) & \cdots & P(s_N|s_1) \\ P(s_1|s_2) & P(s_2|s_2) & \cdots & P(s_N|s_2) \\ \vdots & \vdots & \ddots & \vdots \\ P(s_1|s_N) & P(s_2|s_N) & \cdots & P(s_N|s_N) \\ \end{pmatrix} $$ ## 3、马尔科夫奖励过程(Markov Reward Process) ### 3.1、概念介绍 **马尔科夫奖励过程**是在马尔科夫过程基础上增加了奖励函数 $R$ 和衰减系数 $\gamma$, 用 $$表示 - $R$ : 表示 $S$ 状态下某一时刻的状态$S_t$ 在下一个时刻 $(t + 1)$ 能获得的奖励的期望 $$ R_s = E[R_{t+1}|S_t=s] $$ - $G_t$ : **收获** $G_t$为在一个马尔科夫奖励链上从t时刻开始往后所有的奖励的有衰减的收益总和 $$ G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2 R_{t+3} + \cdots + \gamma^{T-t-1}R_T $$ - $\gamma$ : 折扣因子$(Discount \; factor γ ∈ [0, 1])$ - 1、为了避免出现状态循环的情况 - 2、系统对于将来的预测并不一定都是准确的,所以要打折扣 - 很显然![img](https://img-blog.csdn.net/20181006201559765?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjM4OTM0OQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)越靠近1,考虑的利益越长远。 - $V(s)$ : **状态价值函数(state value function)** 表示从从该状态开始的马尔科夫链收获$G_t$的期望 $$ v(s) = E[G_t|S_t = s] $$ 例子:对于如下状态,我们设定进入 $S_1$ 状态奖励为 5,进入$S_7$ 状态奖励为 10,其余状态奖励为 0。则$R$可以如下表示:$R = [5,0,0,0,0,0,10]$ ,折扣 因子 $\gamma$ 为 0.5。则对于下面两个马尔可夫过程获得的奖励为: - $S_4, S_5, S_6, S_7: $ 0 + 0.5\*0 + 0.5\*0 + 0.125 \*10 = 1.25 - $S_4,S_3,S_2,S_1$ : 0 + 0.5 ×0 + 0.25×0 + 0.125×5 = 0.625 在这里插入图片描述 ### 3.2、Bellman Equation 贝尔曼方程 $$ \begin{align} v(s) & = E[G_t|S_t = s] \\ & = E[R_{t+1} + \gamma(R_{t+2} + R_{t+3} + \cdots)|S_t=s] \\ & =E[R_{t+1} + \gamma v(S_{t+1})|S_t = s] \\ & =\underbrace{E[R_{t+1}|S_t=s]}_{当前的奖励} + \underbrace{\gamma E[v(S_{t+1})|S_t = s]}_{下一时刻状态的价值期望} \end{align} $$ 使用贝尔曼方程状态价值$V$可以表示为: $$ V(s) = \underbrace{R(s)}_{Immediate \; reward} + \underbrace{\gamma \sum_{s' \in S}P(s'|s)V(s')}_{Discounted \; sum \; of \; future \; reward} $$ > S 表示下一时刻的所有状态,s' 表示下一时刻可能的状态 ​ 通过贝尔曼方程,可以看到价值函数$v(s)$有两部分组成,一个是当前获得的奖励的期望,即$R(s)$,另一个是下一时刻的状态期望,即下一时刻 可能的状态能获得奖励期望与对应状态转移概率乘积的和,最后在加上折扣。如下图所示,对于状态 $s_1$,它的价值函数为:$V(s_1) = R(s_1) + \gamma(0.1*V(s_1) + 0.2*V(s_2) + 0.7*V(s_4))$ 在这里插入图片描述 ## 4、马尔科夫决策过程(Markov Decision Process) **马尔科夫决策过程**是在马尔科夫奖励过程的基础上加了 `Decision` 过程,相当于多了一个动作集合,可以用 $$,这里的 P 和 R 都与具体的行为 a 对应,而不像马尔科夫奖励过程那样仅对应于某个状态。 - $A$ 表示有限的行为集合 - $S$表示有限的状态集合 - $P^a$ is dynamics / transition model for each action $$ P(s_{t+1} = s'|s_t = s, a_t = a) = P[S_{t+1}=s'|S_t = s, A_t = a] $$ - $R$ 是奖励函数 $R(s_t=s, a_t = a) = E[R_t|s_t=s, a_t=a]$ ### 4.1、策略 (Policy) 用 $\pi$ 表示策略的集合,其元素 $\pi(a|s)$ 表示某一状态 `s` 采取可能的行为 `a` 的概率 $$ \pi(a|s) = P(A_t=a|S_t=s) $$ 这里需要注意的是: - Policy定义完整定义的个体行为方式,即包括了个体在各状态下的所有行为和概率 - 同时某一确定的Policy是静态的,与时间无关 - Policy仅和当前的状态有关,与历史信息无关,但是个体可以随着时间更新策略 在马尔科夫奖励过程中 策略 $\pi$ 满足以下方程,可以参照下面图来理解 $$ 状态转移概率:\quad P^\pi(s'|s) = \sum_{a \in A}\pi(a|s)P(s'|s, a) \\ 奖励函数:\quad R^\pi(s) = \sum_{a\in A}\pi(a|s)R(s,a) $$ > 状态转移概率可以描述为:在执行策略 $\pi$ 时,状态从 s 转移至 s' 的概率等于执行该状态下所有行为的概率与对应行为能使状态从 s 转移至 s’ 的概率的乘积的和。参考下图 > > 奖励函数可以描述为:在执行策略 $\pi$ 时获得的奖励等于执行该状态下所有行为的概率与对应行为产生的即时奖励的乘积的和。 在这里插入图片描述 > 我们引入策略,也可以理解为行动指南,更加规范的描述个体的行为,既然有了行动指南,我们要判断行动指南的价值,我们需要再引入基于策略的价值函数。 **基于策略的状态价值函数(state value function)** - $V(s)$ 表示从状态 $s$ 开始,遵循当前策略时所获得的收获的期望 $$ v_{\pi}(s) = E_{\pi}[G_t|S_t = s] $$ ​ 其中 $G_t$ 可以参照马科夫奖励过程。我们有了价值衡量标准,如果状态 s 是一个好的状态,如何选择动作到达这个状态,这时就需要判断动作的好坏,衡量行为价值。 **基于策略的行为价值函数(action value function)** - $q_{\pi}(s,a)$当前状态s执行某一具体行为a所能的到的收获的期望 $$ q_{\pi}(s, a) = E_{\pi}[G_t|S_t=s, A_t=a] $$ - 根据 Bellman 公式推导可得(参照马尔科夫奖励过程中 V 的推导) $$ q_{\pi}(s,a) = R(s, a) + \gamma \sum_{s' \in S} P_{(s'|s, a)} \cdot V_{\pi}(s') $$ > 在某一个状态下采取某一个行为的价值,可以分为两部分:其一是离开这个状态的价值,其二是所有进入新的状态的价值于其转移概率乘积的和。参考下图右理解 - 由状态价值函数和行为价值函数的定义,可得两者的关系 $$ v_{\pi}(s) = \sum_{a \in A}\pi(a|s) \cdot q_{\pi}(s,a) $$ > ​ 我们知道策略就是用来描述各个不同状态下执行各个不同行为的概率,而状态价值是遵循当前策略时所获得的收获的期望,即状态 s 的价值体现为在该状态下遵循某一策略而采取所有可能行为的价值按行为发生概率的乘积求和。参照下图左理解 在这里插入图片描述 - 上面两个公式组合可得 **Bellman Expectation Equation** $$ v_{\pi}(s) = \sum_{a \in A}\pi(a|s)\left(R(s, a) + \gamma \sum_{s' \in S} P_{(s'|s, a)} \cdot v_{\pi}(s')\right) $$ $$ q_{\pi}(s,a) = R(s, a) + \gamma \sum_{s' \in S} P_{(s'|s, a)} \cdot \sum_{a' \in A}\pi(a'|s') \cdot q_{\pi}(s',a') $$ ## 5、最优价值函数 解决强化学习问题意味着要寻找一个最优的策略让个体在与环境交互过程中获得始终比其它策略都要多的收获,这个最优策略我们可以用 $\pi_*$ 表示。一旦找到这个最优策略$\pi_*$ ,那么我们就解决了这个强化学习问题。一般来说,比较难去找到一个最优策略,但是可以通过比较若干不同策略的优劣来确定一个较好的策略,也就是局部最优解。 我们一般是通过对应的价值函数来比较策略的优劣,也就是说,寻找较优策略可以通过寻找较优的价值函数来完成。可以定义最优状态价值函数是所有策略下产生的众多状态价值函数中的最大者,即: $$ V_*(s) = max_\pi V_\pi(s) $$ 同理也可以定义最优动作价值函数是所有策略下产生的众多动作状态价值函数中的最大者,即: $$ q_*(s, a) = max_\pi q_\pi (s,a) $$ 我们可以**最大化最优行为价值函数**找到最优策略: $$ \pi^*(a|s) = \begin{cases} 1, & \text{if $a = argmax \; q_*(s,a)$} \\ 0, & \text{otherwise} \end{cases} $$ **Bellman Optimality Equation** 只要我们找到了最大的状态价值函数或者动作价值函数,那么对应的策略 $\pi_*$ 就是我们强化学习问题的解。同时,利用状态价值函数和动作价值函数之间的关系,我们也可以得到: $$ v_*(s) = max_a q_*(s,a) \\ $$ > 当到达 最优的时候,一个状态的价值就等于在当前 状态下最大的那个动作价值 $$ q_*(s,a) = R(s, a) + \gamma \sum_{s' \in S} P_{s's}^a \cdot V_*(s') $$ 把上面两个式子结合起来有**Bellman Optimality Equation** $$ v_*(s) = max_a (R(s, a) + \gamma \sum_{s' \in S} P_{s's}^a \cdot v_*(s')) $$ $$ q_*(s,a) = R(s, a) + \gamma \sum_{s' \in S} P_{s's}^a \cdot max_{a'}q_*(s', a') $$ ## 6、MDP 实例 下面给出一个例子,希望能能好的理解MDP过程 ![UIHumq.png](https://s1.ax1x.com/2020/07/21/UIHumq.png) 例子是一个学生学习考试的MDP。里面左下那个圆圈位置是起点,方框那个位置是终点。上面的动作有study, pub, facebook, quit, sleep,每个状态动作对应的即时奖励R已经标出来了。我们的目标是找到最优的动作价值函数或者状态价值函数,进而找出最优的策略。 为了方便,我们假设衰减因子 $\gamma = 1, \quad \pi(a|s)=0.5$ 对于终点方框位置,由于其没有下一个状态,也没有当前状态的动作,因此其状态价值函数为0。对于其余四个状态,我们依次定义其价值为$v_1, v_2, v_3, v_4$ 分别对应左上,左下,中下,右下位置的圆圈。我们基于 $$ v_{\pi}(s) = \sum_{a \in A}\pi(a|s)\left(R(s, a) + \gamma \sum_{s' \in S} P_{(s'|s, a)} \cdot v_{\pi}(s')\right) $$ 来计算所有状态的价值函数,可以得到如下方程组 - 对于 v_1: $v_1 = 0.5 * (-1 + v_1) + 0.5 * (0 + v_2)$ - 对于 v_2: $v_2 = 0.5 * (-1 + v_1) + 0.5 * (-2 + v_3)$ - 对于 v_3: $v_3 = 0.5 * (0+0) + 0.5 * (-2 + v_4)$ - 对于 v_4: $v_4 = 0.5 * (10+0) + 0.5 * (1 + 0.2 * v_2 + 0.4 * v_3 + 0.4 * v_4)$ 解这个方程组可得:$v_1 = -2.3, v_2 = -1.3, v_3 = 2.7, v_4 = 7.4$ 既是每个状态的价值函数 ![UIHM7V.png](https://s1.ax1x.com/2020/07/21/UIHM7V.png) 上面我们固定了策略$π(a|s)π(a|s),$虽然求出了每个状态的状态价值函数,但是却并不一定是最优价值函数。那么如何求出最优价值函数呢?这里由于状态机简单,求出最优的状态价值函数 $v_*(s)$ 或者动作价值函数 $q_*(s)$ 比较容易。 我们这次以动作价值函数$q_*(s)$ 来为例求解。然后我们按照下面公式, $$ q_*(s, a) = R_s^a + \gamma \sum_{s' \in S} P_{ss'}^a max_{a'}q_*(s',a') $$ 可以得到下面等式 $$ \begin{align} q_*(s_4, study) & = 10 \\ q_*(s_4, pub) & = 1 + 0.2 * max_{a'}q_*(s_2, a') + 0.4 * max_{a'}q_*(s_3, a') + 0.4 * max_{a'}q_*(s_4, a') \\ q_*(s_3, sleep) & = 0 \\ q_*(s_3, study) & = -2 + max_{a'}q_*(s_4, a') \\ q_*(s_2, study) & = -2 + max_{a'}q_*(s_3, a') \\ q_*(s_2, facebook) & = -1 + max_{a'}q_*(s_1, a') \\ q_*(s_1, facebook) & = -1 + max_{a'}q_*(s_1, a') \\ q_*(s_1, quit) & = 0 + max_{a'}q_*(s_2, a') \end{align} $$ 然后求出所有的 $q_*(s,a)$ ,然后再 利用 $v_*(s, a) = max_aq_*(s,a)$ 就可以求出所有的 $v_*(s)$ ,最终结果如下图所示 [![UIHT3Q.png](https://s1.ax1x.com/2020/07/21/UIHT3Q.png)](https://imgchr.com/i/UIHT3Q) ## 7、MDP 小结 虽然MDP可以直接用方程组求解简单的问题,但是却不能解决复杂的问题,因此我们需要寻找其他的方法来求解 下篇文章使用动态规划方法来解决 MDP问题,涉及到 `Policy Iteration` 和 `Value Iteration` 两个算法,还请大家多多关注。 本篇文章是我的强化学习开篇之作,如果觉得文章写的不错,还请各位看官老爷点赞收藏加关注啊,后面会一直写下去的,再此谢谢啦 参考资料: - B站 [周老师强化学习纲要第二讲上](https://www.bilibili.com/video/BV1g7411m7Ms) ================================================ FILE: Doc/10-RL-Policy Gradient.md ================================================ # 强化学习 10 —— Policy Gradient 推导 前面几篇文章[价值函数近似](https://blog.csdn.net/november_chopin/article/details/107911868)、[DQN算法](https://blog.csdn.net/november_chopin/article/details/107912720)、[DQN改进算法DDQN和Dueling DQN](https://blog.csdn.net/november_chopin/article/details/107913317)我们学习了 DQN 算法以及其改进算法 DDQN 和 Dueling DQN 。他们都是对价值函数进行了近似表示,也就是 学习价值函数,然后从价值函数中提取策略,我们把这种方式叫做 Value Based。 ## 一、Value Based 的不足 回顾我们的学习路径,我们从动态规划到蒙地卡罗,到TD到Qleaning再到DQN,一路为计算Q值和V值绞尽脑汁。但大家有没有发现,我们可能走上一个固定的思维,就是我们的学习,一定要算Q值和V值,往死里算。但算Q值和V值并不是我们最终目的呀,我们要找一个策略,能获得最多的奖励。除了这种方法之外,还有一类强化学习算法,就是 Policy Based 算法。 Value Based 强化学习方法在很多领域得到比较好的应用,但是其也有局限性。 - 1)首先就是对连续动作处理能力不足,算法 DQN 我们使用的 CartPole-v1 环境,在这个环境中只有两个动作:控制小车向左或者向右,这就是离散动作。那连续动作就是动作不光有方向,而且还有大小,对小车施加的力越大,小车的动作幅度也会越大。例如自动驾驶控制方向盘,还请读者自行体会。这个时候,使用离散的方式是不好表达的,而使用基于 Policy Based 方法就很容易。 - 2)无法解决随机策略(Stochastic Policy)问题。随机性策略就是把当前状态输入网络,输出的是不同动作的概率分布(比如 40% 的概率向左,60% 的概率向右)或者是对于连续动作输出一个高斯分布。而基于 Value Based 的算法是输出是每个动作的动作价值 $Q(s,a)$ ,然后选择一个价值最大的动作,也就是说输出的是一个确定的动作,这种我们称之为确定性策略(Deterministic Policy)。但是有些问题的最优策略是随机策略,所以此时 基于 Value Based 的方法就不再适用,这时候就可以适用基于 Policy Based 的方法。 ![UvSaF0.png](https://s1.ax1x.com/2020/07/24/UvSaF0.png) ## 二、策略梯度 ### 1、优化目标 类比于我们近似价值函数的过程:$\hat{q}(s, s, w) \approx q_\pi(s, a)$ 。对于 Policy Based 强化学习方法下,我们可以适用同样的方法对策略进行近似,给定一个使用参数 $\theta$ 近似的 $\pi_\theta(s,a)$ ,然后找出最好的 $\theta$ 。 那么我们如何评估策略 $\pi_\theta$ 的好坏呢?也就是我们的优化目标是什么呢? 对于离散动作的环境我们可以优化初始状态收获的期望: $$ J_1(\theta) = V_{\pi_\theta}(s_1) = E_{\pi_\theta}[v_1] $$ 对于那些没有明确初始状态的连续环境,我们可以优化平均价值: $$ J_{avV}(\theta) = \sum_sd_{\pi_{\theta}}(s)V_{\pi_\theta}(s) $$ 或者定义为每一段时间步的平均奖励: $$ J_{avR}(\theta) = \sum_sd_{\pi_\theta}(s)\sum_a\pi_\theta(s,a)R(s,a) $$ > 其中 $d_{\pi_\theta}(s)$ 是基于策略 $\pi_\theta$ 生成的马尔科夫链关于状态 的静态分布 无论我们采用上述哪一种优化 方法,最终对 $\theta$ 求导的梯度都可以表示为: $$ \nabla_\theta J(\theta) = E_{\pi_\theta}[\nabla_\theta\;log\pi_\theta(s,a)R(s,a)] $$ ### 2、Score Function 现在假设策略 $\pi_\theta$ 是可导的,我们现在就来计算策略梯度 $\nabla_\theta \pi_\theta(s,a)$ 。在这里我们可以用到一个 叫做 `likelihood ratio` 的小技巧: $$ \nabla_\theta \pi_\theta(s,a) = \pi_\theta(s,a)\frac{\nabla_\theta \pi_\theta(s,a)}{\pi_\theta(s,a)} \\ = \pi_\theta(s,a) \cdot \nabla_\theta log\pi_\theta(s,a) $$ 这里的 $\nabla_\theta log\pi_\theta(s,a)$ 就叫做 `score function` 。 下面介绍策略函数的形式,对于离散动作一般就是 Softmax Policy。关于 Softmax 函数,相信学过深度学习应该都比较了解,这里不在赘述。对于连续动作环境就是 Gaussian Policy。下面分别介绍 #### 1)Softmax Policy 我们把策略用线性特征组合的方式表示为:$\phi(s,a)^T\theta$ 。此时有: $$ \pi_\theta(s,a) = \frac{exp^{\phi(s,a)^T\theta}}{\sum_{a'}exp^{\phi(s,a)^T\theta}} $$ 此时的 `score function` 为: $$ \begin{aligned} \nabla_\theta log\pi_\theta(s,a) & = \nabla_\theta \left[log\;exp^{\phi(s,a)^T\theta}-\sum_{a'}log\;exp^{\phi(s,a)^T\theta}\right] \\ & = \nabla_\theta\left[\phi(s,a)^T\theta - \sum_{a'}\phi(s,a)^T\theta\right] \\ & = \phi(s,a) - \sum_{a'}\phi(s,a) \end{aligned} $$ #### 2)Gaussian Policy 在连续动作空间中,一般使用 Gaussian policy 。其中的均值是特征的线性组合:$\mu(s) = \phi(s)^T\theta$ 。方差可以固定为 $\sigma^2$ 也可以作为一个参数。那么对于连续动作空间有:$a\;~ \;N(\mu(s), \sigma^2)$ 。 此时的 `score function` 为: $$ \begin{aligned} \nabla_\theta log\pi_\theta(s,a) & = \nabla_\theta log \left[ \frac{1}{\sqrt{2\pi}\sigma} exp \left(-\frac{(a-\mu(s))^2}{2\sigma^2}\right) \right] \\ & = \nabla_\theta \left[ log\;\frac{1}{\sqrt{2\pi}\sigma} - log\; exp \left(\frac{(a-\mu(s))^2}{2\sigma^2}\right) \right] \\ & = - \nabla_\theta\left(\frac{(a-\phi(s)^T\theta)^2}{2\sigma^2}\right) \\ & = \frac{(a-\mu(s))\phi(s)}{\sigma^2} \end{aligned} $$ ### 3、Policy Gradient 推导 对于 Policy Gradient 的求导应该来说是比较复杂的,重点是理解对于一条轨迹如何表示,弄明白符号所代表的含义,推导过程也不是那么复杂。下面我们正式开始 Policy Gradient 的推导,我们首先推导对于一条 MDP 轨迹**只走一步**的 Policy Gradient,然后再推导有多条轨迹的情况。 #### 1)Policy Gradient for One-Step MDPs 我们开始的状态 s 有:$s\;~\;d(s)$ 。对于走一步的奖励 为 $r = R(s,a)$ 。 > 上文有提到其中 $d_{\pi_\theta}(s)$ 是基于策略 $\pi_\theta$ 生成的马尔科夫链关于状态 的静态分布 $$ J(\theta) = E_{\pi_\theta}[r] = \sum_{s\in S}d(s)\sum_{a\in A}\pi_\theta(s,a)\cdot r $$ 那么优化目标就是使奖励尽可能的大,然后使用 `likelihood ratio` 计算 Policy Gradient : $$ \begin{aligned} \nabla J(\theta) & = \sum_{s\in S}d(s)\sum_{a\in A}{\color{red}\pi_\theta(s,a)\; \nabla_\theta log\;\pi_{\theta}(s,a)}\cdot r \\ & =E_{\pi_\theta}[\nabla_\theta log\;\pi_{\theta}(s,a)\cdot r] \end{aligned} $$ #### 2)Policy Gradient for Multi-step MDPs 下面介绍在多条 MDP 轨迹的情况,假设一个状态轨迹如下表示: $$ \tau = (s_0, a_0, r_1,\ldots,s_{T-1}, a_{T-1}, r_T, s_T)\;~\;(\pi_\theta,P(s_{t+1}|s_t, a_t)) $$ > 这里我们把按照策略 $\pi_\theta$ 的状态轨迹表示为概率 P 的形式 我们一条轨迹下所获得的奖励之和表示为:$R(\tau) = \sum_{t=0}^TR(s_t, a_t)$ 。那么**轨迹的奖励期望**就是我们的优化目标。还可以把获得的奖励写成加和的形式:如果我们能表示出每条轨迹发生的概率 $P(\tau;\theta)$,那么每条轨迹的奖励期望就等于**轨迹获得奖励乘以对应轨迹发生的概率**: $$ J(\theta) = E_{\pi_\theta}\left[\sum_{t=0}^TR(s_t, a_t)\right] = \sum_\tau P(\tau;\theta)R(\tau) $$ 此时的最优参数 $\theta^*$ 可以表示为: $$ \theta^* = arg\;max_\theta J(\theta) = arg\;max \sum_\tau P(\tau;\theta)R(\tau) $$ 下面就来求导 $J(\theta)$ : $$ \begin{aligned} \nabla_\theta J(\theta) & = \nabla_\theta \sum_\tau P(\tau;\theta)R(\tau) \\ & = \sum_\tau P(\tau;\theta)\frac{\nabla_\theta(\tau;\theta)}{P{(\tau;\theta)}} R(\tau) \\ & = \sum_\tau P(\tau;\theta) \nabla_\theta log\;P(\tau;\theta) \cdot R(\tau) \end{aligned} $$ 这里$\nabla_\theta J(\theta)$ 是基于**轨迹**来表示的,其实轨迹 $\tau$ 的分布我们是不知道的。所以我们可以采用 MC 采样的方法来近似表示,假如我们采集到 m 条轨迹,那么我们就可以把这 m 条奖励和取平均,就得到对优化目标的近似求导结果: $$ \color{red}\nabla_\theta J(\theta) \approx \frac{1}{m}\sum_{i=1}^mR(\tau_i)\;\nabla_\theta log\;P(\tau_i;\theta) $$ 那一条轨迹发生的概率 $P(\tau;\theta)$ 如何表示呢?若一条轨迹图如下所示,可以看到就是是一系列的概率连乘,(注意,图中下标从 1 开始,而上面公式下标从 0 开始) ![UxSJb9.png](https://s1.ax1x.com/2020/07/24/UxSJb9.png) 那么 $P(\tau;\theta)$ 就可以表示为: $$ P(\tau;\theta) = \mu(s_0) \prod_{t=0}^{T-1}\pi_\theta(a_t|s_t)\cdot p(s_{t+1}|s_t,a_t) $$ 现在我们就把 $\nabla_\theta J(\theta)$ 中的将轨迹分解为状态和动作: $$ \begin{aligned} \nabla_\theta log\;P(\tau;\theta) & = \nabla_\theta log\left[P(\tau;\theta) = \mu(s_0) \prod_{t=0}^{T-1}\pi_\theta(a_t|s_t)\cdot p(s_{t+1}|s_t,a_t) \right] \\ & = \nabla_\theta\left[log\;\mu(s_0) + \sum_{t=0}^{T-1}log\;\pi_\theta(a_t|s_t) + \sum_{t=0}^{T-1}log\;p(s_{t+1}|s_t,a_t) \right] \\ & = \sum_{t=0}^{T-1}\nabla_\theta\;log\;\pi_\theta(a_t|s_t) \end{aligned} $$ 对于一连串的连乘形式,把 `log` 放进去就会变成连加的形式,可以看到,上面式子只有中间一项和参数 $\theta$ 有关,使得最终结果大大简化。这也是为何使用 `likelihood ratio` 的原因,这样就可以消去很多无关的变量。然后就得到了优化目标的最终求导结果: $$ \color{red}\nabla_\theta J(\theta) \approx \frac{1}{m}\sum_{i=1}^mR(\tau_i)\;\sum_{t=0}^{T-1}\nabla_\theta\;log\;\pi_\theta(a_t^i|s_t^i) $$ 对于当前的目标函数 梯度,是基于MC采样得到的,会有比较高的方差 ,下一篇文章将会介绍两种减小方差的方法,以及 Policy Gradient 基础的算法 `REINFORCE` ================================================ FILE: Doc/11-RL-REINFORCE Algo.md ================================================ # 强化学习 11 —— REINFORCE Algorithm 在上篇文章[强化学习——Policy Gradient 公式推导](https://blog.csdn.net/november_chopin/article/details/108032626)我们推导出了 Policy Gradient: $$ \nabla_\theta J(\theta) \approx \frac{1}{m}\sum_{i=1}^mR(\tau_i)\;\sum_{t=0}^{T-1}\nabla_\theta\;log\;\pi_\theta(a_t^i|s_t^i) $$ > 其中的 $R(\tau_i)$ 表示第 i 条轨迹所有的奖励之和。 对于这个式子,我们是基于 MC 采样的方法得来的。对于MC采样的轨迹是没有偏差的。但是因为是采样,所以每条轨迹获得的奖励非常不稳定,造成有比较高的方差。为了减少方差,这里有两个办法:1、使用时间因果关系(Use temporal causality)。2、引入 Baseline ## 一、减小方差 ### 1、使用时序因果关系 Policy gradient estimator: $$ \nabla_\theta J(\theta) \approx \frac{1}{m}\sum_{i=1}^m \left(\;\sum_{t=1}^{T}\nabla_\theta\;log\;\pi_\theta(a_t^i|s_t^i)\right)\left(\sum_{t=1}^Tr(s_t^i, a_t^i) \right) $$ > 我们的目的是为了优化策略函数 $\pi$ ,$\pi$ 有很多要优化的参数 $\theta$。那么我们在每一个点都计算 $\pi$ 的 likelihood,而每个点能获得奖励是一个值,奖励的大小可以表示当前 likelihood的好坏,相当于对相应的 likelihood 进行了加权。我们希望优化过程中,增加奖励大的动作出现的概率,减小奖励小的动作出现的概率。 > > 奖励值的大小可以作为判断当前策略好坏的依据。good action is made more likely, bad action is made less likely. 使用使用时序因果关系可以减少许多不必要的项 $$ \nabla_\theta E_\tau[R] = E_\tau \left[\left(\sum_{t=0}^{T-1}r_t\right) \left( \sum_{t=0}^{T-1}\nabla_\theta\;log\;\pi_\theta(a_t|s_t)\right) \right] $$ 对于一条轨迹中的某一点获得的奖励 $r_{t'}$ 可以表示为如下式。 $$ \nabla_\theta E_\tau[r_{t'}] = E_\tau\left[r_{t'}\sum_{t=0}^{t'}\nabla_\theta\;log\;\pi_\theta(a_t|s_t)\right] $$ 然后把一条轨迹上所有点奖励的导数加起来: $$ \begin{aligned} \nabla_\theta J(\theta) = \nabla_\theta E_{\tau~\pi_\theta}[R] & = E_\tau \left[\sum_{t'=0}^{T-1}r_{t'} \sum_{t=0}^{t'}\nabla_\theta\;log\;\pi_\theta(a_t|s_t)\right] \\ & = E_\tau\left[\sum_{t=0}^{T-1}\nabla_\theta\;log\;\pi_\theta(a_t|s_t) \sum_{\color{red}t'=t}^{T-1}r_{t'} \right] \\ & = E_\tau \left[\sum_{t=0}^{T-1}G_t\cdot \nabla_\theta\;log\;\pi_\theta(a_t|s_t) \right] \end{aligned} $$ > 其中 $G_t = \sum_{t'=t}^{T-1}r_{t'}$ 表示对于一条轨迹第 t 步往后获得的奖励之和。 如果上面式子难以理解,我们可以这样理解:我们都知道当前时刻不能影响过去所已经发生的事,这就是时间因果关系。同样,对于一条轨迹上,**在时刻 $t'$ 时的策略不能影响 $t'$ 时刻之前所获得的奖励**。所以只需要 对 $t'$ 之后所有的奖励加起来即可,和 $t'$ 时刻之前所获得的奖励是无关的。因此 Policy Gradient Estimator 可以表示为如下形式: $$ \nabla_\theta E[R] \approx \frac{1}{m}\sum_{i=1}^m\sum_{t=0}^{T-1}G_t\cdot \nabla_\theta\;log\;\pi_\theta(a_t^i|s_t^i) $$ 由上面的操作我们就得到了 Policy Gradient 中一个非常经典的算法 REINFORCE : `Williams (1992). Simple statistical gradient-following algorithms for connectionist reinforcement learning: introduces REINFORCE algorithm` ![aps8DH.png](https://s1.ax1x.com/2020/07/26/aps8DH.png) ### 2、加入 Baseline 对于一条采样出来的轨迹,它的的奖励 $G_t$ 会有很高的方差,我们可以让 $G_t$ 减去一个值(Baseline),这样就能减小方差,对于加入 Baseline 可以很容易的证明,会减小方差而不会改变整体的期望值,这样就会使得训练过程更加稳定。 $$ \nabla_\theta E_{\tau~\pi_\theta}[R] = E_\tau \left[\sum_{t=0}^{T-1}{\color{red}(G_t-b(s_t))}\cdot \nabla_\theta\;log\;\pi_\theta(a_t|s_t) \right] $$ 一种办法是把奖励的期望作为Baseline,也就是让 $G_t$ 减去它的平均值: $$ b(s_t) = E[r_t+r_{t+1}+\ldots+r_{T-1}] $$ 对于 Baseline 也可以用参数 来拟合,表示为 $b_w(s_t)$ ,在优化过程中同时优化参数 $w$ 和 $\theta$ 。 ## 二、REINFORCE 算法 实例代码使用`CartPole-v1` 离散环境,首先我们来看算法的整体流程。 ### 1、整体流程 首先我们搭建好 policy 网络模型,初始化 参数 $\theta$ ,然后用这个模型采样搜集数据,然后利用搜集到的数据来更新网络参数 $\theta$ ,之后我们就有个一个新的策略网络,然后再用新的策略网络去和环境交互搜集新的数据,去更新策略网络,就这样重复下去,直到训练出一个良好的模型。注意每次搜集的数据只能使用一次,就要丢弃,因为每次更新 $\theta$ 后策略网络就会改变,所以不能用旧的网络采集到的数据去跟新新的网络参数。 aKwSbR.png 具体流程如下所示,在 与环境交互的过程中我们存储了每一步的相关数据,用以计算 $G_t$ 奖励。 ```python for episode in range(TRAIN_EPISODES): state = env.reset() episode_reward = 0 for step in range(MAX_STEPS): # in one episode if RENDER: env.render() action = agent.get_action(state) next_state, reward, done, _ = env.step(action) agent.store_transition(state, action, reward) state = next_state episode_reward += reward if done:break agent.learn() ``` ### 2、计算奖励 ```python def _discount_and_norm_rewards(self): # discount episode rewards discounted_reward_buffer = np.zeros_like(self.reward_buffer) running_add = 0 for t in reversed(range(0, len(self.reward_buffer))): running_add = running_add * self.gamma + self.reward_buffer[t] discounted_reward_buffer[t] = running_add # normalize episode rewards discounted_reward_buffer -= np.mean(discounted_reward_buffer) discounted_reward_buffer /= np.std(discounted_reward_buffer) return discounted_reward_buffer ``` 函数分为两部分,一部分计算G值,一部分把G值进行归一化处理。这里计算的`discounted_reward_buffer`是每一步动作直到episode结束能获的奖励,也就是公式中的 $G_t$ 。注意这里是从最后一个状态逆序 往前算,然后把每一步的奖励添加到列表中。然后对计算得到的奖励列表数据进行归一化,训练效果会更好。 ### 3、梯度更新 我们知道,每次搜集到数据去 更新网络参数 $\theta$ ,那么网络参数是如何更新的呢? 我们可以把它看做监督学习分类的过程,如下图所示,对于环境输入到 策略网络,最终网络输出为三个动作:左、右、开火。右边是 label。loss 函数就是输出动作与label之间的交叉熵,最小化的目标就是其交叉熵,然后跟新网络参数,增加哪个动作出现的概率或者减少哪个动作出现的概率。 $$ H = - \sum_{i=1}^{3}\hat{y}_i log\;y_i \\ Maximize: log\;y_i = logP("left"|s) \\ $$ $$ \theta \leftarrow \theta + \eta\nabla logP("left"|s) $$ ![aKsob4.png](https://s1.ax1x.com/2020/07/30/aKsob4.png) 我们搜集到的每一步数据 `state, action` ,可以把 `state` 看做训练的数据,把 `action` 看做 label。然后最小化其交叉熵,如下代码 所示。在 REINFORCE 算法中,算出的交叉熵还要乘上 $G_t$ 也就是 代码中的 `discounted_reward` ,也就是说 参数的更新根据 $G_t$ 来调整的, 如果 $G_t$ 比较高,那么就会大幅度增加相应动作出现概率,如果某一个动作得到的 $G_t$ 是负数,那么就会相应的减少动作出现概率,这就是带权重的梯度下降。对于这个过程,`tensorlayer` 内置了一个函数 `cross_entropy_reward_loss` ,可以直接实现上述过程,见代码注释部分。 ```python def learn(self): discounted_reward = self._discount_and_norm_rewards() with tf.GradientTape() as tape: _logits = self.model(np.vstack(self.state_buffer)) neg_log_prob = tf.nn.sparse_softmax_cross_entropy_with_logits( logits=_logits, labels=np.array(self.action_buffer)) loss = tf.reduce_mean(neg_log_prob * discounted_reward) # loss = tl.rein.cross_entropy_reward_loss( # logits=_logits, actions=np.array(self.action_buffer), rewards=discounted_reward) grad = tape.gradient(loss, self.model.trainable_weights) self.optimizer.apply_gradients(zip(grad, self.model.trainable_weights)) ``` 对于这部分的理解可以直接看[李宏毅老师的视频](https://www.bilibili.com/video/BV1UE411G78S?p=2) ,讲解很清楚。关于 REINFORCE 的完整代码:[REINFORCE 算法](https://github.com/NovemberChopin/RL_Tutorial/blob/master/code/PG_Discrete.py) ,希望能随手 给个 star,谢谢看官大人了。。。 ## 三、REINFORCE 的不足 策略梯度为我们解决强化学习问题打开了一扇窗,但是我们上面的蒙特卡罗策略梯度reinforce算法却并不完美。由于使用MC采样获取数据,我们需要等到每一个episode结束才能做算法迭代,那么既然 MC 效率比较慢,那能不能用 TD 呢?当然是可以的,就是我们下篇要介绍的 Actor-Critic 算法。 ================================================ FILE: Doc/12-RL-AC Algo.md ================================================ # 强化学习 12 —— Actor-Critic Algorithm ## 一、Actor-Critic 介绍 ### 1、引入 Actor-Critic 我们还是从上篇[强化学习——REINFORCE Algorithm](https://blog.csdn.net/november_chopin/article/details/108033013)推导出的目标函数的梯度说起: $$ \nabla_\theta J(\theta) = E_{\pi_\theta} \left[\sum_{t=0}^{T-1}G_t\cdot \nabla_\theta\;log\;\pi_\theta(a_t|s_t) \right] $$ 其中 $G_t$ 就表示当前采取的行为,到episode结束一共能获得的奖励。对于 $G_t$ 是使用 MC 采样得到的 sample,只有到达最终状态才能逆序计算 $G_t$ ,这也是 REINFORCE 算法效率不高的原因,那么能不能不用等到游戏结束就可以更新参数呢?当然是可以的,那就是不再使用 MC 采样的方式来更新,而是采用的 TD 方式更新。 使用 TD 就可以每隔一步更新或者每隔两步更新,但是存在一个问题:我们如何计算每一个动作的 Q 值呢?因为没有等到episode结束就要更新参数,所以只能来估计 Q 值,很自然的就想到了使用神经网络来估计。然后就可以用参数化 的 $Q$ 来代替 $G_t$ 。所以就需要两个神经网络,此时的目标梯度如下: $$ \nabla_\theta J(\theta) = E_{\pi_\theta} \left[\sum_{t=0}^{T-1}{Q_{\color{red}w}(s_t, a_t)}\cdot \nabla_{\color{red}\theta}\;log\;\pi_\theta(a_t|s_t) \right] $$ 这样就得到了 `Actor-Critic Policy Gradient`。把 `Value Function` 和 `Policy Function` 两者结合起来的一中算法。其包含两个成分: - Actor:Actor 就是指的 Policy Function,是用来和环境交互,做出动作,可以理解为一个”表演者“。 - Critic:Critic 就是式子中的 Q,是一个”评论者“的角色,用来评论 actor 所做出的动作实际能得到多少价值。 我们可以把 Actor-Critic 算法比喻为:Actor在台上跳舞,一开始舞姿并不好看,Critic根据Actor的舞姿打分。Actor通过Critic给出的分数,去学习:如果Critic给的分数高,那么Actor会调整这个动作的输出概率;相反,如果Critic给的分数低,那么就减少这个动作输出的概率。 下面介绍一个最简单的 Actor-Critic 算法:Sample QAC。 ### 2、Sample QAC 算法 Sample QAC 算法使用线性特征组合来逼近 :$Q_w(s,a) = \psi(s,a)^Tw$ 。通过 TD(0) 的方式来更新 参数 $w$ ,Actor使用 policy gradient来优化: ![a9aZ8g.png](https://s1.ax1x.com/2020/07/26/a9aZ8g.png) 首先根据 策略 $\pi_\theta$ 生成一系列样本数据,然后得到TD Target 进一步计算 TD Error ,来更新 价值函数的参数 $w$ ,这里因为是线性特征组合,所以经过求导后直接取 feature(特征)来更新:$w \leftarrow w + \beta\delta\psi(s,a)$ 。然后第二部分,我们得到 $Q_w(s,a)$ 后直接乘以 `score function` 通过 policy gradient 来进行更新。然后一直重复这个步骤,就得到了 Simple QAC 算法。 ### 3、通过 Baseline 减少 Actor-Critic 的方差 同样的,在 AC 算法中也可以引入 Baseline 来减少方差。一般我们都会把平均值作为 Baseline 。回忆动作价值函数 Q:$Q_{\pi,\gamma}(s,a) = E_\pi[r_1+\gamma r_2+\ldots|s_1=s,a_1=a]$ ,而状态价值 $V_{\pi,\gamma}(s)$ 就是动作状态价值 Q 的期望: $$ \begin{aligned} V_{\pi, \gamma}(s) & = E_\pi[r_1+\gamma_2 + \ldots|s_1=s] \\ &= E_{a~\pi}[Q_{\pi,\gamma}(s,a)] \end{aligned} $$ 也就是 说 状态价值函数 `V` 可以天然的做 Baseline,所以就得出了更新的权重:定义 为 `Advantage function`: $$ A_{\pi, \gamma}(s,a) = Q_{\pi, \gamma}(s,a) - V_{\pi, \gamma}(s) $$ 这样我们又需要计算 V,但是贝尔曼公式告诉我们 `Q` 和 `V` 是可以互换的。也就是 `TD-Error`: $$ TD-Error = r + gamma * V(s') - V(s) $$ **而 `TD-Error` 就是 `Actor` 更新时的权重值。所以 `Critic` 网络不需要估计 `Q`,而是去估计 `V`。然后就可以计算出 `TD-Error`,也就是`Advantage function` ,然后最小化 `TD-Error`**。 此时的 policy gradient: $$ \nabla_\theta J(\theta) = E_{\pi_\theta} \left[\sum_{t=0}^{T-1} \nabla_{\theta}\;log\;\pi_\theta(a_t|s_t) {\color{red}A_{\pi, \gamma}(s_t,a_t)} \right] $$ ## 二、代码分析 ### 1、更新流程 本次代码我们还是采用 `CartPole-v1` 环境,在 REINFORCE算法中,agent 需要从头一直跑到尾,直到最终状态才开始进行学习,所以采用的回合更新制。 在AC中agent 采用是每步更新的方式。如下图所示 ![aKHtOJ.png](https://s1.ax1x.com/2020/07/30/aKHtOJ.png) 对于每一个 episode 流程如下,智能体每走一步都要分别更新 Critic 和 Actor 网络。注意:**我们需要先更新Critic,并计算出TD-error。再用TD-error更新Actor**。 ```python while True: if RENDER: env.render() action = self.actor.get_action(state) state_, reward, done, _ = env.step(action) td_error = self.critic.learn(state, reward, state_, done) self.actor.learn(state, action, td_error) state = state_ if done or step >= MAX_STEPS: break ``` ### 2、Critic 的更新 ```python def learn(self, state, reward, state_, done): d = 0 if done else 1 with tf.GradientTape() as tape: v = self.model(np.array([state])) v_ = self.model(np.array([state_])) td_error = reward + d * LAM * v_ - v loss = tf.square(td_error) grads = tape.gradient(loss, self.model.trainable_weights) self.optimizer.apply_gradients(zip(grads, self.model.trainable_weights)) return td_error ``` Critic 网络通过估计当前状态和下一个状态 的 V 值,来计算 TD-Error,然后更新参数。 ### 3、Actor 的更新 Actor的学习本质上就是 PG 的更新,只不过在 AC 中更新的权重变成了 TD-Error。在上一篇 介绍 REINFORCE算法文章中已经详细讲过,传送门[强化学习——REINFORCE 算法推导与代码实现](https://blog.csdn.net/november_chopin/article/details/108033013) 。在此不再赘述。 ```python def learn(self, state, action, td_error): with tf.GradientTape() as tape: _logits = self.model(np.array([state])) _exp_v = tl.rein.cross_entropy_reward_loss( logits=_logits, actions=[action], rewards=td_error) grad = tape.gradient(_exp_v, self.model.trainable_weights) self.optimizer.apply_gradients(zip(grad, self.model.trainable_weights)) ``` 注意的是在这里我们直接使用了 `tensorlayer` 提供的函数 `cross_entropy_reward_loss` 。其实这个函数就是把交叉熵函数包装了一下而已: ```python def cross_entropy_reward_loss(logits, actions, rewards, name=None): cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(labels=actions, logits=logits, name=name) return tf.reduce_sum(tf.multiply(cross_entropy, rewards)) ``` **完整代码**请查看[Actor-Critic算法](https://github.com/NovemberChopin/RL_Tutorial/blob/master/code/AC_Discrete.py) 。还望随手一个 star ,不胜感激 ## 三、AC小结 要理解 AC,就要理解 TD-Error,而TD-Error 的本质就是 $Q(s,a) - V(s)$ 得来的。其中的 $Q(s,a)$ 就是采用了TD的方法 $r + \gamma V(s')$ 得来的。Critic 网络的作用就是最小化 TD-Error。Actor 网络的功能和 REINFORCE 算法中的 策略网络功能一样,就是交叉熵带权重更新。 基本版的Actor-Critic算法虽然思路很好,但是由于难收敛的原因,仍然还可以做改进。基于AC架构的改进算法还有DDPG、TD3等,都会在以后介绍。 ================================================ FILE: Doc/13-RL-DDPG.md ================================================ # 强化学习13——Deep Deterministic Policy Gradient(DDPG) 上篇文章介绍了[强化学习——Actor-Critic算法详解加实战](https://blog.csdn.net/november_chopin/article/details/108170500) 介绍了Actor-Critic,本篇文章将介绍 DDPG 算法,DDPG 全称是 Deep Deterministic Policy Gradient(深度确定性策略梯度算法) 其中 PG 就是我们前面介绍了 Policy Gradient,在[强化学习10——Policy Gradient 推导](https://blog.csdn.net/november_chopin/article/details/108032626) 已经讨论过,那什么是确定性策略梯度呢? ## 一、确定性策略 与确定性策略对应的是随机性策略,就是神经网络输出的是动作的分布,在确定每一步动作时,我们需要得到的策略分布进行采样,对于某些高纬的连续值动作,频繁的在高维空间对动作进行采样,是很耗费计算能力的。 同样,对于DQN算法,其只适用于低维、离散动作的问题,对于连续动作问题,DQN要计算所有可能动作的概率,并计算可能的动作的价值,动作的数量随着自由度的数量呈指数增长,那需要非常的样本量与计算量,所以就有了确定性策略来简化这个问题。 作为随机策略,在相同的策略,在同一个状态处,采用的动作是基于一个概率分布的,即是不确定的。而确定性策略就简单的多,即使用相同的策略,在同一状态下,动作是唯一确定的: $$ a_t = \mu(s|\theta^\mu) $$ ## 二、DDPG 首先要注意一点,**DDPG从名字上像一个策略梯度(PG)算法,但是其实它更接近DQN,或者说DDPG是使用的 Actor-Critic 架构来解决DQN不能处理连续动作控制问题的一个算法**,这点一定要注意。下面来详细解释为什么这么说 ### 1、从 Q-Learning 到 DQN 我们先回忆下Q-Learning的算法流程,在 [强化学习4——时序差分控制算法](https://blog.csdn.net/november_chopin/article/details/107897225) 中已经详细介绍过Q-Learning算法。我们知道,首先我们基于状态 $S_t$,用 $\epsilon-$贪婪法选择到动作 $A_t$ 并执行,进入状态$S_{t+1}$,并得到奖励$R_{t}$,然后利用得到的$$ 来更新Q表格,注意在更新Q表格时,基于状态 $S_{t+1}$ 使用贪心策略选择 $A'$ 。 $$ A' = max_{a'} Q(S',a') $$ 也就是**选择使$Q(S_{t+1}, a)$ 最大的 $a$ 来作为 $A'$ 来更新价值函数**。对应到上图中就是在图下方的三个黑圆圈动作中选择一个使$Q(S', a)$ 最大的动作作为 $A'$ 。 ![ar0z9A.png](https://s1.ax1x.com/2020/08/05/ar0z9A.png) 由于Q-Learning 使用Q表格存储每个状态的所有动作价值,所以面对连续状态既如果状态非常多的情况就不能胜任,所有我们就用函数逼近的方法,使用神经网络来代替 Q 表格,其余流程不变,这样就得到了DQN算法。在[强化学习7——DQN算法详解](https://blog.csdn.net/november_chopin/article/details/107912720) 中已经详细介绍过 DQN算法,下面就简单回忆下DQN算法流程: ![arwWRI.png](https://s1.ax1x.com/2020/08/05/arwWRI.png) 可以看出,DQN用神经网络代替Q表格,loss 函数就是神经网络当前的输出与target之间的差距,然后对损失函数求导更新网络 参数。 ### 2、从DQN 到DDPG 而target的计算方式为 $r + \gamma\;max_{a'}\hat{q}(s',a',w)$ ,即从下一个状态使用max函数选一个最大的动作 Q,当具有有限数量的离散动作值时,计算max不是问题,因为我们可以对所有动作计算其Q值(这也能够得到能够最大化Q值的动作)。但是当动作空间连续时,我们不能穷举所有的可能,这也是DQN不能处理连续动作控制任务的原因。 那么该如何解决这个问题呢?我们知道DQN用神经网络解决了Q-Learning不能解决的连续状态空间问题。那我们可不可以也使用一个神经网络来代替 $max_aQ(s,a)$ 呢?当然是可以的,DDPG就是这样做的,即用一个函数来代替这个过程: $$ max_aQ(s,a) \approx Q(s,\mu(s|\theta)) $$ 其中的 $\theta$ 就是策略网络的参数,根据前面讲过的AC算法,可以联想到使用策略网络充当 Actor,使用DQN中的价值网络充当 Critic(注意这里估计的是 Q值,而不是 V值,不要和AC算法搞混了),Actor部分不使用带 Reward 的加权梯度更新(PG算法更新方式),而是使用Critic网络对action 的梯度对 actor更新。 DDPG既然来自DQN,那当然也有经验回放与双网络,所以在 Critic 部分增加一个目标网络去计算目标 Q值,但是还需要选取动作用来估计目标Q值,由于我们有自己的Actor策略网络,用类似的做法,再增加一个Actor目标网络,所以DDPG算法一共包含了四个网络,分别是:Actor当前网络,Actor目标网络,Critic当前网络,Critic目标网络,2个Actor网络的结构相同,2个Critic网络的结构相同,这四个网络的功能如下: - Actor当前网络:负责策略网络参数 $\theta$ 的迭代更新,负责根据当前状态 S 选择当前动作 A,用于和环境交互生成 S'和 R。 - Actor目标网络:负责根据经验回放池中采样的下一状态 S' 选择最优下一动作 A',网络参数 $\theta$ 定期从 $\theta$ 复制。 - Critic当前网络:负责价值网络参数ww的迭代更新,负责计算负责计算当前Q值 $Q(S,A,w)$ 。目标Q 值 $y_i = R + \gamma Q'(S',A',w')$ 。 - Critic目标网络:负责计算目标Q值中的 $Q'(S',A',w')$ 部分,网络参数 $w'$ 定期从 $w$ 复制。 注意:**Critic 网络的输入有两个:动作和状态,需要一起 输入到 Critic 中** 值得一提的是,DDPG从当前网络到目标网络的复制和我们之前讲到了DQN不一样。在DQN中是直接把将当前Q网络的参数复制到目标Q网络,即 $w' = w$,这样的更新方式为**硬更新**,与之对应的是**软更新**,DDPG就是使用的软更新,即每次参数只更新一点点,即: $$ w' \leftarrow \tau w + (1-\tau)w' \\ \theta \leftarrow \tau \theta + (1-\tau)\theta' $$ 其中 $\tau$ 是更新系数,一般取的比较小。同时,为了学习过程可以增加一些随机性,探索潜在的更优策略,通过Ornstein-Uhlenbeck process(OU过程)为action添加噪声,最终输出的动作A为: $$ A = \pi_\theta(s) + OU_{noise} $$ 对于损失函数,Critic部分和DQN类似,都是使用均方误差: $$ J(w) = \frac{1}{m}\sum_{i=1}^m(y_i - Q(S_i, A_i, w))^2 $$ 对于Actor部分的损失函数,[原论文](https://arxiv.org/pdf/1509.02971.pdf)定义的比较复杂,这里采取一种简单的方式来理解,我们知道 ,Actor 的作用是输出一个动作 A,这个动作 A输入到 Critic 后,可以获得最大的 Q 值。 所以我们的Actor的损失可以简单的理解为得到的反馈Q值越大损失越小,得到的反馈Q值越小损失越大,因此只要对状态估计网络返回的Q值取个负号即可,即: $$ J(\theta) = -\frac{1}{m}\sum_{i=1}^mQ(s_i, a_i, w) $$ 关于DDPG算法完整的流程如下: asY5ng.png ## 三、代码实现 代码使用 `Pendulum-v0` 连续环境,采用 `tensorflow` 学习框架 ### 1、搭建网络 #### Actor 网络 ```python def get_actor(input_state_shape): input_layer = tl.layers.Input(input_state_shape) layer = tl.layers.Dense(n_units=64, act=tf.nn.relu)(input_layer) layer = tl.layers.Dense(n_units=64, act=tf.nn.relu)(layer) layer = tl.layers.Dense(n_units=action_dim, act=tf.nn.tanh)(layer) layer = tl.layers.Lambda(lambda x: action_range * x)(layer) return tl.models.Model(inputs=input_layer, outputs=layer) ``` Actor 网络输入状态 ,输出动作,注意的是,连续环境的动作一般都有一个范围,这个范围在环境中已经定以好,使用 `action_bound = env.action_space.high` 即可获取。 如果 actor 输出的动作超出范围会导致程序异常,所以在网络末端使用 `tanh` 函数把输出映射到 [-1.0, 1.0]之间。然后使用 `lamda` 表达式,把动作映射到相应的范围。 #### Critic 网络 ```python def get_critic(input_state_shape, input_action_shape): state_input = tl.layers.Input(input_state_shape) action_input = tl.layers.Input(input_action_shape) layer = tl.layers.Concat(1)([state_input, action_input]) layer = tl.layers.Dense(n_units=64, act=tf.nn.relu)(layer) layer = tl.layers.Dense(n_units=64, act=tf.nn.relu)(layer) layer = tl.layers.Dense(n_units=1, name='C_out')(layer) return tl.models.Model(inputs=[state_input, action_input], outputs=layer) ``` 在DDPG中我们把 状态和动作同时输入到 Critic 网络中,去估计 $Q(s,a)$ 。所以定义两个输入层,然后连接起来,最后模型的输入部分定义两个输入。 ### 2、主流程 ```python for episode in range(TRAIN_EPISODES): state = env.reset() for step in range(MAX_STEPS): if RENDER: env.render() # Add exploration noise action = agent.get_action(state) state_, reward, done, info = env.step(action) agent.store_transition(state, action, reward, state_) if agent.pointer > MEMORY_CAPACITY: agent.learn() state = state_ if done: break ``` 可以看到,DDPG流程与DQN基本相同,重置状态,然后选择动作,与环境交互,获得S' ,R后,把数据保存起来。如多数据量足够,就对数据 进行抽样,更新网络参数。然后开始更新 s,开始下一步循环。 这里重点看一下 `get_action()` 函数: ```python def get_action(self, s, greedy=False): a = self.actor(np.array([s], dtype=np.float32))[0] if greedy: return a return np.clip( np.random.normal(a, self.var), -self.action_range, self.action_range) ``` `get_action()` 函数用以选取一个动作,然后与环境交互。为了更好的探索环境,我们在训练过程中为动作加入噪声,原始的DDPG作者推荐加入与时间相关的OU噪声,但是更近的结果表明高斯噪声表现地更好。由于后者更为简单,因此其更为常用。 我们这里就采用的后者,为动作添加高斯噪声:这里我们的 Actor 输出的 `a` 作为一个正太分布的平均值,然后加上参数 `VAR`,作为正太分布的方差,然后就可以构造一个正太分布。然后从正太分布中随机选取一个动作 ,我们知道正太分布是有很大概率采样到平均值附近的点,所以利用这个方法,就可以实现一定的探索行为。此外,我们也可以控制 `VAR` 的大小,来控制探索概率的大小。 当测试的时候,选取动作时就不需要探索,因为这时 Actor 要输入有最大 `Q` 值的动作,直接输出动作就可以。所以在`get_action()` 函数中用 一个参数 `greedy` 来控制这两种情况。 ### 3、网络更新 #### Critic 更新 ![dahsTH.png](https://s1.ax1x.com/2020/08/22/dahsTH.png) 如上图所示,Critic 部分的更新和 DQN 是一样的,使用td-error来更新。用目标网络构造 `target`,然后和当前网络输出的 `q` 计算MSE损失,然后更新网络参数。 ```python with tf.GradientTape() as tape: actions_ = self.actor_target(states_) q_ = self.critic_target([states_, actions_]) target = rewards + GAMMA * q_ q_pred = self.critic([states, actions]) td_error = tf.losses.mean_squared_error(target, q_pred) critic_grads = tape.gradient(td_error, self.critic.trainable_weights) self.critic_opt.apply_gradients(zip(critic_grads, self.critic.trainable_weights)) ``` #### Actor 更新 ```python with tf.GradientTape() as tape: actions = self.actor(states) q = self.critic([states, actions]) actor_loss = -tf.reduce_mean(q) # maximize the q actor_grads = tape.gradient(actor_loss, self.actor.trainable_weights) self.actor_opt.apply_gradients(zip(actor_grads, self.actor.trainable_weights)) ``` DDPG采用梯度上升法,Actor作用就是输出一个动作,这个动作输入Critic网络能得到最大的 `Q` 值。由于和梯度下降方向相反,所以需要在 loss 函数前面加上负号。 完整代码地址: [强化学习——DDPG算法Tensorflow2.0实现](https://github.com/NovemberChopin/RL_Tutorial/blob/master/code/DDPG.py) 还望随手给个star,再次不胜感激 ## 四、总结 DDPG通过借鉴AC的架构,在DQN算法的基础上引入了Actor网络,解决了连续控制问题,可以看做是DQN在连续问题上的改进算法。 下篇会介绍DDPG的进化版本的算法,就是TD3算法。 ================================================ FILE: Doc/14-RL-TD3.md ================================================ # 强化学习 14 —— TD3 算法详解 上篇文章 [强化学习 13 —— DDPG算法详解](https://blog.csdn.net/november_chopin/article/details/108171030) 中介绍了DDPG算法,本篇介绍TD3算法。TD3的全称为 Twin Delayed Deep Deterministic Policy Gradient(双延迟深度确定性策略)。可以看出,TD3就是DDPG算法的升级版,所以如果了解了DDPG,那么TD3算法自然不在话下。 ## 一、偏差与方差 在介绍TD3算法之前 ,先搞清楚偏差(bais)和方差(variance)。看下面这张图,可以类比打靶,其中红色部分是靶心,偏差(bais)就是机器学习模型的输出与真实样本的差异, ![a2H9yj.png](https://s1.ax1x.com/2020/08/06/a2H9yj.png) a2HplQ.png ## 二、算法介绍 TD3算法主要对DDPG做了三点改进,将会在下面 一一讲解,两者的代码也很相似,这里只展示改进的部分,如果对DDPG算法不太熟悉可以参考上一篇博客[强化学习 13——DDPG算法详解与实战](https://blog.csdn.net/november_chopin/article/details/108171030) 。 完整的TD3算法代码地址[强化学习——TD3算法代码地址](https://github.com/NovemberChopin/RL_Tutorial/blob/master/code/TD3.py) 还望随手一个 `star`,再此不胜感激 ### 1、双 Critic 网络 我们知道,DDPG源于DQN,而DQN源于Q-Learning,这些算法都是通过估计Q值来寻找最优的策略,在强化学习中,更新Q网络的目标值target为:$y=r+\gamma\;max_{a'}Q(s',a')$ ,因为样本存在 噪声 $\epsilon$,所以真实情况下,有误差的动作价值估计的最大值通常会比真实值更大: $$ E_\epsilon[max_{a'}(Q(s',a')+\epsilon)] \geq max_{a'}Q(s',a') $$ 这就就不可避免的降低了估值函数的准确度,由于估值方法的计算依据贝尔曼方程,即使用后续状态对估值进行更新,这种性质又加剧了精确度的下降。在每一次更新策略时,使用一个不准确的估计值将会导致错误被累加。这些被累加的错误会导致某一个不好的状态被高估,最终导致策略无法被优化到最优,并使算法无法收敛。 在DQN算法中针对Q值过估计的问题采用的是利用双网络分别实现动作的选择和评估,也就是DDQN算法。在TD3算法中,我们也使用 两个 Critic 网络来评估 Q 值,然后选取较小的那个网络的Q值来更新,这样就可以缓解Q值高估现象。这样或许会导致些许低估,低估会导致训练缓慢,但是总比高估要好得多。 注意:这里我们使用了两个 Critic 网络,每个Critic网络都有相应的 Target 网络,可以理解为这是两套独立的 Critic 网络,都在对输入的动作进行评估,然后通过 `min()` 函数求出较小值作为更新目标。所以TD3算法一共用到 6 个网络。 代码实现: ```python self.q_net1 = QNetwork(state_dim, action_dim, hidden_dim) self.q_net2 = QNetwork(state_dim, action_dim, hidden_dim) self.target_q_net1 = QNetwork(state_dim, action_dim, hidden_dim) self.target_q_net2 = QNetwork(state_dim, action_dim, hidden_dim) self.policy_net = PolicyNetwork(state_dim, action_dim, hidden_dim, action_range) self.target_policy_net = PolicyNetwork(state_dim, action_dim, hidden_dim, action_range) ``` 如上所示,包含两套Q网络,用来估计Q值,一套策略网络。具体的网络更新部分和DDPG是流程是一样的,唯一不同的是两个 Critic 网络算出 Q 值后,选取最小值去计算目标值: ```python target_q_min = tf.minimum(self.target_q_net1(target_q_input), self.target_q_net2(target_q_input)) target_q_value = reward + (1 - done) * gamma * target_q_min ``` 然后就是分别对Critic 网络和policy 网络进行更新。 ### 2、延迟 Actor 网络更新 TD3中使用的第二个技巧就是对Policy进行延时更新。在双网络中,我们让target网络与当前网络更新不同步,当前网络更新 d 次之后在对target网络进行更新(复制参数)。这样就可以减少积累误差,从而降低方差。同样的我们也可以policy网络进行延时更新,因为actor-critic方法中参数更新缓慢,进行延时更新一方面可以减少不必要的重复更新,另一方面也可以减少在多次更新中累积的误差。在降低更新频率的同时,还应使用软更新: $$ \theta'\leftarrow \tau\theta + (1-\tau)\theta' $$ 关于 policy 网络延时更新的实现也很简单,只需要一个`if` 语句就可以实现 ```python if self.update_cnt % self.policy_target_update_interval == 0 ``` 其中`update_cnt` 是更新的次数,`policy_target_update_interval` 是policy 网络更新的周期,每当 critic 更新了一定次数后,再更新 policy 网络。 ### 3、目标策略的平滑正则化 Target Policy Smoothing Regularization 上面我们通过延时更新policy来避免误差被过分累积,接下来我们我们再思考能不能把误差本身变小呢?那么我们首先就要弄清楚误差的来源。 误差的根源是值函数估计产生的偏差。知道了原因我们就可以去解决它,在机器学习中消除估计的偏差的常用方法就是对参数更新进行正则化,同样的,我们也可以将这种方法引入强化学习中来: 在强化学习中一个很自然的想法就是:**对于相似的action,他们应该有着相似的value。** 所以我们希望能够对action空间中target action周围的一小片区域的值能够更加平滑,从而减少误差的产生。paper中的做法是对target action的Q值加入一定的噪声 $\epsilon$ : $$ y = r + \gamma\;Q_{\theta'}(s', \pi_{\phi'}(s')+\epsilon)\;\; \epsilon ~clip(N(0,\sigma), -c, c) $$ 这里的噪声可以看作是一种正则化方式,这使得值函数更新更加平滑。 代码实现: ```python def evaluate(self, state, eval_noise_scale): state = state.astype(np.float32) action = self.forward(state) action = self.action_range * action # add noise normal = Normal(0, 1) noise = normal.sample(action.shape) * eval_noise_scale eval_noise_clip = 2 * eval_noise_scale noise = tf.clip_by_value(noise, -eval_noise_clip, eval_noise_clip) action = action + noise return action ``` 如代码所示,给动作加上噪音这部分在策略策略网络评估部分实现,`evaluate()`函数有两个参数,`state` 是输入的状态,参数 `eval_noise_scale` 用于调节噪声的大小。可以看到,首先经过前向计算得到 输出的动作 `action` 。下面详细说下如何给动作加上噪音:首先我们构造一个正太分布,然后根据动作的形状进行取样`normal.sample(action.shape)` ,然后乘以参数 `eval_noise_scale` 实现对噪音进行缩放,为了防止抽出的噪音很大或者很小的情况,我们对噪音进行剪切,范围相当于两倍的`eval_noise_scale` 。最后把噪音加到`action`上并输出。 **算法伪代码:** ![aRZ9c4.png](https://s1.ax1x.com/2020/08/06/aRZ9c4.png) 参考: https://zhuanlan.zhihu.com/p/55307499 https://zhuanlan.zhihu.com/p/86297106?from_voters_page=true ================================================ FILE: Doc/15-RL-PPO.md ================================================ # 强化学习15——PPO算法推导 在[Policy Gradient推导](https://blog.csdn.net/november_chopin/article/details/108032626?spm=1001.2014.3001.5501)和[REINFORCE算法](https://blog.csdn.net/november_chopin/article/details/108033013?spm=1001.2014.3001.5501)两篇文章介绍了PG算法的推导和实现,本篇要介绍的算法是`Proximal Policy Optimization (PPO)`,中文叫近短策略优化算法。PPO由于其非常的好的性能与易于实现等特性,已被作为OpenAI公司的首选算法,可见这个算法的优秀性能,具体可以查看[OpenAI-PPO](https://openai.com/blog/openai-baselines-ppo/) ## 一、Policy Gradient 的不足 - 采样效率低下:PG采用MC采样方式,每次基于当前的策略对环境采样一个episode数据,然后基于这些数据更新策略,这个过程中数据仅仅被利用了一次就扔掉了,相比于DQN等离线学习算法,PG这种更新方式是对数据的极大浪费。 - 训练不稳定:在强化学习中当前的动作选择会影响到将来的情况,不像监督学习的训练样本是相互独立的,如果某一批次的样本不好导致模型训练的很差,只要其他的训练数据没问题最终也能得到一个好的结果。但是在PG中,对于某次策略更新的太大或者太小,就会得到一个不好的Policy,一个不好的和环境交互就会得到一个不好的数据,用这些不好的数据更新的策略很大概率也是不好的。所以训练时候会有下图这种情况,训练过程中会忽然进入一个很差的状态。 ![](http://inews.gtimg.com/newsapp_ls/0/14468661815/0) 在TRPO算法中,对于第一个问题可以使用importance sampling的方式,第二个问题使用Trust region and natural policy gradient方法,TRPO算法是OpenAI在15年提出的算法,做了很多开创性的工作,当然要理解起来论文也不是那么容易的,PPO可以看作是TRPO的一个简化版本,也用到了importance sampling技术,然后效果也不输TRPO,感兴趣的可以自行阅读[Arxiv: TRPO](https://arxiv.org/abs/1502.05477) ## 二、重要性采样 重要性采样(**importance sampling**)是一种近似的采样方法,如下式子所示:我们要求`f(x)`的期望,其中`x`服从`p(x)`分布,可以通过如下入学变换,`x`可以从另外一个分布`q(x)`中采样,这时`f(x)`就需要乘上一个重要性权重$\frac{p(x)}{q(x)}$. 通过这种方式就可以从另外一个分布采样来计算原式,后面只需要乘上一个重要性权重就可以了。 ![](https://s3.bmp.ovh/imgs/2022/01/f9ddb3fb263a34d4.png) 那么重要性权重有什么用呢?上面说了PG算法效率低下,是因为PG属于on-policy算法,那我们能不能把PG变成off-policy算法呢?答案是可以的,方法就是重要性采样。 PG的策略更新梯度为:$\nabla J(\theta)=E_{\tau-p_\theta (\tau)}[R(\tau)\nabla logp_\theta(\tau)]$ ,根据上面的式子,更新梯度可以变为如下形式: $$ \nabla J(\theta)=E_{\tau-p_{\theta'} (\tau)}\left[\frac{p_\theta(\tau)}{p_{\theta'}(\tau)}R(\tau)\nabla logp_\theta(\tau)\right] $$ 此时有两个策略,参数分别为 $\theta'$和$\theta$ ,我们使用$\theta'$ 来和环境交互采样数据,然后用来更新 $\theta$ 策略,这样就变成了`off-policy` 方式,采集到的数据可以进行多次更新。$\theta$ 更新多次之后把参数复制给 $\theta'$ . 下面把使用优势函数的形式改成off-policy形式: 使用优势函数的更新梯度为: $$ E_{(s_t,a_t)-\pi_\theta}[A^\theta(s_t,a_t)\nabla logp_\theta(a_t^n|s_t^n)] $$ 使用重要性采样后有: $$ \begin{aligned} = & E_{(s_t,a_t)-\pi_{\theta'}}\left[\frac{P_\theta(s_t,a_t)}{P_{\theta'}(s_t,a_t)}\color{red}A^{\theta'}(s_t,a_t) \color{black} \nabla logp_\theta(a_t^n|s_t^n)\right] \\ = & E_{(s_t,a_t)-\pi_{\theta'}}\left[\frac{p_\theta(a_t|s_t)}{p_{\theta'}(a_t|s_t)}\cancel{\frac{p_\theta(s_t)}{p_{\theta'}(s_t)}}\color{red}A^{\theta'}(s_t,a_t)\color{black}\nabla logp_\theta(a_t^n|s_t^n)\right] \\ \end{aligned} $$ 上式中需要注意的第一点是对于优势函数的计算,$A^{\theta}(s_t,a_t)$要变为$A^{\theta'}(s_t,a_t)$。因为优势函数是基于采样策略的和环境交互的数据计算的,这里我们的采样策略参数是$\theta'$,所以要换成 $\theta'$。另外一点是对于 $\frac{p_\theta(s_t)}{p_{\theta'}(s_t)}$ 一般认为分子分母是近似相等的,这么处理的原因1是对于$p_\theta(s_t)$ 难以计算(比如游戏中某一状态的概率怎么算?)第二点就是在PPO中我们要求策略 $\theta$ 和$\theta'$ 差别不能太大,这样就算计算出来 $p_\theta(s_t)$ 和 $p_{\theta'}(s_t)$, 最终结果也会接近于1。至于为什么两个分布不能差别太大,下面会具体介绍。到这里我们就得到了优势函数形式PPO损失函数: $$ J^{\theta'}(\theta) = E_{(s_t,a_t)-\pi_{\theta'}}\left[\frac{p_\theta(a_t|s_t)}{p_{\theta'}(a_t|s_t)}A^{\theta'}(s_t,a_t)\right] $$ 其中$J^{\theta'}(\theta)$ 表示用 $\theta'$ 采样,用$\theta$ 更新。 ## 三、梯度更新约束问题 上面我们说了,在PPO算法中这两个分布不能相差太远,下面来解释原因。还是回到下面这个重要性采样例子: $$ E_{x-p}[f(x)] = E_{x-q}[\frac{p(x)}{q(x)}f(s)] $$ **上面这个公式在理论上是没有问题的,即便`p` 分布和`q`分布差别很大也没问题,前提是需要非常大量的采样。但是在实际算法中,由于做不到非常大量的采样,所以这里需要给`p`分布和`q`分布加一个限制,就是不能差别太大。**下面结合图片讲解,如图红线代表`f(x)`,蓝色线为`p(x)`分布,绿色线为 `q(x)`分布,其分布差别很大。 ![](https://s3.bmp.ovh/imgs/2022/01/0a602f083c75025d.png) - 如果不采用重要性采样方法的话,直接从`p(x)`分布采样有限的一些点,那么很大概率上会采样到A区域,那么此时算出来`f(x)`的期望是负的。 - 如果我们在 `q(x)`分布采样的话,很大概率会采样带范围B(采样点为右边六个绿色点),那么此时很容易会得出 `E[f(x)]` 是正数的情况。但实际上根据`p(x)`分布可以看出在采样有限点下 `f(x)` 的期望应该是负的,这显然是有问题的。 - 如果再继续采样一个点(假如最左边的一个点),这个点在 q 分布下被采样到的几率很小,所以要大量采样,在这个点下计算 $\frac{p(x)}{q(x)}$会得到一个非常大的数,最后的结果`f(x)`的期望仍然是负数。 - 也就是说尽管在 q 分布下采样到的点很容易都在 B 区域,但是这里的点的 重要性权重 $\frac{p(x)}{q(x)}$ 很小,虽然采样到B区域以外的点概率很小,但是重要性权重比较高。这也是对乘上 $\frac{p(x)}{q(x)}$ 直观的理解。但是这需要大量采样,在算法中采样数量不多时候是不成立的,所以需要对p分布和q分布做一个限制:不能相差太大。 下面谈谈PPO中的限制,PPO论文提供了两种限制梯度更新方式: - **PPO1** PPO1使用KL距离来表示两个分布的差距大小,并把KL距离作为一个惩罚项,使用参数$\beta$来调节 - 如果KL距离太大了,说明没有起到惩罚的作用,需要增加$\beta$ - 如果KL距离太小了,说明惩罚的太严重了,此时适当的减小 $\beta$ 的值 ![](https://s3.bmp.ovh/imgs/2022/01/b49ff193f2728125.png) - **PPO2** PPO2没有使用 KL 距离限制,而是直接用 clip 剪切。把重要性权重限制在 $(1-\epsilon, 1+\epsilon)$ 范围内。 - 当优势函数 A > 0时,要增加某些动作出现的概率,限制重要性权重,使得梯度不要增加的太快 - 当优势函数 A < 0时,要减小某些动作出现的概率,限制重要性权重,使得梯度不要减小的太快 具体可以结合着图片个式子分析下。这种处理法方法简单粗暴,但效果却出奇的好,不得不说真的很神奇。 ![](https://s3.bmp.ovh/imgs/2022/01/21c2ad87cfa6a679.png) 以上就是PPO理论部分的内容了,代码实现连接见[PPO TF2.X实现](https://github.com/NovemberChopin/RL_Tutorial/blob/master/code/PPO.py),对比着PG的代码和上述理论知识,还是挺好理解的。本篇文章部分图片参考了李宏毅老师的课件,另外李宏毅老师讲的真的不错,简单易懂,这里放上课程链接:[李宏毅强化学习](https://www.bilibili.com/video/BV1UE411G78S?from=search&seid=3408487385073313190&spm_id_from=333.337.0.0)。 另外,本强化学习算法系列尤其是头几篇文章参考了周博磊老师的[强化学习纲要](https://space.bilibili.com/511221970/channel/seriesdetail?sid=764099),周老师的课程更加偏理论一些,对初学者可能不太友好,建议先对算法有个直观的了解然后再观看。 强化学习常用算法打算就介绍这么多,后面会更新一些模仿学习和多智能体强化学习的内容 ================================================ FILE: Doc/2-RL-DP.md ================================================ # 强化学习 2—— 用动态规划求解 MDP 在上一篇文章 [强化学习 1 —— 一文读懂马尔科夫决策过程 MDP](https://blog.csdn.net/november_chopin/article/details/106589197) 介绍了马尔科夫过程,本篇接着来介绍如何使用动态规划方法来求解。 动态规划的关键点有两个: - 一是问题的最优解可以由若干小问题的最优解构成,即通过寻找子问题的最优解来得到问题的最优解。 - 二是可以找到子问题状态之间的递推关系,通过较小的子问题状态递推出较大的子问题的状态。 在上一篇中我们提到的状态价值的贝尔曼方程: $$ v_{\pi}(s) = \sum_{a \in A}\pi(a|s)\left( R(s, a) + \gamma \sum_{s' \in S} P_{(s'|s, a)} \cdot v_\pi(s')\right) $$ 从这个式子我们可以看出,我们可以定义出子问题求解每个状态的状态价值函数,同时这个式子又是一个递推的式子, 意味着利用它,我们可以使用上一个迭代周期内的状态价值来计算更新当前迭代周期某状态 s 的状态价价值。可见,强化学习的问题是**满足**这两个条件的。 可以把 Decision Making 分为两个过程 - **Prediction** (策略评估) - 输入: MDP$$ 和 策略 $\pi $ - 输出:value function $V_\pi$ - **Control** (策略控制,即寻找最优策略) - 输入:MDP$ $ - 输出:最优的价值函数 $V^*$ 和最优的策略 $\pi^*$ ## 一、策略评估 首先,我们来看如何使用动态规划来求解强化学习的预测问题,即求解给定策略的状态价值函数的问题。这个问题的求解过程我们通常叫做策略评估(Policy evaluation) 策略评估的基本思路是从任意一个状态价值函数开始,依据给定的策略,结合贝尔曼期望方程、状态转移概率和奖励同步迭代更新状态价值函数,直至其收敛,得到该策略下最终的状态价值函数。 假设我们在第 t 轮迭代已经计算出了所有的状态的状态价值 $v_t(s')$ ,那么在第 t+1 轮我们可以利用第 t 轮计算出的状态价值计算出第 t+1 轮的状态价值 $v_{t+1}(s)$ 。这是通过贝尔曼方程来完成的,即 **Iteration on Bellman exception backup** $$ V_{t+1}(s) = \sum_{a \in A}\pi(a|s) \left(R(s, a) + \gamma \sum_{s' \in S} P_{(s'|s, a)} \cdot V_t(s')\right) $$ ### 1、策略评估实例 这是一个经典的Grid World的例子。我们有一个4x4的16宫格。只有左上和右下的格子是终止格子。该位置的价值固定为0,个体如果到达了该2个格子,则停止移动,此后每轮奖励都是0。个体在16宫格其他格的每次移动,得到的即时奖励$R$都是-1。注意个体每次只能移动一个格子,且只能上下左右4种移动选择,不能斜着走, 如果在边界格往外走,则会直接移动回到之前的边界格。衰减因子我们定义为 $\gamma=1$。这里给定的策略是随机策略,即每个格子里有25% 的概率向周围的4个格子移动。 ![UoQbYq.png](https://s1.ax1x.com/2020/07/21/UoQbYq.png) 首先我们初始化所有格子的状态价值为0,如上图$k=0$的时候。现在我们开始策略迭代了。由于终止格子的价值固定为 0,我们可以不将其加入迭代过程。在 $k=1$ 的时候,我们利用上面的贝尔曼方程先计算第二行第一个格子的价值: $$ v_1^{(21)} = \frac{1}{4}[(−1+0)+(−1+0)+(−1+0)+(−1+0)] = -1 $$ 第二行第二个格子的价值: $$ v_1^{(22)} = \frac{1}{4}[(−1+0)+(−1+0)+(−1+0)+(−1+0)] = -1 $$ 其他的格子都是类似的,第一轮的状态价值迭代的结果如上图 $k=1$ 的时候。现在我们第一轮迭代完了。开始动态规划迭代第二轮了。还是看第二行第一个格子的价值: $$ v_2^{(21)} = \frac{1}{4}[(−1+0)+(−1−1)+(−1−1)+(−1−1)] = -1.75 $$ 第二行第二个格子的价值是: $$ v_2^{(22)} = \frac{1}{4}[(−1-1)+(−1−1)+(−1−1)+(−1−1)] = -2 $$ 最终得到的结果是上图 $k=2$ 的时候。第三轮的迭代如下: $$ v_3^{(21)} = \frac{1}{4}[(−1−1.7)+(−1−2)+(−1−2)+(−1+0)] = -2.425 $$ 最终得到的结果是上图 $k=3$ 的时候。就这样一直迭代下去,直到每个格子的策略价值改变很小为止。这时我们就得到了所有格子的基于随机策略的状态价值。 ## 二、策略控制 ### 1、Policy Iteration 对于策略控制问题,一种可行的方法就是根据我们之前基于任意一个给定策略评估得到的状态价值来及时调整我们的动作策略,这个方法我们叫做策略迭代 (Policy Iteration)。 如何调整呢?最简单的方法就是贪婪法。考虑一种如下的贪婪策略:**个体在某个状态下选择的行为是其能够到达后续所有可能的状态中状态价值最大的那个状态**。如上面的图右边。当我们计算出最终的状态价值后,我们发现,第二行第一个格子周围的价值分别是0,-18,-20,此时我们用贪婪法,则我们调整行动策略为向状态价值为0的方向移动,而不是随机移动。也就是图中箭头向上。而此时第二行第二个格子周围的价值分别是-14,-14,-20, -20。那么我们整行动策略为向状态价值为-14的方向移动,也就是图中的向左向上。 - 故 Policy Iteration 共分为两个部分 - **Evaluate** the policy $ \pi $,(通过给定的 $ \pi $ 计算 V) - **Improve** the policy $ \pi $,(通过贪心策略) $$ \pi' = greedy(v^{\pi}) $$ ​ 如果我们有 一个 策略 $\pi$,我们可以用策略 估计出它的状态价值函数 $v_\pi(s)$, 然后根据策略改进提取出更好的策略 $\pi'$,接着再计算 $v_{\pi'}(s)$, 然后再获得更好的 策略 $\pi''$,直到相关满足相关终止条件。 在这里插入图片描述 计算公式如下: 1、评估价值 (Evaluate) $$ v_{i}(s) = \sum_{a\in A} \pi(a|s) \left( R(s, a) + \gamma \sum_{s' \in S} P(s'|s, a) \cdot v_{i-1}(s') \right) $$ 2、改进策略(Improve) $$ q_i(s,a) = R(s, a) + \gamma \sum_{s' \in S} P_{(s'|s,a)} \cdot v_i(s') \\ \pi_{i+1}(s) = argmax_a \; q^{\pi_i}(s,a) $$ > 每次迭代都是基于确定的策略,故策略 $\pi$ 不再写出,对应的加上了下标 可以把 $q^{\pi_i}(s,a)$ 想象成一个表格,其中横轴代表不同的状态,纵轴代表每种状态下不同的动作产生的价值,然后在当前状态下选择一个价值最大的行为价值作为当前的状态价值。 Policy Iteration 的具体算法为: 在这里插入图片描述 **Bellman Optimality Equation** 利用状态价值函数和动作价值函数之间的关系,得到 $$ v_*(s) = max_a q_*(s,a) $$ $$ q_*(s,a) = R(s, a) + \gamma \sum_{s' \in S} P_{s's}^a \cdot V_*(s') $$ > 当到达 最优的时候,一个状态的价值就等于在当前 状态下最大的那个动作价值 把上面两个式子结合起来有**Bellman Optimality Equation** $$ v_*(s) = max_a (R(s, a) + \gamma \sum_{s' \in S} P_{s's}^a \cdot v_*(s')) $$ $$ q_*(s,a) = R(s, a) + \gamma \sum_{s' \in S} P_{s's}^a \cdot max_{a'}q_*(s', a') $$ ### 2、Value Iteration 观察第三节的图发现,我们如果用贪婪法调整动作策略,那么当 $k=3$ 的时候,我们就已经得到了最优的动作策略。而不用一直迭代到状态价值收敛才去调整策略。那么此时我们的策略迭代优化为价值迭代。 比如当 $k=2$ 时,第二行第一个格子周围的价值分别是0,-2,-2,此时我们用贪婪法,则我们调整行动策略为向状态价值为0的方向移动,而不是随机移动。也就是图中箭头向上。而此时第二行第二个格子周围的价值分别是-1.7,-1.7,-2, -2。那么我们整行动策略为向状态价值为-1.7的方向移动,也就是图中的向左向上。 我们没有等到状态价值收敛才调整策略,而是随着状态价值的迭代及时调整策略, 这样可以大大减少迭代次数。此时我们的状态价值的更新方法也和策略迭代不同。现在的贝尔曼方程迭代式子如下: $$ v_*(s) = max_a q_*(s,a) \\ \Downarrow \\ v_{i+1}(s) \leftarrow max_{a \in A} \; \left(R(s, a) + \gamma \sum_{s' \in S} P_{(s'|s,a)} \cdot V_i(s')\right) $$ 然后直接提取最优策略 $ \pi $ $$ \pi^*(s) = argmax_a \; q^{\pi}(s,a) \\ \Downarrow \\ \pi^*(s) \leftarrow argmax_a \; \left(R(s, a) + \gamma \sum_{s' \in S} P_{(s'|s,a)} \cdot V_{end}(s')\right) $$ Value Iteration 的算法为: 在这里插入图片描述 ## 三、实例展示 ​ 下面是斯坦福大学的一个网页,可以帮助更好的直观的理解策略迭代和价值迭代两种不同的迭代方式。开始时如下图左上所示,其中每一个格子代表一个状态,每个状态有都有上下左右四个动作,每个状态上都有一个数字,代表当前的状态价值,其中有些状态下半部分还有一个数字,代表进入当前状态所能获得的奖励。我们的策略是四个方向的概率都相等,即各有0.25的概率。要做的就是找出每个状态下的最优策略。 ### 3.1 策略迭代 ​ 如图右上所示,点击$Policy \; Evaluation$ 执行一次策略评估,可以看到有些状态已经发生了变化,相应的状态价值 $V(s)$ 也已经更新,此时再点击$Policy \; Updata$ 来更新策略,如图做下所示,可与i看到,有些状态的策略已经发生了变化,已经在当前的状态价值下提高了策略。如此反复迭代,最后的结果如图右下所示,此时每个状态都有最好的状态价值和策略。 ![tbPeQP.png](https://s1.ax1x.com/2020/06/11/tbPeQP.png) ### 3.2 价值迭代 ​ 价值迭代是不停的迭代状态价值 $V$, 然后提取出相应的动作价值$q$,然后从相应的 q 中寻找一个最大的最为当前状态价值。点击 $Toggle\; Value \; Iteration$等几秒钟就可以看到迭代 结果: tbZpp4.png 可以看出,无论是策略迭代还是价值迭代,最后的结果都是相同的。最后附上网址:https://cs.stanford.edu/people/karpathy/reinforcejs/gridworld_dp.html ## 四、代码理解 ​ gym是 [OpenAI](http://gym.openai.com/) 开放的一个开发、比较各种强化学习算法的工具库,提供了不少内置的环境,是学习强化学习不错的一个平台。我们本次使用其中提供给的一个最简单的环境 FrozenLake-v0。如下如所示,开始时agent在 s 位置,G代表金子,我们要控制 agent 找到金子的同时获得更多的奖励,与上面例子一样,agent在每个状态下有四种动作(上下左右),F代表障碍,H代表洞,要避免调入洞中,具体描述可以访问:http://gym.openai.com/envs/FrozenLake-v0/ 查看。 ![tbmNOs.png](https://s1.ax1x.com/2020/06/11/tbmNOs.png) ### 4.1、Policy Iteration ```python import numpy as np import gym def extract_policy(v, gamma = 1.0): """ 从价值函数中提取策略 """ policy = np.zeros(env.env.nS) for s in range(env.env.nS): q_sa = np.zeros(env.env.nA) for a in range(env.env.nA): q_sa[a] = sum([p * (r + gamma * v[s_]) for p, s_, r, _ in env.env.P[s][a]]) policy[s] = np.argmax(q_sa) return policy def compute_policy_v(env, policy, gamma=1.0): """ 计算价值函数 """ v = np.zeros(env.env.nS) eps = 1e-10 while True: prev_v = np.copy(v) for s in range(env.env.nS): policy_a = policy[s] v[s] = sum([p * (r + gamma * prev_v[s_]) for p, s_, r, _ in env.env.P[s][policy_a]]) if (np.sum((np.fabs(prev_v - v))) <= eps): break return v def policy_iteration(env, gamma = 1.0): """ Policy-Iteration algorithm """ # env.env.nS: 16 # env.env.nA: 4 # env.env.ncol / env.env.nrow: 4 policy = np.random.choice(env.env.nA, size=(env.env.nS)) max_iterations = 200000 gamma = 1.0 for i in range(max_iterations): old_policy_v = compute_policy_v(env, policy, gamma) new_policy = extract_policy(old_policy_v, gamma) if (np.all(policy == new_policy)): print ('Policy-Iteration converged at step %d.' %(i+1)) break policy = new_policy return policy if __name__ == '__main__': env_name = 'FrozenLake-v0' env = gym.make(env_name) optimal_policy = policy_iteration(env, gamma = 1.0) print(optimal_policy) # Policy-Iteration converged at step 6. # [0. 3. 3. 3. 0. 0. 0. 0. 3. 1. 0. 0. 0. 2. 1. 0.] ``` ### 4.2、Value Iteration ```python def extract_policy(v, gamma = 1.0): """ 从状态函数中提取策略 """ policy = np.zeros(env.env.nS) for s in range(env.env.nS): # q_sa: 在状态 s 下的 所有动作价值 q_sa = np.zeros(env.action_space.n) for a in range(env.action_space.n): for next_sr in env.env.P[s][a]: # next_sr is a tuple of (probability, next state, reward, done) p, s_, r, _ = next_sr q_sa[a] += (p * (r + gamma * v[s_])) # print("q_sa: ", q_sa) policy[s] = np.argmax(q_sa) # print('np.argmax(q_sa): ', policy[s]) return policy def value_iteration(env, gamma = 1.0): """ Value-iteration algorithm """ # env.env.nS: 16 # env.env.nA: 4 # env.env.ncol / env.env.nrow: 4 v = np.zeros(env.env.nS) max_iterations = 100000 eps = 1e-20 for i in range(max_iterations): prev_v = np.copy(v) for s in range(env.env.nS): # env.env.P[s][a]]: 状态 s 下动作 a 的概率 q_sa = [sum([p*(r + prev_v[s_]) for p, s_, r, _ in env.env.P[s][a]]) for a in range(env.env.nA)] v[s] = max(q_sa) if (np.sum(np.fabs(prev_v - v)) <= eps): print ('Value-iteration converged at iteration# %d.' %(i+1)) break return v if __name__ == '__main__': env_name = 'FrozenLake-v0' # env 中是 gym 提供的本次问题的环境信息 env = gym.make(env_name) gamma = 1.0 optimal_v = value_iteration(env, gamma) policy = extract_policy(optimal_v, gamma) print('policy:', policy) # Value-iteration converged at iteration# 1373. # policy: [0. 3. 3. 3. 0. 0. 0. 0. 3. 1. 0. 0. 0. 2. 1. 0.] ``` ## 五、总结 本篇博客介绍了如何使用动态规划来求解MDP问题,还介绍了两种迭代算法。可以发现,对于这两个算法,有一个前提条件是奖励 R 和状态转移矩阵 P 我们是知道的,因此我们可以使用策略迭代和价值迭代算法。对于这种情况我们叫做 `Model base`。同理可知,如果我们不知道环境中的奖励和状态转移矩阵,我们叫做 `Model free`。那对于 `Model_free` 情况下应该如何求解 MDP 问题呢?这就是下一篇文章要讲的 蒙特卡洛(MC)采样法。 参考资料: 1、[B站周老师的强化学习纲要第二讲下](https://www.bilibili.com/video/BV1u7411m7rh) 2、博客园 [![返回主页](https://www.cnblogs.com/skins/custom/images/logo.gif)](https://www.cnblogs.com/pinard/)[刘建平Pinard](https://www.cnblogs.com/pinard/) ================================================ FILE: Doc/3-RL-Model-free MC.md ================================================ # 强化学习 3—— Model-free MC ## 一、问题引入 回顾上篇[强化学习 2 —— 用动态规划求解 MDP](https://blog.csdn.net/november_chopin/article/details/107896549)我们使用策略迭代和价值迭代来求解MDP问题 #### 1、策略迭代过程: - 1、评估价值 (Evaluate) $$ v_{i}(s) = \sum_{a\in A} \pi(a|s) \left( {\color{red}R(s, a)} + \gamma \sum_{s' \in S} {\color{red}P(s'|s, a)} \cdot v_{i-1}(s') \right) $$ - 2、改进策略(Improve) $$ q_i(s,a) = {\color{red}R(s, a)} + \gamma \sum_{s' \in S} {\color{red}P_{(s'|s,a)}} \cdot v_i(s') \\ \pi_{i+1}(s) = argmax_a \; q^{\pi_i}(s,a) $$ #### 2、价值迭代过程: $$ v_{i+1}(s) \leftarrow max_{a \in A} \; \left({\color{red}R(s, a)} + \gamma \sum_{s' \in S} {\color{red}P_{(s'|s,a)}} \cdot V_i(s')\right) $$ 然后提取最优策略 $ \pi $ $$ \pi^*(s) \leftarrow argmax_a \; \left({\color{red}R(s, a)} + \gamma \sum_{s' \in S} {\color{red}P_{(s'|s,a)}} \cdot V_{end}(s')\right) $$ 可以发现,对于这两个算法,有一个前提条件是奖励 R 和状态转移矩阵 P 我们是知道的,因此我们可以使用策略迭代和价值迭代算法。对于这种情况我们叫做 `Model base`。同理可知,如果我们不知道环境中的奖励和状态转移矩阵,我们叫做 `Model free`。 不过有很多强化学习问题,我们没有办法事先得到模型状态转化概率矩阵 P,这时如果仍然需要我们求解强化学习问题,那么这就是不基于模型(Model Free)的强化学习问题了。 其实稍作思考,大部分的环境都是 属于 Model Free 类型的,比如 熟悉的雅达利游戏等等。另外动态规划还有一个问题:需要在每一次回溯更新某一个状态的价值时,回溯到该状态的所有可能的后续状态。导致对于复杂问题计算量很大。 对于 Model Free 类型的强化学习,此时需要智能体直接和环境进行交互,环境根据智能体的动作返回下一个状态和相应的奖励给智能体。这时候就需要智能体搜集和环境交互的轨迹(Trajectory / episode)。 对于 Model Free 情况下的 策略评估,我们介绍两种采样方法。蒙特卡洛采样法(Monte Carlo)和时序差分法(Temporal Difference) ## 二、蒙特卡洛采样法(MC) 对于Model Free 我们不知道 奖励 R 和状态转移矩阵,那应该怎么办呢?很自然的,我们就想到,让智能体和环境多次交互,我们通过这种方法获取大量的轨迹信息,然后根据这些轨迹信息来估计真实的 R 和 P。这就是蒙特卡洛采样的思想。 蒙特卡罗法通过采样若干经历完整的状态序列(Trajectory / episode)来估计状态的真实价值。所谓的经历完整,就是这个序列必须是达到终点的。比如下棋问题分出输赢,驾车问题成功到达终点或者失败。有了很多组这样经历完整的状态序列,我们就可以来近似的估计状态价值,进而求解预测和控制问题了。 ### 1、MC 解决预测问题 一个给定策略 $\pi$ 的完整有 T 个状态的状态序列如下 $$ \{S_1, A_1, R_1, S_2, A_2, R_2, \cdots,S_T, A_T, R_T\} $$ 在马尔科夫决策(MDP)过程中,我们对价值函数 $v_\pi(s)$ 的定义: $$ v_\pi(s) = E_\pi[G_t|S_t = s] = E_\pi[R_{t+1} + \gamma R_{t+2} + \gamma^2R_{t+3} | S_t = s] $$ 可以看出每个状态的价值函数等于所有该状态收获的期望,同时这个收获是通过后续的奖励与对应的衰减乘积求和得到。 对于蒙特卡罗法来说,如果要求某一个状态的状态价值,需要智能体与环境交互产生很多条轨迹,然后对所有轨迹所获得的收益取平均值,也就是: $$ G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2R_{t+3}+\cdots \gamma^{T-t-1}R_T $$ $$ v_\pi(s) \approx average(G_t) \quad s.t.\; S_t = s $$ > 上述 $G_t 代表多条轨迹的收益$ 上面预测问题的求解公式里,我们有一个average的公式,意味着要保存所有该状态的收获值之和最后取平均。这样浪费了太多的存储空间。一个较好的方法是在迭代计算收获均值,即每次保存上一轮迭代得到的收获均值与次数,当计算得到当前轮的收获时,即可计算当前轮收获均值和次数。可以通过下面的公式理解: $$ \mu_t = \frac{1}{t}\sum_{j=1}^tx_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) \\ \Downarrow \\ \mu_t = \mu_{t-1} + \frac{1}{t}(x_t-\mu_{t-1}) $$ 这样上面的状态价值公式就可以改写成: $$ N(S_t) \leftarrow N(S_t) + 1 \\ v(S_t) \leftarrow v(S_t) + \frac{1}{N(S_t)}(G_t-v(S_t)) $$ 这样我们无论数据量是多还是少,算法需要的内存基本是固定的 。我们可以把上面式子中 $\frac{1}{N(S_t)}$ 看做一个超参数 $\alpha$ ,可以代表学习率。 $$ v(S_t) \leftarrow v(S_t) + \alpha(G_t-v(S_t)) $$ 对于动作价值函数$Q(S_t, A_t)$, 类似的有: $$ Q(S_t, A_t) = Q(S_t, A_t) + \alpha(G_t - Q(S_t, A_t)) $$ ### 2、MC 解决控制问题 MC 求解控制问题的思路和动态规划策略迭代思路类似。在动态规划策略迭代算法中,每轮迭代先做策略评估,计算出价值 $v_k(s)$ ,然后根据一定的方法(比如贪心法)更新当前 策略 $\pi$ 。最后得到最优价值函数 $v_*$ 和最优策略$\pi_*$ 。在文章开始处有公式,还请自行查看。 对于蒙特卡洛算法策略评估时一般时优化的动作价值函数 $q_*$,而不是状态价值函数 $v_*$ 。所以评估方法是: $$ Q(S_t, A_t) = Q(S_t, A_t) + \alpha(G_t - Q(S_t, A_t)) $$ 蒙特卡洛还有一个不同是一般采用$\epsilon - 贪婪法$更新。$\epsilon -贪婪法$通过设置一个较小的 $\epsilon$ 值,使用 $1-\epsilon$ 的概率贪婪的选择目前认为有最大行为价值的行为,而 $\epsilon$ 的概率随机的从所有 m 个可选行为中选择,具体公式如下: $$ \pi(a|s) = \begin{cases} \epsilon/|A| + 1 - \epsilon, & \text{if $a^* = argmax_a \; q(s,a)$} \\ \epsilon/|A|, & \text{otherwise} \end{cases} $$ 在实际求解控制问题时,为了使算法可以收敛,一般 $\epsilon$ 会随着算法的迭代过程逐渐减小,并趋于0。这样在迭代前期,我们鼓励探索,而在后期,由于我们有了足够的探索量,开始趋于保守,以贪婪为主,使算法可以稳定收敛。 Monte Carlo with $\epsilon - Greedy$ Exploration 算法如下: ![U7piS1.png](https://s1.ax1x.com/2020/07/22/U7piS1.png) ### 3、在 策略评估问题中 MC 和 DP 的不同 **对于动态规划(DP)求解** 通过 `bootstrapping`上个时刻次评估的价值函数 $v_{i-1}$ 来求解当前时刻的 价值函数 $v_i$ 。通过贝尔曼等式来实现: $$ V_{t+1}(s) = \sum_{a \in A}\pi(a|s) \left(R(s, a) + \gamma \sum_{s' \in S} P_{(s'|s, a)} \cdot V_t(s')\right) $$ ![UoXzMF.png](https://s1.ax1x.com/2020/07/21/UoXzMF.png) **对于蒙特卡洛(MC)采样** MC通过一个采样轨迹来更新平均价值 $$ v(S_t) \leftarrow v(S_t) + \alpha(G_t-v(S_t)) $$ ![UoXvxU.png](https://s1.ax1x.com/2020/07/21/UoXvxU.png) MC可以避免动态规划求解过于复杂,同时还可以不事先知道奖励和装填转移矩阵,因此可以用于海量数据和复杂模型。但是它也有自己的缺点,这就是它每次采样都需要一个完整的状态序列。如果我们没有完整的状态序列,或者很难拿到较多的完整的状态序列,这时候蒙特卡罗法就不太好用了。如何解决这个问题呢,就是下节要讲的时序差分法(TD)。 如果觉得文章写的不错,还请各位看官老爷点赞收藏加关注啊,小弟再此谢谢啦 **参考资料:** B 站 [周老师的强化学习纲要第三节上](https://www.bilibili.com/video/BV1N7411Q7aJ) ================================================ FILE: Doc/4-RL-Model-free TD.md ================================================ # 强化学习 4 —— Model Free TD 在上篇文章[强化学习——蒙特卡洛 (MC) 采样法的预测与控制](https://blog.csdn.net/november_chopin/article/details/107896928)中我们讨论了 Model Free 情况下的策略评估问题,主要介绍了蒙特卡洛(MC)采样法的预测与控制问题,这次我们介绍另外一种方法——时序差分法(TD) ## 一、时序差分采样法(TD) 对于MC采样法,如果我们没有完整的状态序列,那么就无法使用蒙特卡罗法求解了。当获取不到完整状态序列时, 可以使用时序差分法(Temporal-Difference, TD)。 ### 1、TD 简介 对于蒙特卡洛采样法计算状态收益的方法是: $$ G_t = R_{t+1} + \gamma R_{t+2} + \gamma^2R_{t+3}+\cdots \gamma^{T-t-1}R_T $$ 而对于时序差分法来说,我们没有完整的状态序列,只有部分的状态序列,那么如何可以近似求出某个状态的收获呢?回顾贝尔曼方程: $$ v_\pi(s) = E_\pi[R_{t+1} + \gamma v_\pi(S_{t+1}) | S_t = s] $$ 我们可以用 $R_{t+1} + \gamma v_\pi(S_{t+1})$ 近似代替 $G_t$ ,此时有: $$ \color{red}{v(S_t) \leftarrow v(S_t) + \alpha(R_{t+1} + \gamma v(S_{t+1})-v(S_t))} $$ - 一般把 $R_{t+1} + \gamma v(S_{t+1})$ 叫做 TD Target - 把叫做 $R_{t+1} + \gamma v(S_{t+1})-v(S_t)$ TD Error - 把 用 TD Target 近似的代替 Gt 的过程称为 引导(Bootstraping) 这样一来,这样我们只需要两个连续的状态与对应的奖励,就可以尝试求解强化学习问题了。 ### 2、n步时序差分 在上面我们用 $ R_{t+1} + \gamma v_\pi(S_{t+1})$ 近似代替 $G_t$ 。即向前一步来近似收获 $G_t$,那我们能不能向前两步呢?当然是可以的,此时 $G_t$ 近似为: $$ G_t^{(2)} = R_{t+1} + \gamma R_{t+2}+\cdots + \gamma^2V(S_{t+2}) $$ 近似价值函数 $v(S_t)$ 为: $$ v(S_t) \leftarrow v(S_t) + \alpha(R_{t+1} + \gamma R_{t+2} + \gamma v(S_{t+2})-v(S_t)) $$ 从两步,到三步,再到n步,我们可以归纳出n步时序差分收获 $G_t^{(n)}$的表达式为: $$ G_t^{(2)} = R_{t+1} + \gamma R_{t+2}+\cdots + \gamma^{n-1}R_{t+n} + \gamma^nV(S_{t+n}) $$ 往前走两步时,对应的算法叫 TD(2) ,往前走三步时,对应的算法叫 (TD3) 。当n越来越大,趋于无穷,或者说趋于使用完整的状态序列时,n步时序差分就等价于蒙特卡罗法了。特别的对于往前走一步的算法,我们叫 TD(0)。 ### 3、TD小结 TD 对比 MC: ![UTe5bn.png](https://s1.ax1x.com/2020/07/21/UTe5bn.png) - TD在知道结果之前就可以学习,也可以在没有结果时学习,还可以在持续进行的环境中学习,而MC则要等到最后结果才能学习,时序差分法可以更快速灵活的更新状态的价值估计。 - TD在更新状态价值时使用的是TD 目标值,即基于即时奖励和下一状态的预估价值来替代当前状态在状态序列结束时可能得到的收获,是当前状态价值的有偏估计,而MC则使用实际的收获来更新状态价值,是某一策略下状态价值的无偏估计,这点 MC 优于 TD。 - 虽然TD得到的价值是有偏估计,但是其方差却比MC得到的方差要低,且对初始值敏感,通常比MC更加高效。 ## 二、TD 解决控制问题 我们知道,TD对比MC有很多优势,比如TD有更低方差,可以学习不完整的序列。所以我们可以在策略控制循环中使用TD来代替MC。因此现在主流的强化学习求解方法都是基于TD的,下面我们就介绍两种最常用的算法,分别是 Sarsa 和 Q-Learning ### 1、Sarsa: 对于一个动作序列如下图所示: ![U7J3wT.png](https://s1.ax1x.com/2020/07/22/U7J3wT.png) 在迭代的时候,我们基于 $\epsilon$ -贪婪法在当前状态 $S_t$ 选择一个动作 $A_t$, 然后会进入到下一个 状态 $S_{t+1}$,同时获得奖励 $R_{t+1}$,在新的状态 $S_{t+1}$ 我们同样基于 $\epsilon$-贪婪法选择一个动作 $A_{t+1}$,然后用它来更新我们的价值函数,更新公式如下: $$ Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha \left[R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) - Q(S_t, A_t)\right] $$ - 注意:**这里我们选择的动作 $A_{t+1}$,就是下一步要执行的动作**,这点是和Q-Learning算法的最大不同 - TD Target $\delta_t = R_{t+1} + \gamma Q(S_{t+1}, A_{t+1})$ - 在每一个 非终止状态 $S_t$ 都要更新 - 进行一次更新,我们要获取5个数据,$$ ,这也是算法名字 Sarsa 的由来 **Sarsa算法流程如下** ![U7J8TU.png](https://s1.ax1x.com/2020/07/22/U7J8TU.png) **n-step Sarsa** 上面的 Sarsa 算法是我们每向前走一步就更新,其实可以 类比 TD,可以向前走多步然后进行更新,就叫 `n-step Sarsa` $$ Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha(q_t^{(n)} - Q(S_t, A_t)) $$ 其中 $q_{(n)}$ 为: $$ q_t^{(n)} = R_{t+1} + \gamma R_{t+2} + \cdots + \gamma^{n-1}R_{t+n} + \gamma^nQ(S_{t+n}, A_{t+n}) $$ ### 2、On-policy 和 Off-policy 比如上面的 Sarsa 算法就是一个 On-Policy 算法。从更新公式可以看出,Sarsa 算法的探索使用 $\epsilon-$贪婪法,而更新value 的动作也是带有 $\epsilon$ 的探索。也就是说探索和更新 $V(s)$ 是用的同一种策略 $\pi$ ,我们就叫 同策略学习(On-Policy Learning)。 而另外一个重要的方法就是**Off-Policy Learning** ,也就是异策略学习。在异策略学习算法中,我们有两个不同的策略,一个策略 $\pi$ 是获取最优的 $V(s)$ (比如使用贪心法),我们称为 **target policy**。而另外一个 策略 $\mu$ 是为了生成不同的轨迹,同时拥有更多的探索性(比如 $\epsilon-$贪婪法),我们称为 **behavior policy**。 强化学习过程主要是**探索**和**利用** 的问题。如何更好的探索环境以采集轨迹,以及如何利用采集到的轨迹经验。Off Policy 其实就是把探索和优化 一分为二,优化的时候我只追求最大化,二不用像 On Policy 那样还要考虑 epsilon 探索,所以Off Policy 的优点就是可以更大程度上保证达到全局最优解。 ![U7JfXt.png](https://s1.ax1x.com/2020/07/22/U7JfXt.png) 如上图所示,就是一个 Off policy 很好的比喻。海盗更具有冒险精神,他可以把与风浪(环境)中获得的轨迹(或者经验)保留下来,然后其的小白就可以利用这些经验来学习如何更好的在海上航行。对于 On-policy,每次更新之后,策略都会发生变化,所以之前产生的 交互数据已经没有价值了,我们要废弃掉,再重新产生新的交互数据。而Off Policy 在这方面就有着优势,他可以反复的利用以前过往的数据,因为他的目标策略和行为策略是分离的。而 Q-Learning 就是一个 Off-Policy 算法。 ### 3、Q-Learning 对于Q-Learning,我们会使用 $\epsilon-$贪婪法来选择新的动作,这部分和SARSA完全相同。但是对于价值函数的更新,Q-Learning使用的是贪婪法,而不是SARSA的 $\epsilon-$贪婪法。这一点就是SARSA和Q-Learning本质的区别。 ![U7XZEd.png](https://s1.ax1x.com/2020/07/22/U7XZEd.png) 首先我们基于状态 $S_t$,用 $\epsilon-$贪婪法选择到动作 $A_t$, 然后执行动作 $A_t$,得到奖励$R_{t}$,并进入状态$S_{t+1}$,对于 Q-Learning,它基于状态 $S_{t+1}$ 没有用 $\epsilon-$贪婪法选择到动作 $A'$, 而是使用贪心策略选择 $A'$ 。也就是**选择使$Q(S_{t+1}, a)$ 最大的 $a$ 来作为 $A'$ 来更新价值函数**。对应到上图中就是在图下方的三个黑圆圈动作中选择一个使$Q(S', a)$ 最大的动作作为 $A'$ 。**注意: 此时选择的动作只会参与价值函数的更新,不会真正的执行。** - 对于 behavior policy $\mu$ 使用 $\epsilon-$ 贪心策略 - 对于 target policy $\pi$ 使用贪心策略选取 $$ \pi(S_{t+1}) = argmax_{a'} \; Q(S_{t+1}, a') $$ 因此 Q-Learning Target : $$ R_{t+1} + \gamma Q(S_{t+1}, A') = R_{t+1} + \gamma Q(S_{t+1}, argmax \; Q(S_{t+1}, a')) \\ = R_{t+1} + \gamma \; max_{a'} Q(S_{t+1}, a') $$ Q-Learning 的更新式子如下: $$ Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha[R_{t+1} + \gamma \; max_aQ(S_{t+1}, a) - Q(S_t, A_t)] $$ **Q-Learning 算法** ![U7Xm4I.png](https://s1.ax1x.com/2020/07/22/U7Xm4I.png) ### 4、Sarsa VS Q-learning 作为时序差分法控制算法的两个经典的算法,他们各自有特点。 Q-Learning直接学习的是最优策略,而Sarsa在学习最优策略的同时还在做探索。这导致我们在学习最优策略的时候,如果用Sarsa,为了保证收敛,需要制定一个策略,使 $\epsilon-$贪婪法的超参数 $\epsilon$ 在迭代的过程中逐渐变小。Q-Learning没有这个烦恼。 另外一个就是Q-Learning直接学习最优策略,但是最优策略会依赖于训练中产生的一系列数据,所以受样本数据的影响较大,因此受到训练数据方差的影响很大,甚至会影响Q函数的收敛。Q-Learning的深度强化学习版Deep Q-Learning也有这个问题。 在学习过程中,Sarsa在收敛的过程中鼓励探索,这样学习过程会比较平滑,不至于过于激进,而 Q-Learning 是直接选择最优的动作,相比于 Sarsa 算法更激进。 比如在 Cliff Walk 问题中,如下所示,Sarsa 算法会走蓝色的安全线,而激进的 Q-Learning 算法则会选择红色的最优路线。(其中灰色部分是悬崖) ![UH94fJ.png](https://s1.ax1x.com/2020/07/22/UH94fJ.png) 关于 Sarsa 和 Q-Learning 算法的代码详解可以看下一篇文章[强化学习——Sarsa 和 Q-Learning 算法实战](https://blog.csdn.net/november_chopin/article/details/107898611) ## 三、DP、MC、TD的对比 ### 1 有无 Bootstrapping 和 Sampling | | Bootstrapping | Sampling | | ---- | :-----------: | :------: | | DP | Yes | | | MC | | Yes | | TD | Yes | Yes | ### 2、DP 和 TD ![UHAUwq.png](https://s1.ax1x.com/2020/07/22/UHAUwq.png) ### 3 对于三种算法的直观理解 - DP UTe4Ds.png - MC UTehuj.png - TD UTeRgg.png ### 3 三种算法之间的关系 ![UTeoEq.png](https://s1.ax1x.com/2020/07/21/UTeoEq.png) > 上图是强化学习方法空间的一个横切面,突出两个重要维度:更新的深度和宽度 > > 这两个维度是与改善价值函数的更新操作的类型联系在一起的。水平方向代表的是:使用采样更新(基于一个采样序列)还是期望更新(基于可能序列的分布)。期望更新需要一个概率分布模型,采样更新只需要一个采样模型即可。 > > 垂直方向对应的是更新的深度,也是“自举(bootstrap)”的程度。从单步时序差分更新到蒙特卡洛更新。中间部分包括了基于 n 步更新的方法(例如基于资格迹的 $\lambda$ 更新) > > 可以看到,动态规划在图示空间的右上方,因为动态规划属于单步期望更新。右下角是期望更新的一个极端情况,每次更新都会遍历虽有可能情况直到到达最后的终止状态(或者在持续任务中,折扣因子的类乘导致之后的收益对当前回报不再有影响为止)。这是启发式搜索中的一中情况。 > > —— 《强化学习》第二版 Sutton 至此,传统的强化学习基本理论就介绍完了,下一篇是SARSA 和Q-Learning算法的代码介绍。当然本系列博客比较基本,若想更深入了解可以读 Sutton 的《强化学习》这本手,这本书[英文电子版云盘地址](https://pan.baidu.com/s/1Ob3ecSjyEYvHhsa6XjEE8A) 提取码: hn77。 最近这几年,随着神经网络的兴起,基于神经网络的强化学习发展的如火如荼,出现了许多新的有意思的算法,当然也更加的强大。所以接下来的文章都是介绍 神经网络 + 强化学习,也就是深度强化学习(DRL)。我这枚菜鸟也会尽自己最大所能,给大家讲解清楚,还请大家不要走开,多多关注! ================================================ FILE: Doc/5-RL-SARSA-QLearning.md ================================================ # 强化学习 5 —— SARSA and Q-Learning 上篇文章 [强化学习——时序差分 (TD) --- SARSA and Q-Learning](https://blog.csdn.net/november_chopin/article/details/107897225) 我们介绍了时序差分TD算法解决强化学习的评估和控制问题,TD对比MC有很多优势,比如TD有更低方差,可以学习不完整的序列。所以我们可以在策略控制循环中使用TD来代替MC。优于TD算法的诸多优点,因此现在主流的强化学习求解方法都是基于TD的。这篇文章会使用就用代码实现 SARSA 和 Q-Learning 这两种算法。 ## 一、算法介绍 关于SARSA 和 Q-Learning算法的详细介绍,本篇博客不做过多介绍,若不熟悉可点击文章开头链接查看。 Sarsa 和 QLearning 时序差分TD解决强化学习控制问题的两种算法,两者非常相似,从更新公式就能看出来: - SARSA: $$ A(S_t, A_t) \leftarrow A(S_t, A_t) + \alpha \left[R_{t+1} + \gamma Q(S_{t+1}, A_{t+1}) - A(S_t, A_t)\right] $$ - Q-Learning $$ Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha[R_{t+1} + \gamma \; max_aQ(S_{t+1}, a) - Q(S_t, A_t)] $$ 可以看出来,两者的区别就在计算 TD-Target 的时候,下一个动作 a' 是如何选取的 对于 Sarsa 来说: - 1)在状态 s' 时,就知道了要采取那个动作 a',并且真的采取了这个动作 - 2)当前动作 a 和下一个动作 a' 都是 根据 $\epsilon$ -贪婪策略选取的,因此称为on-policy学习 对于 Q-Learning: - 1)在状态s'时,只是计算了 在 s' 时要采取哪个 a' 可以得到更大的 Q 值,并没有真的采取这个动作 a'。 - 2)动作 a 的选取是根据当前 Q 网络以及 $\epsilon$-贪婪策略,即每一步都会根据当前的状况选择一个动作A,目标Q值的计算是根据 Q 值最大的动作 a' 计算得来,因此为 off-policy 学习。 ![U7XZEd.png](https://s1.ax1x.com/2020/07/22/U7XZEd.png) ## 二、代码 ### 1、SARSA 定义 SARSA agent 类, ```python class Sarsa: def __init__(self, state_dim, action_dim, lr=0.01, gamma=0.9, e_greed=0.1): self.action_dim = action_dim self.lr = lr self.gamma = gamma self.epsilon = e_greed self.Q = np.zeros((state_dim, action_dim)) def sample(self, state): """ 使用 epsilon 贪婪策略获取动作 return: action """ if np.random.uniform() < self.epsilon: action = np.random.choice(self.action_dim) else: action = self.predict(state) return action def predict(self, state): """ 根据输入观察值,预测输出的动作值 """ all_actions = self.Q[state, :] max_action = np.max(all_actions) # 防止最大的 Q 值有多个,找出所有最大的 Q,然后再随机选择 # where函数返回一个 array, 每个元素为下标 max_action_list = np.where(all_actions == max_action)[0] action = np.random.choice(max_action_list) return action def learn(self, state, action, reward, next_state, next_action, done): """ 更新 Q-table 方法 next_action 就是下一步选的动作,所以直接用 self.Q[next_state, next_action] 然后计算 td-target,然后更新 Q-table """ if done: target_q = reward else: target_q = reward + self.gamma * self.Q[next_state, next_action] self.Q[state, action] += self.lr * (target_q - self.Q[state, action]) ``` 上面代码重点是 `learn()` 方法中的 `Q-table` 的更新,结合公式还是比较容易理解的。下面是每一个 episode 的流程:对于一个 episode 先调用 `reset()` 方法获得初始化状态`state`,然后选择当前的动作 `action` ,使用当前的动作让环境执行一步,获取到下一个状态 `next_state` 以及奖励 `reward` ,然后利用这些数据进行更新Q表格,注意 更新之后要把下一个状态和动作赋值给当前的状态和动作,然后循环。 ```python def run_episode(self, render=False): state = self.env.reset() action = self.model.sample(state) while True: next_state, reward, done, _ = self.env.step(action) next_action = self.model.sample(next_state) # 训练 Q-learning算法 self.model.learn(state, action, reward, next_state, next_action, done) state = next_state action = next_action if render: self.env.render() if done: break ``` 完整代码见[强化学习——SARSA 算法](https://github.com/NovemberChopin/RL_Tutorial/blob/master/code/Sarsa.py) ,劳烦大人点个 `star` 可好? ### 2、Q-Learning 由上可知,Q-Learning 和 SARSA 算法很相似,代码几乎相同,下面就展示下与 SARSA 算法不同的部分 ```python class QLearning: # ... # 其他方法见 SARSA 部分 def learn(self, state, action, reward, next_state, done): """ Q-Learning 更新 Q-table 方法 这里没有明确选择下一个动作 next_action, 而是选择 next_state 下有最大价值的动作 所以用 np.max(self.Q[next_state, :]) 来计算 td-target 然后更新 Q-table """ if done: target_q = reward else: target_q = reward + self.gamma * np.max(self.Q[next_state, :]) self.Q[state, action] += self.lr * (target_q - self.Q[state, action]) ``` 对于 Q-Learning 的算法流程部分 ,和 SARSA 也有些细微区别:在Q-Learning 中的 `learn()` 方法不需要传入 next_action 参数,因为在计算td-target 时只是查看了一下下一个状态的所有动作价值,并选择一个最优动作让环境去执行。还请仔细区分两者的不同: ```python def run_episode(self, render=False): state = self.env.reset() while True: action = self.model.sample(state) next_state, reward, done, _ = self.env.step(action) # 训练 Q-learning算法 self.model.learn(state, action, reward, next_state, done) state = next_state if render: self.env.render() if done: break ``` 完整代码见[强化学习——Q-Learning 算法](https://github.com/NovemberChopin/RL_Tutorial/blob/master/code/Q-Learning.py),劳烦大人点个 `star` 可好? ================================================ FILE: Doc/6-RL-VFA.md ================================================ # 强化学习 6 ——价值函数逼近 上篇文章[强化学习——时序差分 (TD) 控制算法 Sarsa 和 Q-Learning](https://blog.csdn.net/november_chopin/article/details/107897225)我们主要介绍了 Sarsa 和 Q-Learning 两种时序差分控制算法,在这两种算法内部都要维护一张 Q 表格,对于小型的强化学习问题是非常灵活高效的。但是在状态和可选动作非常多的问题中,这张Q表格就变得异常巨大,甚至超出内存,而且查找效率极其低下,从而限制了时序差分的应用场景。近些年来,随着神经网络的兴起,基于深度学习的强化学习称为了主流,也就是深度强化学习(DRL)。 ## 一、函数逼近介绍 我们知道限制 Sarsa 和 Q-Learning 的应用场景原因是需要维护一张巨大的 Q 表格,那么我们能不能用其他的方式来代替 Q表格呢?很自然的,就想到了函数。 $$ \hat{v}(s, w) \approx v_\pi(s) \\ \hat{q}(s,a, w) \approx q_\pi(s, a) \\ \hat{\pi}{a,s,w} \approx \pi(a|s) $$ 也就是说我们可以用一个函数来代替 Q 表格,不断更新 $q(s,a)$ 的过程就可以转化为用参数来拟合逼近真实 q 值的过程。这样学习的过程不是更新 Q 表格,而是更新 参数 w 的过程。 UHzRUg.png 下面是几种不同的拟合方式: 第一种函数接受当前的 状态 S 作为输入,输出拟合后的价值函数 第二种函数同时接受 状态 S 和 动作 a 作为输入,输出拟合后的动作价值函数 第三种函数接受状态 S,输出每个动作对应的动作价值函数 q 常见逼近函数有线性特征组合方式、神经网络、决策树、最近邻等,在这里我们只讨论可微分的拟合函数:线性特征组合和神经网络两种方式。 ### 1、知道真实 V 的函数逼近 对于给定的一个状态 S 我们假定我们知道真实的 $v_\pi(s)$ ,然后我们经过拟合得到 $\hat{v}(s, w)$ ,于是我们就可以使用均方差来计算损失 $$ J(w) = E_\pi[(v_\pi(s) - \hat{v}(s, w))^2] $$ 利用梯度下降去找到局部最小值: $$ \Delta w = -\frac{1}{2}\alpha \nabla_wJ(w) \\ w_{t+1} = w_t + \Delta w $$ 我们可以提取一些特征向量来表示当前的 状态 S,比如对于 gym 的 CartPole 环境,我们可提取的特征有推车的位置、推车的速度、木杆的角度、木杆的角速度等 UHz2VS.png $$ x(s) = (x_1(s), x_2(s), \cdots,x_n(s))^T $$ ###### 此时价值函数 就可以用线性特征组合表示: $$ \hat{v}(s,w) = x(s)^Tw=\sum_{j=1}^nx_j(s)\cdot w_j $$ 此时的损失函数为: $$ J(w) = E_\pi[(v_\pi(s) - x(s)^T w)^2] $$ 因此更新规则为: $$ \Delta w = \alpha(v_\pi(s)-\hat{v}(s,w))\cdot x(s) \\ Update = StepSize\;*\;PredictionError\;*\;FeatureValue $$ ## 二、预测过程中的价值函数逼近 因为我们函数逼近的就是 真实的状态价值,所以在实际的强化学习问题中是没有 $v_\pi(s)$ 的,只有奖励。所以在函数逼近过程的监督数据为: $$ , , \cdots , $$ 所以对于蒙特卡洛我们有: $$ \Delta w = \alpha({\color{red}G_t} - \hat{v}(s_t, w))\nabla_w\hat{v}(s_t, w) \\ = \alpha({\color{red}G_t} - \hat{v}(s_t, w)) \cdot x(s_t) $$ 其中奖励 $G_t$ 是无偏(unbiased)的:$E[G_t] = v_\pi(s_t)$ 。值得一提的是,蒙特卡洛预测过程的函数逼近在线性或者是非线性都能收敛。 对于TD算法,我们使用 $\hat{v}(s_t, w)$ 来代替 TD Target。所以我们在价值函数逼近(VFA)使用的训练数据如下所示: $$ ,,\cdots, $$ 于是对于 TD(0) 在预测过程的函数逼近有: $$ \Delta w = \alpha({\color{red}R_{t+1} + \gamma \hat{v}(s_{t+1}, w)}-\hat{v}(s_t, w))\nabla_w\hat{v}(s_t, w) \\ = \alpha({\color{red}R_{t+1} + \gamma \hat{v}(s_{t+1}, w)}-\hat{v}(s_t, w))\cdot x(s) $$ 因为TD中的 Target 中包含了预测的 $\hat{v}(s,t)$ ,所以它对于真实的 $v_\pi(s_t)$ 是有偏(biased)的,因为我们的监督数据是我们估计出来的,而不是真实的数据。也就是 $E[R_{t+1} + \gamma \hat{v}(s_{t+1}, w)] \neq v_\pi(s_t)$ 。我们把这个过程叫做 semi-gradient,不是完全的梯度下降,而是忽略了权重向量 w 对 Target 的影响。 ## 三、控制过程中的价值函数逼近 类比于MC 和 TD 在使用 Q 表格时的更新公式,对于策略控制过程我们可以得到如下公式。和上面预测过程一样,我们没有真实的 $q_\pi(s,a)$ ,所以我们对其进行了替代: - 对于 MC,Target 是 $G_t$ : $$ \Delta w = \alpha({\color{red}G_t} - \hat{q}(s_t, a_t, w))\nabla_w\hat{v}(s_t, a_t, w) $$ - 对于 Sarsa,TD Target 是 $R_{t+1} + \gamma \hat{q}(s_{t+1}, a_{t+1}, w)$ : $$ \Delta w = \alpha ({\color{red}R_{t+1} + \gamma \hat{q}(s_{t+1}, a_{t+1}, w)} - \hat{q}{(s_t, s_t, w)})\cdot \nabla_w\hat{q}{(s_t, a_t, w)} $$ - 对于 Q-Learning,TD Target 是 $R_{t+1} + \gamma\;max_a\; \hat{q}(s_{t+1}, a_t, w)$ : $$ \Delta w = \alpha ({\color{red}R_{t+1} + \gamma\;max_a\; \hat{q}(s_{t+1}, a_t, w)} - \hat{q}{(s_t, s_t, w)})\cdot \nabla_w\hat{q}{(s_t, a_t, w)} $$ ## 四、关于收敛的问题 ![UbwGbd.png](https://s1.ax1x.com/2020/07/22/UbwGbd.png) 在上图中,对于使用 Q 表格的问题,不管是MC还是 Sarsa 和 Q-Learning 都能找到最优状态价值。如果是一个大规模的环境,我们采用线性特征拟合,其中MC 和 Sarsa 是可以找到一个近似最优解的。当使用非线性拟合(如神经网络),这三种算法都很难保证能找到一个最优解。 其实对于off-policy 的TD Learning强化学习过程收敛是很困难的,主要有以下原因: - 使用函数估计:对于 Sarsa 和 Q-Learning 中价值函数的的近似,其监督数据 Target 是不等于真实值的,因为TD Target 中包含了需要优化的 参数 w,也叫作 半梯度TD,其中会存在误差。 - Bootstrapping:在更新式子中,上面红色字体过程中有 贝尔曼近似过程,也就是使用之前的估计来估计当前的函数,这个过程中也引入了不确定因素。(在这个过程中MC回比TD好一点,因为MC中代替 Target 的 $G_t$ 是无偏的)。 - Off-policy 训练:对于 off-policy 策略控制过程中,我们使用 behavior policy 来采集数据,在优化的时候使用另外的 target policy 策略来优化,两种不同的策略会导致价值函数的估计变的很不准确。 上面三个因素就导致了强化学习训练的死亡三角,也是强化学习相对于监督学习训练更加困难的原因。 下一篇就来介绍本系列的第一个深度强化学习算法 Deep Q-Learning(DQN) 参考资料: - B站周老师的 [强化学习纲要第四节上](https://www.bilibili.com/video/BV1w54y1d7se) ================================================ FILE: Doc/7-RL-DQN.md ================================================ # 强化学习 7——Deep Q-Learning(DQN) 上篇文章[强化学习——状态价值函数逼近](https://blog.csdn.net/november_chopin/article/details/107911868)介绍了价值函数逼近(Value Function Approximation,VFA)的理论,本篇文章介绍大名鼎鼎的DQN算法。DQN算法是 DeepMind 团队在2015年提出的算法,对于强化学习训练苦难问题,其开创性的提出了两个解决办法,在atari游戏上都有不俗的表现。论文发表在了 Nature 上,此后的一些DQN相关算法都是在其基础上改进,可以说是打开了深度强化学习的大门,意义重大。 论文地址:[Mnih, Volodymyr; et al. (2015). **Human-level control through deep reinforcement learning**](https://www.nature.com/articles/nature14236/) ## 一、DQN简介 其实DQN就是 Q-Learning 算法 + 神经网络。我们知道,Q-Learning 算法需要维护一张 Q 表格,按照下面公式来更新: $$ Q(S_t, A_t) \leftarrow Q(S_t, A_t) + \alpha[R_{t+1} + \gamma \; max_aQ(S_{t+1}, a) - Q(S_t, A_t)] $$ 然后学习的过程就是更新 这张 Q表格,如下图所示: UbsT4s.png 而DQN就是用神经网络来代替这张 Q 表格,其余相同,如下图: UbsoNj.png 其更新方式为: $$ Q(S_t, A_t, w) \leftarrow Q(S_t, A_t, w) + \alpha[R_{t+1} + \gamma\;max_a\; \hat{q}(s_{t+1}, a_t, w) - Q(S_t, A_t, w)] $$ 其中 $\Delta w$ : $$ \Delta w = \alpha ({\color{red}R_{t+1} + \gamma\;max_a\; \hat{q}(s_{t+1}, a_t, w)} - \hat{q}{(s_t, s_t, w)})\cdot \nabla_w\hat{q}{(s_t, a_t, w)} $$ ## 二、Experience replay DQN 第一个特色是使用 Experience replay ,也就是经验回放,为何要用经验回放?还请看下文慢慢详述 对于网络输入,DQN 算法是把整个游戏的像素作为 神经网络的输入,具体网络结构如下图所示: ![UbcZhd.png](https://s1.ax1x.com/2020/07/22/UbcZhd.png) 第一个问题就是样本相关度的问题,因为在强化学习过程中搜集的数据就是一个时序的玩游戏序列,游戏在像素级别其关联度是非常高的,可能只是在某一处特别小的区域像素有变化,其余像素都没有变化,所以不同时序之间的样本的关联度是非常高的,这样就会使得网络学习比较困难。 DQN的解决办法就是 经验回放(Experience replay)。具体来说就是用一块内存空间 $D$ ,用来存储每次探索获得数据 $$ 。然后按照以下步骤重复进行: - sample:从 $D$ 中取出一个 batch 的数据 $(s, a, r, s') \in D$ - 对于取出的样本数据计算 Target 值:$r + \gamma\; max_{a'}\hat{Q}(s',a',w)$ - 使用随机梯度下降来更新网络权重 w: $$ \Delta w = \alpha (r + \gamma\;max_{a'}\; \hat{Q}(s', a', w) - \hat{Q}{(s, s, w)})\cdot \nabla_w\hat{Q}{(s, a, w)} $$ 利用经验回放,可以充分发挥 off-policy 的优势,behavior policy 用来搜集经验数据,而 target policy 只专注于价值最大化。 ## 三、Fixed Q targets 第二个问题是前文已经说过的,我们使用 $\hat{q}(s_t, a_t, w)$ 来代替 TD Target,也就是说在TD Target 中已经包含我了我们要优化的 参数 w。也就是说在训练的时候 监督数据 target 是不固定的,所以就使得训练比较困难。 想象一下,我们把 我们要估计的 $\hat{Q}$ 叫做 Q estimation,把它看做一只猫。把我们的监督数据 Q Target 看做是一只老鼠,现在可以把训练的过程看做猫捉老鼠的过程(不断减少之间的距离,类比于我们的 Q estimation 网络拟合 Q Target 的过程)。现在问题是猫和老鼠都在移动,这样猫想要捉住老鼠是比较困难的,如下所示: UqG8cd.png ![UqG31H.png](https://s1.ax1x.com/2020/07/23/UqG31H.png) 那么我们让老鼠在一段时间间隔内不动(固定住),而这期间,猫是可以动的,这样就比较容易抓住老鼠了。在 DQN 中也是这样解决的,我们有两套一样的网络,分别是 Q estimation 网络和 Q Target 网络。要做的就是固定住 Q target 网络,那如何固定呢?比如我们可以让 Q estimation 网路训练10次,然后把 Q estimation 网络更新后的参数 w 赋给 Q target 网络。然后我们再让Q estimation 网路训练10次,如此往复下去,试想如果不固定 Q Target 网络,两个网络都在不停地变化,这样 拟合是很困难的,如果我们让 Q Target 网络参数一段时间固定不变,那么拟合过程就会容易很多。下面是 DQN 算法流程图: [![UqUf1S.png](https://s1.ax1x.com/2020/07/23/UqUf1S.png)](https://imgchr.com/i/UqUf1S) 如上图所示,首先智能体不断与环境交互,获取交互数据$$存入`replay memory` ,当经验池中有足够多的数据后,从经验池中 随机取出一个 `batch_size` 大小的数据,利用当前网络计算 Q的预测值,使用 Q-Target 网络计算出 Q目标值,然后计算两者之间的损失函数,利用梯度下降来更新当前 网络参数,重复若干次后,把当前网络的参数 复制给 Q-Target 网络。 关于DQN的实现代码部分我们下篇介绍 参考资料: - B站 周老师的 [强化学习纲要第四课下](https://www.bilibili.com/video/BV1w54y1d7se) ================================================ FILE: Doc/8-RL-DQN Code.md ================================================ # 强化学习 8 —— DQN 代码 Tensorflow 实现 在上一篇文章[强化学习——DQN介绍](https://blog.csdn.net/november_chopin/article/details/107912720) 中我们详细介绍了DQN 的来源,以及对于强化学习难以收敛的问题DQN算法提出的两个处理方法:经验回放和固定目标值。这篇文章我们就用代码来实现 DQN 算法 ## 一、环境介绍 ### 1、Gym 介绍 本算法以及以后文章要介绍的算法都会使用 由 $OpenAI$ 推出的[$Gym$](http://gym.openai.com/)仿真环境, $Gym$ 是一个研究和开发强化学习相关算法的仿真平台,了许多问题和环境(或游戏)的接口,而用户无需过多了解游戏的内部实现,通过简单地调用就可以用来测试和仿真,并兼容常见的数值运算库如 $TensorFlow$ 。 ```python import gym env = gym.make('CartPole-v1') env.reset() for _ in range(1000): env.render() env.step(env.action_space.sample()) # take a random action env.close() ``` 运行结果如下: aMXZ7Q.gif 以上代码中可以看出,`gym`的核心接口是`Env`。作为统一的环境接口,`Env`包含下面几个核心方法: - `reset(self)`:重置环境的状态,返回观察。如果回合结束,就要调用此函数,重置环境信息 - `step(self, action)`:执行动作 `action` 推进一个时间步长,返回`observation`,` reward`, `done`, `info`。 - `observation`表示环境观测,也就是`state` - `reward` 表示获得的奖励 - `done`表示当前回个是否结束 - `info` 返回一些诊断信息,一般不经常用 - `render(self, mode=‘human’, close=False)`:重新绘制环境的一帧。 - `close(self)`:关闭环境,并清除内存。 以上代码首先导入`gym`库,第2行创建`CartPole-v01`环境,并在第3行重置环境状态。在 for 循环中进行*1000*个时间步长(*timestep)的控制,第5行刷新每个时间步长环境画面,第6行对当前环境状态采取一个随机动作(0或1),最后第7行循环结束后关闭仿真环境。 ### 2、CartPole-v1 环境介绍 CartPole 是gym提供的一个基础的环境,即车杆游戏,游戏里面有一个小车,上有竖着一根杆子,每次重置后的初始状态会有所不同。小车需要左右移动来保持杆子竖直,为了保证游戏继续进行需要满足以下两个条件: - 杆子倾斜的角度 $\theta$ 不能大于15° - 小车移动的位置 x 需保持在一定范围(中间到两边各2.4个单位长度) 对于 `CartPole-v1` 环境,其动作是两个离散的动作左移(0)和右移(1),环境包括小车位置、小车速度、杆子夹角及角变化率四个变量。如下代码所示: ```python import gym env = gym.make('CartPole-v0') print(env.action_space) # Discrete(2) observation = env.reset() print(observation) # [-0.0390601 -0.04725411 0.0466889 0.02129675] ``` 下面以`CartPole-v1` 环境为例,来介绍 DQN 的实现 ## 二、代码实现 ### 1、经验回放池的实现 ```python class ReplayBuffer: def __init__(self, capacity=10000): 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 = int((self.position + 1) % self.capacity) def sample(self, batch_size = args.batch_size): batch = random.sample(self.buffer, batch_size) state, action, reward, next_state, done = map(np.stack, zip(*batch)) return state, action, reward, next_state, done ``` 首先定义一个经验回放池,其容量为 10000,函数 `push` 就是把智能体与环境交互的到的信息添加到经验池中,这里使用的循环队列的实现方式,注意 `position` 指针的运算。当需要用数据来更新算法 时,使用 `sample` 从经验队列中随机挑选 一个 `batch_size` 的数据,使用 zip 函数把每一条数据打包到一起: ```python zip: a=[1,2], b=[2,3], zip(a,b) => [(1, 2), (2, 3)] ``` 然后对每一列数据使用 stack 函数转化为列表后返回 ### 2、网络构造 本系列强化学习的代码,都是使用的 `tensorlayer` ,就是对 `tensorflow` 做了一些封装,使其更加易用,重点是还**专门为强化学习**内置了一些接口,下面是[官网](https://tensorlayercn.readthedocs.io/zh/latest/)介绍: > TensorLayer 是为研究人员和工程师设计的一款基于Google TensorFlow开发的深度学习与强化学习库。 它提供高级别的(Higher-Level)深度学习API,这样不仅可以加快研究人员的实验速度,也能够减少工程师在实际开发当中的重复工作。 TensorLayer非常易于修改和扩展,这使它可以同时用于机器学习的研究与应用。 定义网络模型: ```python def create_model(input_state_shape): input_layer = tl.layers.Input(input_state_shape) layer_1 = tl.layers.Dense(n_units=32, act=tf.nn.relu)(input_layer) layer_2 = tl.layers.Dense(n_units=16, act=tf.nn.relu)(layer_1) output_layer = tl.layers.Dense(n_units=self.action_dim)(layer_2) return tl.models.Model(inputs=input_layer, outputs=output_layer) self.model = create_model([None, self.state_dim]) self.target_model = create_model([None, self.state_dim]) self.model.train() self.target_model.eval() ``` 可以看到`tensorlayer` 使用起来与`tensorflow` 大同小异,只要有`tensorflow`基础一眼就能明白,在上面代码中我们定义一个函数用来生成网络模型。然后创建一个当前网络`model`和一个目标网络`target_model` ,我们知道DQN中的目标网络是起到一个“靶子”的作用,用来评估当前的 target 值,所以我们把它设置为评估模式,调用 `eval()` 函数即可。而 `model` 网络是我们要训练的网络,调用函数 `train()` 设置为训练模式。 ### 3、算法控制流程 ```python for episode in range(train_episodes): total_reward, done = 0, False while not done: action = self.choose_action(state) next_state, reward, done, _ = self.env.step(action) self.buffer.push(state, action, reward, next_state, done) total_reward += reward state = next_state # self.render() if len(self.buffer.buffer) > args.batch_size: self.replay() self.target_update() ``` 关于与环境交互过程在上面已经介绍过了,这里重点看 第 10 行的 if 语句,当经验池的长度大于一个`batch_size` 时,就开始调用`replay()` 函数来更新网络 `model` 的网络参数,然后调用`target_update()` 函数把 `model` 网络参数复制给 `target_model` 网络。 ### 4、网络参数更新 ```python def replay(self): for _ in range(10): states, actions, rewards, next_states, done = self.buffer.sample() # compute the target value for the sample tuple # target [batch_size, action_dim] # target represents the current fitting level target = self.target_model(states).numpy() next_q_value = tf.reduce_max(self.target_model(next_states), axis=1) target_q = rewards + (1 - done) * args.gamma * next_q_value target[range(args.batch_size), actions] = target_q with tf.GradientTape() as tape: q_pred = self.model(states) loss = tf.losses.mean_squared_error(target, q_pred) grads = tape.gradient(loss, self.model.trainable_weights) self.model_optim.apply_gradients(zip(grads, self.model.trainable_weights)) ``` 这部分应该就是 DQN 的核心代码了,在`replay()` 函数中,我们循环更新更新当前网络十次,目的就是改变两个网络的更新频率,有利于网络收敛。 具体的更新部分:我们知道,DQN就是把Q-Learning中的Q表格换成了神经网络,两者之间有很多 相似之处,我们可以类比Q-Learning 的更新方式。对于Q表格形式,我们获取某一个状态的动作价值Q是直接通过下标得到的,那么在神经网络中就需要把状态输入神经网络,经过前向计算得到。 $$ \Delta w = \alpha (r + \gamma\;max_{a'}\; \hat{Q}(s', a', w) - \hat{Q}{(s, a, w)})\cdot \nabla_w\hat{Q}{(s, a, w)} $$ 第三行首先获取一个`batch_size`的数据,这个过程称为 `sample` 。第7行我们首先获取当前的动作价值,target 表示的是根据当前的网络参数计算得到的动作价值。然后第8行先获取当前网络参数下 的下一个状态的所有动作,然后使用`reduce_max()` 函数找出最大的动作价值。然后第9行和第10行利用下一个状态最大的动作价值来计算出 `target_q` ,也就是 $r + \gamma\;max_{a'}\; \hat{Q}(s', a', w)$ 部分,然后更新`target` 。注意上面我们计算target时一直在使用 `target_model` 网络,target网络只有在评估网络状态时才使用。 接着我们使用 `q_pred = self.model(states)` 网络获取当前 网络的状态,也就是 公式中的 $\hat{Q}{(s, a, w)}$ ,利用MSE函数计算其损失函数,最后更新 `model` 网络。 完整代码请参考[强化学习——DQN代码地址](https://github.com/NovemberChopin/RL_Tutorial/blob/master/code/DQN.py) 还请给个 `star` ,谢谢各位了 ## 三、DQN 小结 虽然 DQN 提出的这两个解决方案不错,但是仍然还有问题没有解决,比如: - 目标 Q 值(Q Target )计算是否准确?全部通过 $max\;Q$ 来计算有没有问题? - Q 值代表动作价值,那么单纯的动作价值评估会不会不准确? 对应第一个问题的改进就是 Double DQN ,第二个问题的改进是 Dueling DQN。他们都属与DQN的改进版,我们下篇文章介绍。 ================================================ FILE: Doc/9-RL-DQN-Improvement.md ================================================ # 强化学习 9 —— DQN 改进算法 上篇文章[强化学习——详解 DQN 算法](https://blog.csdn.net/november_chopin/article/details/107913103)我们介绍了 DQN 算法,但是 DQN 还存在一些问题,本篇文章介绍针对 DQN 的问题的改进算法 ## 一、Double DQN 算法 ### 1、算法介绍 DQN的问题有:目标 Q 值(Q Target )计算是否准确?全部通过 $max\;Q$ 来计算有没有问题?很显然,是有问题的,这是因为Q-Learning 本身固有的缺陷---过估计 过估计是指估计得值函数比真实值函数要大,其根源主要在于Q-Learning中的最大化操作,对于 TD Target: $$ r + \gamma\;max_{a'}\; \hat{Q}(s', a', w) $$ 其中的 $max$ 操作使得估计的值函数比值函数的真实值大,因为DQN是一种off-policy的方法,每次学习时,不是使用下一次交互的真实动作,而是使用当前认为价值最大的动作来更新目标值函数,(**注:对于真实的策略来说并在给定的状态下并不是每次都选择使得Q值最大的动作,所以在这里目标值直接选择动作最大的Q值往往会导致目标值要高于真实值**)。Double DQN 的改进方法是将动作的**选择**和动作的**评估**分别用不同的值函数来实现,而在Nature DQN中正好我们提出了两个Q网络。所以计算 `TD Target` 的步骤可以分为下面两步: - 1)通过当前Q估计网络(Q Estimation 网络)获得最大值函数的动作 $a$: $$ a_{max}(s',w) = arg\;max_{a'}Q_{estim}(s', a, w) $$ - 2)然后利用这个选择出来的动作 $a_{max}(s',w)$ 在目标网络 (Q Target) 里面去计算目 Target Q值: $$ r + \gamma\;max_{a'}\; Q_{target}(s', a_{max}(s',w), w) $$ 综合起来 在Double DQN 中的 TD Target 计算为: $$ r + \gamma\;max_{a'}\; Q_{target}(s',arg\;max_{a'}Q_{estim}(s', a, w), w) $$ 除了计算 Target Q 值以外,DDQN 和 DQN 其余流程完全相同。 ### 2、代码展示 由上面可知,Double DQN 和 DQN 唯一不同的地方在于Q值的估计,其余流程一样。这里附上代码: ```python target = self.target_model(states).numpy() # next_q_values [batch_size, action_diim] next_target = self.target_model(next_states).numpy() # next_q_value [batch_size, 1] next_q_value = next_target[ range(args.batch_size), np.argmax(self.model(next_states), axis=1) ] # next_q_value = tf.reduce_max(next_q_value, axis=1) target[range(args.batch_size), actions] = rewards + (1 - done) * args.gamma * next_q_value ``` 完整代码[强化学习——Double DQN 代码地址](https://github.com/NovemberChopin/RL_Tutorial/blob/master/code/DDQN.py) ,劳烦点个 `star` 可好?在此谢谢了 ## 二、Dueling DQN 算法 ### 1、算法简介 在DQN算法中,神经网络输出的 Q 值代表动作价值,那么单纯的动作价值评估会不会不准确?我们知道,$Q(s, a)$ 的值既和 State 有关,又和 action 有关,但是这两种 “有关” 的程度不一样,或者说影响力不一样,而我们希望能反映出两个方面的差异。 Dueling-DQN 算法从网络结构上改进了DQN,神经网络输出的动作价值函数可以分为状态价值函数和**优势函数**,即: $$ Q_\pi(s,a) = V_\pi(s) + A_\pi(s,a) $$ 然后这两个函数利用神经网络来逼近。 先来回顾一下,在前面 MDP 那节介绍过了状态价值函数 $V(s)$ 的定义: $$ v_\pi(s) = \sum_{a\in A} \pi(a|s)\cdot q_\pi(a, s) $$ 状态价值函数就等于在该状态下**所有可能动作所对应的动作值乘以采取该动作的概率的和**。更通俗的讲,值函数 $V(s)$ 是该状态下所有动作值函数关于动作概率的**平均值**;而动作价值函数 $q(s,a)$ 表示在状态 s 下选取 动作 a 所能获得的价值。 那么什么是 优势函数?优势函数 $A_\pi(s,a) = Q_\pi(s,a) - V_\pi(s)$ 。意思是当前动作价值相对于平均价值的大小。所以,这里的优势指的是动作价值相比于当前状态的值的优势。如果优势大于零,则说明该动作比平均动作好,如果优势小于零,则说明当前动作还不如平均动作好。这样那些比平均动作好的动作将会有更大的输出,从而加速网络收敛过程。 ### 2、代码展示 同样的,Dueling DQN 与DQN 的不同之处在与网络结构,其余流程完全一样。这里不再过多解释,下面附上创建模型相关代码 : ```python def create_model(input_state_shape): input_layer = tl.layers.Input(input_state_shape) layer_1 = tl.layers.Dense(n_units=32, act=tf.nn.relu)(input_layer) layer_2 = tl.layers.Dense(n_units=16, act=tf.nn.relu)(layer_1) # state value state_value = tl.layers.Dense(n_units=1)(layer_2) # advantage value q_value = tl.layers.Dense(n_units=self.action_dim)(layer_2) mean = tl.layers.Lambda(lambda x: tf.reduce_mean(x, axis=1, keepdims=True))(q_value) advantage = tl.layers.ElementwiseLambda(lambda x, y: x-y)([q_value, mean]) # output output_layer = tl.layers.ElementwiseLambda(lambda x, y: x+y)([state_value, advantage]) return tl.models.Model(inputs=input_layer, outputs=output_layer) ``` 完整代码[强化学习——Dueling DQN 代码地址]([https://github.com/NovemberChopin/RL_Tutorial/blob/master/code/Dueling%20DQN.py](https://github.com/NovemberChopin/RL_Tutorial/blob/master/code/Dueling DQN.py)) ,劳烦点个 `star` 可好?在此谢谢了 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 jsfantasy 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. ================================================ FILE: README.md ================================================ # RL_Tutorial 这是楼主强化学习的笔记,也可以在[博客](https://blog.csdn.net/november_chopin/category_10080614.html?spm=1001.2014.3001.5482)查看。 #### 1、下载 ``` git clone git@github.com:NovemberChopin/RL_Tutorial.git ``` 推荐环境: - tensorflow: 2.2.0 - tensorlayer: 2.2.3 - tensorflow-probability: 0.6.0 #### 2、教程 `Doc`目录为强化学习教程,博客上就是这部分内容,也算是一个强化学习简明教程吧,对于传统的强化学习部分有详细的介绍,以及公式推导。后面就是常用的强化学习算法,最新的算法比如 `SAC` 和 `PPO` 暂时没有包括在内,希望后续能补充上。 #### 3、代码 `code` 目录为一些算法的实现,一个算法一个文件,比较清晰。 代码结构应该是比较容易理解的,难懂的地方都做了注释。 代码使用 `tensorlayer` 这个框架写的,就是对 `tensorflow` 中的 `layer` 进行了一些包装。使得更加方便与易用。如果能看懂 `tensorflow` ,那么 `tensorlayer` 绝对不在话下,几乎没有学习成本,所以大可不必担心。另外 `tensorlayer` 为强化学习提供了一些API,使得编写强化学习算法更加方便。这是[官方文档](https://tensorlayercn.readthedocs.io/zh/latest/index.html)介绍: >TensorLayer 是为研究人员和工程师设计的一款基于Google TensorFlow开发的深度学习与强化学习库。 它提供高级别的(Higher-Level)深度学习API,这样不仅可以加快研究人员的实验速度,也能够减少工程师在实际开发当中的重复工作。 TensorLayer非常易于修改和扩展,这使它可以同时用于机器学习的研究与应用。 此外,TensorLayer 提供了大量示例和教程来帮助初学者理解深度学习,并提供大量的官方例子程序方便开发者快速找到适合自己项目的例子。 更多细节请点击 [这里](https://github.com/tensorlayer/tensorlayer) 。 注意:代码默认都是以测试模式运行的,程序开始时会读取 `model` 文件夹下相应的模型参数。 - 如果在 `pyCharm` 中运行 `main` 函数或者如下命令行 运行是默认测试模式的。 ``` >>python ./code/DQN.py ``` 如果想要训练模式,可以把代码中参数`--train` 的默认值改为 `True`。 ```python parser.add_argument('--train', dest='train', default=False) ``` - 第二种方法是命令行运行时候传入参数`--train=True` ,如下 ``` (tf2.X) D:\Algorithm\RL_Tutorial>python ./code/DQN.py --train=True ``` 这样就是训练模式了。另外每次训练完后程序都会把参数自动保存。 #### 4、后记 由于水平有限,教程中难免会有错误,还请大家多多指出。 ================================================ FILE: code/AC_Continous.py ================================================ """ Actor-Critic ------------- It uses TD-error as the Advantage. To run ------ python tutorial_AC.py --train/test """ import argparse import time import matplotlib.pyplot as plt import os import gym import numpy as np import tensorflow as tf import tensorlayer as tl import tensorflow_probability as tfp # add arguments in command --train/test parser = argparse.ArgumentParser(description='Train or test neural net motor controller.') parser.add_argument('--train', dest='train', action='store_true', default=False) parser.add_argument('--test', dest='test', action='store_true', default=True) args = parser.parse_args() ##################### hyper parameters #################### ENV_ID = 'Pendulum-v0' # environment id RANDOM_SEED = 2 # random seed, can be either an int number or None RENDER = False # render while training ALG_NAME = 'AC' TRAIN_EPISODES = 500 # number of overall episodes for training TEST_EPISODES = 10 # number of overall episodes for testing MAX_STEPS = 200 # maximum time step in one episode LAM = 0.9 # reward discount in TD error LR_A = 0.0001 # learning rate for actor LR_C = 0.0005 # learning rate for critic class Actor(object): def __init__(self, state_dim, action_dim, action_range, lr=0.001): self.action_range = action_range input_layer = tl.layers.Input([None, state_dim]) layer = tl.layers.Dense(n_units=64, act=tf.nn.relu6)(input_layer) layer = tl.layers.Dense(n_units=32, act=tf.nn.relu6)(layer) act = tl.layers.Dense(n_units=action_dim, act=tf.nn.tanh)(layer) mu = tl.layers.Lambda(lambda x: action_range * x)(act) sigma = tl.layers.Dense(action_dim, act=tf.nn.softplus)(layer) self.model = tl.models.Model(inputs=input_layer, outputs=[mu, sigma]) self.model.train() self.optimizer = tf.optimizers.Adam(lr) def learn(self, state, td_error): with tf.GradientTape() as tape: mu, sigma = self.model(np.array([state], dtype=np.float32)) pi = tfp.distributions.Normal(mu, sigma) action = np.clip(pi.sample(), - self.action_range, self.action_range) log_prob = pi.log_prob(action) loss = - log_prob * td_error grad = tape.gradient(loss, self.model.trainable_weights) self.optimizer.apply_gradients(zip(grad, self.model.trainable_weights)) def get_action(self, state, greedy=False): mu, sigma = self.model(np.array([state])) if greedy: action = mu[0] else: pi = tfp.distributions.Normal(mu, sigma) action = tf.squeeze(pi.sample(1), axis=0)[0] return np.clip(action, -self.action_range, self.action_range) class Critic(object): def __init__(self, state_dim, lr=0.01): input_layer = tl.layers.Input([None, state_dim], name='state') layer = tl.layers.Dense(n_units=32, act=tf.nn.relu)(input_layer) layer = tl.layers.Dense(n_units=1, act=None)(layer) self.model = tl.models.Model(inputs=input_layer, outputs=layer, name='Critic') self.model.train() self.optimizer = tf.optimizers.Adam(lr) def learn(self, state, reward, state_, done): d = 0 if done else 1 with tf.GradientTape() as tape: v = self.model(np.array([state])) v_ = self.model(np.array([state_])) td_error = reward + d * LAM * v_ - v loss = tf.square(td_error) grads = tape.gradient(loss, self.model.trainable_weights) self.optimizer.apply_gradients(zip(grads, self.model.trainable_weights)) return td_error class Agent(): def __init__(self, env): self.state_dim = env.observation_space.shape[0] self.action_dim = env.action_space.shape[0] self.action_range = env.action_space.high self.actor = Actor(self.state_dim, self.action_dim, self.action_range, lr=LR_A) self.critic = Critic(self.state_dim, lr=LR_C) def train(self): if args.train: self.train_episode() if args.test: self.load() self.test_episode() def train_episode(self): t0 = time.time() all_episode_reward = [] for episode in range(TRAIN_EPISODES): state = env.reset().astype(np.float32) step = 0 episode_reward = 0 while True: if RENDER: env.render() action = self.actor.get_action(state) state_, reward, done, _ = env.step(action) state_ = state_.astype(np.float32) episode_reward += reward td_error = self.critic.learn(state, reward, state_, done) self.actor.learn(state, td_error) state = state_ step += 1 if done or step >= MAX_STEPS: break if episode == 0: all_episode_reward.append(episode_reward) else: all_episode_reward.append(all_episode_reward[-1] * 0.9 + episode_reward * 0.1) print('Training | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}' \ .format(episode + 1, TRAIN_EPISODES, episode_reward, time.time() - t0)) env.close() self.save() plt.plot(all_episode_reward) if not os.path.exists('image'): os.makedirs('image') plt.savefig(os.path.join('image', '_'.join([ALG_NAME, ENV_ID]))) def test_episode(self): t0 = time.time() for episode in range(TEST_EPISODES): state = env.reset().astype(np.float32) t = 0 # number of step in this episode episode_reward = 0 while True: env.render() action = self.actor.get_action(state) state_new, reward, done, info = env.step(action) state_new = state_new.astype(np.float32) if done: reward = -20 episode_reward += reward state = state_new t += 1 if done or t >= MAX_STEPS: print('Testing | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}' \ .format(episode + 1, TEST_EPISODES, episode_reward, time.time() - t0)) break env.close() def save(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) tl.files.save_npz(self.actor.model.trainable_weights, name=os.path.join(path, 'model_actor.npz')) tl.files.save_npz(self.critic.model.trainable_weights, name=os.path.join(path, 'model_critic.npz')) print('Succeed to save model weights') def load(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) tl.files.load_and_assign_npz(name=os.path.join(path, 'model_critic.npz'), network=self.critic.model) tl.files.load_and_assign_npz(name=os.path.join(path, 'model_actor.npz'), network=self.actor.model) print('Succeed to load model weights') if __name__ == '__main__': env = gym.make(ENV_ID).unwrapped env.seed(RANDOM_SEED) np.random.seed(RANDOM_SEED) tf.random.set_seed(RANDOM_SEED) agent = Agent(env) agent.train() ================================================ FILE: code/AC_Discrete.py ================================================ """ Actor-Critic ------------- It uses TD-error as the Advantage. To run ------ python tutorial_AC.py --train/test """ import argparse import time import matplotlib.pyplot as plt import os import gym import numpy as np import tensorflow as tf import tensorlayer as tl # add arguments in command --train/test parser = argparse.ArgumentParser(description='Train or test neural net motor controller.') parser.add_argument('--train', dest='train', action='store_true', default=False) parser.add_argument('--test', dest='test', action='store_true', default=True) args = parser.parse_args() ##################### hyper parameters #################### ENV_ID = 'CartPole-v1' # environment id RANDOM_SEED = 2 # random seed, can be either an int number or None RENDER = False # render while training ALG_NAME = 'AC' TRAIN_EPISODES = 200 # number of overall episodes for training TEST_EPISODES = 10 # number of overall episodes for testing MAX_STEPS = 500 # maximum time step in one episode LAM = 0.9 # reward discount in TD error LR_A = 0.001 # learning rate for actor LR_C = 0.01 # learning rate for critic class Actor(object): def __init__(self, state_dim, action_dim, lr=0.001): input_layer = tl.layers.Input([None, state_dim]) layer = tl.layers.Dense(n_units=32, act=tf.nn.relu6)(input_layer) layer = tl.layers.Dense(n_units=action_dim)(layer) self.model = tl.models.Model(inputs=input_layer, outputs=layer) self.model.train() self.optimizer = tf.optimizers.Adam(lr) def learn(self, state, action, td_error): with tf.GradientTape() as tape: _logits = self.model(np.array([state])) _exp_v = tl.rein.cross_entropy_reward_loss( logits=_logits, actions=[action], rewards=td_error) grad = tape.gradient(_exp_v, self.model.trainable_weights) self.optimizer.apply_gradients(zip(grad, self.model.trainable_weights)) def get_action(self, state, greedy=False): _logits = self.model(np.array([state])) _prob = tf.nn.softmax(_logits).numpy() if greedy: return np.argmax(_prob.ravel()) return tl.rein.choice_action_by_probs(_prob.ravel()) class Critic(object): def __init__(self, state_dim, lr=0.01): input_layer = tl.layers.Input([None, state_dim], name='state') layer = tl.layers.Dense(n_units=32, act=tf.nn.relu)(input_layer) layer = tl.layers.Dense(n_units=1, act=None)(layer) self.model = tl.models.Model(inputs=input_layer, outputs=layer, name='Critic') self.model.train() self.optimizer = tf.optimizers.Adam(lr) def learn(self, state, reward, state_, done): d = 0 if done else 1 with tf.GradientTape() as tape: v = self.model(np.array([state])) v_ = self.model(np.array([state_])) td_error = reward + d * LAM * v_ - v loss = tf.square(td_error) grads = tape.gradient(loss, self.model.trainable_weights) self.optimizer.apply_gradients(zip(grads, self.model.trainable_weights)) return td_error class Agent(): def __init__(self, env): self.state_dim = env.observation_space.shape[0] self.action_dim = env.action_space.n self.actor = Actor(self.state_dim, self.action_dim, lr=LR_A) self.critic = Critic(self.state_dim, lr=LR_C) def train(self): if args.train: self.train_episode() if args.test: self.load() self.test_episode() def train_episode(self): t0 = time.time() all_episode_reward = [] for episode in range(TRAIN_EPISODES): state = env.reset().astype(np.float32) step = 0 episode_reward = 0 while True: if RENDER: env.render() action = self.actor.get_action(state) state_, reward, done, _ = env.step(action) state_ = state_.astype(np.float32) episode_reward += reward td_error = self.critic.learn(state, reward, state_, done) self.actor.learn(state, action, td_error) state = state_ step += 1 if done or step >= MAX_STEPS: break if episode == 0: all_episode_reward.append(episode_reward) else: all_episode_reward.append(all_episode_reward[-1] * 0.9 + episode_reward * 0.1) print('Training | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}' \ .format(episode + 1, TRAIN_EPISODES, episode_reward, time.time() - t0)) # Early Stopping for quick check if step >= MAX_STEPS: print("Early Stopping") # Hao Dong: it is important for this task self.save() break env.close() plt.plot(all_episode_reward) if not os.path.exists('image'): os.makedirs('image') plt.savefig(os.path.join('image', '_'.join([ALG_NAME, ENV_ID]))) def test_episode(self): t0 = time.time() for episode in range(TEST_EPISODES): state = env.reset().astype(np.float32) t = 0 # number of step in this episode episode_reward = 0 while True: env.render() action = self.actor.get_action(state, greedy=True) state_new, reward, done, info = env.step(action) state_new = state_new.astype(np.float32) if done: reward = -20 episode_reward += reward state = state_new t += 1 if done or t >= MAX_STEPS: print('Testing | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}' \ .format(episode + 1, TEST_EPISODES, episode_reward, time.time() - t0)) break env.close() def save(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) tl.files.save_npz(self.actor.model.trainable_weights, name=os.path.join(path, 'model_actor.npz')) tl.files.save_npz(self.critic.model.trainable_weights, name=os.path.join(path, 'model_critic.npz')) print('Succeed to save model weights') def load(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) tl.files.load_and_assign_npz(name=os.path.join(path, 'model_critic.npz'), network=self.critic.model) tl.files.load_and_assign_npz(name=os.path.join(path, 'model_actor.npz'), network=self.actor.model) print('Succeed to load model weights') if __name__ == '__main__': env = gym.make(ENV_ID).unwrapped env.seed(RANDOM_SEED) np.random.seed(RANDOM_SEED) tf.random.set_seed(RANDOM_SEED) agent = Agent(env) agent.train() ================================================ FILE: code/DDPG.py ================================================ """ Deep Deterministic Policy Gradient (DDPG) ----------------------------------------- An algorithm concurrently learns a Q-function and a policy. It uses off-policy data and the Bellman equation to learn the Q-function, and uses the Q-function to learn the policy. Reference --------- Deterministic Policy Gradient Algorithms, Silver et al. 2014 Continuous Control With Deep Reinforcement Learning, Lillicrap et al. 2016 MorvanZhou's tutorial page: https://morvanzhou.github.io/tutorials/ Environment ----------- Openai Gym Pendulum-v0, continual action space Prerequisites ------------- tensorflow >=2.0.0a0 tensorflow-proactionsbility 0.6.0 tensorlayer >=2.0.0 To run ------ python tutorial_DDPG.py --train/test """ import argparse import os import random import time import gym import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import tensorlayer as tl # add arguments in command --train/test parser = argparse.ArgumentParser(description='Train or test neural net motor controller.') parser.add_argument('--train', dest='train', action='store_true', default=False) parser.add_argument('--test', dest='test', action='store_true', default=True) args = parser.parse_args() ##################### hyper parameters #################### ENV_ID = 'Pendulum-v0' # environment id RANDOM_SEED = 2 # random seed, can be either an int number or None RENDER = False # render while training ALG_NAME = 'DDPG' TRAIN_EPISODES = 100 # total number of episodes for training TEST_EPISODES = 10 # total number of episodes for training MAX_STEPS = 200 # total number of steps for each episode LR_A = 0.001 # learning rate for actor LR_C = 0.002 # learning rate for critic GAMMA = 0.9 # reward discount TAU = 0.01 # soft replacement MEMORY_CAPACITY = 10000 # size of replay buffer BATCH_SIZE = 32 # update action batch size VAR = 2 # control exploration ############################### DDPG #################################### class ReplayBuffer: """ a ring buffer for storing transitions and sampling for training :state: (state_dim,) :action: (action_dim,) :reward: (,), scalar :next_state: (state_dim,) :done: (,), scalar (0 and 1) or bool (True and False) """ 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 = int((self.position + 1) % self.capacity) # as a ring buffer def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) state, action, reward, next_state, done = map(np.stack, zip(*batch)) # stack for each element return state, action, reward, next_state, done def __len__(self): return len(self.buffer) class DDPG(object): """ DDPG class """ def __init__(self, action_dim, state_dim, action_range, replay_buffer): self.replay_buffer = replay_buffer self.action_dim, self.state_dim, self.action_range = action_dim, state_dim, action_range self.var = VAR W_init = tf.random_normal_initializer(mean=0, stddev=0.3) b_init = tf.constant_initializer(0.1) def get_actor(input_state_shape, name=''): """ Build actor network :param input_state_shape: state :param name: name :return: act """ input_layer = tl.layers.Input(input_state_shape, name='A_input') layer = tl.layers.Dense(n_units=64, act=tf.nn.relu, W_init=W_init, b_init=b_init, name='A_l1')(input_layer) layer = tl.layers.Dense(n_units=64, act=tf.nn.relu, W_init=W_init, b_init=b_init, name='A_l2')(layer) layer = tl.layers.Dense(n_units=action_dim, act=tf.nn.tanh, W_init=W_init, b_init=b_init, name='A_a')(layer) layer = tl.layers.Lambda(lambda x: action_range * x)(layer) return tl.models.Model(inputs=input_layer, outputs=layer, name='Actor' + name) def get_critic(input_state_shape, input_action_shape, name=''): """ Build critic network :param input_state_shape: state :param input_action_shape: act :param name: name :return: Q value Q(s,a) """ state_input = tl.layers.Input(input_state_shape, name='C_s_input') action_input = tl.layers.Input(input_action_shape, name='C_a_input') layer = tl.layers.Concat(1)([state_input, action_input]) layer = tl.layers.Dense(n_units=64, act=tf.nn.relu, W_init=W_init, b_init=b_init, name='C_l1')(layer) layer = tl.layers.Dense(n_units=64, act=tf.nn.relu, W_init=W_init, b_init=b_init, name='C_l2')(layer) layer = tl.layers.Dense(n_units=1, W_init=W_init, b_init=b_init, name='C_out')(layer) return tl.models.Model(inputs=[state_input, action_input], outputs=layer, name='Critic' + name) self.actor = get_actor([None, state_dim]) self.critic = get_critic([None, state_dim], [None, action_dim]) self.actor.train() self.critic.train() def copy_para(from_model, to_model): """ Copy parameters for soft updating :param from_model: latest model :param to_model: target model :return: None """ for i, j in zip(from_model.trainable_weights, to_model.trainable_weights): j.assign(i) self.actor_target = get_actor([None, state_dim], name='_target') copy_para(self.actor, self.actor_target) self.actor_target.eval() self.critic_target = get_critic([None, state_dim], [None, action_dim], name='_target') copy_para(self.critic, self.critic_target) self.critic_target.eval() self.ema = tf.train.ExponentialMovingAverage(decay=1 - TAU) # soft replacement self.actor_opt = tf.optimizers.Adam(LR_A) self.critic_opt = tf.optimizers.Adam(LR_C) def ema_update(self): """ Soft updating by exponential smoothing :return: None """ paras = self.actor.trainable_weights + self.critic.trainable_weights self.ema.apply(paras) for i, j in zip(self.actor_target.trainable_weights + self.critic_target.trainable_weights, paras): i.assign(self.ema.average(j)) def get_action(self, state, greedy=False): """ Choose action :param s: state :param greedy: get action greedy or not :return: act """ action = self.actor(np.array([state]))[0] if greedy: return action return np.clip( np.random.normal(action, self.var), -self.action_range, self.action_range ).astype(np.float32) # add randomness to action selection for exploration def learn(self): """ Update parameters :return: None """ self.var *= .9995 states, actions, rewards, states_, done = self.replay_buffer.sample(BATCH_SIZE) rewards = rewards[:, np.newaxis] done = done[:, np.newaxis] with tf.GradientTape() as tape: actions_ = self.actor_target(states_) q_ = self.critic_target([states_, actions_]) target = rewards + (1 - done) * GAMMA * q_ q_pred = self.critic([states, actions]) td_error = tf.losses.mean_squared_error(target, q_pred) critic_grads = tape.gradient(td_error, self.critic.trainable_weights) self.critic_opt.apply_gradients(zip(critic_grads, self.critic.trainable_weights)) with tf.GradientTape() as tape: actions = self.actor(states) q = self.critic([states, actions]) actor_loss = -tf.reduce_mean(q) # maximize the q actor_grads = tape.gradient(actor_loss, self.actor.trainable_weights) self.actor_opt.apply_gradients(zip(actor_grads, self.actor.trainable_weights)) self.ema_update() def save(self): """ save trained weights :return: None """ path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) tl.files.save_weights_to_hdf5(os.path.join(path, 'actor.hdf5'), self.actor) tl.files.save_weights_to_hdf5(os.path.join(path, 'actor_target.hdf5'), self.actor_target) tl.files.save_weights_to_hdf5(os.path.join(path, 'critic.hdf5'), self.critic) tl.files.save_weights_to_hdf5(os.path.join(path, 'critic_target.hdf5'), self.critic_target) def load(self): """ load trained weights :return: None """ path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'actor.hdf5'), self.actor) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'actor_target.hdf5'), self.actor_target) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'critic.hdf5'), self.critic) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'critic_target.hdf5'), self.critic_target) if __name__ == '__main__': env = gym.make(ENV_ID).unwrapped # reproducible env.seed(RANDOM_SEED) np.random.seed(RANDOM_SEED) tf.random.set_seed(RANDOM_SEED) state_dim = env.observation_space.shape[0] action_dim = env.action_space.shape[0] action_range = env.action_space.high # scale action, [-action_range, action_range] buffer = ReplayBuffer(MEMORY_CAPACITY) agent = DDPG(action_dim, state_dim, action_range, buffer) t0 = time.time() if args.train: # train all_episode_reward = [] for episode in range(TRAIN_EPISODES): state = env.reset().astype(np.float32) episode_reward = 0 for step in range(MAX_STEPS): if RENDER: env.render() # Add exploration noise action = agent.get_action(state) state_, reward, done, info = env.step(action) state_ = np.array(state_, dtype=np.float32) done = 1 if done is True else 0 buffer.push(state, action, reward, state_, done) if len(buffer) >= MEMORY_CAPACITY: agent.learn() state = state_ episode_reward += reward if done: break if episode == 0: all_episode_reward.append(episode_reward) else: all_episode_reward.append(all_episode_reward[-1] * 0.9 + episode_reward * 0.1) print( 'Training | Episode: {}/{} | Episode Reward: {:.4f} | Running Time: {:.4f}'.format( episode + 1, TRAIN_EPISODES, episode_reward, time.time() - t0 ) ) env.close() agent.save() plt.plot(all_episode_reward) if not os.path.exists('image'): os.makedirs('image') plt.savefig(os.path.join('image', '_'.join([ALG_NAME, ENV_ID]))) if args.test: # test agent.load() for episode in range(TEST_EPISODES): state = env.reset().astype(np.float32) episode_reward = 0 for step in range(MAX_STEPS): env.render() state, reward, done, info = env.step(agent.get_action(state, greedy=True)) state = state.astype(np.float32) episode_reward += reward if done: break print( 'Testing | Episode: {}/{} | Episode Reward: {:.4f} | Running Time: {:.4f}'.format( episode + 1, TEST_EPISODES, episode_reward, time.time() - t0 ) ) env.close() ================================================ FILE: code/DDQN.py ================================================ import argparse import os import random import numpy as np import gym import tensorflow as tf import tensorlayer as tl parser = argparse.ArgumentParser() parser.add_argument('--train', dest='train', default=False) parser.add_argument('--test', dest='test', default=True) parser.add_argument('--gamma', type=float, default=0.95) parser.add_argument('--lr', type=float, default=0.005) parser.add_argument('--batch_size', type=int, default=32) parser.add_argument('--eps', type=float, default=0.1) parser.add_argument('--train_episodes', type=int, default=200) parser.add_argument('--test_episodes', type=int, default=10) args = parser.parse_args() ALG_NAME = 'DDQN' ENV_ID = 'CartPole-v1' class ReplayBuffer: def __init__(self, capacity=10000): 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 = int((self.position + 1) % self.capacity) def sample(self, batch_size = args.batch_size): batch = random.sample(self.buffer, batch_size) state, action, reward, next_state, done = map(np.stack, zip(*batch)) """ the * serves as unpack: sum(a,b) <=> batch=(a,b), sum(*batch) ; zip: a=[1,2], b=[2,3], zip(a,b) => [(1, 2), (2, 3)] ; the map serves as mapping the function on each list element: map(square, [2,3]) => [4,9] ; np.stack((1,2)) => array([1, 2]) """ return state, action, reward, next_state, done class Agent: def __init__(self, env): self.env = env self.state_dim = self.env.observation_space.shape[0] self.action_dim = self.env.action_space.n def create_model(input_state_shape): input_layer = tl.layers.Input(input_state_shape) layer_1 = tl.layers.Dense(n_units=32, act=tf.nn.relu)(input_layer) layer_2 = tl.layers.Dense(n_units=16, act=tf.nn.relu)(layer_1) output_layer = tl.layers.Dense(n_units=self.action_dim)(layer_2) return tl.models.Model(inputs=input_layer, outputs=output_layer) self.model = create_model([None, self.state_dim]) self.target_model = create_model([None, self.state_dim]) self.model.train() self.target_model.eval() self.model_optim = tf.optimizers.Adam(lr=args.lr) self.epsilon = args.eps self.buffer = ReplayBuffer() def target_update(self): """Copy q network to target q network""" for weights, target_weights in zip( self.model.trainable_weights, self.target_model.trainable_weights): target_weights.assign(weights) def choose_action(self, state): if np.random.uniform() < self.epsilon: return np.random.choice(self.action_dim) else: q_value = self.model(state[np.newaxis, :])[0] return np.argmax(q_value) def replay(self): for _ in range(10): states, actions, rewards, next_states, done = self.buffer.sample() # targets [batch_size, action_dim] # Target represents the current fitting level target = self.target_model(states).numpy() # next_q_values [batch_size, action_diim] next_target = self.target_model(next_states).numpy() # next_q_value [batch_size, 1] next_q_value = next_target[ range(args.batch_size), np.argmax(self.model(next_states), axis=1) ] # next_q_value = tf.reduce_max(next_q_value, axis=1) target[range(args.batch_size), actions] = rewards + (1 - done) * args.gamma * next_q_value # use sgd to update the network weight with tf.GradientTape() as tape: q_pred = self.model(states) loss = tf.losses.mean_squared_error(target, q_pred) grads = tape.gradient(loss, self.model.trainable_weights) self.model_optim.apply_gradients(zip(grads, self.model.trainable_weights)) def test_episode(self, test_episodes): for episode in range(test_episodes): state = self.env.reset().astype(np.float32) total_reward, done = 0, False while not done: action = self.model(np.array([state], dtype=np.float32))[0] action = np.argmax(action) next_state, reward, done, _ = self.env.step(action) next_state = next_state.astype(np.float32) total_reward += reward state = next_state self.env.render() print("Test {} | episode rewards is {}".format(episode, total_reward)) def train(self, train_episodes=200): if args.train: for episode in range(train_episodes): total_reward, done = 0, False state = self.env.reset().astype(np.float32) while not done: action = self.choose_action(state) next_state, reward, done, _ = self.env.step(action) next_state = next_state.astype(np.float32) self.buffer.push(state, action, reward, next_state, done) total_reward += reward state = next_state # self.render() if len(self.buffer.buffer) > args.batch_size: self.replay() self.target_update() print('EP{} EpisodeReward={}'.format(episode, total_reward)) self.saveModel() if args.test: self.loadModel() self.test_episode(test_episodes=args.test_episodes) def saveModel(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) tl.files.save_weights_to_hdf5(os.path.join(path, 'model.hdf5'), self.model) tl.files.save_weights_to_hdf5(os.path.join(path, 'target_model.hdf5'), self.target_model) print('Saved weights.') def loadModel(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if os.path.exists(path): print('Load DQN Network parametets ...') tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'model.hdf5'), self.model) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'target_model.hdf5'), self.target_model) print('Load weights!') else: print("No model file find, please train model first...") if __name__ == '__main__': env = gym.make(ENV_ID) agent = Agent(env) agent.train(train_episodes=args.train_episodes) env.close() ================================================ FILE: code/DQN.py ================================================ import argparse import os import random import numpy as np import gym import tensorflow as tf import tensorlayer as tl parser = argparse.ArgumentParser() parser.add_argument('--train', dest='train', default=False) parser.add_argument('--test', dest='test', default=True) parser.add_argument('--gamma', type=float, default=0.95) parser.add_argument('--lr', type=float, default=0.005) parser.add_argument('--batch_size', type=int, default=32) parser.add_argument('--eps', type=float, default=0.1) parser.add_argument('--train_episodes', type=int, default=200) parser.add_argument('--test_episodes', type=int, default=10) args = parser.parse_args() ALG_NAME = 'DQN' ENV_ID = 'CartPole-v1' class ReplayBuffer: def __init__(self, capacity=10000): 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 = int((self.position + 1) % self.capacity) def sample(self, batch_size = args.batch_size): batch = random.sample(self.buffer, batch_size) state, action, reward, next_state, done = map(np.stack, zip(*batch)) """ the * serves as unpack: sum(a,b) <=> batch=(a,b), sum(*batch) ; zip: a=[1,2], b=[2,3], zip(a,b) => [(1, 2), (2, 3)] ; the map serves as mapping the function on each list element: map(square, [2,3]) => [4,9] ; np.stack((1,2)) => array([1, 2]) """ return state, action, reward, next_state, done class Agent: def __init__(self, env): self.env = env self.state_dim = self.env.observation_space.shape[0] self.action_dim = self.env.action_space.n def create_model(input_state_shape): input_layer = tl.layers.Input(input_state_shape) layer_1 = tl.layers.Dense(n_units=32, act=tf.nn.relu)(input_layer) layer_2 = tl.layers.Dense(n_units=16, act=tf.nn.relu)(layer_1) output_layer = tl.layers.Dense(n_units=self.action_dim)(layer_2) return tl.models.Model(inputs=input_layer, outputs=output_layer) self.model = create_model([None, self.state_dim]) self.target_model = create_model([None, self.state_dim]) self.model.train() self.target_model.eval() self.model_optim = self.target_model_optim = tf.optimizers.Adam(lr=args.lr) self.epsilon = args.eps self.buffer = ReplayBuffer() def target_update(self): """Copy q network to target q network""" for weights, target_weights in zip( self.model.trainable_weights, self.target_model.trainable_weights): target_weights.assign(weights) def choose_action(self, state): if np.random.uniform() < self.epsilon: return np.random.choice(self.action_dim) else: q_value = self.model(state[np.newaxis, :])[0] return np.argmax(q_value) def replay(self): for _ in range(10): # sample an experience tuple from the dataset(buffer) states, actions, rewards, next_states, done = self.buffer.sample() # compute the target value for the sample tuple # targets [batch_size, action_dim] # Target represents the current fitting level target = self.target_model(states).numpy() # next_q_values [batch_size, action_dim] next_target = self.target_model(next_states) next_q_value = tf.reduce_max(next_target, axis=1) target[range(args.batch_size), actions] = rewards + (1 - done) * args.gamma * next_q_value # use sgd to update the network weight with tf.GradientTape() as tape: q_pred = self.model(states) loss = tf.losses.mean_squared_error(target, q_pred) grads = tape.gradient(loss, self.model.trainable_weights) self.model_optim.apply_gradients(zip(grads, self.model.trainable_weights)) def test_episode(self, test_episodes): for episode in range(test_episodes): state = self.env.reset().astype(np.float32) total_reward, done = 0, False while not done: action = self.model(np.array([state], dtype=np.float32))[0] action = np.argmax(action) next_state, reward, done, _ = self.env.step(action) next_state = next_state.astype(np.float32) total_reward += reward state = next_state self.env.render() print("Test {} | episode rewards is {}".format(episode, total_reward)) def train(self, train_episodes=200): if args.train: for episode in range(train_episodes): total_reward, done = 0, False state = self.env.reset().astype(np.float32) while not done: action = self.choose_action(state) next_state, reward, done, _ = self.env.step(action) next_state = next_state.astype(np.float32) self.buffer.push(state, action, reward, next_state, done) total_reward += reward state = next_state # self.render() if len(self.buffer.buffer) > args.batch_size: self.replay() self.target_update() print('EP{} EpisodeReward={}'.format(episode, total_reward)) # if episode % 10 == 0: # self.test_episode() self.saveModel() if args.test: self.loadModel() self.test_episode(test_episodes=args.test_episodes) def saveModel(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) tl.files.save_weights_to_hdf5(os.path.join(path, 'model.hdf5'), self.model) tl.files.save_weights_to_hdf5(os.path.join(path, 'target_model.hdf5'), self.target_model) print('Saved weights.') def loadModel(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if os.path.exists(path): print('Load DQN Network parametets ...') tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'model.hdf5'), self.model) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'target_model.hdf5'), self.target_model) print('Load weights!') else: print("No model file find, please train model first...") if __name__ == '__main__': env = gym.make(ENV_ID) agent = Agent(env) agent.train(train_episodes=args.train_episodes) env.close() ================================================ FILE: code/Dueling DQN.py ================================================ import argparse import os import random import numpy as np import gym import tensorflow as tf import tensorlayer as tl parser = argparse.ArgumentParser() parser.add_argument('--train', dest='train', default=False) parser.add_argument('--test', dest='test', default=True) parser.add_argument('--gamma', type=float, default=0.95) parser.add_argument('--lr', type=float, default=0.005) parser.add_argument('--batch_size', type=int, default=32) parser.add_argument('--eps', type=float, default=0.1) parser.add_argument('--train_episodes', type=int, default=200) parser.add_argument('--test_episodes', type=int, default=10) args = parser.parse_args() ALG_NAME = 'DuelineDQN' ENV_ID = 'CartPole-v1' class ReplayBuffer: def __init__(self, capacity=10000): 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 = int((self.position + 1) % self.capacity) def sample(self, batch_size = args.batch_size): batch = random.sample(self.buffer, batch_size) state, action, reward, next_state, done = map(np.stack, zip(*batch)) """ the * serves as unpack: sum(a,b) <=> batch=(a,b), sum(*batch) ; zip: a=[1,2], b=[2,3], zip(a,b) => [(1, 2), (2, 3)] ; the map serves as mapping the function on each list element: map(square, [2,3]) => [4,9] ; np.stack((1,2)) => array([1, 2]) """ return state, action, reward, next_state, done class Agent: def __init__(self, env): self.env = env self.state_dim = self.env.observation_space.shape[0] self.action_dim = self.env.action_space.n def create_model(input_state_shape): input_layer = tl.layers.Input(input_state_shape) layer_1 = tl.layers.Dense(n_units=32, act=tf.nn.relu)(input_layer) layer_2 = tl.layers.Dense(n_units=16, act=tf.nn.relu)(layer_1) # state value state_value = tl.layers.Dense(n_units=1)(layer_2) # advantage value q_value = tl.layers.Dense(n_units=self.action_dim)(layer_2) mean = tl.layers.Lambda(lambda x: tf.reduce_mean(x, axis=1, keepdims=True))(q_value) advantage = tl.layers.ElementwiseLambda(lambda x, y: x-y)([q_value, mean]) # output output_layer = tl.layers.ElementwiseLambda(lambda x, y: x+y)([state_value, advantage]) return tl.models.Model(inputs=input_layer, outputs=output_layer) self.model = create_model([None, self.state_dim]) self.target_model = create_model([None, self.state_dim]) self.model.train() self.target_model.eval() self.model_optim = tf.optimizers.Adam(lr=args.lr) self.epsilon = args.eps self.buffer = ReplayBuffer() def target_update(self): """Copy q network to target q network""" for weights, target_weights in zip( self.model.trainable_weights, self.target_model.trainable_weights): target_weights.assign(weights) def choose_action(self, state): if np.random.uniform() < self.epsilon: return np.random.choice(self.action_dim) else: q_value = self.model(state[np.newaxis, :])[0] return np.argmax(q_value) def replay(self): for _ in range(10): states, actions, rewards, next_states, done = self.buffer.sample() target = self.target_model(states).numpy() # next_q_values [batch_size, action_dim] next_target = self.target_model(next_states) next_q_value = tf.reduce_max(next_target, axis=1) target[range(args.batch_size), actions] = rewards + (1 - done) * args.gamma * next_q_value # use sgd to update the network weight with tf.GradientTape() as tape: q_pred = self.model(states) loss = tf.losses.mean_squared_error(target, q_pred) grads = tape.gradient(loss, self.model.trainable_weights) self.model_optim.apply_gradients(zip(grads, self.model.trainable_weights)) def train(self, train_episodes=200): if args.train: for episode in range(train_episodes): total_reward, done = 0, False state = self.env.reset().astype(np.float32) while not done: action = self.choose_action(state) next_state, reward, done, _ = self.env.step(action) next_state = next_state.astype(np.float32) self.buffer.push(state, action, reward, next_state, done) total_reward += reward state = next_state # self.render() if len(self.buffer.buffer) > args.batch_size: self.replay() self.target_update() print('EP{} EpisodeReward={}'.format(episode, total_reward)) self.saveModel() if args.test: self.loadModel() self.test_episode(test_episodes=args.test_episodes) def test_episode(self, test_episodes): for episode in range(test_episodes): state = self.env.reset().astype(np.float32) total_reward, done = 0, False while not done: action = self.model(np.array([state], dtype=np.float32))[0] action = np.argmax(action) next_state, reward, done, _ = self.env.step(action) next_state = next_state.astype(np.float32) total_reward += reward state = next_state self.env.render() print("Test {} | episode rewards is {}".format(episode, total_reward)) def saveModel(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) tl.files.save_weights_to_hdf5(os.path.join(path, 'model.hdf5'), self.model) tl.files.save_weights_to_hdf5(os.path.join(path, 'target_model.hdf5'), self.target_model) print('Saved weights.') def loadModel(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if os.path.exists(path): print('Load DQN Network parametets ...') tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'model.hdf5'), self.model) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'target_model.hdf5'), self.target_model) print('Load weights!') else: print("No model file find, please train model first...") if __name__ == '__main__': env = gym.make(ENV_ID) agent = Agent(env) agent.train(train_episodes=args.train_episodes) env.close() ================================================ FILE: code/PG_Continous.py ================================================ # ---------------------------------- # Policy Gradient for Continuous Env # Env: Pendulum-v0 # Problem: Can't convergence # ---------------------------------- import argparse import os import time import gym import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import tensorflow_probability as tfp import tensorlayer as tl parser = argparse.ArgumentParser(description='Train or test neural net motor controller.') parser.add_argument('--train', dest='train', action='store_true', default=True) parser.add_argument('--test', dest='test', action='store_true', default=True) args = parser.parse_args() ##################### hyper parameters #################### ENV_ID = 'Pendulum-v0' # environment id RANDOM_SEED = 1 # random seed, can be either an int number or None RENDER = False # render while training ALG_NAME = 'PG' TRAIN_EPISODES = 500 TEST_EPISODES = 10 MAX_STEPS = 200 class PolicyGradient: def __init__(self, state_dim, action_dim, action_range, lr=0.001, gamma=0.99): self.gamma = gamma self.action_range = action_range self.state_buffer, self.action_buffer, self.reward_buffer = [], [], [] input_layer = tl.layers.Input([None, state_dim], dtype=tf.float32) layer = tl.layers.Dense(n_units=100, act=tf.nn.relu)(input_layer) a = tl.layers.Dense(n_units=action_dim, act=tf.nn.tanh)(layer) mu = tl.layers.Lambda(lambda x: x * action_range)(a) sigma = tl.layers.Dense(n_units=action_dim, act=tf.nn.softplus)(layer) self.model = tl.models.Model(inputs=input_layer, outputs=[mu, sigma]) self.model.train() self.optimizer = tf.optimizers.Adam(lr) def get_action(self, state): s = state[np.newaxis, :].astype(np.float32) mu, sigma = self.model(s) pi = tfp.distributions.Normal(mu, sigma) a = tf.squeeze(pi.sample(1), axis=0)[0] return np.clip(a, -self.action_range, self.action_range) def store_transition(self, s, a, r): self.state_buffer.append(np.array([s], np.float32)) self.action_buffer.append(a) self.reward_buffer.append(r) def learn(self): discount_reward_buffer_norm = self._discount_and_norm_reward() with tf.GradientTape() as tape: mu, sigma = self.model(np.vstack(self.state_buffer)) pi = tfp.distributions.Normal(mu, sigma) action = tf.clip_by_value(pi.sample(), -self.action_range, self.action_range) log_prob = pi.log_prob(action) loss = tf.reduce_sum(- log_prob * discount_reward_buffer_norm) grad = tape.gradient(loss, self.model.trainable_weights) self.optimizer.apply_gradients(zip(grad, self.model.trainable_weights)) self.state_buffer, self.action_buffer, self.reward_buffer = [], [], [] def _discount_and_norm_reward(self): """ compute discount_and_norm_rewards """ discount_reward_buffer = np.zeros_like(self.reward_buffer) running_add = 0 for t in reversed(range(0, len(self.reward_buffer))): # Gt = R + gamma * V' running_add = self.reward_buffer[t] + self.gamma * running_add discount_reward_buffer[t] = running_add # normalize episode rewards discount_reward_buffer -= np.mean(discount_reward_buffer) discount_reward_buffer /= np.std(discount_reward_buffer) return discount_reward_buffer def save(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) tl.files.save_weights_to_hdf5(os.path.join(path, 'pg_policy.hdf5'), self.model) print("Succeed to save model weights !") def load(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'pg_policy.hdf5'), self.model) print("Succeed to load model weights !") if __name__ == '__main__': env = gym.make(ENV_ID).unwrapped np.random.seed(RANDOM_SEED) tf.random.set_seed(RANDOM_SEED) env.seed(RANDOM_SEED) agent = PolicyGradient( state_dim=env.observation_space.shape[0], action_dim=env.action_space.shape[0], action_range=env.action_space.high ) t0 = time.time() if args.train: all_episode_reward = [] for episode in range(TRAIN_EPISODES): state = env.reset() episode_reward = 0 for step in range(MAX_STEPS): # in one episode if RENDER: env.render() action = agent.get_action(state) next_state, reward, done, info = env.step(action) agent.store_transition(state, action, reward) state = next_state episode_reward += reward if done: break agent.learn() print( 'Training | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}'.format( episode + 1, TRAIN_EPISODES, episode_reward, time.time() - t0 ) ) if episode == 0: all_episode_reward.append(episode_reward) else: all_episode_reward.append(all_episode_reward[-1] * 0.9 + episode_reward * 0.1) # agent.save() plt.plot(all_episode_reward) if not os.path.exists('image'): os.makedirs('image') plt.savefig(os.path.join('image', '_'.join([ALG_NAME, ENV_ID]))) if args.test: # test # agent.load() for episode in range(TEST_EPISODES): state = env.reset() episode_reward = 0 for step in range(MAX_STEPS): env.render() state, reward, done, info = env.step(agent.get_action(state, True)) episode_reward += reward if done: break print( 'Testing | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}'.format( episode + 1, TEST_EPISODES, episode_reward, time.time() - t0 ) ) env.close() ================================================ FILE: code/PG_Discrete.py ================================================ """ To run ------ python tutorial_PG.py --train/test """ import argparse import os import time import gym import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import tensorlayer as tl parser = argparse.ArgumentParser(description='Train or test neural net motor controller.') parser.add_argument('--train', dest='train', action='store_true', default=False) parser.add_argument('--test', dest='test', action='store_true', default=True) args = parser.parse_args() ##################### hyper parameters #################### ENV_ID = 'CartPole-v1' # environment id RANDOM_SEED = 1 # random seed, can be either an int number or None RENDER = False # render while training ALG_NAME = 'PG' TRAIN_EPISODES = 200 TEST_EPISODES = 10 MAX_STEPS = 500 ############################### PG #################################### class PolicyGradient: """ PG class """ def __init__(self, state_dim, action_num, learning_rate=0.02, gamma=0.99): self.gamma = gamma self.state_buffer, self.action_buffer, self.reward_buffer = [], [], [] W_init = tf.random_normal_initializer(mean=0, stddev=0.3) b_init = tf.constant_initializer(0.1) input_layer = tl.layers.Input([None, state_dim], tf.float32) layer = tl.layers.Dense(n_units=30, act=tf.nn.tanh, W_init=W_init, b_init=b_init)(input_layer) all_act = tl.layers.Dense(n_units=action_num, act=None, W_init=W_init,b_init=b_init)(layer) self.model = tl.models.Model(inputs=input_layer, outputs=all_act) self.model.train() self.optimizer = tf.optimizers.Adam(learning_rate) def get_action(self, s, greedy=False): """ choose action with probabilities. :param s: state :param greedy: choose action greedy or not :return: act """ _logits = self.model(np.array([s], np.float32)) _probs = tf.nn.softmax(_logits).numpy() if greedy: return np.argmax(_probs.ravel()) return tl.rein.choice_action_by_probs(_probs.ravel()) def store_transition(self, s, a, r): """ store data in memory buffer :param s: state :param a: act :param r: reward :return: """ self.state_buffer.append(np.array([s], np.float32)) self.action_buffer.append(a) self.reward_buffer.append(r) def learn(self): """ update policy parameters via stochastic gradient ascent :return: None """ discounted_reward = self._discount_and_norm_rewards() with tf.GradientTape() as tape: _logits = self.model(np.vstack(self.state_buffer)) neg_log_prob = tf.nn.sparse_softmax_cross_entropy_with_logits( logits=_logits, labels=np.array(self.action_buffer) ) loss = tf.reduce_mean(neg_log_prob * discounted_reward) # loss = tl.rein.cross_entropy_reward_loss( # logits=_logits, actions=np.array(self.action_buffer), rewards=discounted_reward) grad = tape.gradient(loss, self.model.trainable_weights) self.optimizer.apply_gradients(zip(grad, self.model.trainable_weights)) self.state_buffer, self.action_buffer, self.reward_buffer = [], [], [] # empty episode data def _discount_and_norm_rewards(self): """ compute discount_and_norm_rewards :return: discount_and_norm_rewards """ # discount episode rewards discounted_reward_buffer = np.zeros_like(self.reward_buffer) running_add = 0 for t in reversed(range(0, len(self.reward_buffer))): running_add = running_add * self.gamma + self.reward_buffer[t] discounted_reward_buffer[t] = running_add # normalize episode rewards discounted_reward_buffer -= np.mean(discounted_reward_buffer) discounted_reward_buffer /= np.std(discounted_reward_buffer) return discounted_reward_buffer def save(self): """ save trained weights :return: None """ path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) tl.files.save_weights_to_hdf5(os.path.join(path, 'pg_policy.hdf5'), self.model) def load(self): """ load trained weights :return: None """ path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'pg_policy.hdf5'), self.model) if __name__ == '__main__': env = gym.make(ENV_ID).unwrapped # reproducible np.random.seed(RANDOM_SEED) tf.random.set_seed(RANDOM_SEED) env.seed(RANDOM_SEED) agent = PolicyGradient( action_num=env.action_space.n, state_dim=env.observation_space.shape[0], ) t0 = time.time() if args.train: all_episode_reward = [] for episode in range(TRAIN_EPISODES): state = env.reset() episode_reward = 0 for step in range(MAX_STEPS): # in one episode if RENDER: env.render() action = agent.get_action(state) next_state, reward, done, _ = env.step(action) agent.store_transition(state, action, reward) state = next_state episode_reward += reward if done: break agent.learn() print( 'Training | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}'.format( episode + 1, TRAIN_EPISODES, episode_reward, time.time() - t0 ) ) if episode == 0: all_episode_reward.append(episode_reward) else: all_episode_reward.append(all_episode_reward[-1] * 0.9 + episode_reward * 0.1) env.close() agent.save() plt.plot(all_episode_reward) if not os.path.exists('image'): os.makedirs('image') plt.savefig(os.path.join('image', '_'.join([ALG_NAME, ENV_ID]))) if args.test: # test agent.load() for episode in range(TEST_EPISODES): state = env.reset() episode_reward = 0 for step in range(MAX_STEPS): env.render() state, reward, done, info = env.step(agent.get_action(state, True)) episode_reward += reward if done: break print( 'Testing | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}'.format( episode + 1, TEST_EPISODES, episode_reward, time.time() - t0 ) ) env.close() ================================================ FILE: code/PPO.py ================================================ """ To run ------ python tutorial_PPO.py --train/test """ import argparse import os import time import gym import numpy as np import matplotlib.pyplot as plt import tensorflow as tf import tensorflow_probability as tfp import tensorlayer as tl parser = argparse.ArgumentParser(description='Train or test neural net motor controller.') parser.add_argument('--train', dest='train', action='store_true', default=False) parser.add_argument('--test', dest='test', action='store_false', default=True) args = parser.parse_args() ##################### hyper parameters #################### ENV_NAME = 'Pendulum-v0' # environment name ALG_NAME = 'PPO' RANDOMSEED = 1 # random seed EP_MAX = 1000 # total number of episodes for training EP_LEN = 200 # total number of steps for each episode GAMMA = 0.9 # reward discount A_LR = 0.0001 # learning rate for actor C_LR = 0.0002 # learning rate for critic BATCH = 32 # update batchsize A_UPDATE_STEPS = 10 # actor update steps C_UPDATE_STEPS = 10 # critic update steps EPS = 1e-8 # epsilon # 注意:这里是PPO1和PPO2的相关的参数。 METHOD = [ dict(name='kl_pen', kl_target=0.01, lam=0.5), # KL penalty PPO1 dict(name='clip', epsilon=0.2), # Clipped surrogate objective, find this is better PPO2 ][1] # choose the method for optimization # ppo-penalty parameters KL_TARGET = 0.01 LAM = 0.5 # ppo-clip parameters EPSILON = 0.2 ############################### PPO #################################### class PPO(object): ''' PPO 类 ''' def __init__(self, state_dim, action_dim, action_bound, method='clip'): def build_critic(input_state_dim): input_layer = tl.layers.Input(input_state_dim, tf.float32) l1 = tl.layers.Dense(100, tf.nn.relu)(input_layer) output_layer = tl.layers.Dense(1)(l1) return tl.models.Model(input_layer, output_layer) def build_actor(input_state_dim, action_dim): ''' actor 网络,输出mu和sigma ''' input_layer = tl.layers.Input(input_state_dim, tf.float32) l1 = tl.layers.Dense(100, tf.nn.relu)(input_layer) a = tl.layers.Dense(action_dim, tf.nn.tanh)(l1) mu = tl.layers.Lambda(lambda x: x * action_bound)(a) sigma = tl.layers.Dense(action_dim, tf.nn.softplus)(l1) model = tl.models.Model(input_layer, [mu, sigma]) return model # 构建critic网络, 输入state,输出V值 self.critic = build_critic([None, state_dim]) self.critic.train() # actor有两个actor 和 actor_old, actor_old的主要功能是记录行为策略的版本。 # 输入时state,输出是描述动作分布的mu和sigma self.actor = build_actor([None, state_dim], action_dim) self.actor_old = build_actor([None, state_dim], action_dim) self.actor.train() self.actor_old.eval() self.actor_opt = tf.optimizers.Adam(A_LR) self.critic_opt = tf.optimizers.Adam(C_LR) self.method = method if method == 'penalty': self.kl_target = KL_TARGET self.lam = LAM elif method == 'clip': self.epsilon = EPSILON self.state_buffer, self.action_buffer = [], [] self.reward_buffer, self.cumulative_reward_buffer = [], [] self.action_bound = action_bound def choose_action(self, s): ''' Choose action :param s: state :return: clipped act ''' s = s[np.newaxis, :].astype(np.float32) mu, sigma = self.actor(s) pi = tfp.distributions.Normal(mu, sigma) # 用mu和sigma构建正态分布 a = tf.squeeze(pi.sample(1), axis=0)[0] # 根据概率分布随机出动作 return np.clip(a, -self.action_bound, self.action_bound) def store_transition(self, state, action, reward): """ Store state, action, reward at each step """ self.state_buffer.append(state) self.action_buffer.append(action) self.reward_buffer.append(reward) def a_train(self, state, action, adv): """ 更新策略网络(policy network) """ state = np.array(state, np.float32) action = np.array(action, np.float32) adv = np.array(adv, np.float32) with tf.GradientTape() as tape: # 构建两个正态分布pi,oldpi。 mu, sigma = self.actor(state) pi = tfp.distributions.Normal(mu, sigma) mu_old, sigma_old = self.actor_old(state) oldpi = tfp.distributions.Normal(mu_old, sigma_old) # ratio = tf.exp(pi.log_prob(self.tfa) - oldpi.log_prob(self.tfa)) # 在新旧两个分布下,同样输出a的概率的比值 # 除以(oldpi.prob(tfa) + EPS),其实就是做了import-sampling。怎么解释这里好呢 # 本来我们是可以直接用pi.prob(tfa)去跟新的,但为了能够更新多次,我们需要除以(oldpi.prob(tfa) + EPS)。 # 在AC或者PG,我们是以1,0作为更新目标,缩小动作概率到1or0的差距 # 而PPO可以想作是,以oldpi.prob(tfa)出发,不断远离(增大or缩小)的过程。 ratio = pi.prob(action) / (oldpi.prob(action) + EPS) # 这个的意义和带参数更新是一样的。 surr = ratio * adv # 我们还不能让两个分布差异太大。 # PPO1 if METHOD['name'] == 'kl_pen': tflam = METHOD['lam'] kl = tfp.distributions.kl_divergence(oldpi, pi) kl_mean = tf.reduce_mean(kl) aloss = -(tf.reduce_mean(surr - tflam * kl)) else: # clipping method, find this is better aloss = -tf.reduce_mean( tf.minimum(surr, tf.clip_by_value( ratio, 1. - METHOD['epsilon'], 1. + METHOD['epsilon']) * adv) ) a_gard = tape.gradient(aloss, self.actor.trainable_weights) self.actor_opt.apply_gradients(zip(a_gard, self.actor.trainable_weights)) if METHOD['name'] == 'kl_pen': return kl_mean def update_old_pi(self): ''' 更新actor_old参数。 ''' for pi, oldpi in zip(self.actor.trainable_weights, self.actor_old.trainable_weights): oldpi.assign(pi) def c_train(self, reward, state): ''' 更新Critic网络 ''' # reward 是我们预估的 能获得的奖励 reward = np.array(reward, dtype=np.float32) with tf.GradientTape() as tape: advantage = reward - self.critic(state) # td-error loss = tf.reduce_mean(tf.square(advantage)) grad = tape.gradient(loss, self.critic.trainable_weights) self.critic_opt.apply_gradients(zip(grad, self.critic.trainable_weights)) def update(self): ''' Update parameter with the constraint of KL divergent ''' s = np.array(self.state_buffer, np.float32) a = np.array(self.action_buffer, np.float32) r = np.array(self.cumulative_reward_buffer, np.float32) self.update_old_pi() adv = (r - self.critic(s)).numpy() # adv = (adv - adv.mean())/(adv.std()+1e-6) # sometimes helpful # update actor if METHOD['name'] == 'kl_pen': for _ in range(A_UPDATE_STEPS): kl = self.a_train(s, a, adv) if kl > 4 * METHOD['kl_target']: # this in in google's paper break if kl < METHOD['kl_target'] / 1.5: # adaptive lambda, this is in OpenAI's paper METHOD['lam'] /= 2 elif kl > METHOD['kl_target'] * 1.5: METHOD['lam'] *= 2 METHOD['lam'] = np.clip( METHOD['lam'], 1e-4, 10 ) # sometimes explode, this clipping is MorvanZhou's solution # PPO2 clipping method, find this is better (OpenAI's paper) else: for _ in range(A_UPDATE_STEPS): self.a_train(s, a, adv) for _ in range(C_UPDATE_STEPS): self.c_train(r, s) self.state_buffer.clear() self.action_buffer.clear() self.cumulative_reward_buffer.clear() self.reward_buffer.clear() def finish_path(self, next_state, done): """ Calculate cumulative reward :param next_state: :return: None """ if done: v_s_ = 0 else: v_s_ = self.critic(np.array([next_state], dtype=np.float32))[0, 0] discounted_r = [] for r in self.reward_buffer[::-1]: v_s_ = r + GAMMA * v_s_ discounted_r.append(v_s_) discounted_r.reverse() discounted_r = np.array(discounted_r)[:, np.newaxis] self.cumulative_reward_buffer.extend(discounted_r) self.reward_buffer.clear() def save_ckpt(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_NAME])) if not os.path.exists(path): os.makedirs(path) tl.files.save_weights_to_hdf5(os.path.join(path, 'actor.hdf5'), self.actor) tl.files.save_weights_to_hdf5(os.path.join(path, 'actor_old.hdf5'), self.actor_old) tl.files.save_weights_to_hdf5(os.path.join(path, 'critic.hdf5'), self.critic) print('save weights success!') def load_ckpt(self): path = os.path.join('model', '_'.join([ALG_NAME, ENV_NAME])) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'actor.hdf5'), self.actor) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'actor_old.hdf5'), self.actor_old) tl.files.load_hdf5_to_weights_in_order(os.path.join(path, 'critic.hdf5'), self.critic) print("load weight!") if __name__ == '__main__': env = gym.make(ENV_NAME).unwrapped env.seed(RANDOMSEED) np.random.seed(RANDOMSEED) tf.random.set_seed(RANDOMSEED) ppo = PPO( state_dim=env.observation_space.shape[0], action_dim = env.action_space.shape[0], action_bound = env.action_space.high, ) if args.train: all_ep_r = [] for episode in range(EP_MAX): state = env.reset() buffer_s, buffer_a, buffer_r = [], [], [] episode_reward = 0 t0 = time.time() for t in range(EP_LEN): # env.render() action = ppo.choose_action(state) state_, reward, done, _ = env.step(action) ppo.store_transition(state, action, (reward + 8) / 8) state = state_ episode_reward += reward if (t + 1) % BATCH == 0 or t == EP_LEN - 1: ppo.finish_path(state_, done) ppo.update() if episode == 0: all_ep_r.append(episode_reward) else: all_ep_r.append(all_ep_r[-1] * 0.9 + episode_reward * 0.1) print( 'Episode: {}/{} | Episode Reward: {:.4f} | Running Time: {:.4f}'.format( episode, EP_MAX, episode_reward, time.time() - t0 ) ) ppo.save_ckpt() plt.plot(all_ep_r) if not os.path.exists('image'): os.makedirs('image') plt.savefig(os.path.join('image', '_'.join([ALG_NAME, ENV_NAME]))) if args.test: ppo.load_ckpt() for episode in range(10): state = env.reset() rewards = 0 for i in range(EP_LEN): env.render() next_state, reward, done, _ = env.step(ppo.choose_action(state)) rewards += reward state = next_state if done: break print('Testing | Episode: {}/{} | Episode Reward: {:.4f}'.format( episode + 1, 10, rewards)) env.close() ================================================ FILE: code/Q-Learning.py ================================================ import argparse import gym import numpy as np parser = argparse.ArgumentParser() parser.add_argument('--train', dest='train', action='store_true', default=False) parser.add_argument('--test', dest='test', action='store_true', default=True) args = parser.parse_args() class QLearning: def __init__(self, state_dim, action_dim, lr=0.01, gamma=0.9, e_greed=0.1): self.action_dim = action_dim self.lr = lr self.gamma = gamma self.epsilon = e_greed self.Q = np.zeros((state_dim, action_dim)) def sample(self, state): if np.random.uniform() < self.epsilon: action = np.random.choice(self.action_dim) else: action = self.predict(state) return action def predict(self, state): """ 根据输入观察值,预测输出的动作值 """ all_actions = self.Q[state, :] max_action = np.max(all_actions) # 防止最大的 Q 值有多个,找出所有最大的 Q,然后再随机选择 # where函数返回一个 array, 每个元素为下标 max_action_list = np.where(all_actions == max_action)[0] action = np.random.choice(max_action_list) return action def learn(self, state, action, reward, next_state, done): if done: target_q = reward else: target_q = reward + self.gamma * np.max(self.Q[next_state, :]) self.Q[state, action] += self.lr * (target_q - self.Q[state, action]) def save(self): npy_file = './model/qlearning_table.npy' np.save(npy_file, self.Q) print(npy_file + ' saved.') def load(self, npy_file='./model/qlearning_table.npy'): self.Q = np.load(npy_file) print(npy_file + ' loaded.') class Agent: def __init__(self, env): self.env = env self.lr = 0.1, self.gamma = 0.9, self.e_greed = 0.1 # type(self.state_dim): # type(env.observation_space.n): self.model = QLearning(env.observation_space.n, env.action_space.n) def train(self, max_episode): if args.train: for episode in range(max_episode): ep_reward, ep_steps = self.run_episode(render=False) if episode % 20 == 0: print('Episode %03s: steps = %02s , reward = %.1f' % (episode, ep_steps, ep_reward)) self.model.save() if args.test: self.model.load() self.test_episode(render=True) def run_episode(self, render=False): total_reward = 0 total_steps = 0 state = self.env.reset() while True: action = self.model.sample(state) next_state, reward, done, _ = self.env.step(action) # 训练 Q-learning算法 self.model.learn(state, action, reward, next_state, done) state = next_state total_reward += reward total_steps += 1 if render: self.env.render() if done: break return total_reward, total_steps def test_episode(self, render=False): total_reward = 0 actions = [] state = self.env.reset() while True: action = self.model.predict(state) next_state, reward, done, _ = self.env.step(action) state = next_state total_reward += reward actions.append(action) if render: self.env.render() if done: break print('test reward = %.1f' % (total_reward)) print('test action is: ', actions) if __name__ == '__main__': # 使用gym创建迷宫环境,设置is_slippery为False降低环境难度 env = gym.make("FrozenLake-v0", is_slippery=False) agent = Agent(env=env) agent.train(500) ================================================ FILE: code/SAC.py ================================================ """ Soft Actor-Critic (SAC) ------------------ Actor policy in SAC is stochastic, with off-policy training. And 'soft' in SAC indicates the trade-off between the entropy and expected return. The additional consideration of entropy term helps with more explorative policy. And this implementation contains an automatic update for the entropy factor. This version of Soft Actor-Critic (SAC) implementation contains 5 networks: 2 Q net, 2 target Q net, 1 policy net. It uses alpha loss. Reference --------- paper: https://arxiv.org/pdf/1812.05905.pdf Environment --- Openai Gym Pendulum-v0, continuous action space https://gym.openai.com/envs/Pendulum-v0/ Prerequisites -------------- tensorflow >=2.0.0a0 tensorflow-probability 0.6.0 tensorlayer >=2.0.0 && pip install box2d box2d-kengz --user To run ------ python tutorial_SAC.py --train/test """ import argparse import os import random import time import gym import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import tensorflow_probability as tfp import tensorlayer as tl from tensorlayer.layers import Dense from tensorlayer.models import Model Normal = tfp.distributions.Normal tl.logging.set_verbosity(tl.logging.DEBUG) # add arguments in command --train/test parser = argparse.ArgumentParser(description='Train or test neural net motor controller.') parser.add_argument('--train', dest='train', action='store_true', default=False) parser.add_argument('--test', dest='test', action='store_true', default=True) args = parser.parse_args() ##################### hyper parameters #################### ENV_ID = 'Pendulum-v0' # environment id RANDOM_SEED = 2 # random seed RENDER = False # render while training # RL training ALG_NAME = 'SAC' TRAIN_EPISODES = 100 # total number of episodes for training TEST_EPISODES = 10 # total number of episodes for training MAX_STEPS = 200 # total number of steps for each episode EXPLORE_STEPS = 100 # 500 for random action sampling in the beginning of training BATCH_SIZE = 256 # update batch size HIDDEN_DIM = 32 # size of hidden layers for networks UPDATE_ITR = 3 # repeated updates for single step SOFT_Q_LR = 3e-4 # q_net learning rate POLICY_LR = 3e-4 # policy_net learning rate ALPHA_LR = 3e-4 # alpha learning rate POLICY_TARGET_UPDATE_INTERVAL = 3 # delayed update for the policy network and target networks REWARD_SCALE = 1. # value range of reward REPLAY_BUFFER_SIZE = 5e5 # size of the replay buffer AUTO_ENTROPY = True # automatically updating variable alpha for entropy ############################### SAC #################################### class ReplayBuffer: """ a ring buffer for storing transitions and sampling for training :state: (state_dim,) :action: (action_dim,) :reward: (,), scalar :next_state: (state_dim,) :done: (,), scalar (0 and 1) or bool (True and False) """ 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 = int((self.position + 1) % self.capacity) # as a ring buffer def sample(self, BATCH_SIZE): batch = random.sample(self.buffer, BATCH_SIZE) state, action, reward, next_state, done = map(np.stack, zip(*batch)) # stack for each element """ the * serves as unpack: sum(a,b) <=> batch=(a,b), sum(*batch) ; zip: a=[1,2], b=[2,3], zip(a,b) => [(1, 2), (2, 3)] ; the map serves as mapping the function on each list element: map(square, [2,3]) => [4,9] ; np.stack((1,2)) => array([1, 2]) """ return state, action, reward, next_state, done def __len__(self): return len(self.buffer) class SoftQNetwork(Model): """ the network for evaluate values of state-action pairs: Q(s,a) """ def __init__(self, num_inputs, num_actions, hidden_dim, init_w=3e-3): super(SoftQNetwork, self).__init__() input_dim = num_inputs + num_actions w_init = tf.keras.initializers.glorot_normal( seed=None ) # glorot initialization is better than uniform in practice # w_init = tf.random_uniform_initializer(-init_w, init_w) self.linear1 = Dense(n_units=hidden_dim, act=tf.nn.relu, W_init=w_init, in_channels=input_dim, name='q1') self.linear2 = Dense(n_units=hidden_dim, act=tf.nn.relu, W_init=w_init, in_channels=hidden_dim, name='q2') self.linear3 = Dense(n_units=1, W_init=w_init, in_channels=hidden_dim, name='q3') def forward(self, input): x = self.linear1(input) x = self.linear2(x) x = self.linear3(x) return x class PolicyNetwork(Model): """ the network for generating non-determinstic (Gaussian distributed) action from the state input """ def __init__( self, num_inputs, num_actions, hidden_dim, action_range=1., init_w=3e-3, log_std_min=-20, log_std_max=2 ): super(PolicyNetwork, self).__init__() self.log_std_min = log_std_min self.log_std_max = log_std_max w_init = tf.keras.initializers.glorot_normal(seed=None) # w_init = tf.random_uniform_initializer(-init_w, init_w) self.linear1 = Dense(n_units=hidden_dim, act=tf.nn.relu, W_init=w_init, in_channels=num_inputs, name='policy1') self.linear2 = Dense(n_units=hidden_dim, act=tf.nn.relu, W_init=w_init, in_channels=hidden_dim, name='policy2') self.linear3 = Dense(n_units=hidden_dim, act=tf.nn.relu, W_init=w_init, in_channels=hidden_dim, name='policy3') self.mean_linear = Dense( n_units=num_actions, W_init=w_init, b_init=tf.random_uniform_initializer(-init_w, init_w), in_channels=hidden_dim, name='policy_mean' ) self.log_std_linear = Dense( n_units=num_actions, W_init=w_init, b_init=tf.random_uniform_initializer(-init_w, init_w), in_channels=hidden_dim, name='policy_logstd' ) self.action_range = action_range self.num_actions = num_actions def forward(self, state): x = self.linear1(state) x = self.linear2(x) x = self.linear3(x) mean = self.mean_linear(x) log_std = self.log_std_linear(x) log_std = tf.clip_by_value(log_std, self.log_std_min, self.log_std_max) return mean, log_std def evaluate(self, state, epsilon=1e-6): """ generate action with state for calculating gradients """ state = state.astype(np.float32) mean, log_std = self.forward(state) std = tf.math.exp(log_std) # no clip in evaluation, clip affects gradients flow normal = Normal(0, 1) z = normal.sample(mean.shape) action_0 = tf.math.tanh(mean + std * z) # TanhNormal distribution as actions; reparameterization trick action = self.action_range * action_0 # according to original paper, with an extra last term for normalizing different action range log_prob = Normal(mean, std).log_prob(mean + std * z) - tf.math.log(1. - action_0**2 + epsilon) - np.log(self.action_range) # both dims of normal.log_prob and -log(1-a**2) are (N,dim_of_action); # the Normal.log_prob outputs the same dim of input features instead of 1 dim probability, # needs sum up across the dim of actions to get 1 dim probability; or else use Multivariate Normal. log_prob = tf.reduce_sum(log_prob, axis=1)[:, np.newaxis] # expand dim as reduce_sum causes 1 dim reduced return action, log_prob, z, mean, log_std def get_action(self, state, greedy=False): """ generate action with state for interaction with envronment """ mean, log_std = self.forward([state]) std = tf.math.exp(log_std) normal = Normal(0, 1) z = normal.sample(mean.shape) action = self.action_range * tf.math.tanh( mean + std * z ) # TanhNormal distribution as actions; reparameterization trick action = self.action_range * tf.math.tanh(mean) if greedy else action return action.numpy()[0] def sample_action(self, ): """ generate random actions for exploration """ a = tf.random.uniform([self.num_actions], -1, 1) return self.action_range * a.numpy() class SAC: def __init__( self, state_dim, action_dim, action_range, hidden_dim, replay_buffer, SOFT_Q_LR=3e-4, POLICY_LR=3e-4, ALPHA_LR=3e-4 ): self.replay_buffer = replay_buffer # initialize all networks self.soft_q_net1 = SoftQNetwork(state_dim, action_dim, hidden_dim) self.soft_q_net2 = SoftQNetwork(state_dim, action_dim, hidden_dim) self.target_soft_q_net1 = SoftQNetwork(state_dim, action_dim, hidden_dim) self.target_soft_q_net2 = SoftQNetwork(state_dim, action_dim, hidden_dim) self.policy_net = PolicyNetwork(state_dim, action_dim, hidden_dim, action_range) self.soft_q_net1.train() self.soft_q_net2.train() self.target_soft_q_net1.eval() self.target_soft_q_net2.eval() self.policy_net.train() self.log_alpha = tf.Variable(0, dtype=np.float32, name='log_alpha') self.alpha = tf.math.exp(self.log_alpha) print('Soft Q Network (1,2): ', self.soft_q_net1) print('Policy Network: ', self.policy_net) # set mode self.soft_q_net1.train() self.soft_q_net2.train() self.target_soft_q_net1.eval() self.target_soft_q_net2.eval() self.policy_net.train() # initialize weights of target networks self.target_soft_q_net1 = self.target_ini(self.soft_q_net1, self.target_soft_q_net1) self.target_soft_q_net2 = self.target_ini(self.soft_q_net2, self.target_soft_q_net2) self.soft_q_optimizer1 = tf.optimizers.Adam(SOFT_Q_LR) self.soft_q_optimizer2 = tf.optimizers.Adam(SOFT_Q_LR) self.policy_optimizer = tf.optimizers.Adam(POLICY_LR) self.alpha_optimizer = tf.optimizers.Adam(ALPHA_LR) def target_ini(self, net, target_net): """ hard-copy update for initializing target networks """ for target_param, param in zip(target_net.trainable_weights, net.trainable_weights): target_param.assign(param) return target_net def target_soft_update(self, net, target_net, soft_tau): """ soft update the target net with Polyak averaging """ for target_param, param in zip(target_net.trainable_weights, net.trainable_weights): target_param.assign( # copy weight value into target parameters target_param * (1.0 - soft_tau) + param * soft_tau ) return target_net def update(self, batch_size, reward_scale=10., auto_entropy=True, target_entropy=-2, gamma=0.99, soft_tau=1e-2): """ update all networks in SAC """ state, action, reward, next_state, done = self.replay_buffer.sample(batch_size) reward = reward[:, np.newaxis] # expand dim done = done[:, np.newaxis] reward = reward_scale * (reward - np.mean(reward, axis=0)) / ( np.std(reward, axis=0) + 1e-6 ) # normalize with batch mean and std; plus a small number to prevent numerical problem # Training Q Function new_next_action, next_log_prob, _, _, _ = self.policy_net.evaluate(next_state) target_q_input = tf.concat([next_state, new_next_action], 1) # the dim 0 is number of samples target_q_min = tf.minimum( self.target_soft_q_net1(target_q_input), self.target_soft_q_net2(target_q_input) ) - self.alpha * next_log_prob target_q_value = reward + (1 - done) * gamma * target_q_min # if done==1, only reward q_input = tf.concat([state, action], 1) # the dim 0 is number of samples with tf.GradientTape() as q1_tape: predicted_q_value1 = self.soft_q_net1(q_input) q_value_loss1 = tf.reduce_mean(tf.losses.mean_squared_error(predicted_q_value1, target_q_value)) q1_grad = q1_tape.gradient(q_value_loss1, self.soft_q_net1.trainable_weights) self.soft_q_optimizer1.apply_gradients(zip(q1_grad, self.soft_q_net1.trainable_weights)) with tf.GradientTape() as q2_tape: predicted_q_value2 = self.soft_q_net2(q_input) q_value_loss2 = tf.reduce_mean(tf.losses.mean_squared_error(predicted_q_value2, target_q_value)) q2_grad = q2_tape.gradient(q_value_loss2, self.soft_q_net2.trainable_weights) self.soft_q_optimizer2.apply_gradients(zip(q2_grad, self.soft_q_net2.trainable_weights)) # Training Policy Function with tf.GradientTape() as p_tape: new_action, log_prob, z, mean, log_std = self.policy_net.evaluate(state) new_q_input = tf.concat([state, new_action], 1) # the dim 0 is number of samples """ implementation 1 """ predicted_new_q_value = tf.minimum(self.soft_q_net1(new_q_input), self.soft_q_net2(new_q_input)) # """ implementation 2 """ # predicted_new_q_value = self.soft_q_net1(new_q_input) policy_loss = tf.reduce_mean(self.alpha * log_prob - predicted_new_q_value) p_grad = p_tape.gradient(policy_loss, self.policy_net.trainable_weights) self.policy_optimizer.apply_gradients(zip(p_grad, self.policy_net.trainable_weights)) # Updating alpha w.r.t entropy # alpha: trade-off between exploration (max entropy) and exploitation (max Q) if auto_entropy is True: with tf.GradientTape() as alpha_tape: alpha_loss = -tf.reduce_mean((self.log_alpha * (log_prob + target_entropy))) alpha_grad = alpha_tape.gradient(alpha_loss, [self.log_alpha]) self.alpha_optimizer.apply_gradients(zip(alpha_grad, [self.log_alpha])) self.alpha = tf.math.exp(self.log_alpha) else: # fixed alpha self.alpha = 1. alpha_loss = 0 # Soft update the target value nets self.target_soft_q_net1 = self.target_soft_update(self.soft_q_net1, self.target_soft_q_net1, soft_tau) self.target_soft_q_net2 = self.target_soft_update(self.soft_q_net2, self.target_soft_q_net2, soft_tau) def save(self): # save trained weights path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) extend_path = lambda s: os.path.join(path, s) tl.files.save_npz(self.soft_q_net1.trainable_weights, extend_path('model_q_net1.npz')) tl.files.save_npz(self.soft_q_net2.trainable_weights, extend_path('model_q_net2.npz')) tl.files.save_npz(self.target_soft_q_net1.trainable_weights, extend_path('model_target_q_net1.npz')) tl.files.save_npz(self.target_soft_q_net2.trainable_weights, extend_path('model_target_q_net2.npz')) tl.files.save_npz(self.policy_net.trainable_weights, extend_path('model_policy_net.npz')) np.save(extend_path('log_alpha.npy'), self.log_alpha.numpy()) # save log_alpha variable def load_weights(self): # load trained weights path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) extend_path = lambda s: os.path.join(path, s) tl.files.load_and_assign_npz(extend_path('model_q_net1.npz'), self.soft_q_net1) tl.files.load_and_assign_npz(extend_path('model_q_net2.npz'), self.soft_q_net2) tl.files.load_and_assign_npz(extend_path('model_target_q_net1.npz'), self.target_soft_q_net1) tl.files.load_and_assign_npz(extend_path('model_target_q_net2.npz'), self.target_soft_q_net2) tl.files.load_and_assign_npz(extend_path('model_policy_net.npz'), self.policy_net) self.log_alpha.assign(np.load(extend_path('log_alpha.npy'))) # load log_alpha variable if __name__ == '__main__': # initialization of env env = gym.make(ENV_ID).unwrapped state_dim = env.observation_space.shape[0] action_dim = env.action_space.shape[0] action_range = env.action_space.high # scale action, [-action_range, action_range] # reproducible env.seed(RANDOM_SEED) random.seed(RANDOM_SEED) np.random.seed(RANDOM_SEED) tf.random.set_seed(RANDOM_SEED) # initialization of buffer replay_buffer = ReplayBuffer(REPLAY_BUFFER_SIZE) # initialization of trainer agent = SAC(state_dim, action_dim, action_range, HIDDEN_DIM, replay_buffer, SOFT_Q_LR, POLICY_LR, ALPHA_LR) t0 = time.time() # training loop if args.train: frame_idx = 0 all_episode_reward = [] # need an extra call here to make inside functions be able to use model.forward state = env.reset().astype(np.float32) agent.policy_net([state]) for episode in range(TRAIN_EPISODES): state = env.reset().astype(np.float32) episode_reward = 0 for step in range(MAX_STEPS): if RENDER: env.render() if frame_idx > EXPLORE_STEPS: action = agent.policy_net.get_action(state) else: action = agent.policy_net.sample_action() next_state, reward, done, _ = env.step(action) next_state = next_state.astype(np.float32) done = 1 if done is True else 0 replay_buffer.push(state, action, reward, next_state, done) state = next_state episode_reward += reward frame_idx += 1 if len(replay_buffer) > BATCH_SIZE: for i in range(UPDATE_ITR): agent.update( BATCH_SIZE, reward_scale=REWARD_SCALE, auto_entropy=AUTO_ENTROPY, target_entropy=-1. * action_dim ) if done: break if episode == 0: all_episode_reward.append(episode_reward) else: all_episode_reward.append(all_episode_reward[-1] * 0.9 + episode_reward * 0.1) print( 'Training | Episode: {}/{} | Episode Reward: {:.4f} | Running Time: {:.4f}'.format( episode + 1, TRAIN_EPISODES, episode_reward, time.time() - t0 ) ) agent.save() plt.plot(all_episode_reward) if not os.path.exists('image'): os.makedirs('image') plt.savefig(os.path.join('image', '_'.join([ALG_NAME, ENV_ID]))) if args.test: agent.load_weights() # need an extra call here to make inside functions be able to use model.forward state = env.reset().astype(np.float32) agent.policy_net([state]) for episode in range(TEST_EPISODES): state = env.reset().astype(np.float32) episode_reward = 0 for step in range(MAX_STEPS): env.render() state, reward, done, info = env.step(agent.policy_net.get_action(state, greedy=True)) state = state.astype(np.float32) episode_reward += reward if done: break print( 'Testing | Episode: {}/{} | Episode Reward: {:.4f} | Running Time: {:.4f}'.format( episode + 1, TEST_EPISODES, episode_reward, time.time() - t0 ) ) env.close() ================================================ FILE: code/Sarsa.py ================================================ import argparse import gym import numpy as np parser = argparse.ArgumentParser() parser.add_argument('--train', dest='train', action='store_true', default=False) parser.add_argument('--test', dest='test', action='store_true', default=True) args = parser.parse_args() class Sarsa: def __init__(self, state_dim, action_dim, lr=0.01, gamma=0.9, e_greed=0.1): self.action_dim = action_dim self.lr = lr self.gamma = gamma self.epsilon = e_greed self.Q = np.zeros((state_dim, action_dim)) def sample(self, state): if np.random.uniform() < self.epsilon: action = np.random.choice(self.action_dim) else: action = self.predict(state) return action def predict(self, state): """ 根据输入观察值,预测输出的动作值 """ all_actions = self.Q[state, :] max_action = np.max(all_actions) # 防止最大的 Q 值有多个,找出所有最大的 Q,然后再随机选择 # where函数返回一个 array, 每个元素为下标 max_action_list = np.where(all_actions == max_action)[0] action = np.random.choice(max_action_list) return action def learn(self, state, action, reward, next_state, next_action, done): if done: target_q = reward else: target_q = reward + self.gamma * self.Q[next_state, next_action] self.Q[state, action] += self.lr * (target_q - self.Q[state, action]) def save(self): npy_file = './model/sarsa_q_table.npy' np.save(npy_file, self.Q) print(npy_file + ' saved.') def load(self, npy_file='./model/sarsa_q_table.npy'): self.Q = np.load(npy_file) print(npy_file + ' loaded.') class Agent: def __init__(self, env): self.env = env self.lr = 0.1, self.gamma = 0.9, self.e_greed = 0.1 # type(self.state_dim): # type(env.observation_space.n): self.model = Sarsa(env.observation_space.n, env.action_space.n) def train(self, max_episode): if args.train: for episode in range(max_episode): ep_reward, ep_steps = self.run_episode(render=False) if episode % 20 == 0: print('Episode %03s: steps = %02s , reward = %.1f' % (episode, ep_steps, ep_reward)) self.model.save() if args.test: self.model.load() self.test_episode(render=True) def run_episode(self, render=False): total_reward = 0 total_steps = 0 state = self.env.reset() action = self.model.sample(state) while True: next_state, reward, done, _ = self.env.step(action) next_action = self.model.sample(next_state) # 训练 Q-learning算法 self.model.learn(state, action, reward, next_state, next_action, done) state = next_state action = next_action total_reward += reward total_steps += 1 if render: self.env.render() if done: break return total_reward, total_steps def test_episode(self, render=False): total_reward = 0 actions = [] state = self.env.reset() while True: action = self.model.predict(state) next_state, reward, done, _ = self.env.step(action) state = next_state total_reward += reward actions.append(action) if render: self.env.render() if done: break print('test reward = %.1f' % (total_reward)) print('test action is: ', actions) # if __name__ == '__main__': # # 使用gym创建迷宫环境,设置is_slippery为False降低环境难度 # env = gym.make("FrozenLake-v0", is_slippery=False) # # agent = Agent(env=env) # agent.train(500) if __name__ == '__main__': # 使用gym创建迷宫环境,设置is_slippery为False降低环境难度 env = gym.make("FrozenLake-v0", is_slippery=False) agent = Agent(env=env) agent.train(500) ================================================ FILE: code/TD3.py ================================================ """ Twin Delayed DDPG (TD3) ------------------------ DDPG suffers from problems like overestimate of Q-values and sensitivity to hyper-parameters. Twin Delayed DDPG (TD3) is a variant of DDPG with several tricks: * Trick One: Clipped Double-Q Learning. TD3 learns two Q-functions instead of one (hence "twin"), and uses the smaller of the two Q-values to form the targets in the Bellman error loss functions. * Trick Two: "Delayed" Policy Updates. TD3 updates the policy (and target networks) less frequently than the Q-function. * Trick Three: Target Policy Smoothing. TD3 adds noise to the target action, to make it harder for the policy to exploit Q-function errors by smoothing out Q along changes in action. The implementation of TD3 includes 6 networks: 2 Q-net, 2 target Q-net, 1 policy net, 1 target policy net Actor policy in TD3 is deterministic, with Gaussian exploration noise. Reference --------- original paper: https://arxiv.org/pdf/1802.09477.pdf Environment --- Openai Gym Pendulum-v0, continuous action space https://gym.openai.com/envs/Pendulum-v0/ Prerequisites --- tensorflow >=2.0.0a0 tensorflow-probability 0.6.0 tensorlayer >=2.0.0 && pip install box2d box2d-kengz --user To run ------- python tutorial_TD3.py --train/test """ import argparse import os import random import time import gym import matplotlib.pyplot as plt import numpy as np import tensorflow as tf import tensorflow_probability as tfp import tensorlayer as tl from tensorlayer.layers import Dense from tensorlayer.models import Model Normal = tfp.distributions.Normal tl.logging.set_verbosity(tl.logging.DEBUG) # add arguments in command --train/test parser = argparse.ArgumentParser(description='Train or test neural net motor controller.') parser.add_argument('--train', dest='train', action='store_true', default=False) parser.add_argument('--test', dest='test', action='store_true', default=True) args = parser.parse_args() ##################### hyper parameters #################### # choose env ENV_ID = 'Pendulum-v0' # environment id RANDOM_SEED = 2 # random seed RENDER = False # render while training # RL training ALG_NAME = 'TD3' TRAIN_EPISODES = 100 # total number of episodes for training TEST_EPISODES = 10 # total number of episodes for training MAX_STEPS = 200 # maximum number of steps for one episode BATCH_SIZE = 64 # update batch size EXPLORE_STEPS = 500 # 500 for random action sampling in the beginning of training HIDDEN_DIM = 64 # size of hidden layers for networks UPDATE_ITR = 3 # repeated updates for single step Q_LR = 3e-4 # q_net learning rate POLICY_LR = 3e-4 # policy_net learning rate POLICY_TARGET_UPDATE_INTERVAL = 3 # delayed steps for updating the policy network and target networks EXPLORE_NOISE_SCALE = 1.0 # range of action noise for exploration EVAL_NOISE_SCALE = 0.5 # range of action noise for evaluation of action value REWARD_SCALE = 1. # value range of reward REPLAY_BUFFER_SIZE = 5e5 # size of replay buffer ############################### TD3 #################################### class ReplayBuffer: """ a ring buffer for storing transitions and sampling for training :state: (state_dim,) :action: (action_dim,) :reward: (,), scalar :next_state: (state_dim,) :done: (,), scalar (0 and 1) or bool (True and False) """ 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 = int((self.position + 1) % self.capacity) # as a ring buffer def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) state, action, reward, next_state, done = map(np.stack, zip(*batch)) # stack for each element return state, action, reward, next_state, done def __len__(self): return len(self.buffer) class QNetwork(Model): """ the network for evaluate values of state-action pairs: Q(s,a) """ def __init__(self, num_inputs, num_actions, hidden_dim, init_w=3e-3): super(QNetwork, self).__init__() input_dim = num_inputs + num_actions w_init = tf.random_uniform_initializer(-init_w, init_w) self.linear1 = Dense(n_units=hidden_dim, act=tf.nn.relu, W_init=w_init, in_channels=input_dim, name='q1') self.linear2 = Dense(n_units=hidden_dim, act=tf.nn.relu, W_init=w_init, in_channels=hidden_dim, name='q2') self.linear3 = Dense(n_units=1, W_init=w_init, in_channels=hidden_dim, name='q3') def forward(self, input): x = self.linear1(input) x = self.linear2(x) x = self.linear3(x) return x class PolicyNetwork(Model): """ the network for generating non-determinstic (Gaussian distributed) action from the state input """ def __init__(self, num_inputs, num_actions, hidden_dim, action_range=1., init_w=3e-3): super(PolicyNetwork, self).__init__() w_init = tf.random_uniform_initializer(-init_w, init_w) self.linear1 = Dense(n_units=hidden_dim, act=tf.nn.relu, W_init=w_init, in_channels=num_inputs, name='policy1') self.linear2 = Dense(n_units=hidden_dim, act=tf.nn.relu, W_init=w_init, in_channels=hidden_dim, name='policy2') self.linear3 = Dense(n_units=hidden_dim, act=tf.nn.relu, W_init=w_init, in_channels=hidden_dim, name='policy3') self.output_linear = Dense( n_units=num_actions, W_init=w_init, b_init=tf.random_uniform_initializer(-init_w, init_w), in_channels=hidden_dim, name='policy_output' ) self.action_range = action_range self.num_actions = num_actions def forward(self, state): x = self.linear1(state) x = self.linear2(x) x = self.linear3(x) output = tf.nn.tanh(self.output_linear(x)) # unit range output [-1, 1] return output def evaluate(self, state, eval_noise_scale): """ generate action with state for calculating gradients; eval_noise_scale: as the trick of target policy smoothing, for generating noisy actions. """ state = state.astype(np.float32) action = self.forward(state) action = self.action_range * action # add noise normal = Normal(0, 1) noise = normal.sample(action.shape) * eval_noise_scale eval_noise_clip = 2 * eval_noise_scale noise = tf.clip_by_value(noise, -eval_noise_clip, eval_noise_clip) action = action + noise return action def get_action(self, state, explore_noise_scale, greedy=False): """ generate action with state for interaction with environment """ action = self.forward([state]) action = self.action_range * action.numpy()[0] if greedy: return action # add noise normal = Normal(0, 1) noise = normal.sample(action.shape) * explore_noise_scale action += noise return action.numpy() def sample_action(self): """ generate random actions for exploration """ a = tf.random.uniform([self.num_actions], -1, 1) return self.action_range * a.numpy() class TD3: def __init__( self, state_dim, action_dim, action_range, hidden_dim, replay_buffer, policy_target_update_interval=1, q_lr=3e-4, policy_lr=3e-4 ): self.replay_buffer = replay_buffer # initialize all networks self.q_net1 = QNetwork(state_dim, action_dim, hidden_dim) self.q_net2 = QNetwork(state_dim, action_dim, hidden_dim) self.target_q_net1 = QNetwork(state_dim, action_dim, hidden_dim) self.target_q_net2 = QNetwork(state_dim, action_dim, hidden_dim) self.policy_net = PolicyNetwork(state_dim, action_dim, hidden_dim, action_range) self.target_policy_net = PolicyNetwork(state_dim, action_dim, hidden_dim, action_range) print('Q Network (1,2): ', self.q_net1) print('Policy Network: ', self.policy_net) # initialize weights of target networks self.target_q_net1 = self.target_ini(self.q_net1, self.target_q_net1) self.target_q_net2 = self.target_ini(self.q_net2, self.target_q_net2) self.target_policy_net = self.target_ini(self.policy_net, self.target_policy_net) # set train mode self.q_net1.train() self.q_net2.train() self.target_q_net1.eval() self.target_q_net2.eval() self.policy_net.train() self.target_policy_net.eval() self.update_cnt = 0 self.policy_target_update_interval = policy_target_update_interval self.q_optimizer1 = tf.optimizers.Adam(q_lr) self.q_optimizer2 = tf.optimizers.Adam(q_lr) self.policy_optimizer = tf.optimizers.Adam(policy_lr) def target_ini(self, net, target_net): """ hard-copy update for initializing target networks """ for target_param, param in zip(target_net.trainable_weights, net.trainable_weights): target_param.assign(param) return target_net def target_soft_update(self, net, target_net, soft_tau): """ soft update the target net with Polyak averaging """ for target_param, param in zip(target_net.trainable_weights, net.trainable_weights): target_param.assign( # copy weight value into target parameters target_param * (1.0 - soft_tau) + param * soft_tau ) return target_net def update(self, batch_size, eval_noise_scale, reward_scale=10., gamma=0.9, soft_tau=1e-2): """ update all networks in TD3 """ self.update_cnt += 1 state, action, reward, next_state, done = self.replay_buffer.sample(batch_size) reward = reward[:, np.newaxis] # expand dim done = done[:, np.newaxis] new_next_action = self.target_policy_net.evaluate( next_state, eval_noise_scale=eval_noise_scale ) # clipped normal noise reward = reward_scale * (reward - np.mean(reward, axis=0)) / ( np.std(reward, axis=0) + 1e-6 ) # normalize with batch mean and std; plus a small number to prevent numerical problem # Training Q Function target_q_input = tf.concat([next_state, new_next_action], 1) # the dim 0 is number of samples target_q_min = tf.minimum(self.target_q_net1(target_q_input), self.target_q_net2(target_q_input)) target_q_value = reward + (1 - done) * gamma * target_q_min # if done==1, only reward q_input = tf.concat([state, action], 1) # input of q_net with tf.GradientTape() as q1_tape: predicted_q_value1 = self.q_net1(q_input) q_value_loss1 = tf.reduce_mean(tf.square(predicted_q_value1 - target_q_value)) q1_grad = q1_tape.gradient(q_value_loss1, self.q_net1.trainable_weights) self.q_optimizer1.apply_gradients(zip(q1_grad, self.q_net1.trainable_weights)) with tf.GradientTape() as q2_tape: predicted_q_value2 = self.q_net2(q_input) q_value_loss2 = tf.reduce_mean(tf.square(predicted_q_value2 - target_q_value)) q2_grad = q2_tape.gradient(q_value_loss2, self.q_net2.trainable_weights) self.q_optimizer2.apply_gradients(zip(q2_grad, self.q_net2.trainable_weights)) # Training Policy Function if self.update_cnt % self.policy_target_update_interval == 0: with tf.GradientTape() as p_tape: # 更新actor的时候,我们不需要加上noise,这里是希望actor能够寻着最大值。加上noise并没有任何意义 new_action = self.policy_net.evaluate( state, eval_noise_scale=0.0 ) # no noise, deterministic policy gradients new_q_input = tf.concat([state, new_action], 1) # """ implementation 1 """ # predicted_new_q_value = tf.minimum(self.q_net1(new_q_input),self.q_net2(new_q_input)) """ implementation 2 """ predicted_new_q_value = self.q_net1(new_q_input) policy_loss = -tf.reduce_mean(predicted_new_q_value) p_grad = p_tape.gradient(policy_loss, self.policy_net.trainable_weights) self.policy_optimizer.apply_gradients(zip(p_grad, self.policy_net.trainable_weights)) # Soft update the target nets self.target_q_net1 = self.target_soft_update(self.q_net1, self.target_q_net1, soft_tau) self.target_q_net2 = self.target_soft_update(self.q_net2, self.target_q_net2, soft_tau) self.target_policy_net = self.target_soft_update(self.policy_net, self.target_policy_net, soft_tau) def save(self): # save trained weights path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) extend_path = lambda s: os.path.join(path, s) tl.files.save_npz(self.q_net1.trainable_weights, extend_path('model_q_net1.npz')) tl.files.save_npz(self.q_net2.trainable_weights, extend_path('model_q_net2.npz')) tl.files.save_npz(self.target_q_net1.trainable_weights, extend_path('model_target_q_net1.npz')) tl.files.save_npz(self.target_q_net2.trainable_weights, extend_path('model_target_q_net2.npz')) tl.files.save_npz(self.policy_net.trainable_weights, extend_path('model_policy_net.npz')) tl.files.save_npz(self.target_policy_net.trainable_weights, extend_path('model_target_policy_net.npz')) def load(self): # load trained weights path = os.path.join('model', '_'.join([ALG_NAME, ENV_ID])) extend_path = lambda s: os.path.join(path, s) tl.files.load_and_assign_npz(extend_path('model_q_net1.npz'), self.q_net1) tl.files.load_and_assign_npz(extend_path('model_q_net2.npz'), self.q_net2) tl.files.load_and_assign_npz(extend_path('model_target_q_net1.npz'), self.target_q_net1) tl.files.load_and_assign_npz(extend_path('model_target_q_net2.npz'), self.target_q_net2) tl.files.load_and_assign_npz(extend_path('model_policy_net.npz'), self.policy_net) tl.files.load_and_assign_npz(extend_path('model_target_policy_net.npz'), self.target_policy_net) if __name__ == '__main__': # initialization of env env = gym.make(ENV_ID).unwrapped state_dim = env.observation_space.shape[0] action_dim = env.action_space.shape[0] action_range = env.action_space.high # scale action, [-action_range, action_range] # reproducible env.seed(RANDOM_SEED) random.seed(RANDOM_SEED) np.random.seed(RANDOM_SEED) tf.random.set_seed(RANDOM_SEED) # initialization of buffer replay_buffer = ReplayBuffer(REPLAY_BUFFER_SIZE) # initialization of trainer agent = TD3( state_dim, action_dim, action_range, HIDDEN_DIM, replay_buffer, POLICY_TARGET_UPDATE_INTERVAL, Q_LR, POLICY_LR ) t0 = time.time() # training loop if args.train: frame_idx = 0 all_episode_reward = [] # need an extra call here to make inside functions be able to use model.forward state = env.reset().astype(np.float32) agent.policy_net([state]) agent.target_policy_net([state]) for episode in range(TRAIN_EPISODES): state = env.reset().astype(np.float32) episode_reward = 0 for step in range(MAX_STEPS): if RENDER: env.render() if frame_idx > EXPLORE_STEPS: action = agent.policy_net.get_action(state, EXPLORE_NOISE_SCALE) else: action = agent.policy_net.sample_action() next_state, reward, done, _ = env.step(action) next_state = next_state.astype(np.float32) done = 1 if done is True else 0 replay_buffer.push(state, action, reward, next_state, done) state = next_state episode_reward += reward frame_idx += 1 if len(replay_buffer) > BATCH_SIZE: for i in range(UPDATE_ITR): agent.update(BATCH_SIZE, EVAL_NOISE_SCALE, REWARD_SCALE) if done: break if episode == 0: all_episode_reward.append(episode_reward) else: all_episode_reward.append(all_episode_reward[-1] * 0.9 + episode_reward * 0.1) print( 'Training | Episode: {}/{} | Episode Reward: {:.4f} | Running Time: {:.4f}'.format( episode + 1, TRAIN_EPISODES, episode_reward, time.time() - t0 ) ) agent.save() plt.plot(all_episode_reward) if not os.path.exists('image'): os.makedirs('image') plt.savefig(os.path.join('image', '_'.join([ALG_NAME, ENV_ID]))) if args.test: agent.load() # need an extra call here to make inside functions be able to use model.forward state = env.reset().astype(np.float32) agent.policy_net([state]) for episode in range(TEST_EPISODES): state = env.reset().astype(np.float32) episode_reward = 0 for step in range(MAX_STEPS): env.render() action = agent.policy_net.get_action(state, EXPLORE_NOISE_SCALE, greedy=True) state, reward, done, info = env.step(action) state = state.astype(np.float32) episode_reward += reward if done: break print( 'Testing | Episode: {}/{} | Episode Reward: {:.4f} | Running Time: {:.4f}'.format( episode + 1, TEST_EPISODES, episode_reward, time.time() - t0 ) ) env.close() ================================================ FILE: code_pytorch/DQN.py ================================================ import gym import numpy as np import os import torch import time import torch.nn as nn import torch.nn.functional as F from network import BasicQNetwork from buffer import ReplayBUffer from parameter import get_common_args algo = "DQN" env_name = "CartPole-v1" isTrain = True isTest = False class Agent(): def __init__(self, state_dim, action_dim, args, double_dqn = False): self.replay_buffer = ReplayBUffer(args) self.state_dim = state_dim self.action_dim = action_dim self.gamma = args.gamma self.tau = args.tau self.learning_rate = args.lr_q self.batch_size = args.batch_size self.epsilon = args.epsilon self.epsilon_min = args.epsilon_min self.epsilon_delta = args.epsilon_delta self.double_dqn = double_dqn self.device = args.device self.q_network = BasicQNetwork(self.state_dim, self.action_dim, args) self.target_q_network = BasicQNetwork(self.state_dim, self.action_dim, args) self.optimizer = torch.optim.Adam(self.q_network.parameters(), lr=self.learning_rate) self.loss_function = nn.MSELoss() for p in self.target_q_network.parameters(): p.requires_grad = False def e_greedy_action(self,state): with torch.no_grad(): state = torch.FloatTensor(state).view(1, -1).to(self.device) if np.random.rand() < self.epsilon: action = np.random.randint(0,self.action_dim) else: action = self.q_network(state).argmax().item() self.epsilon = max(self.epsilon_min, self.epsilon-self.epsilon_delta) return action def action(self,state): with torch.no_grad(): state = torch.FloatTensor(state.reshape(1, -1)).to(self.device) action = self.q_network(state).argmax().item() return action def learn(self): #update the param of target network for param, target_param in zip(self.q_network.parameters(), self.target_q_network.parameters()): target_param.data.copy_(self.tau * param.data + (1 - self.tau) * target_param.data) state_batch, action_batch, reward_batch, next_state_batch, done_batch = self.replay_buffer.sample(self.batch_size) state_batch = torch.FloatTensor(state_batch).to(self.device) action_batch = torch.LongTensor(action_batch).to(self.device).view(self.batch_size,1) reward_batch = torch.FloatTensor(reward_batch).to(self.device).view(self.batch_size,1) next_state_batch = torch.FloatTensor(next_state_batch).to(self.device) done_batch = torch.FloatTensor(done_batch).to(self.device).view(self.batch_size,1) with torch.no_grad(): if self.double_dqn: argmax_a = self.q_network(next_state_batch).argmax(dim=1).unsqueeze(-1) next_q_value = self.target_q_network(next_state_batch).gather(1,argmax_a) else: next_q_value = self.target_q_network(next_state_batch).max(1)[0].view(self.batch_size,1) #self.eval_net(state_batch)得到一个batch_size x action_size的tensor #.gather(1,action_batch)获取得到的tensor位置上对应的动作所对应的Q值,得到一个actio_batch一样的tensor q_value = self.q_network(state_batch).gather(1,action_batch) target_q_value = reward_batch + (1 - done_batch) * self.gamma * next_q_value loss = self.loss_function(q_value,target_q_value) self.optimizer.zero_grad() loss.backward() self.optimizer.step() # print("q_network = ", print(self.q_network.state_dict()['layer1.weight'])) return loss.item() def test_episode(self, test_episodes): for episode in range(test_episodes): state = self.env.reset().astype(np.float32) total_reward, done = 0, False while not done: action = self.model(np.array([state], dtype=np.float32))[0] action = np.argmax(action) next_state, reward, done, _ = self.env.step(action) next_state = next_state.astype(np.float32) total_reward += reward state = next_state self.env.render() print("Test {} | episode rewards is {}".format(episode, total_reward)) def train(self, train_episodes=200): if isTrain: for episode in range(train_episodes): total_reward, done = 0, False state = self.env.reset().astype(np.float32) while not done: action = self.choose_action(state) next_state, reward, done, _ = self.env.step(action) next_state = next_state.astype(np.float32) self.buffer.push(state, action, reward, next_state, done) total_reward += reward state = next_state # self.render() if len(self.buffer.buffer) > args.batch_size: self.replay() self.target_update() print('EP{} EpisodeReward={}'.format(episode, total_reward)) self.saveModel() if isTest: self.loadModel() self.test_episode(test_episodes=args.test_episodes) def save(self): if not self.double_dqn: dir = 'model_torch/{}_{}'.format(algo, env_name) else: dir = 'model_torch/{}'.format(algo, env_name) if not os.path.exists(dir): # make the path print("dont have this dir") os.mkdir(dir) dir = dir + '/model.pth' torch.save(self.q_network.state_dict(), dir) def load(self, path): self.q_network.load_state_dict(torch.load(path)) print('q_network load successed') if __name__ == '__main__': args = get_common_args() env = gym.make(env_name) agent = Agent() ================================================ FILE: code_pytorch/PG_Continue.py ================================================ import argparse import math import os import time import gym import matplotlib.pyplot as plt import numpy as np import torch from torch import distributions import torch.nn as nn import torch.nn.functional as F from torch.distributions import Categorical import torch.distributions parser = argparse.ArgumentParser(description='Train or test neural net motor controller.') parser.add_argument('--train', dest='train', action='store_true', default=True) parser.add_argument('--test', dest='test', action='store_true', default=False) args = parser.parse_args() ##################### hyper parameters #################### ENV_ID = 'Pendulum-v1' # environment id RANDOM_SEED = 1 # random seed, can be either an int number or None RENDER = False # render while training ALG_NAME = 'PG-Continue' TRAIN_EPISODES = 200 TEST_EPISODES = 10 MAX_STEPS = 200 hidden_1 = 32 hidden_2 = 16 var = 1 var_delta = 0.999 ############################### PG #################################### class Net(nn.Module): def __init__(self, state_dim, action_dim, action_bound): super(Net, self).__init__() self.layer1 = nn.Linear(state_dim, hidden_1) self.layer2 = nn.Linear(hidden_1, hidden_2) self.output = nn.Linear(hidden_2, action_dim) self.action_bound = action_bound def forward(self, state): x = F.relu(self.layer1(state)) x = F.relu(self.layer2(x)) x = self.output(x) output = torch.tanh(x) * self.action_bound return output class PolicyGradient: def __init__(self, state_dim, action_dim, action_bound, learning_rate=0.01, gamma=0.9): self.gamma = gamma self.state_buffer, self.action_buffer, self.reward_buffer = [], [], [] self.action_bound = action_bound self.var = var self.var_delta = var_delta self.model = Net(state_dim, action_dim, action_bound) self.optimizer = torch.optim.Adam(self.model.parameters(), learning_rate) def get_action(self, state, greedy=False): state = torch.FloatTensor(state).view(1, -1) action = self.model(state) if greedy: return action[0].item() with torch.no_grad(): action = np.clip(np.random.normal(action[0], self.var), -self.action_bound, self.action_bound) self.var = self.var * self.var_delta return action def store_transition(self, s, a, r): self.state_buffer.append(s) self.action_buffer.append(a) self.reward_buffer.append(r) def learn(self): """ update policy parameters via stochastic gradient ascent :return: None """ discounted_reward = self._discount_and_norm_rewards() state = torch.FloatTensor(self.state_buffer) reward = torch.FloatTensor(discounted_reward) mu = self.model(state) pi = distributions.Normal(mu, self.var) action = np.clip(pi.sample(sample_shape=mu.shape), -self.action_bound, self.action_bound) loss = -torch.sum(pi.log_prob(action) * reward) self.optimizer.zero_grad() loss.backward() self.optimizer.step() self.state_buffer, self.action_buffer, self.reward_buffer = [], [], [] # empty episode data def _discount_and_norm_rewards(self): """ compute discount_and_norm_rewards :return: discount_and_norm_rewards """ # discount episode rewards discounted_reward_buffer = np.zeros_like(self.reward_buffer) running_add = 0 for t in reversed(range(0, len(self.reward_buffer))): running_add = running_add * self.gamma + self.reward_buffer[t] discounted_reward_buffer[t] = running_add # normalize episode rewards discounted_reward_buffer -= np.mean(discounted_reward_buffer) discounted_reward_buffer /= np.std(discounted_reward_buffer) return discounted_reward_buffer def save(self): """ save trained weights :return: None """ path = os.path.join('model_torch', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) path = path + '/model.pth' torch.save(self.model.state_dict(), path) def load(self): """ load trained weights :return: None """ path = os.path.join('model_torch', '_'.join([ALG_NAME, ENV_ID])) path = path + '/model.pth' self.model.load_state_dict(torch.load(path)) print("load parameters successed") if __name__ == '__main__': env = gym.make(ENV_ID).unwrapped # reproducible np.random.seed(RANDOM_SEED) torch.manual_seed(RANDOM_SEED) env.seed(RANDOM_SEED) agent = PolicyGradient( action_dim=env.action_space.shape[0], state_dim=env.observation_space.shape[0], action_bound=env.action_space.high[0] ) t0 = time.time() if args.train: all_episode_reward = [] for episode in range(TRAIN_EPISODES): state = env.reset() episode_reward = 0 for step in range(MAX_STEPS): # in one episode if RENDER: env.render() action = agent.get_action(state) next_state, reward, done, _ = env.step(action) agent.store_transition(state, action, reward) state = next_state episode_reward += reward if done: break agent.learn() print( 'Training | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}'.format( episode + 1, TRAIN_EPISODES, episode_reward, time.time() - t0 ) ) if episode == 0: all_episode_reward.append(episode_reward) else: all_episode_reward.append(all_episode_reward[-1] * 0.9 + episode_reward * 0.1) env.close() agent.save() plt.plot(all_episode_reward) if not os.path.exists('image_torch'): os.makedirs('image_torch') plt.savefig(os.path.join('image_torch', '_'.join([ALG_NAME, ENV_ID]))) if args.test: # test agent.load() for episode in range(TEST_EPISODES): state = env.reset() episode_reward = 0 for step in range(MAX_STEPS): env.render() action = agent.get_action(state, True) action = [action] state, reward, done, info = env.step(action) episode_reward += reward if done: break print( 'Testing | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}'.format( episode + 1, TEST_EPISODES, episode_reward, time.time() - t0 ) ) env.close() ================================================ FILE: code_pytorch/PG_Discreate.py ================================================ import argparse import os import time import gym import matplotlib.pyplot as plt import numpy as np import torch import torch.nn as nn from torch.distributions import Categorical parser = argparse.ArgumentParser(description='Train or test neural net motor controller.') parser.add_argument('--train', dest='train', action='store_true', default=True) parser.add_argument('--test', dest='test', action='store_true', default=True) args = parser.parse_args() ##################### hyper parameters #################### ENV_ID = 'CartPole-v1' # environment id RANDOM_SEED = 1 # random seed, can be either an int number or None RENDER = False # render while training ALG_NAME = 'PG-Discreate' TRAIN_EPISODES = 200 TEST_EPISODES = 10 MAX_STEPS = 500 hidden_node = 32 ############################### PG #################################### class Net(nn.Module): def __init__(self, s_dim, hidden, a_num): super(Net, self).__init__() self.net = nn.Sequential(nn.Linear(s_dim, hidden), nn.ReLU(), nn.Linear(hidden, a_num), nn.Softmax(dim=1)) def forward(self, s): return self.net(s) class PolicyGradient: def __init__(self, state_dim, action_num, learning_rate=0.01, gamma=0.9): self.gamma = gamma self.state_buffer, self.action_buffer, self.reward_buffer = [], [], [] self.model = Net(state_dim, hidden_node, action_num) self.optimizer = torch.optim.Adam(self.model.parameters(), learning_rate) def get_action(self, s, greedy=False): """ choose action with probabilities. :param s: state :param greedy: choose action greedy or not :return: act """ s = torch.FloatTensor(s).view(1, -1) _probs = self.model(s) dist = Categorical(_probs) if greedy: return torch.argmax(_probs).detach().item() action = (dist.sample()).detach().item() return action def store_transition(self, s, a, r): self.state_buffer.append(s) self.action_buffer.append(a) self.reward_buffer.append(r) def learn(self): """ update policy parameters via stochastic gradient ascent :return: None """ discounted_reward = self._discount_and_norm_rewards() state = torch.FloatTensor(self.state_buffer) action = torch.FloatTensor(self.action_buffer) reward = torch.FloatTensor(discounted_reward) prob = self.model(state) dist = Categorical(probs=prob) loss = -torch.sum(dist.log_prob(action) * reward) self.optimizer.zero_grad() loss.backward() self.optimizer.step() self.state_buffer, self.action_buffer, self.reward_buffer = [], [], [] # empty episode data def _discount_and_norm_rewards(self): """ compute discount_and_norm_rewards :return: discount_and_norm_rewards """ # discount episode rewards discounted_reward_buffer = np.zeros_like(self.reward_buffer) running_add = 0 for t in reversed(range(0, len(self.reward_buffer))): running_add = running_add * self.gamma + self.reward_buffer[t] discounted_reward_buffer[t] = running_add # normalize episode rewards discounted_reward_buffer -= np.mean(discounted_reward_buffer) discounted_reward_buffer /= np.std(discounted_reward_buffer) return discounted_reward_buffer def save(self): """ save trained weights :return: None """ path = os.path.join('model_torch', '_'.join([ALG_NAME, ENV_ID])) if not os.path.exists(path): os.makedirs(path) path = path + '/model.pth' torch.save(self.model.state_dict(), path) def load(self): """ load trained weights :return: None """ path = os.path.join('model_torch', '_'.join([ALG_NAME, ENV_ID])) path = path + '/model.pth' self.model.load_state_dict(torch.load(path)) print("load parameters successed") if __name__ == '__main__': env = gym.make(ENV_ID).unwrapped # reproducible np.random.seed(RANDOM_SEED) torch.manual_seed(RANDOM_SEED) env.seed(RANDOM_SEED) agent = PolicyGradient( action_num=env.action_space.n, state_dim=env.observation_space.shape[0], ) t0 = time.time() if args.train: all_episode_reward = [] for episode in range(TRAIN_EPISODES): state = env.reset() episode_reward = 0 for step in range(MAX_STEPS): # in one episode if RENDER: env.render() action = agent.get_action(state) next_state, reward, done, _ = env.step(action) agent.store_transition(state, action, reward) state = next_state episode_reward += reward if done: break agent.learn() print( 'Training | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}'.format( episode + 1, TRAIN_EPISODES, episode_reward, time.time() - t0 ) ) if episode == 0: all_episode_reward.append(episode_reward) else: all_episode_reward.append(all_episode_reward[-1] * 0.9 + episode_reward * 0.1) env.close() agent.save() plt.plot(all_episode_reward) if not os.path.exists('image_torch'): os.makedirs('image_torch') plt.savefig(os.path.join('image_torch', '_'.join([ALG_NAME, ENV_ID]))) if args.test: # test agent.load() for episode in range(TEST_EPISODES): state = env.reset() episode_reward = 0 for step in range(MAX_STEPS): env.render() action = agent.get_action(state, True) state, reward, done, info = env.step(action) episode_reward += reward if done: break print( 'Testing | Episode: {}/{} | Episode Reward: {:.0f} | Running Time: {:.4f}'.format( episode + 1, TEST_EPISODES, episode_reward, time.time() - t0 ) ) env.close() ================================================ FILE: code_pytorch/buffer.py ================================================ import random import numpy as np class ReplayBUffer: def __init__(self, args): random.seed(args.seed) self.size = args.replay_buffer_size self.batch_size = args.batch_size self.buffer = [] self.current_idx = 0 def remember(self, state, action, reward, next_state, done): if len(self.buffer) < self.size: self.buffer.append(None) self.buffer[self.current_idx] = (state, action, reward, next_state, done) self.current_idx = (self.current_idx + 1) % self.size def sample(self, batch_size): batch = random.sample(self.buffer, batch_size) state, action, reward, next_state, done = map(np.stack, zip(*batch)) return state, action, reward, next_state, done def __len__(self): return len(self.buffer) ================================================ FILE: code_pytorch/network.py ================================================ import torch import torch.nn as nn import torch.nn.functional as F class BasicQNetwork(nn.Module): def __init__(self, input_size, action_size, args): super(BasicQNetwork, self).__init__() # print("normal weight") self.layer1 = nn.Linear(input_size, args.num_units_1) self.layer1.weight.data.normal_(0, 1) self.layer2 = nn.Linear(args.num_units_1, args.num_units_2) self.layer2.weight.data.normal_(0, 1) self.layer_out = nn.Linear(args.num_units_2, action_size) self.layer_out.weight.data.normal_(0, 1) def forward(self, q_network_input): x = F.relu(self.layer1(q_network_input)) x = F.relu(self.layer2(x)) x = self.layer_out(x) x = F.relu(x) return x ================================================ FILE: code_pytorch/parameter.py ================================================ import argparse def get_common_args(): parser = argparse.ArgumentParser() parser.add_argument('--seed', type=int, default=123, help='random seed') parser.add_argument('--replay_buffer_size', type=int, default=10000, help='the size of replay buffer') parser.add_argument('--batch_size', type=int, default=512, help='the size of batch') parser.add_argument('--gamma', type=float, default=0.99, help='discount factor') parser.add_argument('--tau', type=float, default=0.01, help="how depth we exchange the par of the nn") parser.add_argument('--n_step', type=int, default=5, help='the number of step for n_step_TD') parser.add_argument("--lr_q", type=float, default=5e-4, help="learning rate for adam optimizer") parser.add_argument("--lr_a", type=float, default=1e-4, help="learning rate for adam optimizer") parser.add_argument("--lr_c", type=float, default=1e-4, help="learning rate for adam optimizer") parser.add_argument('--epsilon', type=float, default=0.2, help='the probability of randomly selected action') parser.add_argument('--epsilon_min', type=float, default=0.001, help='the min probability of randomly selected action') parser.add_argument('--epsilon_delta', type=float, default=0.00001, help='the delta of epsilon') parser.add_argument('--var', type=float, default=1., help='the variance of DDPG used to randomly select actions') parser.add_argument('--var_delta', type=float, default=0.99999, help='the delta of variance') parser.add_argument('--episodes', type=int, default=1000, help='the number of episodes') parser.add_argument('--episodes_to_save', type=int, default=100, help='the number of steps to save the model') parser.add_argument('--max_steps', type=int, default=1000, help='the max number of step for a episode') parser.add_argument('--learn_steps', type=int, default=50, help='the steps to train network') parser.add_argument("--num_units_1", type=int, default=128, help="number of units in the mlp") parser.add_argument("--num_units_2", type=int, default=64, help="number of units in the mlp") parser.add_argument("--device", type=str, default="cpu", help="use CPU or GPU") parser.add_argument("--action_bound", type=int, default=2, help="the bound of action") parser.add_argument("--eva_period", type=int, default=5, help="the evaluate time") args = parser.parse_args() return args