第六章 函数逼近-强化学习理论学习与代码实现(强化学习导论第二版)

119 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。 在这里插入图片描述 获取更多资讯,赶快关注上面的公众号吧!

【强化学习系列】

@[TOC]

第六章 函数逼近

在前面的章节中我们介绍的都是表格型强化学习,但是当问题的状态空间很大,表格型强化学习需要为每一个状态存储其每一个可选动作的估计值,势必需要很大的内存占用,例如西洋双陆棋的状态空间为102010^{20},计算机围棋的状态空间为1017010^{170},甚至像直升机控制这样的问题其状态空间为无限大。那么该如何才能将之前讲的无模型预测和控制扩展到这种情况呢?这就是本部分要讲的函数近似或逼近

6.1 学习目标

  • 理解相较于查表法函数逼近的动机;
  • 理解如何将函数逼近集成到现有的算法中;
  • 代码实现线性函数逼近的Q学习。

6.2 值函数近似

之前的内容是通过查表法来表达值函数,一种方式就是记录每个状态的值V(s),另一种就是状态-动作值函数Q(s,a),但是面对大规模MDPs时,可能会有太多的状态或动作而无法内存存储,哪怕能存储,对于这么大的一张表格进行学习,速度也是很慢的。因此一种简单的方式就是建立参数近似函数来逼近真正的价值。 v^(s,w)vπ(s)\hat{v}(s, \mathbf{w}) \approx v_{\pi}(s)  or q^(s,a,w)qπ(s,a)\text { or } \hat{q}(s, a, \mathbf{w}) \approx q_{\pi}(s, a) 其中w\mathbf{w}表示权重,或者说是逼近器的参数,通过对特征基进行加权求和。 通过这样的方式,只要给定状态或状态-动作对,就能给出相应的近似值,一方面减少了内存占用,更重要的是可以预测未知状态下的价值,从而大大增强泛化性。而w\mathbf{w}可以借助MC或TD学习进行更新。

值函数近似就像是一个黑盒子,只要给定输入就能得到输出,根据输入和输出的不同,可以分为三种类型,如图1所示。

  1. 输入状态,输出值函数;
  2. 输入状态和动作,输出状态-动作值函数;
  3. 输入状态,输出每个动作的状态-动作值函数。
图1 值函数近似的不同类型

应该选择什么样的函数逼近器呢?目前有很多方式可以实现,如下所示,但一般考虑选择可微分的函数逼近器,例如下面的线性特征组合和神经网络。除此之外,还需要一种适用于非平稳、非独立同分布数据的训练方法。

  1. 线性特征组合
  2. 神经网络
  3. 决策树
  4. 最近邻域
  5. 傅里叶/小波基
  6. ……

6.2 增量式方法

6.2.1 梯度下降

梯度下降

J(w)J(\mathbf{w})为参数向量w\mathbf{w}的可微函数,J(w)J(\mathbf{w})的梯度定义为对J(w)J(\mathbf{w})w\mathbf{w}的各个维度上分别进行偏微分,即

\frac{\partial J(\mathbf{w})}{\partial \mathbf{w}_{1}} \\ \vdots \\ \frac{\partial J(\mathbf{w})}{\partial \mathbf{w}_{n}} \end{array}\right)$$ 为了找到$J(\mathbf{w})$的局部最小值,只需要沿着负梯度方向更新$\mathbf{w}$即可: $$\Delta \mathbf{w}=-\frac{1}{2} \alpha \nabla_{\mathbf{w}} J(\mathbf{w})$$ 其中$\alpha$为步长参数。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/af57b55a7461491ca3dfd7c1c087e812~tplv-k3u1fbpfcp-zoom-1.image) ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7c9e2ab2aced4c6980a98daadd1a029b~tplv-k3u1fbpfcp-zoom-1.image) #### 随机梯度下降的值函数近似 一个好的逼近器就是要尽量减少近似值和真实值之间的误差,因此函数逼近的目标就是找到一个参数向量$\mathbf{w}$,使得近似值$\hat{v}(s, \mathbf{w})$和$v_{\pi}(s)$之间的均方误差最小: $$J(\mathbf{w})=\mathbb{E}_{\pi}\left[\left(v_{\pi}(S)-\hat{v}(S, \mathbf{w})\right)^{2}\right]$$ 通过使用梯度下降,可以按照如下进行参数更新: $$\begin{aligned} \Delta \mathbf{w} &=-\frac{1}{2} \alpha \nabla_{\mathbf{w}} J(\mathbf{w}) \\ &=\alpha \mathbb{E}_{\pi}\left[\left(v_{\pi}(S)-\hat{v}(S, \mathbf{w})\right) \nabla_{\mathbf{w}} \hat{v}(S, \mathbf{w})\right] \end{aligned}$$ 而在实际更新时并不是使用全梯度下降,而是随机采样一个状态,去评判该状态下的真实价值与近似值之间的误差: $$\Delta \mathbf{w}=\alpha\left(v_{\pi}(S)-\hat{v}(S, \mathbf{w})\right) \nabla_{\mathbf{w}} \hat{v}(S, \mathbf{w})$$ ### 6.2.2 线性函数逼近 #### 特征向量 可以使用一组特征向量来表达状态,特征向量中每个元素都是状态的一个具体表现: $$\mathbf{x}(S)=\left(\begin{array}{c} \mathbf{x}_{1}(S) \\ \vdots \\ \mathbf{x}_{n}(S) \end{array}\right)$$ 例如: - 机器人与地标的距离 - 股票市场的走势 - 象棋中车和卒的局面 #### 线性值函数近似 使用特征向量的最简单的方法就是对这些特征进行线性组合来表达近似值函数: $$\hat{v}(S, \mathbf{w})=\mathbf{x}(S)^{\top} \mathbf{w}=\sum_{j=1}^{n} \mathbf{x}_{j}(S) \mathbf{w}_{j}$$ 同样目标函数是真实值与近似值之间的均方误差: $$J(\mathbf{w})=\mathbb{E}_{\pi}\left[\left(v_{\pi}(S)-\mathbf{x}(S)^{\top} \mathbf{w}\right)^{2}\right]$$ 同样采用随机梯度下降,可以得到如下很简单的参数更新规则: $$\begin{aligned} \nabla_{\mathbf{w}} \hat{v}(S, \mathbf{w}) &=\mathbf{x}(S) \\ \Delta \mathbf{w} &=\alpha\left(v_{\pi}(S)-\hat{v}(S, \mathbf{w})\right) \mathbf{x}(S) \end{aligned}$$ 因此,在线性值函数近似情况下,更新规则可以概括为: **Update $=$ step-size $\times$ prediction error $\times$ feature value** #### 查表特征 查表实际上是线性值函数近似的一种特殊情况,查表特征可以表示为: $$\mathbf{x}^{\text {table}}(S)=\left(\begin{array}{c} \mathbf{1}\left(S=s_{1}\right) \\ \vdots \\ \mathbf{1}\left(S=s_{n}\right) \end{array}\right)$$ 对于状态1,如果处在状态1,则对应位置的值为1否则为0,其他状态以此类推(**类似于独热编码**)。 只要给定参数向量$\mathbf{w}$,就可以通过查表特征与权重的点积计算近似值函数。 $$\hat{v}(S, \mathbf{w})=\left(\begin{array}{c} \mathbf{1}\left(S=s_{1}\right) \\ \vdots \\ \mathbf{1}\left(S=s_{n}\right) \end{array}\right) \cdot\left(\begin{array}{c} \mathbf{w}_{1} \\ \vdots \\ \mathbf{w}_{n} \end{array}\right)$$ ### 6.2.3 增量预测算法 #### 增量预测算法 前面讲的两种方法(梯度下降和线性逼近)其实有一个前提,就是真实价值函数是已知的,这样就变成了**监督学习**,但是很遗憾的是,真实值函数往往**不能提前已知**,并且强化学习中也**只能给出奖励值**,最基本的方法就是利用之前的蒙特卡洛方法和时序差分方法来设立真实值函数的目标。 + 对于MC,目标就是回报值$G_{t}$,则对应更新为: $$\Delta \mathbf{w}=\alpha\left(G_{t}-\hat{v}\left(S_{t}, \mathbf{w}\right)\right) \nabla_{\mathbf{w}} \hat{v}\left(S_{t}, \mathbf{w}\right)$$ + 对于TD(0),目标为TD目标$R_{t+1}+\gamma \hat{v}\left(S_{t+1}, \mathbf{w}\right)$: $$\Delta \mathbf{w}=\alpha\left(R_{t+1}+\gamma \hat{v}\left(S_{t+1}, \mathbf{w}\right)-\hat{v}\left(S_{t}, \mathbf{w}\right)\right) \nabla_{\mathbf{w}} \hat{v}\left(S_{t}, \mathbf{w}\right)$$ + 对于TD($\lambda$),目标为$\lambda$回报$G_{t}^{\lambda}$: $$\Delta \mathbf{w}=\alpha\left(G_{t}^{\lambda}-\hat{v}\left(S_{t}, \mathbf{w}\right)\right) \nabla_{\mathbf{w}} \hat{v}\left(S_{t}, \mathbf{w}\right)$$ #### 基于值函数近似的蒙特卡洛 其实上面讲的设定目标的过程和监督学习很相近,当使用蒙特卡洛方法时,主要使用的是return$G_{t}$,这个过程就是完善建立训练数据,但这个过程是逐渐累积完成的,首先看到了状态$S_{1}$,在该状态下有一个轨迹,然后得到一个回报$G_{1}$,接下来状态$S_{2}$得到回报$G_{2}$,以此类推直到结束状态。 $$\left\langle S_{1}, G_{1}\right\rangle,\left\langle S_{2}, G_{2}\right\rangle, \ldots,\left\langle S_{T}, G_{T}\right\rangle$$ 现在需要做的基本上和监督学习一下,把上述过程生成的数据看成真实数据,只需要调整值函数近似模型去不断逼近这些数值即可。 例如使用线性蒙特卡洛策略评估的更新方式如下: $$\begin{aligned} \Delta \mathbf{w} &=\alpha\left(G_{t}-\hat{v}\left(S_{t}, \mathbf{w}\right)\right) \nabla_{\mathbf{w}} \hat{v}\left(S_{t}, \mathbf{w}\right) \\ &=\alpha\left(G_{t}-\hat{v}\left(S_{t}, \mathbf{w}\right)\right) \mathbf{x}\left(S_{t}\right) \end{aligned}$$ 在蒙特卡洛中$G_{t}$是**无偏**的,通过随机梯度下降,蒙特卡洛评估总能收敛到最优,但是学习过程会比较漫长。 #### 基于值函数近似的TD学习 TD学习同样采用了相同的思想,只不过现在的TD目标$R_{t+1}+\gamma \hat{v}\left(S_{t+1}, \mathbf{w}\right)$是**有偏**的,但仍然可以将监督学习应用以下训练数据: $$\left\langle S_{1}, R_{2}+\gamma \hat{v}\left(S_{2}, \mathbf{w}\right)\right\rangle,\left\langle S_{2}, R_{3}+\gamma \hat{v}\left(S_{3}, \mathbf{w}\right)\right\rangle, \ldots,\left\langle S_{T-1}, R_{T}\right\rangle$$ 例如对于线性TD(0),更新如下: $$\begin{aligned} \Delta \mathbf{w} &=\alpha\left(R+\gamma \hat{v}\left(S^{\prime}, \mathbf{w}\right)-\hat{v}(S, \mathbf{w})\right) \nabla_{\mathbf{w}} \hat{v}(S, \mathbf{w}) \\ &=\alpha \delta \mathbf{x}(S) \end{aligned}$$ 线性TD(0)能收敛或接近全局最优。 #### 基于值函数近似的TD($\lambda$) $G_{t}^{\lambda}$也是真实值函数的**有偏估计**,可以将监督学习应用到以下训练数据: $$\left\langle S_{1}, G_{1}^{\lambda}\right\rangle,\left\langle S_{2}, G_{2}^{\lambda}\right\rangle, \ldots,\left\langle S_{T-1}, G_{T-1}^{\lambda}\right\rangle$$ 例如对于前向视角线性TD($\lambda$),更新如下: $$\begin{aligned} \Delta \mathbf{w} &=\alpha\left(G_{t}^{\lambda}-\hat{v}\left(S_{t}, \mathbf{w}\right)\right) \nabla_{\mathbf{w}} \hat{v}\left(S_{t}, \mathbf{w}\right) \\ &=\alpha\left(G_{t}^{\lambda}-\hat{v}\left(S_{t}, \mathbf{w}\right)\right) \mathbf{x}\left(S_{t}\right) \end{aligned}$$ 对于后向视角线性TD($\lambda$),更新如下: $$\begin{aligned} \delta_{t} &=R_{t+1}+\gamma \hat{v}\left(S_{t+1}, \mathbf{w}\right)-\hat{v}\left(S_{t}, \mathbf{w}\right) \\ E_{t} &=\gamma \lambda E_{t-1}+\mathbf{x}\left(S_{t}\right) \\ \Delta \mathbf{w} &=\alpha \delta_{t} E_{t} \end{aligned}$$ 但是对于前向和后向线性TD($\lambda$)是**等效**的。 ### 6.2.4 增量控制算法 #### 基于值函数近似的控制 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/893833df22e747b0b254876f939c7427~tplv-k3u1fbpfcp-zoom-1.image) 控制部分采用广义迭代策略的概念,先进行策略评估,这里运用的是近似策略评估$\hat{q}(\cdot, \cdot, \mathbf{w}) \approx q_{\pi}$,然后进行$\epsilon$-greedy策略改进。 #### 动作值函数近似 这里同样使用参数向量$\mathbf{w}$来近似表达动作值函数: $$\hat{q}(S, A, \mathbf{w}) \approx q_{\pi}(S, A)$$ 目标函数就是最小化近似动作值函数$\hat{q}(S, A, \mathbf{w})$与真实动作值函数$q_{\pi}(S, A)$之间的均方误差: $$J(\mathbf{w})=\mathbb{E}_{\pi}\left[\left(q_{\pi}(S, A)-\hat{q}(S, A, \mathbf{w})\right)^{2}\right]$$ 使用随机梯度下降进行参数更新,找到局部最优: $$\begin{aligned} -\frac{1}{2} \nabla_{\mathbf{w}} J(\mathbf{w}) &=\left(q_{\pi}(S, A)-\hat{q}(S, A, \mathbf{w})\right) \nabla_{\mathbf{w}} \hat{q}(S, A, \mathbf{w}) \\ \Delta \mathbf{w} &=\alpha\left(q_{\pi}(S, A)-\hat{q}(S, A, \mathbf{w})\right) \nabla_{\mathbf{w}} \hat{q}(S, A, \mathbf{w}) \end{aligned}$$ #### 线性动作值函数近似 使用特征向量表达状态和动作: $$\mathbf{x}(S, A)=\left(\begin{array}{c} \mathbf{x}_{1}(S, A) \\ \vdots \\ \mathbf{x}_{n}(S, A) \end{array}\right)$$ 通过特征的加权线性组合可以表达动作值函数: $$\hat{q}(S, A, \mathbf{w})=\mathbf{x}(S, A)^{\top} \mathbf{w}=\sum_{j=1}^{n} \mathbf{x}_{j}(S, A) \mathbf{w}_{j}$$ 随机梯度下降更新如下: $$\begin{aligned} \nabla_{\mathbf{w}} \hat{q}(S, A, \mathbf{w}) &=\mathbf{x}(S, A) \\ \Delta \mathbf{w} &=\alpha\left(q_{\pi}(S, A)-\hat{q}(S, A, \mathbf{w})\right) \mathbf{x}(S, A) \end{aligned}$$ #### 增量控制算法 + 对于**MC**,目标是$G_{t}$,更新如下: $$\Delta \mathbf{w}=\alpha\left(G_{t}-\hat{q}\left(S_{t}, A_{t}, \mathbf{w}\right)\right) \nabla_{\mathbf{w}} \hat{q}\left(S_{t}, A_{t}, \mathbf{w}\right)$$ + 对于TD(0),目标是TD目标$R_{t+1}+\gamma Q\left(S_{t+1}, A_{t+1}\right)$,更新如下: $$\Delta \mathbf{w}=\alpha\left(R_{t+1}+\gamma \hat{q}\left(S_{t+1}, A_{t+1}, \mathbf{w}\right)-\hat{q}\left(S_{t}, A_{t}, \mathbf{w}\right)\right) \nabla_{\mathbf{w}} \hat{q}\left(S_{t}, A_{t}, \mathbf{w}\right)$$ + 对于前向$T D(\lambda)$,目标为动作值$\lambda$-回报,更新如下: $$\Delta \mathbf{w}=\alpha\left(q_{t}^{\lambda}-\hat{q}\left(S_{t}, A_{t}, \mathbf{w}\right)\right) \nabla_{\mathbf{w}} \hat{q}\left(S_{t}, A_{t}, \mathbf{w}\right)$$ + 对于后向$T D(\lambda)$,等效的更新如下: $$\begin{aligned} \delta_{t} &=R_{t+1}+\gamma \hat{q}\left(S_{t+1}, A_{t+1}, \mathbf{w}\right)-\hat{q}\left(S_{t}, A_{t}, \mathbf{w}\right) \\ E_{t} &=\gamma \lambda E_{t-1}+\nabla_{\mathbf{w}} \hat{q}\left(S_{t}, A_{t}, \mathbf{w}\right) \\ \Delta \mathbf{w} &=\alpha \delta_{t} E_{t} \end{aligned}$$ ## 6.3 代码实现 代码实现部分我们选用Mountain Car作为环境,环境使用**位置和速度**表达状态,动作有3个:**向左,不动和向右**。下面给出了**线性逼近的Q学习算法**代码。 ``` import gym import itertools import matplotlib import numpy as np import sys import sklearn.pipeline import sklearn.preprocessing if "../" not in sys.path: sys.path.append("../") from Lib import plotting from sklearn.linear_model import SGDRegressor from sklearn.kernel_approximation import RBFSampler matplotlib.style.use('ggplot') env = gym.envs.make("MountainCar-v0") # 特征预处理:归一化为均值为0,方差为1 # 从观察空间中采样部分样本 observation_examples = np.array([env.observation_space.sample() for x in range(10000)]) scaler = sklearn.preprocessing.StandardScaler() scaler.fit(observation_examples) # 用于将状态转换成特征表达 # 使用不同方差的RBF核来覆盖空间的不同部分 featurizer = sklearn.pipeline.FeatureUnion([ ("rbf1", RBFSampler(gamma=5.0, n_components=100)), ("rbf2", RBFSampler(gamma=2.0, n_components=100)), ("rbf3", RBFSampler(gamma=1.0, n_components=100)), ("rbf4", RBFSampler(gamma=0.5, n_components=100)) ]) featurizer.fit(scaler.transform(observation_examples)) class Estimator(): """ 值函数逼近器. """ def __init__(self): # 为环境的动作空间中的每个动作创建一个单独的模型。或者,我们可以以某种方式将动作编码到特性中,但是这样更容易编码。 self.models = [] for _ in range(env.action_space.n): model = SGDRegressor(learning_rate="constant") # 需要调用一次partial_fit来初始化模型 # 或者在预测时获取NotFittedError model.partial_fit([self.featurize_state(env.reset())], [0]) self.models.append(model) def featurize_state(self, state): """ 返回状态的特征化表达. """ scaled = scaler.transform([state]) featurized = featurizer.transform(scaled) return featurized[0] def predict(self, s, a=None): """ 进行值函数预测. 参数: s: 需要预测的状态 a: (可选) 需要预测的动作 返回值: 如果给定了动作a,则返回一个数值作为预测结果 如果没有给定a,则返回一个向量来预测环境中的所有动作,其中pred[i]为对动作i的预测 """ features = self.featurize_state(s) if not a: return np.array([m.predict([features])[0] for m in self.models]) else: return self.models[a].predict([features])[0] def update(self, s, a, y): """ 给定状态和动作,更新逼近器参数以靠近目标y """ features = self.featurize_state(s) self.models[a].partial_fit([features], [y]) def make_epsilon_greedy_policy(estimator, epsilon, nA): """ 根据给定的Q函数逼近器和epsilon,创建epsilon贪婪策略. 参数: 逼近器: 返回给定状态下的q值 epsilon: 随机选择动作的概率between 0 and 1 nA: 环境中动作数量 返回值: 返回一个函数,以观察为参数,以长度为nA的numpy数组的形式返回每个动作的概率 """ def policy_fn(observation): A = np.ones(nA, dtype=float) * epsilon / nA q_values = estimator.predict(observation) best_action = np.argmax(q_values) A[best_action] += (1.0 - epsilon) return A return policy_fn def q_learning(env, estimator, num_episodes, discount_factor=1.0, epsilon=0.1, epsilon_decay=1.0): """ 使用函数逼近进行离策略TD控制的Q学习. 遵循epsilon贪婪策略以寻找最优贪婪策略. 参数: env: OpenAI环境. estimator: 动作值函数逼近器 num_episodes: 迭代次数. discount_factor: Gamma折扣因子. epsilon: 随机选择动作的概率betwen 0 and 1. epsilon_decay: 每个片段中,epsilon都以该因子进行衰减 返回值: 一个片段状态对象,包括两个numpy数组,用于分别存放片段长度和片段奖励. """ # 进行必要的统计 stats = plotting.EpisodeStats( episode_lengths=np.zeros(num_episodes), episode_rewards=np.zeros(num_episodes)) for i_episode in range(num_episodes): # 正在遵循的策略 policy = make_epsilon_greedy_policy( estimator, epsilon * epsilon_decay ** i_episode, env.action_space.n) last_reward = stats.episode_rewards[i_episode - 1] sys.stdout.flush() # 重置环境 state = env.reset() # 只针对SARSA有用 next_action = None # 环境中迭代执行每一步 for t in itertools.count(): # 选择动作 # 如果使用的时SARSA,next_action在前一步已经确定了 if next_action is None: action_probs = policy(state) action = np.random.choice(np.arange(len(action_probs)), p=action_probs) else: action = next_action # 单步执行 next_state, reward, done, _ = env.step(action) # 更细统计 stats.episode_rewards[i_episode] += reward stats.episode_lengths[i_episode] = t # TD更新 q_values_next = estimator.predict(next_state) # 学习Q-Learning的TD目标 td_target = reward + discount_factor * np.max(q_values_next) # 使用下面的代码进行SARSA在策略控制 # next_action_probs = policy(next_state) # next_action = np.random.choice(np.arange(len(next_action_probs)), p=next_action_probs) # td_target = reward + discount_factor * q_values_next[next_action] # 使用目标更新函数逼近器 estimator.update(state, action, td_target) print("\rStep {} @ Episode {}/{} ({})".format(t, i_episode + 1, num_episodes, last_reward), end="") if done: break state = next_state return stats estimator = Estimator() # 注意: 对于Mountain Car游戏,不必保证epsilon>0.0 # 因为对所有状态的初始估计太过乐观,从而导致对所有状态进行探索. stats = q_learning(env, estimator, 100, epsilon=0.0) plotting.plot_cost_to_go_mountain_car(env, estimator) plotting.plot_episode_stats(stats, smoothing_window=25) ``` 最终学习到的位置和速度与值函数的关系如下。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7dea3fd1c3da4a908873b1014bbb774a~tplv-k3u1fbpfcp-zoom-1.image) 整体上随着迭代次数的增多,小车爬上山坡需要的部属逐渐下降,如下所示。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8e43f675885949cea971e4faa83018ae~tplv-k3u1fbpfcp-zoom-1.image) 随着迭代次数的增加,片段奖励呈现上升趋势,如下所示。 ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f776d266212f46c3a55259f9d51d9136~tplv-k3u1fbpfcp-zoom-1.image)