《深度学习入门4:强化学习》笔记

185 阅读8分钟

Chapter1:老虎机问题

一般问题

问题描述: 假如有n个老虎机,每个老虎机有一定的得分概率分布,如果你可以玩很多次,每次选择一个老虎机玩,如何做可以使得自己的得分最高?

数学表示:

  • 奖励:RR,即老虎机的返回值
  • 行动:aa,即对老虎机的选择
  • 行动价值:q(a)=E[RA]q(a) =\mathbb{E}[R|A],采取某个行动所得到奖励的期望值,q(a)q(a)表示真实的期望值,Q(a)Q(a)表示估计的期望值

问题的本质是对老虎机概率分布的估计,即估计每台老虎机的QQ值,简单来说是对老虎机输出的数学期望进行估计,根据大数定律,可以使用输出的平均值估计其数学期望,老虎机输出的平均值计算公式如下所示,随着n增加,其估计理论上也是越来越接近真实数学期望

Qn=R1+R2+R3+...+Rnn=Qn1+RnQn1nQ_n=\frac{R_1+R_2+R_3+...+R_n}{n}=Q_{n-1}+\frac{R_n-Q_{n-1}}{n}

Bandit类实现:

  • rate代表输出概率列表,列表的索引是输出的值,例如[0.2,0.4,0.4],则其0.2的概率输出0,以此类推,rand控制概率列表的长度
  • value代表输出的数学期望
  • play函数表示玩一次老虎机,按照概率随机输出
import numpy as np
np.random.seed(11)
class Bandit:
    def __init__(self):
        rand = np.random.rand(10)
        sum_rand = sum(rand)
        self.rate = [i/sum_rand for i in rand]
        self.value = sum([index*i for i,index in enumerate(self.rate)])

    def play(self):
        rand = np.random.rand()
        for index,value in enumerate(self.rate):
            rand-=value
            if rand<0:
                return index

Agent类实现:

  • Q_list表示对老虎机输出数学期望的估计
  • n_list记录玩老虎机的次数
  • epsilon表示探索的概率,如果每次都是选择Q值最高的老虎机,则会陷入局部最优。比如最开始Q值都是0,现在随机选择一个老虎机,其Q值会变的大于0,如果每次选择Q最高的老虎机,则会一直选择这个老虎机,导致无法获得最优解。
class Agent:
    def __init__(self,epsilon,action_size):
        self.Q_list = np.zeros(action_size)
        self.n_list = np.zeros(action_size)
        self.epsilon = epsilon
    def update(self,action,reward):
        self.n_list[action]+=1
        self.Q_list[action]+=(reward-self.Q_list[action])/self.n_list[action]

    def decide_action(self):
        if np.random.rand()<self.epsilon:
            return np.random.randint(0,len(self.Q_list))
        else:
            return np.argmax(self.Q_list)

实践操作:

import matplotlib.pyplot as plt
steps = 1000
num_of_bandit = 10
bandits = [Bandit() for i in range(num_of_bandit)]
bandits_values = [bandit.value for bandit in bandits]
best_choice = np.argmax(bandits_values)



agent = Agent(0.1,num_of_bandit)
win_rate = []
rewards = []

total_reward = 0


for n in range(steps):
    action = agent.decide_action()
    reward = bandits[action].play()
    agent.update(action,reward)
    total_reward+=reward
    rewards.append(total_reward)
    win_rate.append(total_reward/(n+1))

print(total_reward)
print(bandits_values)
plt.plot(win_rate)
plt.show()

理论上其得分的平均值会慢慢趋近于数学期望最高的老虎机的数学期望:

image.png

非稳态问题

上个例子中,老虎机的概率分布不会变化,属于稳态问题,而真实场景中,环境的状态会发生改变,也就是老虎机的概率分布会变化,这叫非稳态问题

对于非稳态问题,Q值的更新方式变化为:

Qn=Qn1+α(RnQn1)=αRn+α(1α)Rn1+...α(1α)n1R1+α(1α)nQ0\begin{aligned} Q_n&=Q_{n-1}+\alpha(R_n-Q_{n-1}) \\ &= \alpha R_n+\alpha (1-\alpha)R_{n-1}+...\alpha (1-\alpha)^{n-1} R_1+\alpha (1-\alpha)^nQ_0 \end{aligned}

在稳态问题中,使用平均值对QnQ_n进行更新,体现为所有时刻的RR对其权值都是一样的,都是1/n1/n,而在非稳态问题中,我们希望最近的RRQnQ_n的影响更大,所以使用α\alpha将之前的RR的权重变小,这种权重指数级减少的计算,称为指数移动平均

NonStatBandit类实现:

  • 使用change_rate在每一次调用play时,给老虎机的概率分布增加噪声
class NonStatBandit:
    def __init__(self):
        rand = np.random.rand(10)
        sum_rand = sum(rand)
        self.rate = [i/sum_rand for i in rand]
        self.value = sum([index*i for i,index in enumerate(self.rate)])

    def play(self):
        rand = np.random.rand()

        change_rate = np.random.randn(10)
        avg_change_rate = np.average(change_rate)
        change_rate = [0.1*(i-avg_change_rate)  for i in change_rate]
        self.rate = [self.rate[i]+change_rate[i] for i in range(len(self.rate))]


        for index,value in enumerate(self.rate):
            rand-=value
            if rand<0:
                return index

AlphaAgent类实现:

class AlphaAgent:
    def __init__(self,epsilon,action_size,alpha):
        self.Q_list = np.zeros(action_size)
        self.epsilon = epsilon
        self.alpha = alpha
    def update(self,action,reward):
        self.Q_list[action]+=(reward-self.Q_list[action])*self.alpha

    def decide_action(self):
        if np.random.rand()<self.epsilon:
            return np.random.randint(0,len(self.Q_list))
        else:
            return np.argmax(self.Q_list)

两种方法对比:

这里每种方法都进行多次实验,次数为runs,每次实验的步数为steps,统计不同实验下,每一个step的收益平均值

steps = 1000
runs = 200
all_rates = np.zeros((runs,steps))
for run in range(runs):
    steps = 1000
    num_of_bandit = 10
    # bandits = [Bandit() for i in range(num_of_bandit)]
    bandits = [NonStatBandit() for i in range(num_of_bandit)]
    agent = Agent(0.1,num_of_bandit)
    win_rate = []
    rewards = []
    total_reward = 0

    for n in range(steps):
        action = agent.decide_action()
        reward = bandits[action].play()
        agent.update(action,reward)
        total_reward+=reward
        rewards.append(total_reward)
        win_rate.append(total_reward/(n+1))

    all_rates[run] = win_rate

avg_rates = np.average(all_rates,axis=0)
print(avg_rates)
#
all_rates_nonstat = np.zeros((runs,steps))
for run in range(runs):
    steps = 1000
    num_of_bandit = 10
    bandits = [NonStatBandit() for i in range(num_of_bandit)]
    agent = AlphaAgent(0.1,num_of_bandit,0.8)
    win_rate = []
    rewards = []
    total_reward = 0

    for n in range(steps):
        action = agent.decide_action()
        reward = bandits[action].play()
        agent.update(action,reward)
        total_reward+=reward
        rewards.append(total_reward)
        win_rate.append(total_reward/(n+1))

    all_rates_nonstat[run] = win_rate

avg_rates_nonsta = np.average(all_rates_nonstat,axis=0)

plt.plot(avg_rates,label ='sample average')
plt.plot(avg_rates_nonsta,label ='alpha const update')
plt.legend()
plt.show()

可以看到,使用移动平均的效果更好:

image.png

Chapter2:马尔可夫决策过程

本章讨论的是environmentAgentAction而发生变化的问题

数学表达

  • 状态迁移函数: 给定当前状态ss和行动aa,输出下一个状态ss^{'}的函数,既s=f(s,a)s^{'}=f(s,a)
  • 状态迁移概率: 给定当前状态ss和行动aa,输出下一个状态ss^{'}的概率,既P(ss,a)P(s^{'}|s,a),因为即使采取了行动,下个状态不是确定的,比如当确定向左移动,但是因为打滑之类的问题而没有发生移动
    • 状态迁移需要上一时刻的信息,既ss^{'}只收到状态ss和行动aa的影响,这是马尔可夫性的体现
  • 奖励函数: r(s,a,s)r(s,a,s^{'}),在下面的例子中,r(s,a,s)=r(s)r(s,a,s^{'})=r(s^{'})
    • 奖励函数不是确定的,比如在ss^{'}时,0.8的几率被攻击,奖励为-10等
  • 策略: 只基于当前状态对下一步行动的确定,使用π(as)\pi(a|s)表示在ss状态下采取aa行动的概率

MDP的目标

  • 回合制任务: 有结束的任务,例如下围棋
  • 连续性任务: 没有结束的任务,例如仓库存储

收益(return)

收益是指在给定策略时,某个状态下,未来时间内的奖励的总和,公式表示为:

Gt=Rt+γRt+1+γ2Rt+2+γ3Rt+3...G_t=R_t+\gamma R_{t+1} + \gamma ^2 R_{t+2}+\gamma ^3 R_{t+3}...

γ\gamma被称为折现率(discount rate),具体原因有两个:

  • 防止在连续性任务重,收益无限大的情况
  • 收益实质上是对未来收益的一种预测,显然距离tt越远的预测是越不可信的,且这种设置使得收益更关注当前的收益,这是一种贪心的体现

策略或状态变化在一些问题中是随机的,所以我们需要使用数学期望描述收益,称为状态价值函数

νπ(s)=Eπ[GtSt=s]\nu _{\pi}(s) = \mathbb{E}_{\pi}[G_t|S_t = s]

策略π\pi初始状态ss是需要给定的,然后计算这个条件下未来的收益的数学期望,显然我们需要找到使得这个期望最大的策略π\pi

最优策略: 如果策略π\pi ^{'}在任意的ss下,νπ(s)\nu _{\pi^{'}}(s)都是最大的,那么π\pi ^{'}为最优策略。可以证明在MDP问题中,一定存在一个最优策略

这里举一个例子,只存在两个状态,当吃到苹果时+1,苹果会无限刷新,撞到墙-1:

Screenshot_20241202_144609_com.newskyer.draw.jpg

策略一共有四种,值得注意的是,这个例子中状态转移和策略都是确定的

s=L1s=L1s=L2s=L2
π1(s)\pi_1(s)RightRight
π2(s)\pi_2(s)RightLeft
π3(s)\pi_3(s)LeftRight
π4(s)\pi_4(s)LeftLeft

随便算一个,假设γ=0.9\gamma=0.9

νπ1(s=L1)=1+0.9×(1)+0.92×(1)...=8\nu_{\pi_1}(s=L1) = 1+0.9×(-1)+0.9^2×(-1)...=-8

计算所有策略,很显然π2(s)\pi_2(s)是最优策略

Chapter3:贝尔曼方程

上面的例子中,状态转移和策略都是确定的,当面临随机性时,我们不能使用上面的方法计算,而需要使用贝尔曼方程

贝尔曼方程

回忆收益公式:

Gt=Rt+γRt+1+γ2Rt+2+γ3Rt+3...=Rt+γGt+1G_t=R_t+\gamma R_{t+1} + \gamma ^2 R_{t+2}+\gamma ^3 R_{t+3}... = R_t +\gamma G_{t+1}

那么状态价值函数可以写为:

νπ(s)=Eπ[GtSt=s]=Eπ[RtSt=s]+γEπ[Gt+1St=s]\nu _{\pi}(s) = \mathbb{E}_{\pi}[G_t|S_t = s] = \mathbb{E}_{\pi}[R_t|S_t = s]+\gamma \mathbb{E}_{\pi}[G_{t+1}|S_t = s]

其中

Eπ[RtSt=s]=asπ(as)p(ss,a)r(s,a,s)Eπ[Gt+1St=s]=asπ(as)p(ss,a)Eπ[Gt+1St+1=s]\mathbb{E}_{\pi}[R_t|S_t = s] = \sum_{a} \sum_{s^{'}} \pi(a|s)p(s^{'}|s,a)r(s,a,s^{'}) \\ \mathbb{E}_{\pi}[G_{t+1}|S_t = s] = \sum_{a} \sum_{s^{'}}\pi(a|s)p(s^{'}|s,a)\mathbb{E}_{\pi}[G_{t+1}|S_{t+1} = s]
  • 第一个部分:本质上就是求出每一种可能性的概率及其奖励,然后加和
  • 第二个部分:确定初始时刻状态,下一时刻开始的收益,应该等于确认下一时刻状态,下一时刻收益×初始时刻到下一时刻状态的概率2

那么贝尔曼方程为:

νπ(s)=Eπ[RtSt=s]+γEπ[Gt+1St=s]=asπ(as)p(ss,a)r(s,a,s)+γasπ(as)p(ss,a)νπ(s)=asπ(as)p(ss,a){r(s,a,s)+γνπ(s}\begin{aligned} \nu _{\pi}(s) &= \mathbb{E}_{\pi}[R_t|S_t = s]+\gamma \mathbb{E}_{\pi}[G_{t+1}|S_t = s] \\&=\sum_{a} \sum_{s^{'}} \pi(a|s)p(s^{'}|s,a)r(s,a,s^{'})+\gamma \sum_{a} \sum_{s^{'}}\pi(a|s)p(s^{'}|s,a)\nu _{\pi}(s^{'}) \\&=\sum_{a} \sum_{s^{'}}\pi(a|s)p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'}\} \end{aligned}

贝尔曼方程表示的是“当前状态ss的价值函数νπ(s)\nu_{\pi}(s)”和“下一状态ss^{'}的价值函数νπ(s)\nu_{\pi}(s^{'})”之间的关系,其最大的意义在于将无限的计算转换为有限的联立方程。

对于上面那个例子,假设策略为0.5概率向左移动,0.5向右移动,采用贝尔曼方程求解,这里由于状态转移是固定的,那么当s=f(s,a)s^{'}=f(s,a)时,贝尔曼方程可以写为:

νπ(s)=aπ(as){r(s,a,s)+γνπ(s)}\nu _{\pi}(s)=\sum_{a} \pi(a|s)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\}

这里行动只有往左或者往右,那么其还可写成:

νπ(s)=π(a=Lefts){r(s,a=Left,s)+γνπ(s}+π(a=Rights){r(s,a=Right,s)+γνπ(s)}\begin{aligned} \nu _{\pi}(s)=&\pi(a=Left|s)\{r(s,a=Left,s^{'})+\gamma \nu _{\pi}(s^{'}\} \\&+\pi(a=Right|s)\{r(s,a=Right,s^{'})+\gamma \nu _{\pi}(s^{'})\} \end{aligned}

那么:

νπ(L1)=0.5{1+0.9νπ(L1)}+0.5{1+0.9νπ(L2)}νπ(L2)=0.5{0+0.9νπ(L1)}+0.5{1+0.9νπ(L2)}\nu _{\pi}(L1)=0.5\{-1+0.9\nu _{\pi}(L1)\}+0.5\{1+0.9\nu _{\pi}(L2)\} \\ \nu _{\pi}(L2)=0.5\{0+0.9\nu _{\pi}(L1)\}+0.5\{-1+0.9\nu _{\pi}(L2)\}

求解可得,νπ(L1)=2.25,νπ(L2)=2.75\nu _{\pi}(L1)=-2.25, \nu _{\pi}(L2)=-2.75

行动价值函数(Q函数)

状态价值函数需要两个条件,即策略状态,在这个基础上再考虑一个条件,也就是第一次的行动aa,就构成了Q函数:

qπ(s,a)=Eπ[Gt+1St=s,At=a]q_{\pi}(s,a) = \mathbb{E}_{\pi}[G_{t+1}|S_t = s,A_t = a]

Q函数中行动的选择不受到策略的影响,是自己选择的,后面的行动就是按照策略来了,那么显然Q函数和状态价值函数有如下关系:

νπ(s)=aπ(as)qπ(s,a)\nu _{\pi}(s) = \sum_{a}\pi(a|s)q_{\pi}(s,a)

按照同样的分析,可以获得Q函数的表达式:

qπ(s,a)=Eπ[RtSt=s,At=a]+γEπ[Gt+1St=s,At=a]=sp(ss,a){r(s,a,s)+γνπ(s)}\begin{aligned} q_{\pi}(s,a)&=\mathbb{E}_{\pi}[R_t|S_t = s,A_t = a]+\gamma \mathbb{E}_{\pi}[G_{t+1}|S_t = s,A_t = a] \\&=\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\} \end{aligned}

那么,使用Q函数的贝尔曼方程为:

qπ(s,a)=sp(ss,a){r(s,a,s)+γaπ(as)qπ(s,a)}q_{\pi}(s,a)=\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \sum_{a^{'}}\pi(a^{'}|s^{'})q_{\pi}(s^{'},a^{'})\}

贝尔曼最优方程

回忆贝尔曼方程:

νπ(s)=aπ(as)sp(ss,a){r(s,a,s)+γνπ(s)}\begin{aligned} \nu _{\pi}(s) =\sum_{a} \pi(a|s)\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\} \end{aligned}

最优策略应该使得sp(ss,a){r(s,a,s)+γνπ(s}\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'}\}最大(因为最优策略是确定的,那么π(as)\pi(a|s)部分可以省略),于是贝尔曼最优方程为:

ν(s)=maxasp(ss,a){r(s,a,s)+γν(s)}\begin{aligned} \nu _{*}(s) =\max_a \sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{*}(s^{'})\} \end{aligned}

同理,Q函数的贝尔曼最优方程为:

q(s,a)=sp(ss,a){r(s,a,s)+γmaxaq(s,a)} q_{*}(s,a)=\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \max_{a^{'}} q_{*}(s^{'},a^{'})\}

最优策略应该是可以使得价值函数最大的策略,用数学语言描述就是:

π(s)=arg maxasp(ss,a){r(s,a,s)+γν(s)}=arg maxaq(s,a)\begin{aligned} \pi_{*}(s) &= \argmax_{a}\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{*}(s^{'})\} \\&=\argmax_{a}q_{*}(s,a) \end{aligned}

最优收益的获取:

还是之前的例子,由于状态转移是确定的,那么最优贝尔曼方程可以写成:

ν(s)=maxa{r(s,a,s)+γν(s)}\begin{aligned} \nu _{*}(s) =\max_a\{r(s,a,s^{'})+\gamma \nu _{*}(s^{'})\} \end{aligned}

于是:

ν(L1)=max{1+0.9ν(L1),1+0.9ν(L2)}ν(L2)=max{1+0.9ν(L2),1+0.9ν(L1)}\nu_{*}(L1)=max\{-1+0.9\nu_{*}(L1),1+0.9\nu_{*}(L2)\}\\ \nu_{*}(L2)=max\{-1+0.9\nu_{*}(L2),1+0.9\nu_{*}(L1)\}

求解可得,ν(L1)=5.26,ν(L2)=4.73\nu_{*}(L1)=5.26,\nu_{*}(L2)=4.73

最优策略的获取:

最优策略π(s)\pi_{*}(s)表示为:

π(s)=arg maxa(sp(ss,a){r(s,a,s)+γνπ(s)})\pi_{*}(s) = \argmax_{a}(\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\})

在这个例子中,当s=L1s=L1时,如果其行动是往左走,则:

sp(ss,a){r(s,a,s)+γνπ(s)}=1+0.9×5.26=3.734\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\}=-1+0.9×5.26=3.734

如果往右走,则:

sp(ss,a){r(s,a,s)+γνπ(s)}=1+0.9×4.73=5.257\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\}=1+0.9×4.73=5.257

显然,π(L1)=Right\pi_{*}(L1)=Right,同理π(L2)=Left\pi_{*}(L2)=Left

总计

【贝尔曼方程】

νπ(s)=asπ(as)p(ss,a){r(s,a,s)+γνπ(s}qπ(s,a)=sp(ss,a){r(s,a,s)+γaπ(as)qπ(s,a)}\begin{aligned} &\nu _{\pi}(s) =\sum_{a} \sum_{s^{'}}\pi(a|s)p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'}\} \\&q_{\pi}(s,a)=\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \sum_{a^{'}}\pi(a^{'}|s^{'})q_{\pi}(s^{'},a^{'})\} \end{aligned}

【贝尔曼最优方程】

ν(s)=maxasp(ss,a){r(s,a,s)+γν(s)}q(s,a)=sp(ss,a){r(s,a,s)+γmaxaq(s,a)}\begin{aligned} &\nu _{*}(s) =\max_a \sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{*}(s^{'})\} \\&q_{*}(s,a)=\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \max_{a^{'}} q_{*}(s^{'},a^{'})\} \end{aligned}

【最优策略】

π(s)=arg maxasp(ss,a){r(s,a,s)+γν(s)}=arg maxaq(s,a)\begin{aligned} \pi_{*}(s) &= \argmax_{a}\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{*}(s^{'})\} \\&=\argmax_{a}q_{*}(s,a) \end{aligned}

Chapter4:动态规划法

按照上面的做法,得到最优贝尔曼方程的解需要解一个联立方程,在上述的例子中环境只有两个,所有这个联立方程可求,当环境变复杂,问题变得多变,解方程的计算量指数级上升,所以我们需要用动态规划的方法计算最优贝尔曼方程最优策略

在强化学习中通常设计两项任务:

  • 策略评估: 求给定策略π\pi的价值函数νπ(s)\nu _{\pi}(s)
  • 策略控制: 控制策略并将其调整为最优策略

迭代策略评估

回忆贝尔曼方程:

νπ(s)=aπ(as)sp(ss,a){r(s,a,s)+γνπ(s)}\begin{aligned} \nu _{\pi}(s) =\sum_{a} \pi(a|s)\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\} \end{aligned}

使用DP进行策略评估的思路是从贝尔曼方程衍生出来的,思路是将贝尔曼方程变形为“更新式”,表达为:

Vk+1(s)=asπ(as)p(ss,a){r(s,a,s)+γVk(s)}V_{k+1}(s)=\sum_{a} \sum_{s^{'}}\pi(a|s)p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma V _{k}(s^{'})\}

主要特点是用下一个状态的当前迭代的价值函数Vk(s)V _{k}(s^{'})来更新当前状态的下一个迭代的价值函数Vk+1(s)V_{k+1}(s)

例如对于上面那个例子,一开始的初始价值函数为V0(L1)V_0(L1)V0(L2)V_0(L2),这个00代表第0次迭代,然后用上面那个公式计算V1(L1)V_1(L1)V2(L2)V_2(L2),以此类推。

如何证明这个迭代式的有效性?(来自豆包) image.png

在上面的例子中,不存在状态转移概率,所有可以简写为:

Vk+1(s)=aπ(as){r(s,a,s)+γVk(s)}V_{k+1}(s)=\sum_{a}\pi(a|s)\{r(s,a,s^{'})+\gamma V _{k}(s^{'})\}

对上面的例子进行迭代评估实验:

  • 这里使用delta表征上下两次迭代的改变量,当改变量小于阈值时,停止迭代
V = {'l1':0,'l2':0}
new_V = V.copy()
n = 1
while(1):
    new_V['l1'] = 0.5*(-1+0.9*V['l1'])+0.5*(1+0.9*V['l2'])
    new_V['l2'] = 0.5 * (0 + 0.9 * V['l1']) + 0.5 * (-1 + 0.9 * V['l2'])
    delta = max(abs(new_V['l1']-V['l1']),abs(new_V['l2']-V['l2']))
    V = new_V.copy()
    n+=1
    if delta<0.0001:
        print(n)
        break

print(V)
# 77
# {'l1': -2.249167525908671, 'l2': -2.749167525908671}

迭代策略评估的其他方式-覆盖方式

  • 上述方式:上面的方法是全部计算出新迭代的所有状态的价值函数,再进行下一轮迭代
  • 覆盖方式:在计算出新的某个状态的价值函数后,直接将其用于计算其他状态的价值函数。
    • 例如我们先利用V0(L1)V_0(L1)V0(L2)V_0(L2)计算出了V1(L1)V_1(L1),然后利用V1(L1)V_1(L1)V0(L2)V_0(L2)计算V1(L2)V_1(L2),以此类推。
V = {'l1':0,'l2':0}
n = 1
while(1):
    t = 0.5*(-1+0.9*V['l1'])+0.5*(1+0.9*V['l2'])
    delta = abs(t-V['l1'])
    V['l1'] = t

    t = 0.5 * (0 + 0.9 * V['l1']) + 0.5 * (-1 + 0.9 * V['l2'])
    delta = max(abs(t - V['l2']),delta)
    V['l2']=t

    n+=1
    if delta<0.0001:
        print(n)
        break

print(V)
# 61
# {'l1': -2.2493782177156936, 'l2': -2.7494201578106514}

GridWorld问题

在之前的例子中,我们使用了一个只有两个状态的问题,在这一章中使用一个更为复杂的场景,包含一个奖励点,一个惩罚点和一个无法移动点:

Screenshot_20241205_213218_com.newskyer.draw.jpg

class GridWorld:
    def __init__(self):
        self.RewardMap = [[0,0,0,1],
                          [0,None,0,-1],
                          [0,0,0,0]]
        self.Actions = {'up':[-1,0],
                       'down':[1,0],
                       'left':[0,-1],
                       'right':[0,1]}
        self.GoalState = [0,3]
        self.StartState = [2,0]
        self.WallState = [1,1]

        self.Width = len(self.RewardMap[0])
        self.Height = len(self.RewardMap)

    def NextState(self,State,Action):
        Move = self.Actions[Action]
        NextState = [State[0] + Move[0],State[1] + Move[1]]
        if NextState[0]<0 or NextState[1]<0 or NextState[0]>=self.Height or NextState[1]>=self.Width:
            NextState = State
        elif NextState == self.WallState:
            NextState = State

        return NextState

    def GetReward(self,NextState):
        return self.RewardMap[NextState[0]][NextState[1]]

下面使用迭代策略评估来估计在随机策略下的价值函数值,回忆一次价值函数的迭代公式:

Vk+1(s)=aπ(as){r(s,a,s)+γVk(s)}V_{k+1}(s)=\sum_{a}\pi(a|s)\{r(s,a,s^{'})+\gamma V _{k}(s^{'})\}
def DPOneStep(pi,V,env,gamma=0.9):
    for Row in range(env.Height):
        for Col in range(env.Width):
            State = (Row,Col)
            if State == env.GoalState:
                V[State] = 0
                continue

            NewV = 0
            ActionProbs = pi[State]
            for Action , ActionProb in ActionProbs.items():
                NextState = env.NextState(State,Action)
                Reward = env.GetReward(NextState)
                NewV += ActionProb*(Reward+gamma*V[NextState])

            V[State] = NewV
    return V

def PolicyEval(pi,V,env,gamma=0.9,threshold=0.0001):
    while(1):
        OldV = V.copy()
        V = DPOneStep(pi,V,env,gamma=gamma)

        Delta = 0
        for Key,Value in V.items():
            T = abs(Value-OldV[Key])
            if T>Delta:
                Delta=T

        if Delta<threshold:
            break
    return V

image.png

策略迭代法

最优策略π(s)\pi_{*}(s)表示为:

π(s)=arg maxasp(ss,a){r(s,a,s)+γν(s)}=arg maxaq(s,a)\begin{aligned} \pi_{*}(s) &= \argmax_{a}\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{*}(s^{'})\} \\&=\argmax_{a}q_{*}(s,a) \end{aligned}

同理,也可以以此为基础提出最优策略的迭代公式:

π(s)=arg maxasp(ss,a){r(s,a,s)+γνπ(s)}\begin{aligned} \pi^{'}(s) &= \argmax_{a}\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\} \end{aligned}

其中νπ\nu _{\pi} 表示当前策略下的价值函数,π(s)\pi^{'}(s)表示下一次迭代的策略。

至此,我们得到了两个迭代公式:

  • 价值函数迭代公式: 给定策略即可求出当前策略的价值函数
  • 最优策略迭代公式: 给定价值函数即可迭代更新策略

可以观察到上述两个公式是互相运用的,基于这个前提,策略迭代的基本思路是重复评估和改进,具体的说是用π0\pi_0评估得到V0V_0,然后再用V0V_0更新策略得到π1\pi_1,以此类推。

实施策略迭代法:

在上面的例子中不存在状态转移概率,所以策略的迭代公式为:

π(s)=arg maxa{r(s,a,s)+γνπ(s)}\begin{aligned} \pi^{'}(s) &= \argmax_{a}\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\} \end{aligned}
  • GreedyPolicy函数实现了最优策略迭代公式,主要思路是计算出每个ActionAction{r(s,a,s)+γνπ(s)}\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\},然后存储到ActionScore中,最后得到最大值的ActionAction
  • PolicyIter函数实现了重复评估和改进,迭代结束的标准是策略收敛,当前策略即是最优策略
import numpy as np
from collections import defaultdict
def GreedyPolicy(env,V,gamma = 0.9):
    pi = {}
    for Row in range(env.Height):
        for Col in range(env.Width):
            State = [Row,Col]
            ActionScore = {}
            for Action,StateChange in env.Actions.items():
                NextState = env.NextState(State,Action)
                Reward = env.GetReward(NextState)
                ActionScore[str(Action)]=Reward+gamma*V[str(NextState)]

            MaxScore = -1 #确保BestAction有值
            for Action,Score in ActionScore.items():
                if Score>MaxScore:
                    MaxScore = Score
                    BestAction = Action
            pi[str(State)] = {'left':0,'right':0,'up':0,'down':0}
            pi[str(State)][BestAction] = 1
    return pi

def PolicyIter(pi,V,env,gamma=0.9,threshold=0.001):
    while(1):
        V = PolicyEval(pi,V,env,gamma, threshold)
        NewPi = GreedyPolicy(env,V,gamma)

        if NewPi==pi:
            break
        pi = NewPi
        # print(V,pi)
    return pi,V




env = GridWorld()

V = defaultdict(lambda :0)
pi = defaultdict(lambda :{'left':0.25,'right':0.25,'up':0.25,'down':0.25})
V = PolicyEval(pi, V, env)

BestPi,BestV = PolicyIter(pi,V,env)
print(BestPi)
print(BestV)

for State,Action in BestPi.items():
    print(State,Action)

价值迭代法

观察两个迭代公式:

V(s)=asπ(as)p(ss,a){r(s,a,s)+γV(s)}V^{'}(s)=\sum_{a} \sum_{s^{'}}\pi(a|s)p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma V(s^{'})\}
π(s)=arg maxasp(ss,a){r(s,a,s)+γνπ(s)}\begin{aligned} \pi^{'}(s) &= \argmax_{a}\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\} \end{aligned}

观察到sp(ss,a){r(s,a,s)+γνπ(s)}\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\}部分的计算是重复的,可以合并为:

V(s)=maxasp(ss,a){r(s,a,s)+γV(s)}V^{'}(s)=\max_{a}\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma V(s^{'})\}

回忆贝尔曼最优方程:

ν(s)=maxasp(ss,a){r(s,a,s)+γν(s)}\begin{aligned} \nu _{*}(s) =\max_a \sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{*}(s^{'})\} \end{aligned}

可以看到上面的合并公式就是贝尔曼最优方程的迭代形式

价值迭代法的实现:

  • ValueIterOneStep函数实现了上述迭代公式,将sp(ss,a){r(s,a,s)+γV(s)}\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma V(s^{'})\}存储到ActionScores,选出最大的值即可
def ValueIterOneStep(V,env,gamma = 0.9):
    for Row in range(env.Height):
        for Col in range(env.Width):
            State = (Row,Col)
            if State == env.GoalState:
                V[State] = 0
                continue

            ActionScores = []
            for Action,StateChange in env.Actions.items():
                NextState = env.NextState(State,Action)
                Reward = env.GetReward(NextState)
                ActionScores.append(Reward+gamma*V[NextState])

            V[State] = max(ActionScores)
    return V
def ValueIter(V,env,gamma=0.9,threshold=0.001):
    while(1):
        OldV=V.copy()
        V = ValueIterOneStep(V,env,gamma)

        Delta = 0
        for Key,Value in V.items():
            T = abs(Value-OldV[Key])
            if T>Delta:
                Delta=T

        if Delta<threshold:
            break
    return V

env = GridWorld()

V = defaultdict(lambda :0)
pi = defaultdict(lambda :{'left':0.25,'right':0.25,'up':0.25,'down':0.25})
V = PolicyEval(pi, V, env)

V = ValueIter(V,env)
pi = GreedyPolicy(env,V)

价值迭代.png

总结

动态规划法本质上就是利用贝尔曼方程的迭代形式,其要求是已知环境模型,一步一步迭代而避免了求解复杂的联立方程,具体分为三个方法:

  • 策略评估:已知策略迭代计算价值函数
  • 策略迭代法:策略和价值函数互相迭代
  • 价值迭代法:先迭代计算出最优价值函数,然后再直接得到最优策略

Chapter5:蒙特卡洛方法

大部分情况下环境模型是位置的,我们必须从环境中的反复的采集来完成我们的估计,蒙特卡洛方法是对数据进行反复采样并根据结果进行估计的方法的总称。

模型的表示方法分为分布模型样本模型

  • 分布模型:表示出模型的具体概率分布
  • 样本模型:通过采样样本以估计模型的概率分布

例如对于2个骰子的点数和模型,我们当然可以一一列举可能出现的36种可能性,表示每种点数和的概率,这个是分布模型。也可以不断的实验,通过采用的结果计算期望值,这就是蒙特卡洛方法

在前面的老虎机部分时,我们用采样的平均值估计数学期望,这就是蒙特卡洛方法

使用蒙特卡洛方法计算价值函数

回顾价值函数的定义:

νπ(s)=Eπ[GtSt=s]\nu _{\pi}(s) = \mathbb{E}_{\pi}[G_t|S_t = s]

按照“使用平均值进行期望值估计”的思想,价值函数估计值可以表示为:

Vπ(s)=G(1)+G(2)+...+G(n)n=Vn1(s)+1n{G(n)Vn1(s)}\begin{aligned} V_{\pi}(s) &= \frac{G^{(1)}+G^{(2)}+...+G^{(n)}}{n} \\&= V_{n-1}(s)+\frac{1}{n}\{G^{(n)}-V_{n-1}(s)\} \end{aligned}

其中G(n)G^{(n)}表示第nn轮测试的收益,例如从某个初始点开始,进行nn次测试,每次到达目标点为止(注意此处只能适用回合制问题

在这个思路的前提下,假设一共有mmStateState,那么一共需要进行m×nm×n次测试,显然这样的更新方式是效率很低的。例如从AA状态出发,经过了BBCC到达终点,其实这一次测试可以更新三个状态的价值函数,然而上面的思想却只更新AA状态的价值函数。

假设从AA状态出发,经过了BBCC到达终点,AABB的收益是R0R_0,以此类推,那么各个状态的价值函数估计可以表示为:

GA=R0+γR1+γ2R2=R0+γGBGB=R1+γR2=R1+γGCGC=R2\begin{aligned} G_A &= R_0 + \gamma R_1 + \gamma^2 R_2 = R_0+\gamma G_B \\G_B&= R_1+\gamma R_2 = R_1+\gamma G_C \\G_C&=R_2 \end{aligned}

因为更新GAG_A需要用到GBG_B,为了更新的独立性,我们从后往前更新:

GC=R2GB=R1+γGCGA=R0+γGB\begin{aligned} G_C&=R_2 \\G_B&= R_1+\gamma G_C \\G_A &= R_0+\gamma G_B \end{aligned}

仍然在GridWorld问题中应用,创建RandomAgent类实现:

  • GetAction:随机获取下一个行动
  • Move:记录一次移动的状态和收益,并更新当前状态
  • Update:更新价值函数
class RandomAgent:
    def __init__(self,StartState):
        self.Cnts = defaultdict(lambda : 0)
        self.Records = []
        self.StartState = StartState
        self.State = StartState #指示当前的状态

        self.V = defaultdict(lambda : 0)
        self.gamma = 0.9
        self.pi = defaultdict(lambda : {'left':0.25,'right':0.25,'up':0.25,'down':0.25})

    def GetAction(self,State):
        ActionProbs = self.pi[State]
        return np.random.choice(list(ActionProbs.keys()),p=list(ActionProbs.values()))

    def Move(self,NextState,State,Reward):
        self.Records.append([State,Reward])
        self.State = NextState

    def Update(self):
        G = 0
        for Data in reversed(self.Records):
            State, Reward = Data
            G = Reward + self.gamma * G
            self.Cnts[State] += 1
            self.V[State] += (G - self.V[State]) / self.Cnts[State]

    def Reset(self): # 返回起点
        self.State = self.StartState
        self.Records = []

运行上述方法:

steps = 1000
env = GridWorld()
Agent = RandomAgent(env.StartState)

for step in range(steps):
    Agent.Reset()
    while(1):
        State = Agent.State
        Action = Agent.GetAction(State)
        NextState = env.NextState(State,Action)
        Reward = env.GetReward(NextState)
        Agent.Move(NextState,State,Reward)
        if NextState == env.GoalState:
            Agent.Update()
            break
        State = NextState

image.png

使用蒙特卡洛方法实现策略控制

回忆最优策略公式:

π(s)=arg maxasp(ss,a){r(s,a,s)+γν(s)}=arg maxaq(s,a)\begin{aligned} \pi_{*}(s) &= \argmax_{a}\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{*}(s^{'})\} \\&=\argmax_{a}q_{*}(s,a) \end{aligned}

在本例子中,我们不知道环境参数,也就是不知道p(ss,a)p(s^{'}|s,a)部分,所以只能实验Q函数公式来进行策略控制。

与价值函数迭代类似,Q函数迭代公式为:

Qn(s,a)=G(1)+G(2)+...+G(n)n=Qn1(s,a)+1n{G(n)Qn1(s,a)}\begin{aligned} Q_{n}(s,a) &= \frac{G^{(1)}+G^{(2)}+...+G^{(n)}}{n} \\&= Q_{n-1}(s,a)+\frac{1}{n}\{G^{(n)}-Q_{n-1}(s,a)\} \end{aligned}

蒙特卡洛方法实现策略控制:

  • GreedyProbs实现了策略的迭代更新,主要就是找到Q函数最大的Action
  • Update函数增加了对策略的更新,其余不变
def GreedyProbs(Q,State,Actions = ['left','right','up','down']):
    qs = [Q[(State,Action)] for Action in Actions]
    BestAction = np.argmax(qs)
    ActionProbs = {Action: 0 for Action in Actions}
    ActionProbs[Actions[BestAction]] = 1
    return ActionProbs

class McAgent:
    def __init__(self,StartState):
        self.Cnts = defaultdict(lambda : 0)
        self.Records = []
        self.StartState = StartState
        self.State = StartState #指示当前的状态

        self.Q = defaultdict(lambda : 0)
        self.gamma = 0.9
        self.pi = defaultdict(lambda : {'left':0.25,'right':0.25,'up':0.25,'down':0.25})

    def GetAction(self,State):
        ActionProbs = self.pi[State]
        return np.random.choice(list(ActionProbs.keys()),p=list(ActionProbs.values()))

    def Move(self,NextState,Action,State,Reward):
        self.Records.append([State,Action,Reward])
        self.State = NextState

    def Update(self):
        G = 0
        for Data in reversed(self.Records):
            State, Action, Reward = Data
            G = Reward + self.gamma * G
            Key = (State, Action)
            self.Cnts[Key] += 1
            self.Q[Key] += (G - self.Q[Key]) / self.Cnts[Key]
            self.pi[State] = GreedyProbs(self.Q,State)

    def Reset(self): # 返回起点
        self.State = self.StartState
        self.Records = []
        
steps = 1000
env = GridWorld()
Agent = McAgent(env.StartState)

for step in range(steps):
    Agent.Reset()
    while(1):
        State = Agent.State
        Action = Agent.GetAction(State)
        NextState = env.NextState(State,Action)
        Reward = env.GetReward(NextState)
        Agent.Move(NextState,Action,State,Reward)
        if NextState == env.GoalState:
            Agent.Update()
            break
        State = NextState

使用上述方法有两个弊端:

  • GreedyProbs函数中不应该使用绝对的贪婪算法,这样会导致两种弊端:
    • 一旦确定从初始点到终点的最佳路径,那么这个路径不会变化,对于不在这个路径上的状态,其价值函数是无法更新的
    • 如果在第一次更新时,在初始点的最佳行动是往左(在这个问题中是可能的,因为撞墙是不会有惩罚的,只会停在原地不动),那么程序会陷入死循环,即一直撞墙
  • Update函数中不应该使用完全平均,而应该使用移动平均,具体原因可以类似于老虎机问题中的非稳态情况

ε\varepsilon-greedy算法

其实就是在选取最优行动时,不完全实现贪婪,而保持一些“探索性”,具体实现方式是e_GreedyProbs函数中,输出不再是[0,0,1,0]的形式,而是类似[0.1,0.1,0.7,0.1]这样在当前最优行动的选择概率最大的情况下,又有一定的概率选择其他行动的结果

def e_GreedyProbs(Q,State,Epsilon=0, Actions = ['left','right','up','down']):
    qs = [Q[(State,Action)] for Action in Actions]
    BaseProbs = Epsilon/len(Actions)

    BestAction = np.argmax(qs)
    ActionProbs = {Action: BaseProbs for Action in Actions}
    ActionProbs[Actions[BestAction]] += (1 - Epsilon)
    return ActionProbs

同时将Update函数更新,主要变化是引入常数α\alpha以更新QQ值:

def Update(self):
    G = 0
    for Data in reversed(self.Records):
        State, Action, Reward = Data
        G = Reward + self.gamma * G
        Key = (State, Action)
        self.Cnts[Key] += 1
        self.Q[Key] += (G - self.Q[Key]) * self.Alpha
        self.pi[State] = e_GreedyProbs(self.Q,State,self.Epsilon)

这样就不会有死循环的问题,也可以保证所有状态都有概率走到。

异策略型

在上面的方法中,我们采用ε\varepsilon-greedy算法进行策略的更新,事实上这是一种妥协,我们必须设置一定程度的“探索”概率以完成所有的状态更新。理想情况下我们想用完全的“贪婪”,不希望有“探索”行为。,那么可以采用异策略型

先介绍一些概念:

  • 目标策略:是我们希望学习和优化的策略。它通常是在理论分析或者算法设计中定义的理想策略,用于计算目标值(如在计算价值函数的目标值时),以此来引导行为策略朝着更好的方向学习。
  • 行为策略:智能体在与环境交互过程中用于生成动作的策略。简单来说,它决定了智能体在每个状态下如何实际选择动作
  • 同策略型:目标策略和行为策略没有区分
  • 异策略型:有区分,分布是两个策略分布

在策略的更新中,我们想要最后的策略是完全的“贪婪”,但是如果我们真的按照完全"贪婪”则无法完成训练。所以我们可以设置一个策略,他具有“探索”行为,称之为行为策略,智能体的实际行动还是按照这个行为策略。又设置另外一个策略,完全按照"贪婪",称之为目标策略,是我们希望的策略更新方向。

由于行为策略和目标策略不同,在估计目标策略的价值函数时会遇到问题。因为按照行为策略收集的数据来直接估计目标策略的价值函数是不准确的。重要性采样就是用于解决这个问题的技术。

重要性采集

假如我们要估计π\pi分布的数学期望,那最简单的办法是采集这个分布的值,然后算平均数:

sampling:xiπEπ(x)=xπ(x)=x1+x2+x3...xnnsampling:x_i\sim\pi \\\mathbb{E}_{\pi}(x) =\sum x \pi(x)= \frac{x_1+x_2+x_3...x_n}{n}

假如xx不是从π\pi分布中采集的,而是从bb分布中采集的,那如何估计π\pi分布的数学期望呢?

sampling:xibEπ(x)=xπ(x)b(x)b(x)=Eb([xπ(x)b(x)])=Eb(ρx)sampling:x_i\sim b \\ \mathbb{E}_{\pi}(x) =\sum x \frac{\pi(x)}{b(x)}b(x) = \mathbb{E}_{b}([x\frac{\pi(x)}{b(x)}]) = \mathbb{E}_{b}(\rho x)

也就是说,我们通过行为策略采集到了数据,但是我们需要估计的是目标策略的价值函数,所以通过采集ρx\rho x来估计目标策略的价值函数。

当这两个分概率分布差别较大时,π(x)b(x)\frac{\pi(x)}{b(x)}不稳定,导致采集到的值xπ(x)b(x)x\frac{\pi(x)}{b(x)}与真实的数学期望之间的差距较大,采集到的值的方差很大,意味着方法的稳定性较差,保证两个分布的概率分布尽可能一样是解决这个问题的方法,在这里,两个分布的主要差异是探索系数,当Epsilon设置合理时,可以达到这个目的。

下面介绍了蒙特卡洛方法中异型策略的实现方式,由于蒙特卡洛的数据是一个回合,所以从前面时间步到后

6D1165383C26BCE9768763B80D225663.png

class McOffAgent:
    def __init__(self,StartState):
        self.Cnts = defaultdict(lambda : 0)
        self.Records = []
        self.StartState = StartState
        self.State = StartState #指示当前的状态

        self.Epsilon = 0.2
        self.Alpha = 0.1
        self.b = defaultdict(lambda : {'left':0.25,'right':0.25,'up':0.25,'down':0.25})

        self.Q = defaultdict(lambda : 0)
        self.gamma = 0.9
        self.pi = defaultdict(lambda : {'left':0.25,'right':0.25,'up':0.25,'down':0.25})

    def GetAction(self,State):
        ActionProbs = self.b[State]
        return np.random.choice(list(ActionProbs.keys()),p=list(ActionProbs.values()))

    def Move(self,NextState,Action,State,Reward):
        self.Records.append([State,Action,Reward])
        self.State = NextState

    def Update(self):
        G = 0
        rho = 1
        for Data in reversed(self.Records):
            State, Action, Reward = Data
            G = Reward + self.gamma * G * rho
            Key = (State, Action)

            self.Q[Key] += (G - self.Q[Key]) * self.Alpha
            rho *= self.pi[State][Action] / self.b[State][Action]
            self.pi[State] = e_GreedyProbs(self.Q,State,0)
            self.b[State] = e_GreedyProbs(self.Q,State,self.Epsilon)

    def Reset(self): # 返回起点
        self.State = self.StartState

Chapter6:TD 方法

TD:Temporal Difference,时间差分

蒙特卡洛方法的弊端在于,其只能应用于回合制问题,且当一个回合过长时,其更新速度很慢。 面对连续性问题或回合很长的回合制问题时,TD方法更有效。

  • 动态规划法

  • 蒙特卡洛方法

  • TD方法

νπ(s)=asπ(as)p(ss,a){r(s,a,s)+γνπ(s)}=Eπ[Rt+γVπ(St+1)St=s]\begin{aligned} \nu _{\pi}(s) &=\sum_{a} \sum_{s^{'}}\pi(a|s)p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \nu _{\pi}(s^{'})\} \\&=\mathbb{E}_{\pi}[R_t + \gamma V_{\pi}(S_{t+1})|S_t = s] \end{aligned}

TD方法使用的更新公式为:

Vπ=Vπ(St)+α(Rt+γVπ(St+1)Vπ(St))V_{\pi}^{'} = V_{\pi}(S_t) + \alpha(R_t + \gamma V_{\pi}(S_{t+1})-V_{\pi}(S_{t}))

仍然在GridWorld中进行实验,主要关注Update方法

class TDAgent:
    def __init__(self,StartState):
        self.Cnts = defaultdict(lambda : 0)
        self.Records = []
        self.StartState = StartState
        self.State = StartState #指示当前的状态

        self.Alpha = 0.1

        self.V = defaultdict(lambda : 0)
        self.gamma = 0.9
        self.pi = defaultdict(lambda : {'left':0.25,'right':0.25,'up':0.25,'down':0.25})

    def GetAction(self,State):
        ActionProbs = self.pi[State]
        return np.random.choice(list(ActionProbs.keys()),p=list(ActionProbs.values()))


    def Update(self,Reward,State,NextState,GoalState):
        if NextState == GoalState:
            Target = Reward + self.gamma * 0
        else:            
            Target = Reward + self.gamma * self.V[NextState]
        self.V[State] +=self.Alpha * (Target - self.V[State])


    def Reset(self): # 返回起点
        self.State = self.StartState
        self.Records = []
steps = 10000
env = GridWorld()
Agent = TDAgent(env.StartState)

for step in range(steps):
    Agent.Reset()
    while(1):
        State = Agent.State
        Action = Agent.GetAction(State)
        NextState = env.NextState(State,Action)
        Reward = env.GetReward(NextState)
        Agent.Update(Reward, State, NextState,env.GoalState) #每一步都要更新
        if NextState == env.GoalState:
            break
        Agent.State = NextState

image.png

SARSA

同样,在未知环境模型的情况下,我们还是实验Q函数来进行策略估计和策略控制

回忆TD方法的策略估计公式:

Vπ(St)=Vπ(St)+α(Rt+γVπ(St+1)Vπ(St))V_{\pi}^{'}(S_t) = V_{\pi}(S_t) + \alpha(R_t + \gamma V_{\pi}(S_{t+1})-V_{\pi}(S_{t}))

将其运用到Q函数中可以表示为:

Qπ(St,At)=Qπ(St,At)+α(Rt+γQπ(St+1,At+1)Qπ(St,At))Q_{\pi}^{'}(S_t,A_t) = Q_{\pi}(S_t,A_t) + \alpha(R_t + \gamma Q_{\pi}(S_{t+1},A_{t+1})-Q_{\pi}(S_t,A_t))

使用SARSA方法进行策略控制:

  • Update函数实现了Q函数的更新
    • Records中保存了(St,At)(S_t,A_t)(St+1,At+1)(S_{t+1},A_{t+1})
  • 策略的更新采用了ε\varepsilon-greedy算法
class SarsaAgent:
    def __init__(self,StartState):
        self.Cnts = defaultdict(lambda : 0)
        self.Records = []
        self.StartState = StartState
        self.State = StartState #指示当前的状态
        self.Alpha = 0.8
        self.Q = defaultdict(lambda : 0)
        self.gamma = 0.9
        self.pi = defaultdict(lambda : {'left':0.25,'right':0.25,'up':0.25,'down':0.25})
        self.Epsilon = 0.2
        self.Records = []
    def GetAction(self,State):
        ActionProbs = self.pi[State]
        return np.random.choice(list(ActionProbs.keys()),p=list(ActionProbs.values()))
        
    def Update(self,Reward,State,Action,GoalState):
        self.Records.append((State,Action,Reward))
        if len(self.Records)<2:
            return
        State,Action,Reward = self.Records[-2]
        NextState,NextAction,_ = self.Records[-1]
        if NextState == GoalState:
            Target = Reward + self.gamma * 0
        else:
            Target = Reward + self.gamma * self.Q[(NextState,NextAction)]
        self.Q[(State,Action)] +=self.Alpha * (Target - self.Q[(State,Action)])
        self.pi[State] = e_GreedyProbs(self.Q,State,self.Epsilon)

    def Reset(self): # 返回起点
        self.State = self.StartState
        self.Records = []

TD1.pngTD2.png

异策略型的SARSA

在TD问题中,由于只考虑了上下两层状态,所以权重表示为:

ρ=π(At+1St+1)b(At+1St+1)\rho = \frac{\pi(A_{t+1}|S_{t+1})}{b(A_{t+1}|S_{t+1})}

因此其更新公式为

sampling:At+1bQπ(St,At)=Qπ(St,At)+α{ρ(Rt+γQπ(St+1,At+1))Qπ(St,At)}sampling:A_{t+1}\sim b \\ Q_{\pi}^{'}(S_t,A_t) = Q_{\pi}(S_t,A_t) + \alpha\{\rho(R_t + \gamma Q_{\pi}(S_{t+1},A_{t+1}))-Q_{\pi}(S_t,A_t)\}
class SarsaOffAgent:
    def __init__(self,StartState):
        self.Cnts = defaultdict(lambda : 0)
        self.Records = []
        self.StartState = StartState
        self.State = StartState #指示当前的状态

        self.Alpha = 0.8

        self.Q = defaultdict(lambda : 0)
        self.gamma = 0.9
        self.pi = defaultdict(lambda : {'left':0.25,'right':0.25,'up':0.25,'down':0.25})
        self.b = defaultdict(lambda: {'left': 0.25, 'right': 0.25, 'up': 0.25, 'down': 0.25})
        self.Epsilon = 0.2
        # self.Records = deque(maxlen=2)
        self.Records = []
    def GetAction(self,State):
        ActionProbs = self.b[State]
        return np.random.choice(list(ActionProbs.keys()),p=list(ActionProbs.values()))


    def Update(self,Reward,State,Action,GoalState):
        self.Records.append((State,Action,Reward))
        if len(self.Records)<2:
            return
        State,Action,Reward = self.Records[-2]
        NextState,NextAction,_ = self.Records[-1]

        if NextState == GoalState:
            Target = Reward + self.gamma * 0
            rho = 1
        else:
            rho = self.pi[(NextState,NextAction)] / self.b[(NextState,NextAction)]
            Target = rho * (Reward + self.gamma * self.Q[(NextState, NextAction)])

        self.Q[(State,Action)] +=self.Alpha * (Target - self.Q[(State,Action)])
        self.pi[State] = e_GreedyProbs(self.Q,State,0)
        self.b[State] = e_GreedyProbs(self.Q,State,self.Epsilon)


    def Reset(self): # 返回起点
        self.State = self.StartState
        self.Records = []

steps = 10000
env = GridWorld()
Agent = SarsaAgent(env.StartState)

for step in range(steps):
    Agent.Reset()
    while(1):
        State = Agent.State
        Action = Agent.GetAction(State)
        NextState = env.NextState(State,Action)
        Reward = env.GetReward(NextState)
        Agent.Update(Reward, State,Action,env.GoalState)
        if NextState ==env.GoalState:
            Agent.Update(None,NextState,None,env.GoalState)
            break
        Agent.State = NextState

Q学习(重要)

重要性采集事实上容易变得不稳定,尤其当两者策略的概率分布差别变大时,权重ρ\rho的变化就会大,SARSA中的更新方向就会发生变化,从而使得Q函数的更新变得不稳定,Q学习就是解决这个问题的方法。Q学习具有下列三个特点

  • 采用TD方法
  • 异策略型
  • 不使用重要性采样

为了联合SARS了解Q学习,回忆贝尔曼方程

qπ(s,a)=sp(ss,a){r(s,a,s)+γaπ(as)qπ(s,a)}=Eπ[Rt+γQπ(St+1,At+1)St=s,At=a]\begin{aligned} q_{\pi}(s,a)&=\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \sum_{a^{'}}\pi(a^{'}|s^{'})q_{\pi}(s^{'},a^{'})\} \\& =\mathbb{E}_{\pi}[R_t + \gamma Q_{\pi}(S_{t+1},A_{t+1})|S_t = s,A_t=a] \end{aligned}

其考虑到了状态转移概率下p(ss,a)p(s^{'}|s,a)的所有下一个状态,又考虑到了策略π(as)\pi(a^{'}|s^{'})下的所有下一个动作。

EE424612F2271E59A66E76468882F639.png

回忆SARSA的公式:

Qπ(St,At)=Qπ(St,At)+α{Rt+γQπ(St+1,At+1)Qπ(St,At)}Q_{\pi}^{'}(S_t,A_t) = Q_{\pi}(S_t,A_t) + \alpha\bigg\{R_t + \gamma Q_{\pi}(S_{t+1},A_{t+1})-Q_{\pi}(S_t,A_t)\bigg\}

考虑SARSA的本质,其实是贝尔曼方程的一种采样,其先基于p(ss,a)p(s^{'}|s,a)对下一个状态进行采样,然后基于π(as)\pi(a|s)对下一步的行动采样,于是Q函数的更新方向就是Rt+γQπ(St+1,At+1)R_t + \gamma Q_{\pi}(S_{t+1},A_{t+1})

3F0B72ABDF5BE8A633B5F855DD6AE4C2.png

如果说SARSA对应着贝尔曼方程,那么Q方法就是对应着贝尔曼最优方程,其在选择下一个动作时,不再依靠策略进行采样,而是直接选择最优的动作

回顾贝尔曼最优方程

q(s,a)=sp(ss,a){r(s,a,s)+γmaxaq(s,a)}=E[Rt+γmaxaq(s,a)St=s,At=a]\begin{aligned} q_{*}(s,a)&=\sum_{s^{'}}p(s^{'}|s,a)\{r(s,a,s^{'})+\gamma \max_{a^{'}} q_{*}(s^{'},a^{'})\} \\&=\mathbb{E}[R_t + \gamma \max_{a^{'}} q_{*}(s^{'},a^{'})|S_t = s,A_t=a] \end{aligned}

将其写成采样形式:

Q(St,At)=E[Rt+γmaxaQ(St+1,a)]\begin{aligned} Q^{'}(S_t,A_t)=\mathbb{E}[R_t + \gamma \max_{a^{'}}Q(S_{t+1},a^{'})] \end{aligned}
Q(St,At)=Q(St,At)+α{Rt+γmaxaQ(St+1,a)Q(St,At)}Q^{'}(S_t,A_t) = Q(S_t,A_t) + \alpha \bigg\{ R_t + \gamma \max_{a^{'}}Q(S_{t+1},a^{'})-Q(S_t,A_t)\bigg\}

由于对At+1A_{t+1}的选择直接使用的maxmax,不需要重要性选择进行修正。

读到这里时可能会有读者产生疑问:在选择动作时,Q学习采样的是maxmax,但是在状态转移时为什么却选择基于采样? image.png

Q学习的实现:

class QLearningAgent:
    def __init__(self,StartState):
        self.Cnts = defaultdict(lambda : 0)
        self.Records = []
        self.StartState = StartState
        self.State = StartState #指示当前的状态

        self.Alpha = 0.8

        self.Q = defaultdict(lambda : 0)
        self.gamma = 0.9
        self.pi = defaultdict(lambda : {'left':0.25,'right':0.25,'up':0.25,'down':0.25})
        self.b = defaultdict(lambda: {'left': 0.25, 'right': 0.25, 'up': 0.25, 'down': 0.25})
        self.Epsilon = 0.2

    def GetAction(self,State):
        ActionProbs = self.b[State]
        return np.random.choice(list(ActionProbs.keys()),p=list(ActionProbs.values()))

    def Update(self,Reward,State,Action,NextState,GoalState):
        if NextState == GoalState:
            MaxQNextState = 0
        else:
            NextStateScore = [self.Q[(NextState,a)] for a in ['up','down','left','right']]
            MaxQNextState = max(NextStateScore)
        Target = Reward+self.gamma * MaxQNextState
        self.Q[(State,Action)] +=self.Alpha * (Target - self.Q[(State,Action)])
        self.pi[State] = e_GreedyProbs(self.Q,State,0)
        self.b[State] = e_GreedyProbs(self.Q,State,self.Epsilon)


    def Reset(self): # 返回起点
        self.State = self.StartState

steps = 10000
env = GridWorld()
Agent = QLearningAgent(env.StartState)

for step in range(steps):
    Agent.Reset()
    while(1):
        State = Agent.State
        Action = Agent.GetAction(State)
        NextState = env.NextState(State,Action)
        Reward = env.GetReward(NextState)
        Agent.Update(Reward, State,Action,NextState,env.GoalState)
        if NextState == env.GoalState:
            break
        Agent.State = NextState

样本模型版的Q学习

样本模型是对应于分布模型,其最大的特点是不保存特定的概率分布。

  • 观察Q学习代码,其实pi完全没有参与Q函数的更新,由于其在需要的时候,可以由Q函数值马上得到,所以其实可以直接删除
  • 对于策略b,其本质上也是根据Q函数的ε\varepsilon-greedy算法一种实现,在GetAction函数中再现这一过程即可,也可以删除
class QLearningAgent_2:
    def __init__(self,StartState):
        self.Cnts = defaultdict(lambda : 0)
        self.Records = []
        self.StartState = StartState
        self.State = StartState #指示当前的状态

        self.Alpha = 0.8

        self.Q = defaultdict(lambda : 0)
        self.gamma = 0.9
        self.Epsilon = 0.2
        self.ActionSize = ['up','down','left','right']

    def GetAction(self,State):
        if np.random.rand()<self.Epsilon:
            return np.random.choice(self.ActionSize)
        else:
            qs = {a:self.Q[(State,a)] for a in self.ActionSize}
            BestQ = -1
            BestAction = None
            for k,v in qs.items():
                if v>BestQ:
                    BestQ = v
                    BestAction = k
            return BestAction
    def Update(self,Reward,State,Action,NextState,GoalState):
        if NextState == GoalState:
            MaxQNextState = 0
        else:
            NextStateScore = [self.Q[(NextState,a)] for a in self.ActionSize]
            MaxQNextState = max(NextStateScore)
        Target = Reward+self.gamma * MaxQNextState
        self.Q[(State,Action)] +=self.Alpha * (Target - self.Q[(State,Action)])


    def Reset(self): # 返回起点
        self.State = self.StartState

Discussion

前面3-6章基本上都是针对贝尔曼方程进行的一些工作:

强化学习前期.png

Chapter7:神经网络和Q学习

在前面的方法中,我们都是用一个表格形式存储Q函数,但是复杂情况下,状态数量太多,无法将其存储为表格形式,用更加紧凑的函数近似Q函数是一种处理方法,其中最有效的便是神经网络

机器学习和深度学习的基础知识此处省略

求Q函数的过程抽象为神经网络,有两种形式

  • 输入状态ss和行动aa,输出Q函数值Q(s,a)Q(s,a)
  • 输入状态ss,输入所有行动的Q函数列表

首先我们需要知道神经网络的输入输出,以及其误差函数的设计,这个地方书上写的比较模糊,书上采用了下面这个公式说明,也就是Q学习的迭代公式

Qπ(St,At)=Qπ(St,At)+α{Rt+γmaxaQπ(St+1,a)Qπ(St,At)}Q_{\pi}^{'}(S_t,A_t) = Q_{\pi}(S_t,A_t) + \alpha \bigg\{ R_t + \gamma \max_{a^{'}}Q_{\pi}(S_{t+1},a^{'})-Q_{\pi}(S_t,A_t)\bigg\}

然后书上的代码表达的意思是,神经网络拟合了Q函数值,然后误差函数的形式为:

loss(Rt+γmaxaQπ(St+1,a),Qπ(St,At))loss( R_t +\gamma \max_{a^{'}}Q_{\pi}(S_{t+1},a^{'}),Q_{\pi}(S_t,A_t))

当然书上的做法是没有问题的,但是但从这两个地方会产生疑惑:这个误差函数的理论依据是什么?跟Q学习的迭代公式好像也没关系啊。其实应该是按照下面这个公式:

Q(St,At)=E[Rt+γmaxaQ(St+1,a)]\begin{aligned} Q^{'}(S_t,A_t)=\mathbb{E}[R_t + \gamma \max_{a^{'}}Q(S_{t+1},a^{'})] \end{aligned}

所以,下一次迭代的Q(St,At)Q^{'}(S_t,A_t)需要接近Rt+γmaxaQ(St+1,a)R_t + \gamma \max_{a^{'}}Q(S_{t+1},a^{'}),由于神经网络本来就是不断更新和迭代的模型,其内部本身就蕴含数学期望的概念,所以上面这个公式才是神经网络更新的基础公式

接下来在GridWorld上实现这一操作:

首先是onehot操作,将状态转换为onehot格式以便输入神经网络:

def OneHot(state):
    height = 3
    width = 4
    state_one_hot = np.zeros(height*width)
    state_one_hot[state[0]*width+state[1]] = 1
    return  torch.tensor(state_one_hot[np.newaxis,:],dtype=torch.float)

定义模拟Q函数的神经网络QNet,由两个线性层组成:

class QNet(nn.Module):
    def __init__(self,input_size,hidden_size,output_size):
        super().__init__()
        self.l1 = nn.Linear(input_size,hidden_size)
        self.l2 = nn.Linear(hidden_size,output_size)

    def forward(self, x):
        x = F.relu(self.l1(x))
        x = self.l2(x)
        return x

定义QLearningAgent:

  • GetAction:将状态输入神经网络,输出最大的动作即为选择的动作
  • Update:更新神经网络
    • targetRt+γmaxaQ(St+1,a)R_t + \gamma \max_{a^{'}}Q(S_{t+1},a^{'})
    • qQ(St,At)Q^{'}(S_t,A_t)
class QLearningAgent:
    def __init__(self,start_state):
        self.start_state = start_state
        self.state = start_state #指示当前的状态

        self.lr = 0.01
        self.epsilon = 0.1
        self.alpha = 0.1
        self.gamma = 0.9
        self.actions =  ['left','right','up','down']
        self.num_states_col = 4
        self.num_states_row = 3

        self.qnet = QNet(self.num_states_col*self.num_states_row,100,len(self.actions))
        self.optimizer = optim.SGD(self.qnet.parameters(),lr = self.lr)

    def GetAction(self,state):
        if np.random.rand() < self.epsilon:
            return np.random.choice(self.actions)
        else:
            y =  self.qnet(OneHot(state))
            return self.actions[y.data.argmax().numpy()]

    def Update(self, reward, state, action, next_state, goal_state):
        if next_state == goal_state:
            next_q = torch.tensor([0])
        else:
            next_qs = self.qnet(OneHot(next_state))
            next_q = next_qs.max(axis = 1)[0].detach()


        target = self.gamma * next_q + reward
        qs = self.qnet(OneHot(state))

        q = qs[:,self.actions.index(action)]

        loss1 = F.mse_loss(q, target)

        self.optimizer.zero_grad()
        loss1.backward()
        self.optimizer.step()

        return loss1.data
    def Reset(self): # 返回起点
        self.state = self.start_state
env = GridWorld()
agent = QLearningAgent(env.StartState)

steps = 1000
history_loss = []
for step in range(steps):
    agent.Reset()
    total_loss = 0
    cnt = 0
    while(1):
        state = agent.state
        action = agent.GetAction(state)
        next_state = env.NextState(state,agent.actions[action])
        reward = env.GetReward(next_state)
        loss = agent.Update(reward, state,action,next_state,env.GoalState)
        cnt+=1
        total_loss+=loss
        if next_state == env.GoalState:
            break
        agent.state = next_state
    history_loss.append(total_loss/cnt)

Chapter8:DQN

DQN

DQN是一种运用神经网络模拟Q函数的改进形式,主要改进有两个方面:经验回放目标网络

经验回放

在一般的有监督学习中,我们一般会采用小批次的概念,也就是说每次训练从数据集中拿出一部分数据来进行训练。一个批次中的数据都是独立的,这样可以防止出现数据偏差

但是在上面神经网络和Q学习的实践中,数据之间有很强的相关性,比如这一时刻训练的数据是DtD_t,下一时刻训练数据是Dt+1D_{t+1},显然这两个数据直接是直接相关的(比如DtD_t中的状态和行动都影响了Dt+1D_{t+1}的状态),这导致了一定程度的偏差。

解决方法--经验回放:将数据都存储起来,在训练时再随机提取出小批量数据进行训练

目标网络

在监督学习中,数据的label是不会发生改变的,但是在chapter7中的做法中,实际上数据的label会随着训练而变化,例如同一个state,在不同时间下的target是不一样的。在为了弥补这一问题,提出目标网络

image.png

目标网络: 设立应该跟主神经网络结构一样的网络,这个网络参数不随时变化target由这个网络得到,使得训练目标是相对稳定的。但是这个网络也不能不更新,应该设置一个时间定期与主训练网络同步参数。

  • 主神经网络,其Q函数用QθQ_{\theta}表示,其参数一直在训练和更新
  • 目标网络:其Q函数用QθQ_{\theta ^ {'}}表示,其参数不随时更新,只是一段时间后与主神经网络同步
  • 主神经网络更新:Rrward+γmaxaQθ(St+1,a)Rrward+\gamma \max_{a^{'}}Q_{\theta ^ {'}}(S_{t+1},a^{'})--逼近-->Qθ(St,At)Q_{\theta}(S_t,A_t)

这里用gym库模拟倒立摆的问题:

1a64906badd062445a321ca615c7cfd5.png

  • ReplayBuffer:实现经验回放,本质上是使用队列完成,列表也能实现这一功能。
class ReplayBuffer:
    def __init__(self,max_len,batch_size):
        self.deque = deque(maxlen=max_len)
        self.batch_size = batch_size
    def __len__(self):
        return len(self.deque)

    def Add(self,state,action,reward,next_state,done):
        self.deque.append((state,action,reward,next_state,done))

    def GetBatch(self):
        data = random.sample(self.deque,self.batch_size)
        state = torch.tensor(np.stack([x[0] for x in data]))
        action = torch.tensor(np.array([x[1] for x in data]).astype(np.int32))
        reward = torch.tensor(np.array([x[2] for x in data]).astype(np.float32))
        next_state = torch.tensor(np.stack([x[3] for x in data]))
        done = torch.tensor(np.array([x[4] for x in data]).astype(np.int32))
        return state, action, reward, next_state, done
  • DQNAgent:实现数据记录和参数更新
    • sync_qnet:将qnet_target的参数和qnet同步
    • Update:神经网络训练,此处tagret是由qnet_target得到,而qqnet得到,这就实现了目标网络
class DQNAgent:
    def __init__(self,start_state):
        self.start_state = start_state
        self.state = start_state #指示当前的状态

        self.lr = 0.0005
        self.epsilon = 0.1
        self.alpha = 0.1
        self.gamma = 0.98

        self.action_size = 2 #倒立摆的动作只有两种


        self.batch_size = 32
        self.max_len = 10000

        self.qnet = QNet(4,100,self.action_size)
        self.qnet_target = QNet(4, 100, self.action_size)

        self.optimizer = optim.Adam(self.qnet.parameters(),lr = self.lr)
        self.replay_buffer = ReplayBuffer(self.max_len,self.batch_size)

    def sync_qnet(self):
        # self.qnet_target = copy.deepcopy(self.qnet)
        self.qnet_target.load_state_dict(self.qnet.state_dict())

    def GetAction(self, state):
        if np.random.rand() < self.epsilon:
            return np.random.choice(self.action_size)
        else:

            state = torch.tensor(state[np.newaxis, :])
            qs = self.qnet(state)
            return qs.argmax().item()

    def Update(self, reward, state, action, next_state, done):
        self.replay_buffer.Add(state,action,reward,next_state,done)
        if len(self.replay_buffer)<self.batch_size:
            return
        state, action, reward, next_state, done = self.replay_buffer.GetBatch()

        next_qs = self.qnet_target(next_state)
        next_q = next_qs.max(axis = 1)[0].detach()

        target = self.gamma * (1-done)*next_q + reward
        qs = self.qnet(state)
        q = qs[np.arange(len(action)),action]

        loss_fn = nn.MSELoss()
        loss = loss_fn(q, target)
        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

    def Reset(self): # 返回起点
        self.state = self.start_state

这里与我们之前的做法有一些差别,gym库的env提供了step函数,不需要我们像之前一样调用各种函数。另外,当前的state是保存在env中的:

env = gym.make('CartPole-v0')
replay_buffer = ReplayBuffer(10000,32)
start_state = env.reset()
agent = DQNAgent(start_state[0])
steps = 300
history_reward = []

for step in range(steps):
    state=env.reset()[0]
    total_reward = 0
    done = False


    while(1):
        action = agent.GetAction(state)
        next_state, reward, done, info,_ = env.step(action)

        agent.Update(reward, state,action,next_state,done)
        total_reward+=reward

        if done:
            break
        state = next_state
    if step%20 == 0:
        agent.sync_qnet()
    if step % 10 == 0:
        print("episode :{}, total reward : {}".format(step, total_reward))

    history_reward.append(total_reward)
#

DQN1.png

Double DQN

回忆DQN的训练方式,Qθ(St,At)Q_{\theta}(S_t,A_t)逼近的目标为:

Rrward+γmaxaQθ(St+1,a)Rrward+\gamma \max_{a^{'}}Q_{\theta ^ {'}}(S_{t+1},a^{'})

问题是,如果对包含误差的估计值(QθQ_{\theta ^ {'}})使用max算子,那么与使用真正的Q函数基线计算的情况相比结果会过大。

解决方法是Double DQNQθ(St,At)Q_{\theta}(S_t,A_t)逼近的目标为:

Rrward+γQθ(St+1,arg maxaQθ(St+1,a))Rrward+\gamma Q_{\theta ^ {'}}\bigg(S_{t+1},\argmax_aQ_{\theta}(S_{t+1},a)\bigg)

也就是说,对于目标网络的动作选择,来自于主神经网络,而不采用max算子,这样的好处是可以避免过估计,使得训练更稳定。

优先级经验回放

在之前的经验回放中,我们的做法是把数据放在一起,然后随机选取进行训练,一种优化方法是设置数据的优先级

其实本质上是在存储数据时,同时记录这个数据的误差δ\delta然后选取数据时,误差越大的数据选取的概率越高。 可以理解为,误差越大的数据能使得网络的更新更大。

δt=Rrward+γmaxaQθ(St+1,a)Qθ(St,At)\delta_t =| Rrward+\gamma \max_{a^{'}}Q_{\theta ^ {'}}(S_{t+1},a^{'})-Q_{\theta}(S_t,A_t)|

chapter9:策略梯度法

前面所有的方法本质上都是对价值函数的更新迭代从而得到最优策略,这类方法叫基于价值的方法。如果我们的最终目的是得到最优策略,为什么不直接模拟策略呢,得到收益最大的策略,是一个典型的神经网优化问题,这个过程中不借助价值函数直接表示策略,称为基于策略的方法

具体思路: 训练一个神经网络,输入为状态,输入为行动概率,更新的误差为收益的数学期望负数,我们的目的是误差最小,也就是收益最大,这是一个很简单的思想

假设在一个回合中,基于策略行动,我们得到了“状态,行动,奖励”构成的时间序列τ\tau,称之为"轨迹”:

τ=(S0,A0,R0,S1...)\tau = (S_0,A_0,R_0,S_1...)

那么在这个轨迹下,收益为:

G(τ)=R0+γR1+γ2R2...G(\tau) = R_0 + \gamma R_1 + \gamma ^2 R_2...

误差函数为表示为:

J(θ)=Eτπ[G(τ)]J(\theta) = \mathbb{E}_{\tau \sim \pi}[G(\tau)]

τπ\tau \sim \pi表示策略得到的轨迹,然后确定误差函数的梯度为:

θJ(θ)=Eτπ[t=0Tθlogπθ(AtSt)G(τ)]\nabla_{\theta} J(\theta)=\mathbb{E}_{\tau \sim \pi} \bigg[\sum_{t=0}^{T}\nabla_{\theta} \log\pi_{\theta}(A_{t}|S_{t})G(\tau)\bigg]

数学期望的计算还是可以使用蒙特卡洛方法进行近似,为了简化,我们这里直接采样1次当作数学期望:

θJ(θ)=t=0Tθlogπθ(AtSt)G(τ)\nabla_{\theta} J(\theta)=\sum_{t=0}^{T}\nabla_{\theta} \log\pi_{\theta}(A_{t}|S_{t})G(\tau)

还是用倒立摆的问题实践上述方法:

  • Policy:定义神经网络,输入需要进行softmax,因为表示的是动作的概率
class Policy(nn.Module):
    def __init__(self, input_size,hidden_size,action_size):
        super().__init__()
        self.l1 = nn.Linear(input_size, hidden_size)
        self.l2 = nn.Linear(hidden_size, action_size)

    def forward(self, x):
        x = F.relu(self.l1(x))
        x = F.softmax(self.l2(x))
        return x
  • Agent:实现数据记录和参数更新
    • Add:记录数据,记录的是当前步的reward和采取行动的概率,当回合结束拿出来训练模型
    • Update:神经网络训练,一个回合训练一次
class Agent:
    def __init__(self,start_state):
        self.start_state = start_state
        self.state = start_state #指示当前的状态

        self.lr = 0.0005
        self.epsilon = 0.1
        self.alpha = 0.1
        self.gamma = 0.98

        self.action_size = 2 #倒立摆的动作只有两种

        self.pi = Policy(4,100,self.action_size)
        self.optimizer = optim.Adam(self.pi.parameters(),lr = self.lr)

        self.records = []


    def GetAction(self, state):

        state = torch.tensor(state[np.newaxis, :])
        probs = self.pi(state)[0]
        action = np.random.choice(len(probs),p = probs.data.numpy())
        return action,probs[action]

    def Add(self,reward,probs):
        self.records.append((reward,probs))

    def Update(self):
        G=0
        loss = 0
        for reward,probs in reversed(self.records):
            G=reward+self.gamma*G

        for reward, probs in self.records:
            loss+= -torch.log(probs)*G
        self.optimizer.zero_grad()
        loss.backward()#求导
        self.optimizer.step()
        self.records = []

policy1.png

做完这些,我有一个疑问:既然G是表征策略好坏的直接标准,为什么不能直接对其求梯度呢?(来自豆包) image.png

REINFORCE算法

回顾关于策略的梯度法公式:

θJ(θ)=Eτπ[t=0Tθlogπθ(AtSt)G(τ)]\nabla_{\theta} J(\theta)=\mathbb{E}_{\tau \sim \pi} \bigg[\sum_{t=0}^{T}\nabla_{\theta} \log\pi_{\theta}(A_{t}|S_{t})G(\tau)\bigg]

其实本质上是对当前策略的评估,如果当前策略较好,其G值就会高,那么

但是我们会发现,在不同的时间t下,logπθ(AtSt)\log\pi_{\theta}(A_{t}|S_{t})的权重都是一样的,都是这个回合的收益G(τ)G(\tau),但是其实动作的好坏和之前的收益是无关的,如果我们能直接体现当前时间的动作的收益,那么收敛速度会快很多。那么可以将梯度公式改为:

θJ(θ)=Eτπ[t=0Tθlogπθ(AtSt)Gt]Gt=Rt+γRt+1+γ2Rt+2...\begin{aligned} \nabla_{\theta} J(\theta)&=\mathbb{E}_{\tau \sim \pi} \bigg[\sum_{t=0}^{T}\nabla_{\theta} \log\pi_{\theta}(A_{t}|S_{t})G_t\bigg] \\G_t &= R_t + \gamma R_{t+1} + \gamma ^2 R_{t+2}... \end{aligned}

那么每一步的动作都和这个动作本身的收益绑定了,也更能收敛最优动作。在代码中只需要更改Update函数中G的计算方式:

def Update(self):
    G=0
    loss = 0
    for reward,probs in reversed(self.records):
        G=reward+self.gamma*G
        loss += -torch.log(probs) * G


    self.optimizer.zero_grad()
    loss.backward()
    self.optimizer.step()
    self.records = []

REINFOCE1.png

可以证明上述两种方,在无限样本的情况下,都会收敛到正确的值,但是REINFORCE的方差更小,收敛速度更快,其权重中没有了无关的数据。

基线

基线的思路是利用另外一个函数,通过做差的方式减小数据的方差

可以通过一个例子说明这个原理,假设有三个同学的成绩,分别为90,40,50,方差是466.67,但是如果我们拿这三个同学之前十次考试的平均成绩(82,46,49)作为基准,而这次的成绩可以表示为(8,-6,1),方差缩小到32.67

可以针对收益GtG_t运用这个原理,其中b(St)b(S_t)可以是任意用状态作为输入的函数,实践中常常使用价值函数,那么这种情况下还需要额外训练价值函数

θJ(θ)=Eτπ[t=0TGtθlogπθ(AtSt)]=Eτπ[t=0T(Gtb(St))θlogπθ(AtSt)]\begin{aligned} \nabla_{\theta} J(\theta)&=\mathbb{E}_{\tau \sim \pi} \bigg[\sum_{t=0}^{T}G_t\nabla_{\theta} \log\pi_{\theta}(A_{t}|S_{t})\bigg] \\ &= \mathbb{E}_{\tau \sim \pi} \bigg[\sum_{t=0}^{T}(G_t-b(S_t))\nabla_{\theta} \log\pi_{\theta}(A_{t}|S_{t})\bigg] \end{aligned}

这里用倒立摆的例子说明基准的意义,假设某个时刻倒立摆的角度很大,在3步后不管采取什么行动都会失败,此时的Gt=3G_t=3,那么按照之前的方法,其还是会趋向于选择一个动作去逼近,但是其实是没有意义的。如果按照基线的思想,此时b(St)=3b(S_t)=3,那么其梯度为0,则不会更新,避免了无意义的训练和更新。

Actor-Critic

actor:行动者,表示策略;critic:评论者,表示价值函数。Actor-Critic的意思是使用价值函数评价策略的好坏。

强化学习的算法大致可以分为基于价值的方法和基于策略的方法,在之前的文章中介绍了两类方法,现在考虑一种使用二者的方法:

回忆带基线的REINFORCE算法梯度表示:

θJ(θ)=Eτπ[t=0T(Gtb(St))θlogπθ(AtSt)]\begin{aligned} \nabla_{\theta} J(\theta)= \mathbb{E}_{\tau \sim \pi} \bigg[\sum_{t=0}^{T}(G_t-b(S_t))\nabla_{\theta} \log\pi_{\theta}(A_{t}|S_{t})\bigg] \end{aligned}

理论上函数bb可以是任意以状态为输入的函数,此处我们使用基于神经网络建模的价值函数作为基线,可以表示为:

θJ(θ)=Eτπ[t=0T(GtVω(St))θlogπθ(AtSt)]\begin{aligned} \nabla_{\theta} J(\theta)= \mathbb{E}_{\tau \sim \pi} \bigg[\sum_{t=0}^{T}(G_t-V_{\omega}(S_t))\nabla_{\theta} \log\pi_{\theta}(A_{t}|S_{t})\bigg] \end{aligned}

其中ω\omega表示神经网络参数,Vω(St)V_{\omega}(S_t)表示将状态输入神经网络所输出的价值函数值,根据TD方法的原理,训练神经网络时,我们使用Rt+γVω(St+1)R_t+\gamma V_{\omega}(S_{t+1})去逼近Vω(St)V_{\omega}(S_t),这样的好吃是只需要采样下一状态的值即可进行训练。

θJ(θ)=Eτπ[t=0T(Rt+γVω(St+1)Vω(St))θlogπθ(AtSt)]\begin{aligned} \nabla_{\theta} J(\theta)= \mathbb{E}_{\tau \sim \pi} \bigg[\sum_{t=0}^{T}(R_t+\gamma V_{\omega}(S_{t+1})-V_{\omega}(S_t))\nabla_{\theta} \log\pi_{\theta}(A_{t}|S_{t})\bigg] \end{aligned}

接下来还是在倒立摆的例子中实现这一思想:

  • 首先分别定义两个网络模型
    • 这里的ValueNet要区别于之前的DQN中的网络,这里是针对价值函数,所以输出只有一个
class PolicyNet(nn.Module):
    def __init__(self, input_size,hidden_size,output_size):
        super().__init__()
        self.l1 = nn.Linear(input_size, hidden_size)
        self.l2 = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x = F.relu(self.l1(x))
        x = F.softmax(self.l2(x))
        return x

class ValueNet(nn.Module):
    def __init__(self, input_size,hidden_size,output_size):
        super().__init__()
        self.l1 = nn.Linear(input_size, hidden_size)
        self.l2 = nn.Linear(hidden_size, output_size)

    def forward(self, x):
        x = F.relu(self.l1(x))
        x = self.l2(x)
        return x
  • 定义智能体
    • ValueNetRt+γVω(St+1)R_t+\gamma V_{\omega}(S_{t+1})-->Vω(St)V_{\omega}(S_t)
    • PolicyNetmax[((Rt+γVω(St+1)Vω(St))logπθ(AtSt)]\max \bigg [((R_t+\gamma V_{\omega}(S_{t+1})-V_{\omega}(S_t)) \log\pi_{\theta}(A_{t}|S_{t})\bigg]
class Agent:
    def __init__(self,start_state):
        self.start_state = start_state
        self.state = start_state #指示当前的状态

        self.lr = 0.0005
        self.epsilon = 0.1
        self.alpha = 0.1
        self.gamma = 0.98

        self.action_size = 2 #倒立摆的动作只有两种

        self.pi = PolicyNet(4,100,self.action_size)
        self.optimizer_pi = optim.Adam(self.pi.parameters(),lr = self.lr)

        self.value = ValueNet(4,100,1)
        self.optimizer_value = optim.Adam(self.value.parameters(),lr = self.lr)

        self.loss_func = nn.MSELoss()



    def GetAction(self, state):

        state = torch.tensor(state[np.newaxis, :])
        probs = self.pi(state)[0]
        action = np.random.choice(len(probs),p = probs.data.numpy())
        return action,probs[action]


    def Update(self,state,reward,prob,next_state,done):
        state = torch.tensor(state[np.newaxis, :])
        next_state = torch.tensor(next_state[np.newaxis, :])

        target = (reward+(1-done)*self.gamma*self.value(next_state))
        v = self.value(state)
        loss_for_value = self.loss_func(target,v)

        loss_for_pi = -(target - v)*torch.log(prob)

        self.optimizer_pi.zero_grad()
        self.optimizer_value.zero_grad()

        loss_for_pi.backward(retain_graph=True)
        loss_for_value.backward(retain_graph=True)

        self.optimizer_pi.step()
        self.optimizer_value.step()

基于策略的方法的优点

  • 策略直接模型化,更高效
    • 我们最终的目的是得到最优策略,对于某些价值函数很复杂却最优策略简单的问题,基于策略的方法显然更高效
  • 在连续的行动空间中也能使用
    • 之前的例子都是离散的行动空间,例如之前的GridWorld,行动也只有四个,对于连续情况下难以运用, 例如行动是速度和方向等。这种情况可以采用离散化的方法,但是这是很困难的。如果采用基于策略的方法就能实现,例如直接输出速度等。
  • 行动的选择概率平滑地变化
    • 之前对于策略的选择一般基于greed算法,如果Q函数发生大的变化,则选择的动作也会变化,而策略的方法是通过概率来是西安,更新的过程中会比较平稳,训练也比较稳定。

Chapter10:进一步学习

模型分类

扫描全能王 2024-12-14 14.04_2.jpg 有无模型体现的是是否使用环境模型,如果未知模型则训练环境模型即可,在本文大部分情况下都是不使用模型的情况。

A3C

Asynchronous(异步) Advantage Actor-Critic算法

A3C需要多个本地神经网络和一个全局神经网络,本地网络在各自环境中独自训练,然后将训练结果的梯度发送到全局网络。全局网络使用这些梯度异步更新权重参数。在更新全局网络的同时,定期同步全局网络和本地网络的权重参数。

扫描全能王 2024-12-14 14.04_1.jpg

A3C的优点:

  • 加快训练速度
  • 多个Agent独立行动,可以得到更加多样化的数据,减少数据之间的相关性,使得训练更加稳定。

另外,AC3的Actor-Critic将共享神经网络的权重,在靠近输入侧的神经网络的权重是共享的,这样的好处是加快训练速度,减小成本。

扫描全能王 2024-12-14 14.04_3.jpg

A2C

A2C是同步更新参数的方法,不采取异步更新参数的方式。其主要的特点是在本地不需要神经网络,只是独自运行智能代理,将不同环境中的状态作为数据进行批数据汇总进行训练。然后将神经网络的输出中对下一个行动采用,分发给各个环境。

扫描全能王 2024-12-14 14.04_4.jpg

其最大的优点是在不减少训练精度的情况下,本地无需神经网络的训练,可以大大节省GPU资源。

DDPG

Deep Deterministic Policy Gradient

在传统DQN中,使用到了max算子计算动作,但是这只能处理离散情况,我们需要在连续情况下像一个方法取代这个算子,使其能适应连续性问题。DDPG的解决方法是设立Actor网络,使其能直接根据状态输出连续的动作值,其算法特点为:

  • Actor - Critic 架构
    • Actor 网络(策略网络) :根据状态输出行动,这里的行动一般是连续且确定的,参数用θ\theta表示,即a=μθ(s)a = μ_{\theta}(s)
    • Critic 网络(价值网络):用于评估 Actor 网络生成的动作的价值。它接收状态和动作作为输入,输出一个 Q 值,即Q=Qω(s,a)Q = Q_{\omega}(s,a)
  • 目标网络
    • 为了使学习过程更加稳定,DDPG 采用了目标网络。它包括目标 Actor 网络和目标 Critic 网络
  • 训练过程
    • Critic 网络训练yt=rt+γQ(st+1,μ(st+1θ)ω)y_t = r_t + γQ'(s_{t + 1}, μ'(s_{t + 1}|θ')|ω'),损失函数为L(ω)=(1/N)(Q(si,aiω)yi)2)L(ω)=(1/N)∑(Q(s_i,a_i|ω)-y_i)²),这里最大的区别是,将maxmax算子省略了,取而代之的是Actor 网络输出的动作。主要是因为Actor 网络输出是确定的行动,为了简化计算,就进行替换。
    • Actor 网络训练θJ(θ)=(1/N)aQ(si,aω)a=μ(siθ)θμ(siθ)∇_θJ(θ)=(1/N)∑∇_aQ(s_i,a|ω)|_{a = μ(s_i|θ)}∇_θμ(s_i|θ),其实直观来解释就是希望得到使得Q最大的动作,这是一个最大化问题

image.png

DQN的改进

分类DQN

之前的DQN中都是输出的Q函数的数学期望,在分类DQN中输出的是Q函数的概率分布

  • 价值分布表示:在分类 DQN 中,它将 Q - 值表示为一个离散的概率分布。假设动作价值函数的取值范围被划分为NN个区间(例如N=51N=51),对于每个动作aa在状态ss下,网络输出一个概率分布p(s,a)p(s,a),其中p(s,a)ip(s,a)_i表示Q(s,a)Q(s,a)落在第个ii区间的概率。
  • 训练过程:通过最小化预测的概率分布和基于贝尔曼方程计算得到的目标概率分布之间的差异来训练网络。例如,在一个简单的游戏环境中,智能体在某个状态下采取动作后,根据获得的奖励和下一个状态的价值分布来计算目标概率分布,然后调整网络参数,使得预测的概率分布向目标概率分布靠近
  • 优势分析
    • 提高估计精度:这种方式能够更细致地捕捉 Q - 值的不确定性。相比传统 DQN 只估计一个单一的 Q - 值,分类 DQN 提供了 Q - 值可能取值的概率分布信息,在一些复杂的、具有随机因素的环境中,可以更好地估计价值。
    • 处理风险偏好:可以根据概率分布来考虑不同的风险偏好策略。例如,如果智能体是风险厌恶型的,可以选择具有高概率获得相对稳定回报的动作;如果是风险偏好型的,可以选择有一定概率获得高回报的动作,尽管可能伴随着高风险。

Noisy Network

之前策略的选择是根据ε\varepsilon-greedy算法,超参数ε\varepsilon的选择对模型的结果有很大的影响,且离散的超参数也会使得训练不够稳定。一般来说我们希望理想情况下什么超参数都不要设置,于是我们可以将ε\varepsilon的思想隐蔽入神经网络中。

具体的做法是通过在神经网络的参数或者激活函数中添加噪声来鼓励智能体在训练过程中探索更多不同的动作,从而提高学习效率和策略的鲁棒性。

相比之下Noisy Network的优势体现在两个方面:

  • 高效探索连续动作空间
    • 在连续动作空间场景下,ε\varepsilon-greedy 算法就显得比较笨拙。因为在连续动作空间中,随机选择一个动作可能会导致动作过于离散、跳跃,不利于找到一个平滑的最优策略。而 Noisy Network 可以通过在网络参数或激活函数上添加噪声,自然地对连续动作进行小幅度的扰动,从而更高效地探索连续动作空间。例如,在自动驾驶中,车辆的转向和速度控制是连续动作,Noisy Network 可以对控制动作进行微调,而ε\varepsilon-greedy 可能会导致车辆突然转向或者急加速、急刹车等不合理的动作。
  • 基于策略的探索
    • Noisy Network 的探索是基于当前策略的。因为噪声是添加在神经网络内部,而神经网络是学习到的策略的表示。所以,这种探索是与策略紧密相关的,是在策略的基础上进行微调。相比之下,ε\varepsilon-greedy 的随机探索部分与当前策略没有直接关联。例如,在一个复杂的游戏策略学习中,Noisy Network 会根据当前游戏状态和已经学到的策略,通过噪声来微调动作,而ε\varepsilon-greedy 可能会在不考虑当前策略的情况下随机选择一个动作,可能会破坏已经建立的良好策略模式

Rainbow

Rainbow 是一种先进的深度强化学习算法,它整合了多种对 DQN(Deep Q - Network)的改进技术,目的是提升算法在强化学习任务中的性能和效率。

  • 组成技术:
    • Double DQN
    • Prioritized Experience Replay
    • Dueling DQN
    • Noisy DQN
    • Categorical DQN
  • 训练过程和优势
    • 训练过程
      • Rainbow 算法的训练过程综合了上述各种技术。在经验回放阶段,利用 Prioritized Experience Replay 抽取样本;在网络结构上,采用 Dueling DQN 和 Noisy DQN 的特点;在计算目标 Q 值时,应用 Double DQN 的方法;在 Q - 值估计上,有 Categorical DQN 的概率分布表示。通过这些技术的协同作用,不断更新网络参数,使智能体能够更好地学习策略。