时序差分方法:Q-learning、SARSA和SARSA-Lambda【附带三个算法的代码实现】

时序差分方法是强化学习理论中最核心的内容,是强化学习领域最重要的成果,没有之一。与动态规划的方法和蒙特卡罗的方法比,时序差分的方法主要的不同点在值函数估计上面。

动态规划方法计算值函数是通过下式:
V ( S t ) ← E π [ R t + 1 + γ V ( S t + 1 ) ] = ∑ a π ( a ∣ S t ) ∑ s ′ , r p ( s ′ , r ∣ S t , a ) [ r + γ V ( s ′ ) ] V\left(S_{t}\right) \leftarrow E_{\pi}\left[R_{t+1}+\gamma V\left(S_{t+1}\right)\right]=\sum_{a} \pi\left(a \mid S_{t}\right) \sum_{s^{\prime}, r} p\left(s^{\prime}, r \mid S_{t}, a\right)\left[r+\gamma V\left(s^{\prime}\right)\right] V(St)Eπ[Rt+1+γV(St+1)]=aπ(aSt)s,rp(s,rSt,a)[r+γV(s)]
上式给出了值函数估计的计算公式,从公式中可以看到,动态规划方法计算值函数时用到了当前状态 s s s的所有后继状态 s ′ s' s处的值函数。值函数的计算用到了bootstapping的方法。所谓bootstrpping本意是指自举,此处是指当前值函数的计算用到了后继状态的值函数。即用后继状态的值函数估计当前值函数。特别注意,此处后继的状态是由模型公式 p ( s ′ , r ∣ S t , a ) p(s',r|S_t,a) p(s,rSt,a)计算得到的。由模型公式和动作集,可以计算状态 s s s所有的后继状态 s ′ s' s。当没有模型时,后继状态无法全部得到,只能通过试验和采样的方法每次试验得到一个后继状态 s ′ s' s

无模型时,我们可以采用蒙特卡罗的方法利用经验平均来估计当前状态的值函数。蒙特卡罗方法利用经验平均估计状态的值函数,所谓的经验是指一次试验,而一次试验要等到终止状态出现才结束。下式中的 G t G_t Gt是状态 S t S_t St后直到终止状态所有回报的返回值。
V ( S t ) ← V ( S t ) + α ( G t − V ( S t ) ) V\left(S_{t}\right) \leftarrow V\left(S_{t}\right)+\alpha\left(G_{t}-V\left(S_{t}\right)\right) V(St)V(St)+α(GtV(St))

相比于动态规划的方法,蒙特卡罗的方法需要等到每次试验结束,所以学习速度慢,学习效率不高。很自然的,可以想到借鉴动态规划中bootstrapping的方法,在不等到试验结束时就估计当前的值函数。事实上,这就是时间差分方法的精髓。时间差分方法结合了蒙特卡罗的采样方法(即做试验)和动态规划方法的bootstrapping(利用后继状态的值函数估计当前值函数),其示意图如下
MC.png DP.png TD.png
TD方法更新值函数的公式为
V ( S t ) ← V ( S t ) + α ( R t + 1 + γ V ( S t + 1 ) − V ( S t ) ) V\left(S_{t}\right) \leftarrow V\left(S_{t}\right)+\alpha\left(R_{t+1}+\gamma V\left(S_{t+1}\right)-V\left(S_{t}\right)\right) V(St)V(St)+α(Rt+1+γV(St+1)V(St))
其中 R t + 1 + γ V ( S t + 1 ) R_{t+1}+\gamma V(S_{t+1}) Rt+1+γV(St+1)称为TD目标,与(1)式中的 G t G_t Gt对应,两者不同之处是TD目标利用了bootstraping方法估计当前值函数。其中,
δ t = R t + 1 + γ V ( S t + 1 ) − V ( S t ) \delta_t=R_{t+1}+\gamma V(S_{t+1})-V(S_t) δt=Rt+1+γV(St+1)V(St)
被称为时序差分误差(TD Error)。

DP、MC、TD方法的异同

蒙特卡罗的方法使用的是值函数最原始的定义,该方法利用所有回报的累积和估计值函数。DP方法和TD方法则利用一步预测方法计算当前状态值函数。其共同点是利用了bootstrapping方法,不同的是,DP方法利用模型计算后继状态,而TD方法利用试验得到后继状态

从统计学的角度来看,蒙特卡罗方法和时间差分方法都是利用样本去估计值函数的方法,既然是统计方法,就可以从期望方差两个指标对两种方法进行对比。

  • 蒙特卡罗方法:蒙特卡罗方法中的返回值 G t = R t + 1 + γ R t + 2 + ⋯ + γ T − 1 R T G_t=R_{t+1}+\gamma R_{t+2}+\cdots+\gamma^{T-1}R_T Gt=Rt+1+γRt+2++γT1RT,其期望便是值函数的定义,因此蒙特卡罗方法是无偏估计。但是蒙特卡罗方法每次得到的 G t G_t Gt值要等到最终状态出现,在这个过程中要经历很多随机的状态和动作,因此每次得到的 G t G_t Gt随机性很大,所以尽管期望等于真值,但方差无穷大
  • 时间差分方法:时间差分方法的TD目标为 R t + 1 + γ V ( S t + 1 ) R_{t+1}+\gamma V(S_{t+1}) Rt+1+γV(St+1),若 V ( S t + 1 ) V(S_{t+1}) V(St+1)采用真实值,则TD估计也是无偏估计,然而在试验中 V ( S t + 1 ) V(S_{t+1}) V(St+1)用的也是估计值,因此时间差分估计方法属于有偏估计。跟蒙特卡罗方法相比,时间差分方法只用到了一步随机状态和动作,因此TD目标的随机性比蒙特卡罗方法中的 G t G_t Gt,因此其方差也比蒙特卡罗方法的方差小

时序差分

时间差分方法在每个episode中,当agent处于 S t S_t St的状态,采用Moving Average的方法对状态下 S t S_t St 的价值函数 V ( S t ) V(S_t) V(St) 进行软更新:
V ( S t ) ← ( 1 − α ) V ( S t ) + α G t V(S_t) \leftarrow (1-\alpha)V(S_t) + \alpha G_t V(St)(1α)V(St)+αGt
其中的 G t G_t Gt就是回报函数,而 α \alpha α是一个比较小的数,称为学习率。若 α \alpha α很小, 1 − α 1-\alpha 1α接近1,所以这个更新公式的右侧在结果上很接近 V ( S t ) V(S_t) V(St),但是往 G t G_t Gt的方向移动了一点点,也就是说状态 S t S_t St 的价值 V ( S t ) V(S_t) V(St) 往这一个episode中 S t S_t St处的回报值 G t G_t Gt的方向更新了一点点,挪动的长度就等于 α ∣ V ( S t ) − G t ∣ \alpha |V(S_t)-G_t| αV(St)Gt

以离散型的概率分布为例,离散型概率分布的期望实际上就是各个随机变量取值的加权和,也就是说权重越大,这个取值对期望值的贡献越大,影响越大,而衡量随机变量取值权重的就是这个取值的概率。

然而上式中的 G t G_t Gt 我们是不知道的(因为 G t G_t Gt需要这个episode结束才能计算)。所以需要继续近似,在右式中展开乘法并带入 G t G_t Gt 的定义式:
( 1 − α ) V ( S t ) + α G t = V ( S t ) + α ( G t − V ( S t ) ) = V ( S t ) + α ( ∑ k = 0 ∞ γ k R t + k + 1 − V ( S t ) ) = V ( S t ) + α ( R t − 1 + γ ( R t − 2 + γ R t + 3 + … ) − V ( S t ) ) = V ( S t ) + α ( R t + 1 + γ G t + 1 − V ( S t ) ) ≈ V ( S t ) + α ( R t + 1 + γ V ( S t + 1 ) − V ( S t ) ) \begin{aligned} (1-\alpha) V\left(S_{t}\right)+\alpha G_{t} =& V\left(S_{t}\right)+\alpha\left(G_{t}-V\left(S_{t}\right)\right) \\ =& V\left(S_{t}\right)+\alpha\left(\sum_{k=0}^{\infty} \gamma^{k} R_{t+k+1}-V\left(S_{t}\right)\right) \\ =& V\left(S_{t}\right)+\alpha\left(R_{t-1}+\gamma\left(R_{t-2}+\gamma R_{t+3}+\ldots\right)-V\left(S_{t}\right)\right) \\ =& V\left(S_{t}\right)+\alpha\left(R_{t+1}+\gamma G_{t+1}-V\left(S_{t}\right)\right) \\ \approx & V\left(S_{t}\right)+\alpha\left(R_{t+1}+\gamma V\left(S_{t+1}\right)-V\left(S_{t}\right)\right) \end{aligned} (1α)V(St)+αGt====V(St)+α(GtV(St))V(St)+α(k=0γkRt+k+1V(St))V(St)+α(Rt1+γ(Rt2+γRt+3+)V(St))V(St)+α(Rt+1+γGt+1V(St))V(St)+α(Rt+1+γV(St+1)V(St))
首先 G t G_t Gt我们是不知道的,所以需要做替换。因为我们是用单次的 G t G_t Gt来更新价值的,我们是用若干次的软更细近似向真实期望值的逼近,所以当模型收敛时,来一次模型测试时,agent处于 S t S_t St 状态的 G t G_t Gt V ( S t ) V(S_t) V(St) 与该处 G t G_t Gt的期望值应该是很接近的。所以我们就直接用 S t + 1 S_{t+1} St+1状态现存的价值来代替这次episode S t + 1 S_{t+1} St+1 状态下的 G t + 1 G_{t+1} Gt+1,这样也符合模型收敛的方向,随着训练的进行,更新越来越准确。
最终可以确定更新式:
V ( S t ) ← V ( S t ) + α ( R t + 1 + γ V ( S t + 1 ) − V ( S t ) ) V\left(S_{t}\right) \leftarrow V\left(S_{t}\right)+\alpha\left(R_{t+1}+\gamma V\left(S_{t+1}\right)-V\left(S_{t}\right)\right) V(St)V(St)+α(Rt+1+γV(St+1)V(St))

价值函数 V ( s ) V(s) V(s)是为了综合衡量一个状态的潜在价值,但仅仅有状态还不够,还需要加入动作,来评估一个状态下采取某个动作的潜在价值。我们把动作状态价值函数叫做Q函数,事实上Q表格里面的值也就是Q函数。与价值函数相比,Q函数也就只是多了一个约束条件–动作。其定义如下:
Q ( s , a ) = E [ G t ∣ S t = s , A t = a ] = E [ ∑ k = 0 ∞ γ k R t + k + 1 ∣ S t = s , A t = a ] \begin{aligned} Q(s, a) &=\mathbb{E}\left[G_{t} \mid S_{t}=s, A_{t}=a\right] \\ &=\mathbb{E}\left[\sum_{k=0}^{\infty} \gamma^{k} R_{t+k+1} \mid S_{t}=s, A_{t}=a\right] \end{aligned} Q(s,a)=E[GtSt=s,At=a]=E[k=0γkRt+k+1St=s,At=a]
所以Q函数描述了在状态 S t S_{t} St 下采取动作 A t A_{t} At取得的回报值 G t G_{t} Gt 的期望值。

采用和上式推导完全相同的思路,可以得出Q函数的更新方式:
Q ( S t , A t ) ← Q ( S t , A t ) + α [ R t + 1 + γ Q ( S t + 1 , A t + 1 ) − Q ( S t , A t ) ] Q(S_t,A_t) \leftarrow Q(S_t,A_t)+\alpha [R_{t+1}+\gamma Q(S_{t+1},A_{t+1})-Q(S_t,A_t)] Q(St,At)Q(St,At)+α[Rt+1+γQ(St+1,At+1)Q(St,At)]
可以证明,只要不断使用上式更新迭代 S t S_{t} St 下的价值函数,最终 S t S_{t} St下的价值函数 V ( S t ) V(S_{t}) V(St)会收敛到一个值。即使用TD的RL模型的收敛性是确定的

SARSA 算法

SARSA 算法理论基础

既然我们可以用时序差分方法来估计价值函数,那一个很自然的问题是,我们能否用类似策略迭代的方法来进行强化学习。策略评估已经可以通过时序差分算法实现,那么在不知道奖励函数和状态转移函数的情况下该怎么进行策略提升呢?答案是可以直接用时序差分算法来估计动作价值函数 Q Q Q
Q ( s t , a t ) ← Q ( s t , a t ) + α [ r t + γ Q ( s t + 1 , a t + 1 ) − Q ( s t , a t ) ] Q\left(s_{t}, a_{t}\right) \leftarrow Q\left(s_{t}, a_{t}\right)+\alpha\left[r_{t}+\gamma Q\left(s_{t+1}, a_{t+1}\right)-Q\left(s_{t}, a_{t}\right)\right] Q(st,at)Q(st,at)+α[rt+γQ(st+1,at+1)Q(st,at)]
然后我们用贪婪算法来选取在某个状态下动作价值最大的那个动作,即 arg ⁡ max ⁡ a Q ( s , a ) \operatorname{arg} \max _{a} Q(s, a) argmaxaQ(s,a)。这样就形成了一个完整的强化学习算法:用贪婪算法根据动作价值选取动作来和环境交互,再根据得到的数据用时序差分算法更新动作价值估计。

然而这个简单的算法存在两个需要进一步考虑的问题。

  • 第一,如果要用时序差分算法来准确地估计策略的状态价值函数,我们需要用极大量的样本来进行更新。但实际上我们可以忽略这一点,直接用一些样本来评估策略,然后就可以更新策略了。我们可以这么做的原因是策略提升可以在策略评估未完全进行的情况进行,这其实是广义策略迭代(generalized policy iteration)的思想。
  • 第二,如果在策略提升中一直根据贪婪算法得到一个确定性策略,可能会导致某些状态动作对 ( s , a ) (s,a) (s,a)无法访问到,以至于无法对其动作价值进行估计,进而无法保证策略提升后的策略比之前的好。简单常用的解决方案是使用 ϵ \epsilon ϵ-greedy策略。

现在,我们就可以得到一个实际的基于时序差分方法的强化学习算法。这个算法被称为 SARSA,因为它的动作价值更新用到了当前状态 s s s、当前动作 a a a、获得的奖励 r r r、下一个状态 s ′ s' s和下一个动作 a ′ a' a,将这些符号拼接后就得到了算法名称。

SARSA是一种On-Policy的TD算法。说SARSA是On-Policy的算法是因为SARSA的一次软更新需要
S t , A t , S t + 1 , A t + 1 S_{t}, A_{t}, S_{t+1}, A_{t+1} St,At,St+1,At+1 这四个量 (随着 S t S_{t} St S t + 1 S_{t+1} St+1的确定, R t + 1 R_{t+1} Rt+1也随之确定) ,而这四个量中的 A t A_{t} At A t + 1 A_{t+1} At+1的确定方式都是通过同一张表格(更新前后的表格属于不同的表格,此时的 “同” 指的是根据Q表格选取动作 A t A_{t} At 和动作 A t + 1 A_{t+1} At+1 之间表格没有更新过) 。而此处Q表格就是 agent 行使的准则,即Policy。

我们将这种更新一次所需要的参量都需要依托同一个Policy称为On-Policy(也就是行动策略和评估策略是同一个策略)。SARSA的初始算法如下


  • 初始化 Q ( s , a ) Q(s,a) Q(s,a)
  • for 序列 e = 1 → E e=1\rightarrow E e=1E do
    • 得到初始状态 s s s
    • ϵ \epsilon ϵ-greedy 策略根据选择当前状态 s s s下的动作 a a a
    • for 时间步 t = 1 → T t=1\rightarrow T t=1T do :
      • 得到环境反馈的 r r r s ′ s' s
      • ϵ \epsilon ϵ-greedy 策略根据 Q Q Q选择当前状态 s ′ s' s下的动作 a ′ a' a
      • Q ( s , a ) ← Q ( s , a ) + α [ r + γ Q ( s ′ , a ′ ) − Q ( s , a ) ] Q(s, a) \leftarrow Q(s, a)+\alpha\left[r+\gamma Q\left(s^{\prime}, a^{\prime}\right)-Q(s, a)\right] Q(s,a)Q(s,a)+α[r+γQ(s,a)Q(s,a)]
      • s ← s ′ , a ← a ′ s \leftarrow s^{\prime}, a \leftarrow a^{\prime} ss,aa
    • end for
  • end for

SARSA 算法代码实现

SARSA算法代码实现如下:(环境是CartPole-v0)

import numpy as np
import gym


class SARSAAgent:
    def __init__(self, obs_n, act_n, lr=0.1, gamma=0.9, epsilon=0.95):
        self.act_n = act_n
        self.obs_n = obs_n
        self.lr = lr
        self.gamma = gamma
        self.epsilon = epsilon
        self.Q = np.zeros((obs_n, act_n))

    def sample(self, obs):
        if np.random.uniform(0, 1) < self.epsilon:
            action = self.predict(obs)
        else:
            action = np.random.choice(self.act_n)
        return action

    def predict(self, obs):
        q_list = self.Q[obs, :]
        action = np.random.choice(np.where(q_list == np.max(q_list))[0])  # np.where返回索引
        return action

    def learn(self, s, a, r, s_, a_, done):
        predict_Q = self.Q[s, a]
        if done:
            target_Q = r
        else:
            target_Q = r + self.gamma * self.Q[s_, a_]
        self.Q[s, a] += self.lr * (target_Q - predict_Q)

    def save(self):
        np.save("./q_table.npy", self.Q)
        print("./q_table.npy" + "saved!")

    def restore(self):
        self.Q = np.load("./q_table.npy")
        print("./q_table.npy" + "loaded!")


def run_episode(env, agent, is_render=True):
    total_steps = 0
    total_reward = 0
    obs = env.reset()
    action = agent.sample(obs)

    while True:
        next_obs, reward, done, _ = env.step(action)
        next_action = agent.sample(next_obs)
        agent.learn(obs, action, reward, next_obs, next_action, done)

        action = next_action
        obs = next_obs
        total_reward += reward
        total_steps += 1  # 计算step数

        if is_render: env.render()
        if done: break
    return total_reward, total_steps


def test_episode(env, agent):
    total_reward = 0
    obs = env.reset()
    while True:
        action = agent.predict(obs)  # greedy
        next_obs, reward, done, _ = env.step(action)
        total_reward += reward
        obs = next_obs
        # time.sleep(0.5)
        # env.render()
        if done:
            # print('test reward = %.1f' % total_reward)
            break
    return total_reward


def main():
    env = gym.make("CartPole-v0")
    agent = SARSAAgent(
        obs_n=env.observation_space.n,
        act_n=env.action_space.n
    )

    is_render = True
    plot_data = []
    for episode in range(2001):
        ep_reward, ep_steps = run_episode(env, agent, is_render)
        print('Episode %s: steps = %s , reward = %.1f' % (episode, ep_steps, ep_reward))

        if episode % 100 == 0 and episode > 0:
            r = []
            for i in range(20):
                r.append(test_episode(env, agent))
            plot_data.append(r.count(1))
    return plot_data


if __name__ == '__main__':
    for i in range(20):
        SARSA_data = main()
        print(SARSA_data)

多步 SARSA 算法

多步 SARSA 算法理论基础

可以从上面的算法看出,在更新当前值函数时,用到了下一个状态的值函数。那么以此推理,可以利用后继 n n n个状态的值函数来更新当前状态的值函数。首先,可以用 G t ( 1 ) = R t + 1 + γ V ( S t + 1 ) G_t^{(1)}=R_{t+1}+\gamma V(S_{t+1}) Gt(1)=Rt+1+γV(St+1)表示TD目标,利用第二步值函数来估计当前值函数可表示为: G t ( 2 ) = R t + 1 + γ R t + 2 + γ 2 V ( S t + 1 ) G_t^{(2)}=R_{t+1}+\gamma R_{t+2}+\gamma^2V(S_{t+1}) Gt(2)=Rt+1+γRt+2+γ2V(St+1),以此类推,利用第 n n n步的值函数更新当前值函数,可表示为:
G t ( n ) = R t + 1 + γ R t + 2 + ⋯ + γ n − 1 R t + n + γ n V ( S t + n ) G_{t}^{(n)}=R_{t+1}+\gamma R_{t+2}+\cdots+\gamma^{n-1} R_{t+n}+\gamma^{n} V\left(S_{t+n}\right) Gt(n)=Rt+1+γRt+2++γn1Rt+n+γnV(St+n)

如下图所示为利用 n n n步值函数估计当前值函数的示意图。再审视一下刚刚的结论:可以利用 n n n步值函数来估计当前值函数,也就是说当前值函数有 n n n种估计方法。那么哪种估计值更接近真实值呢?我们不知道,但是我们可以对这 n n n个估计值利用加权的方法进行融合,这就是 T D ( λ ) TD(\lambda) TD(λ)
n_step.png
我们在 G t ( n ) G_t^{(n)} Gt(n)前乘以加权因子 ( 1 − λ ) λ n − 1 (1-\lambda)\lambda^{n-1} (1λ)λn1,乘这个加权是因为:
G t λ = ( 1 − λ ) G t ( 1 ) + ( 1 − λ ) λ G t ( 2 ) + ⋯ + ( 1 − λ ) λ n − 1 G t ( n ) ≈ [ ( 1 − λ ) + ( 1 − λ ) λ + ⋯ + ( 1 − λ ) λ n − 1 ] V ( S t ) = V ( S t ) \begin{aligned} G_{t}^{\lambda}&=(1-\lambda) G_{t}^{(1)}+(1-\lambda) \lambda G_{t}^{(2)}+\cdots+(1-\lambda) \lambda^{n-1} G_{t}^{(n)} \\ & \approx\left[(1-\lambda)+(1-\lambda) \lambda+\cdots+(1-\lambda) \lambda^{n-1}\right] V\left(S_{t}\right) \\ &=V\left(S_{t}\right) \end{aligned} Gtλ=(1λ)Gt(1)+(1λ)λGt(2)++(1λ)λn1Gt(n)[(1λ)+(1λ)λ++(1λ)λn1]V(St)=V(St)
利用 G t λ G_{t}^{\lambda} Gtλ来更新当前状态的值函数的方法称为 T D ( λ ) TD(\lambda) TD(λ)的方法。对于 T D ( λ ) TD(\lambda) TD(λ)的理解一般可以从两个视角进行解读。

第一个视角是前向视角,该视角也是 T D ( λ ) TD(\lambda) TD(λ)的定义。
TD_lambda_1.png
上图所示为 T D ( λ ) TD(\lambda) TD(λ)方法的前向视角解释。假设一个人坐在状态流上拿着望远镜看向前方,前方是那些将来的状态。当估计当前状态的值函数时, T D ( λ ) TD(\lambda) TD(λ)的定义中可以看到,它需要用将来时刻的值函数。也就是说, T D ( λ ) TD(\lambda) TD(λ)前向观点通过观看将来状态的值函数来估计当前的值函数。
V ( S t ) ← V ( S t ) + α ( G t ( λ ) − V ( S t ) ) V\left(S_{t}\right) \leftarrow V\left(S_{t}\right)+\alpha\left(G_{t}^{(\lambda)}-V\left(S_{t}\right)\right) V(St)V(St)+α(Gt(λ)V(St))
其中 G t λ = ( 1 − λ ) ∑ n = 1 ∞ λ n − 1 G t ( n ) G_{t}^{\lambda}=(1-\lambda) \sum_{n=1}^{\infty} \lambda^{n-1} G_{t}^{(n)} Gtλ=(1λ)n=1λn1Gt(n),而
G t ( n ) = R t + 1 + γ R t + 2 + ⋯ + γ n − 1 R t + n + γ n V ( S t + n ) G_{t}^{(n)}=R_{t+1}+\gamma R_{t+2}+\cdots+\gamma^{n-1} R_{t+n}+\gamma^{n} V\left(S_{t+n}\right) Gt(n)=Rt+1+γRt+2++γn1Rt+n+γnV(St+n)
利用 T D ( λ ) TD(\lambda) TD(λ)的前向观点估计值函数时, G t λ G_{t}^{\lambda} Gtλ的计算用到了将来时刻的值函数,因此需要等到整个试验结束之后。这跟蒙特卡罗方法相似。现在想寻求一种不需要等到试验结束就可以更新当前状态的值函数的更新方法,这种增量式的更新方法需要利用 T D ( λ ) TD(\lambda) TD(λ)的后向观点。
TD_lambda_2.png

上图为 T D ( λ ) TD(\lambda) TD(λ)后向观点示意图,人骑坐在状态流上,手里拿着话筒,面朝已经经历过的状态流,获得当前回报并利用下一个状态的值函数得到TD偏差后,此人会向已经经历过的状态喊话,告诉这些已经经历过的状态处的值函数需要利用当前时刻的TD偏差进行更新。此时过往的每个状态值函数更新的大小应该跟距离当前状态的步数有关。假设当前状态为 s t s_t st,TD偏差为 δ t \delta_t δt,那么 s t − 1 s_{t-1} st1处的值函数更新应该乘以一个衰减因子 γ λ \gamma \lambda γλ,状态 s t − 2 s_{t-2} st2处的值函数更新应该乘以 ( γ λ ) 2 (\gamma \lambda)^2 (γλ)2,以此类推。故 T D ( λ ) TD(\lambda) TD(λ)更新过程为:

  1. 首先计算当前状态的TD偏差: δ t = R t + 1 + γ V ( S t + 1 ) − V ( S t ) \delta_{t}=R_{t+1}+\gamma V\left(S_{t+1}\right)-V\left(S_{t}\right) δt=Rt+1+γV(St+1)V(St)
  2. 更新适合度轨迹: E t ( s ) = { γ λ E t − 1 ,  if  s ≠ s t γ λ E t − 1 + 1 ,  if  s = s t E_{t}(s)=\left\{\begin{array}{l}\gamma \lambda E_{t-1}, \text { if } s \neq s_{t} \\ \gamma \lambda E_{t-1}+1, \text { if } s=s_{t}\end{array}\right. Et(s)={γλEt1, if s=stγλEt1+1, if s=st
  3. 对于状态空间中的每个状态 s s s,更新值函数: V ( s ) ← V ( s ) + α δ t E t ( s ) V(s) \leftarrow V(s)+\alpha \delta_{t} E_{t}(s) V(s)V(s)+αδtEt(s)

T D ( λ ) TD(\lambda) TD(λ)前向观点和后向观点的异同:

  1. 前向观点需要等到一次试验之后再更新当前状态的值函数;而后向观点不需要等到值函数结束后再更新值函数,而是每一步都在更新值函数,是增量式方法。
  2. 前向观点在一次试验结束后更新值函数时,更新完当前状态的值函数后,此状态的值函数就不再改变。而后向观点,在每一步计算完当前的TD误差后,其他状态的值函数需要利用当前状态的TD误差进行更新。
  3. 在一次试验结束后,前向观点和后向观点每个状态的值函数的更新总量是相等的,都是 G t λ G_t^{\lambda} Gtλ。也就是说, T D ( λ ) TD(\lambda) TD(λ)前向观点和后向观点其实是等价的。

S a r s a ( λ ) Sarsa(\lambda) Sarsa(λ)的伪代码:


Input:初始化 Q ( s , a ) , ∀ s ∈ S , a ∈ A ( s ) Q(s, a), \forall s \in S, a \in A(s) Q(s,a),sS,aA(s),给定参数 α , β \alpha,\beta α,β

  1. 给定起始状态 s s s,并根据 ϵ \epsilon ϵ贪婪策略在状态 s s s选择动作 a a a,对所有的 s ∈ S , a ∈ A ( s ) s\in S,a\in A(s) sS,aA(s), E ( s , a ) = 0 E(s,a)=0 E(s,a)=0
  2. 根据 ϵ \epsilon ϵ贪婪策略在状态 s s s选择动作 a a a,得到回报 r r r和下一状态 s ′ s' s,在状态 s ′ s' s根据 ϵ \epsilon ϵ贪婪策略得到动作 a a a
  3. δ ← r + γ Q ( s ′ , a ′ ) − Q ( s , a ) \delta \leftarrow r+\gamma Q(s',a')-Q(s,a) δr+γQ(s,a)Q(s,a) E ( s , a ) ← E ( s , a ) + 1 E(s,a)\leftarrow E(s,a)+1 E(s,a)E(s,a)+1
  4. 对所有的 s ∈ S , a ∈ A ( s ) s\in S, a\in A(s) sS,aA(s) Q ( s , a ) ← Q ( s , a ) + α δ E ( s , a ) Q(s,a) \leftarrow Q(s,a)+\alpha \delta E(s,a) Q(s,a)Q(s,a)+αδE(s,a) E ( s , a ) ← γ λ E ( s , a ) E(s,a)\leftarrow\gamma\lambda E(s,a) E(s,a)γλE(s,a)
  5. s = s ′ s =s' s=s a = a ′ a = a' a=a,返回第3步
  6. 返回第2步,直到所有的 Q ( s , a ) Q(s,a) Q(s,a)收敛为止
  7. 输出最终策略: π ( s ) = arg ⁡ max ⁡ a Q ( s , a ) \pi(s)=\arg\max\limits_a Q(s,a) π(s)=argamaxQ(s,a)

多步 SARSA 算法代码实现

SARSA-lambda算法实现如下:(环境是CartPole-v0)

import numpy as np
import gym
import pandas as pd


class SARSALambdaAgent:
    def __init__(self, obs_n, act_n, lr=0.1, gamma=0.9, epsilon=0.95, trace_decay=0.95):
        self.act_n = act_n
        self.obs_n = obs_n
        self.lr = lr
        self.gamma = gamma
        self.epsilon = epsilon
        self.lambda_ = trace_decay
        self.Q = np.zeros((obs_n, act_n))
        self.eligibility_trace = self.Q.copy()

    def sample(self, obs):
        if np.random.uniform(0, 1) < self.epsilon:
            action = self.predict(obs)
        else:
            action = np.random.choice(self.act_n)
        return action

    def predict(self, obs):
        q_list = self.Q[obs, :]
        action = np.random.choice(np.where(q_list == np.max(q_list))[0])  # np.where返回索引
        return action

    def learn(self, s, a, r, s_, a_, done):
        predict_Q = self.Q[s, a]
        if done:
            target_Q = r
        else:
            target_Q = r + self.gamma * self.Q[s_, a_]
        TD_error = target_Q - predict_Q

        # 对于经历过的 state-action, 我们让他+1, 证明他是得到 reward 路途中不可或缺的一环
        # self.eligibility_trace[s, a] += 1

        # 更有效的方式
        self.eligibility_trace[s, :] *= 0
        self.eligibility_trace[s, a] = 1

        self.Q[s, a] += self.lr * TD_error * self.eligibility_trace[s, a]

        # 随着时间衰减 eligibility trace 的值, 离获取 reward 越远的步, 他的"不可或缺性"越小
        self.eligibility_trace[s, a] *= self.gamma * self.lambda_

    def save(self):
        np.save("./q_table.npy", self.Q)
        print("./q_table.npy" + "saved!")

    def restore(self):
        self.Q = np.load("./q_table.npy")
        print("./q_table.npy" + "loaded!")


def run_episode(env, agent, is_render=False):
    total_steps = 0
    total_reward = 0
    obs = env.reset()
    action = agent.sample(obs)

    while True:
        # env.render()
        next_obs, reward, done, _ = env.step(action)
        next_action = agent.sample(next_obs)
        agent.learn(obs, action, reward, next_obs, next_action, done)

        action = next_action
        obs = next_obs
        total_reward += reward
        total_steps += 1  # 计算step数

        if is_render: env.render()
        if done: break
    return total_reward, total_steps


def test_episode(env, agent):
    total_reward = 0
    obs = env.reset()
    while True:
        action = agent.predict(obs)  # greedy
        next_obs, reward, done, _ = env.step(action)
        total_reward += reward
        obs = next_ob
        # env.render()
        if done:
            break
    return total_reward


def run():
    env = gym.make("CliffWalking")
    agent = SARSALambdaAgent(
        obs_n=env.observation_space.n,
        act_n=env.action_space.n
    )

    is_render = False
    plot_data = []
    for episode in range(2001):
        agent.eligibility_trace *= 0  # 每回合开始前清零
        ep_reward, ep_steps = run_episode(env, agent, is_render)
        print('Episode %s: steps = %s , reward = %.1f' % (episode, ep_steps, ep_reward))

        if episode % 100 == 0 and episode > 0:
            r = []
            for i in range(20):
                r.append(test_episode(env, agent))
            plot_data.append(r.count(1))
    return plot_data


if __name__ == '__main__':
    Q_data = []
    for i in range(50):
        data = run()
        Q_data.append(data)

    datas = pd.DataFrame(Q_data)
    ave_data = [round(np.average(datas.iloc[:, i])) for i in range(20)]
    print(ave_data)

Q-Learning 算法

Q-Learning 算法理论基础

Q-Learning算法与SARSA算法在前几步上完全相同,从SARSA的第四步往后就开始不一样。SARSA第四步开始是选取动作 A t + 1 A_{t+1} At+1,因为必须确定 A t + 1 A_{t+1} At+1,才能使用上式软更新Q表格,否则式子中会出现未知量。

但这么做有一个小问题,观察SARSA算法,在第五步更新时用到了 t + 1 t+1 t+1时刻的动作 A t + 1 A_{t+1} At+1。也就是说,使用SARSA更新Q表格 t t t 时刻的 Q ( S t , A t ) Q(S_t,A_t) Q(St,At) 之前就需要做出下一时刻 t + 1 t+1 t+1的动作 A t + 1 A_{t+1} At+1。所以SARSA算法在更新表格时会将下一刻的动作趋势也考虑进去。总结就是,SARSA是先做 t + 1 t+1 t+1 时刻的动作,再更新 t t t 时刻的表格。这么做更新的表格具有一定的滞后性,训练出的agent往往会走鲁棒性最强的路线(最谨慎的动作方法)。

但事实上,我们还可以先更新 t t t 时刻的表格,再做 t + 1 t+1 t+1时刻的动作。像这样步步为营,更新一步,走一步的思路更加符合常人的思路。为了达到这样的目的,我们就不需要agent在更新 t t t时刻的Q表格前做出 t + 1 t+1 t+1 时刻的动作,在表达式上也就是需要将 A t + 1 A_{t+1} At+1 这个量给消除。

考察 Q ( S t + 1 , A t + 1 ) Q\left(S_{t+1}, A_{t+1}\right) Q(St+1,At+1)
Q ( S t + 1 , A t + 1 ) = Q ( S t + 1 , argmax ⁡ a Q ( S t + 1 , a ) ) = max ⁡ a ∈ A Q ( S t + 1 , a ) Q\left(S_{t+1}, A_{t+1}\right)=Q\left(S_{t+1}, \underset{a}{\operatorname{argmax}} Q\left(S_{t+1}, a\right)\right)=\max _{a \in A} Q\left(S_{t+1}, a\right) Q(St+1,At+1)=Q(St+1,aargmaxQ(St+1,a))=aAmaxQ(St+1,a)
将上式带入可得Q-Learning算法的更新式:
Q ( S t , A t ) ← Q ( S t , A t ) + α ( R t + 1 + γ max ⁡ a ∈ A Q ( S t + 1 , a ) − Q ( S t , A t ) ) Q\left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\alpha\left(R_{t+1}+\gamma \max _{a \in A} Q\left(S_{t+1}, a\right)-Q\left(S_{t}, A_{t}\right)\right) Q(St,At)Q(St,At)+α(Rt+1+γaAmaxQ(St+1,a)Q(St,At))

SARSA相比,再去掉更新前选择 t + 1 t+1 t+1时刻的动作的第四步就是Q-Learning算法。

下面给出Q-Learning算法的伪代码:


Input:初始化 Q 表格 (可以直接创建一个全 0 矩阵)

  1. t t t时刻 agent 处于状态 S t S_{t} St ,根据 Q Q Q表格选择最佳的 A t = argmax ⁡ a Q ( S t , a ) A_{t}={\operatorname{argmax}}_a Q\left(S_{t}, a\right) At=argmaxaQ(St,a)
  2. agent采取 A t A_{t} At 与环境交互得到奖励 R t + 1 R_{t+1} Rt+1 与下一时刻的状态 S t + 1 S_{t+1} St+1
  3. if S t + 1 = d o n e S_{t+1} = done St+1=done then
    • Q ( S t , A t ) ← Q ( S t , A t ) + α ( R t + 1 − Q ( S t , A t ) ) Q\left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\alpha\left(R_{t+1}-Q\left(S_{t}, A_{t}\right)\right) Q(St,At)Q(St,At)+α(Rt+1Q(St,At))
  4. end
  5. else
    • Q ( S t , A t ) ← Q ( S t , A t ) + α ( R t − 1 + γ max ⁡ a ∈ A Q ( S t + 1 , a ) − Q ( S t , A t ) ) Q\left(S_{t}, A_{t}\right) \leftarrow Q\left(S_{t}, A_{t}\right)+\alpha\left(R_{t-1}+\gamma \max _{a \in A} Q\left(S_{t+1}, a\right)-Q\left(S_{t}, A_{t}\right)\right) Q(St,At)Q(St,At)+α(Rt1+γmaxaAQ(St+1,a)Q(St,At))
    • t ← t + 1 t \leftarrow t+1 tt+1
    • 并回到第 2 步
  6. end

事实上,在 S t S_t St和Q表格相同的前提下,SARSAQ-Learning的更新式完全等价,不同的是两者执行的策略:

  • SARSA在 t t t 时刻表格更新前选取 t + 1 t+1 t+1时刻的动作
  • Q-Learning则在 t t t时刻表格更新后选取 t + 1 t+1 t+1时刻的动作。

简单说,SARSA是先执行再更新,Q-Learning是先更新再执行。但很明显,这两种更新方法本质上都是贪心,所以存在漏洞。

Q-Learning 算法代码实现

Q-Learning算法代码实现如下:(环境是Pendulum-v0)

import numpy as np
import matplotlib.pyplot as plt
import gym
import math


def moving_average(a, window_size):
    """滑动平均"""
    cumulative_sum = np.cumsum(np.insert(a, 0, 0))
    middle = (cumulative_sum[window_size:] - cumulative_sum[:-window_size]) / window_size
    r = np.arange(1, window_size - 1, 2)
    begin = np.cumsum(a[:window_size - 1])[::2] / r
    end = (np.cumsum(a[:-window_size:-1])[::2] / r)[::-1]
    return np.concatenate((begin, middle, end))


class QLearning:
    def __init__(self):
        self.env = gym.make('Pendulum-v0')

        np.random.seed(0)
        self.env.seed(0)

        self.num_states = self.env.observation_space.shape[0]

        self.gamma = 0.9
        self.lr = 0.1
        self.max_steps = 200  # steps for 1 episode
        self.num_episodes = 10000  # number of episodes
        self.epsilon = 0.95

        # uniform distributed sample with size
        self.q_table = np.random.uniform(low=-1, high=1, size=(30 * 20, 5)) * 2

    def bins(self, clip_min, clip_max, num):
        """分箱处理函数,把[clip_min,clip_max]区间平均分为num段,位于i段区间的特征值x会被离散化为i"""
        return np.linspace(clip_min, clip_max, num + 1)[1:-1]

    def digitize_state(self, observation):
        """get the discrete state in total 1296 states"""
        cosTheta, sinTheta, thetaDot = observation
        theta = math.acos(cosTheta)
        if sinTheta < 0:
            theta *= -1
        # 分别对各个连续特征值进行离散化(分箱处理)
        digitized = [np.digitize(theta, bins=self.bins(-math.pi, math.pi, 20)),
                     np.digitize(thetaDot, bins=self.bins(-8.0, 8.0, 30))]
        return digitized[0] + 20 * digitized[1]

    def select_action(self, observation, episode):
        """epsilon-greedy"""
        state = self.digitize_state(observation)
        epsilon = self.epsilon + (1 / (episode + 1))

        # 使用两个Q表的均值来选择动作
        if np.random.uniform(0, 1) < epsilon:
            action = np.argmax(self.q_table[state, :])  # 查表得到最佳行动
        else:
            action = np.random.randint(0, 4)
        return action

    def max_q_value(self, state):
        state = self.digitize_state(state)
        return self.q_table[state, :].max()

    def update(self, observation, action, reward, observation_next, done):
        state = self.digitize_state(observation)
        state_next = self.digitize_state(observation_next)

        action_next_Q_values = self.q_table[state_next, :]
        if done:
            target_Q = reward
        else:
            max_Q_action = np.random.choice(np.where(action_next_Q_values == action_next_Q_values.max())[0])
            target_Q = reward + self.gamma * self.q_table[state_next, max_Q_action]

        self.q_table[state, action] += self.lr * (target_Q - self.q_table[state, action])

    def run(self):
        reward_ep = []
        max_q_value_list = []
        max_q_value = 0
        for episode in range(self.num_episodes):  # 1000 episodes
            observation = self.env.reset()  # initialize environment
            total_reward = 0
            for step in range(self.max_steps):  # steps in one episode
                action = self.select_action(observation, episode)

                max_q_value = self.max_q_value(observation) * 0.005 + max_q_value * 0.995  # 平滑处理
                max_q_value_list.append(max_q_value)  # 保存每个状态的最大Q值

                observation_next, reward, done, _ = self.env.step([action - 2])
                self.update(observation, action, reward, observation_next, done)
                observation = observation_next

                total_reward += reward

                if done:
                    reward_ep.append(total_reward)
                    # print('{0} Episode: Total Reward: {1}'.format(episode, total_reward))
                    break
        return reward_ep, max_q_value_list


if __name__ == '__main__':
    ql = QLearning()

    reward_ep, max_q_value_list = ql.run()

    episodes_list = list(range(len(reward_ep)))
    mv_return = moving_average(reward_ep, 5)
    plt.figure()
    plt.plot(episodes_list, mv_return)
    plt.xlabel('Episodes')
    plt.ylabel('Returns')
    plt.title('Q-Learning on {}'.format("Pendulum-v0"))
    plt.show()

    frames_list = list(range(len(max_q_value_list)))
    plt.figure()
    plt.plot(frames_list, max_q_value_list)
    plt.axhline(0, c='orange', ls='--')
    plt.axhline(10, c='red', ls='--')
    plt.xlabel('Frames')
    plt.ylabel('Q value')
    plt.title('Q-Learning on {}'.format("Pendulum-v0"))
    plt.show()

代码运行结果如下:
在这里插入图片描述

\quad
\quad

参考:

  • 《深入浅出强化学习》
  • 《强化学习精要》
  • 《动手学强化学习》

持续更新中…

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Q-learningSARSA都属于时序差分强化学习方法,而不是蒙特卡洛强化学习方法时序差分强化学习是一种结合了动态规划和蒙特卡洛方法强化学习方法。它通过使用经验数据进行增量式的更新,同时利用了当前和未来的估计来逼近最优函数。 具体来说,Q-learningSARSA都是基于Q函数时序差分强化学习算法。 1. Q-learning:Q-learning是一种基于动态规划的无模型强化学习算法。它使用了时序差分(TD)方法,通过不断迭代更新Q函数的估计,使其逼近最优的Q。Q-learning算法通过将当前状态和动作的估计与下一个状态和动作的最大估计相结合,来更新Q函数的估计。 2. SARSASARSA是一种基于时序差分强化学习算法,也是一种模型-free的强化学习算法SARSA算法使用了时序差分方法,通过不断迭代更新Q函数的估计。与Q-learning不同的是,SARSA算法采用了一个策略(Policy)来决定下一个动作,并在更新Q时使用下一个动作的估计时序差分强化学习方法与蒙特卡洛强化学习方法相比,具有更高的效率和更好的适应性。它可以在每个时间步骤中进行更新,不需要等到任务结束后才进行更新,从而更快地收敛到最优策略。而蒙特卡洛强化学习方法则需要等到任务结束后才能获取完整的回报信息,进行全局更新。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奋斗的西瓜瓜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值