表格型方法

对于免模型的强化学习方法,agent多次尝试与environmet交互之后,就能对其不同状态做出判断,用状态动作价值来表达在某个状态下某个动作的好坏。如果Q表格是一张已经训练好的表格,这张表格就像是一本生活手册。通过查看这本手册,我们就知道在熊发怒的时候,装死的价值会高一点;在熊离开的时候,我们 偷偷逃跑会比较容易获救。这张表格里面 Q 函数的意义就是我们选择了某个动作后,最后能不能成功,就 需要我们去计算在某个状态下选择某个动作,后续能够获得多少总奖励。如果可以预估未来的总奖励的大 小,我们就知道在当前的状态下选择哪个动作价值更高。我们选择某个动作是因为这样未来可以获得的价 值会更高。所以强化学习的目标导向性很强,环境给出的奖励是非常重要的反馈,它根据环境的奖励来做选择。

​ 我们所求的Q表格,它的行数是所有状态的数量,列数是所有可采取的动作数量,在格子世界中大概如图所示:

hNV2t.png

​ 最开始的时候,Q 表格会全部初始化为 0。智能体会不断和环境交互得到不同的轨迹,当交互的 次数足够多的时候,我们就可以估算出每一个状态下,每个动作的平均总奖励,进而更新 Q 表格。

​ 在强化学习里面,我们可以每走一步更新一次 Q 表格,用下一个状态的 Q 值来更新当前状态的 Q 值,这 种单步更新的方法被称为时序差分方法。

免模型方法

蒙特卡洛

蒙特卡洛方法是基于采样的方法,就是把多次采样值的均值作为该随机变量的期望。

给定策略$\pi$,我们让我们让智能体与环境进行交互,可以得到很多轨迹。 每个轨迹都有对应的回报:
$$
G_t=r_{t+1}+\gamma r_{t+2}+\gamma^2r_{t+3}+…
$$
我们求出从状态s出发所有轨迹的回报的平均值,将其作为一个策略s状态的价值:$V_\pi(s)$

假设现在有样本$x_1,x_2,…,x_t$,我们可以把均值转化成均值增量的形式:

hNoeA.png

若在每个回合中,如果时间t时状态s被访问了,那么:

  • 状态s 的访问数$N(s)$增加1,$N(s) \leftarrow N(s)+1$
  • 状态s的总回报$S(s)$增加$G_t$,$S(s)\leftarrow S(s)+G_t$

状态s的价值可以通过回报的平均来估计,即$V(s)=S(s)/N(s)$

那将均值增量应用到蒙特卡洛方法中:

hNBqS.png

hSl3C.png

时序差分

A4Zt7.png

比如我们需要使用模型估计从纽约到亚特兰大的时间,模型初始时估计值为1000分钟

A4jaX.png

而我们真实的时间为860分钟,传统的BP算法会计算真实时间与估计时间的loss,然后求导进行参数更新。

但是若我们没有到亚特兰大,而是在中途有事耽误了呢?传统的BP算法就无法计算了,因为我们没有y label。

4gMMP.png

比如我们花了300分钟到了华盛顿之后就不走了,TD算法会给出华盛顿到亚特兰大的时间估计,然后:
$$
loss=\frac{1}{2}(T_{NYC2At}^e-(T_{NYC2DC}^a+T_{DC2At}^e))^2
$$

$$
=\frac{1}{2}(T_{NYC2DC}^e-T_{NYC2DC}^a)^2
$$

其中上标表示:e-估计值,a-实际值,下标表示不同城市
$$
TD target=T_{NYC2DC}^a+T_{DC2At}^e
$$
TDtaeget肯定比模型最开始的估计值要好,因为其中有一部分是真实值。

$$
Q(s_t,a_t;w)\approx R_t^a+\gamma Q(s_{t+1},a_{t+1};w)
$$
对比一下,也是一个真实值加上一个预测值!左边为模型预测值,右边为TDtarget。上述公式又是哪来的呢?
$$
\begin{aligned}
G_t&=R_t+\gamma R_{t+1}+\gamma^2R_{t+2}+…\\
&=R_t+\gamma(R_{t+1}+\gamma R_{t+2}+…)\\
&=R_t+\gamma G_{t+1}
\end{aligned}
$$

那么用估计得到的回报$r_{t+1}+\gamma V(s_{t+1})$更新上一时刻的值$V(s_t)$:
$$
V(s_t)\leftarrow V(s_t)+\alpha(r_{t+1}+\gamma V(s_{t+1})-V(s_t))
$$
​ 对比一下蒙特卡洛方法和时序差分方法。在蒙特卡洛方法里面,$G_{i,t}$ 是实际得到的值(可以看成 目标),因为它已经把一条轨迹跑完了,可以算出每个状态实际的回报。时序差分不等轨迹结束,往前走 一步,就可以更新价值函数.

进一步比较时序差分方法和蒙特卡洛方法:

  • 时序差分方法可以在线学习(online learning),每走一步就可以更新,效率高。蒙特卡洛方法必 须等游戏结束时才可以学习
  • 时序差分方法可以从不完整序列上进行学习。蒙特卡洛方法只能从完整的序列上进行学习
  • 时序差分方法可以在连续的环境下(没有终止)进行学习。蒙特卡洛方法只能在有终止的情况下 学习。
  • 时序差分方法利用了马尔可夫性质,在马尔可夫环境下有更高的学习效率。蒙特卡洛方法没有假设环境具有马尔可夫性质,利用采样的价值来估计某个状态的价值,在不是马尔可夫的环境下更加有效.

我们可以把时序差分方法进行进一步的推广。之前是只往前走一步,即 TD(0)。我 们可以调整步数(step),变成 n 步时序差分(n-step TD)。比如 TD(2),即往前走两步,利用两步得到 的回报,使用自举来更新状态的价值。这样我们就可以通过步数来调整算法需要的实际奖励

hSJek.png

如果时序差分方法需要更广度的更新,就变成了动态规划方法(因为动态规划方法 是把所有状态都考虑进去来进行更新)。如果时序差分方法需要更深度的更新,就变成了蒙特卡洛方法

Sarsa算法:同策略时序差分控制

时序差法方法是给定一个策略,然后我们去估计他的价值函数,Sarsa算法很简单,他将原本时序差分方法更新V的过程,变成了更新Q的过程:
$$
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)]
$$
Sarsa直接估计Q表格,得到Q表格后,就可以更新策略。

该算法由于需要知道当前的状态、当前的动作、奖励、下一步状态、下一步动作,即$(s_t,a_t,R_t,s_{t+1},a_{t+1})$,所以叫做Sarsa算法。

hST3S.png

上述算法每执行一个动作就更新一次价值和策略,所以也叫做单步Sarsa算法,那很明显,也有n步Sarsa算法

对于Sarsa,在t时刻的价值为:
$$
Q_t=r_{t+1}+\gamma Q(s_{t+1},a_{t+1})
$$
对于n步Sarsa,其n步回报为:
$$
Q_t^n=r_{t+1}+\gamma r_{t+2}+…\gamma^{n-1}r_{t+n}+\gamma ^nQ(s_{t+n},a_{t+n})
$$
加上资格迹衰减参数得Sarsa$(\lambda)$的Q回报:
$$
Q_t^\lambda=(1-\lambda)\sum_{n=1}^\infty\lambda^{n-1}Q_t^n
$$
n步Sarsa更新策略为:
$$
Q(s_t,a_t)\leftarrow Q(s_t,a_t)+\alpha(Q_t^\lambda-Q(s_t,a_t))
$$

Q-learning:异策略时序差分控制

Sarsa 是一种同策略(on-policy)算法,它优化的是它实际执行的策略,它直接用下一步会执行的动作去优化 Q 表格。同策略在学习的过程中,只存在一种策略,它用一种策略去做动作的选取,也用一种策略去做优化。所以 Sarsa 知道它下一步的动作有可能会跑到悬崖那边去,它就会在优化自己的策略的时 候,尽可能离悬崖远一点。这样子就会保证,它下一步哪怕是有随机动作,它也还是在安全区域内。

hSaXJ.png

Q 学习是一种异策略(off-policy)算法。如图 3.31 所示,异策略在学习的过程中,有两种不同的策 略:目标策略(target policy)和行为策略(behavior policy)。目标策略是我们需要去学习的策略,一 般用 π 来表示。目标策略就像是在后方指挥战术的一个军师,它可以根据自己的经验来学习最优的策略, 不需要去和环境交互。行为策略是探索环境的策略,一般用 µ 来表示。行为策略可以大胆地去探索到所有 可能的轨迹,采集轨迹,采集数据,然后把采集到的数据“喂”给目标策略学习。而且“喂”给目标策略的数据中并不需要 $a_{t+1}$ ,而 Sarsa 是要有$a_{t+1}$ 的。行为策略像是一个战士,可以在环境里面探索所有的 动作、轨迹和经验,然后把这些经验交给目标策略去学习。比如目标策略优化的时候,Q 学习不会管我们下一步去往哪里探索,它只选取奖励最大的策略。

hSkCF.png

Q 学习有两种策略:行为策略和目标策略。目标策略 π 直接在 Q 表格上使用贪心策略,取它下一步能得到的所有状态,即
$$
\pi(s_{t+1})=\arg\max_{a’} Q(S_{t+1},a’)
$$
行为策略 µ 可以是一个随机的策略,但我们采取 ε-贪心策略,让行为策略不至于是完全随机的,它是基 于 Q 表格逐渐改进的.

那么Q-learning的更新公式为:
$$
Q(s_t,a_t)\leftarrow Q(s_t,a_t)+\alpha[r_{t+1}+\gamma\max_aQ(s_{t+1},a)-Q(s_t,a_t)]
$$
Sarsa 在更新 Q 表格的时候,它用到的是 A′ 。我们要获取下一个 Q 值的时候,A′ 是下一个步骤一定会执行的动作,这个动作有可能是 ε-贪心方法采样出来的动作,也有 可能是最大化 Q 值对应的动作,也有可能是随机动作,但这是它实际执行的动作。但是 Q 学习在更新 Q 表格的时候,它用到的是 Q 值 Q(S ′ , a) 对应的动作,它不一定是下一个步骤会执行的实际的动作,因为我们下一个实际会执行的动作可能会探索。Q 学习默认的下一个动作不是通过行为策略来选取的,Q 学习直接看 Q 表格,取它的最大化的值,它是默认 A′ 为最佳策略选取的动作,所以 Q 学习在学习的时候,不需要传入 A′,即$a_{t+1}$的值。

hoWOk.png

Q-learning和Sarsa算法分别实现CliffWalking

我们首先简单介绍 CliffWalking-v0 环境,该环境中文名为悬崖寻路(cliffwalking),是一个迷宫类问 题。如图 3.36 所示,在一个 4 × 12 的网格中,智能体以网格的左下角位置为起点,以网格的右下角位置 为终点,目标是移动智能体到达终点位置,智能体每次可以在上、下、左、右这 4 个方向中移动一步,每 移动一步会得到 −1 单位的奖励。

4gbGg.png

起终点之间是一段悬崖,即编号为 37 46 的网格,智能体移动过程中会有如下的限制:

  • 智能体不能移出网格,如果智能体想执行某个动作移出网格,那么这一步智能体不会移动,但是 这个操作依然会得到 −1 单位的奖励
  • 如果智能体“掉入悬崖”,会立即回到起点位置,并得到 −100 单位的奖励
  • 当智能体移动到终点时,该回合结束,该回合总奖励为各步奖励之和。
from argparse import Action
from time import sleep
import gym
import math
import numpy as np
import turtle
import argparse
parse=argparse.ArgumentParser()
parse.add_argument('-lr',default=0.1)
parse.add_argument('-gamma',default=0.9)
parse.add_argument('-epsilon_start',default=0.1)
parse.add_argument('-epsilon_end',default=0.01)
parse.add_argument('-epsilon_decay',default=20)
parse.add_argument('-train_eps',default=1000)
args=parse.parse_args()


class CliffWalkingWapper(gym.Wrapper):#结果可视化
def __init__(self, env):
gym.Wrapper.__init__(self, env)
self.t = None
self.unit = 50
self.max_x = 12
self.max_y = 4

def draw_x_line(self, y, x0, x1, color='gray'):
assert x1 > x0
self.t.color(color)
self.t.setheading(0)
self.t.up()
self.t.goto(x0, y)
self.t.down()
self.t.forward(x1 - x0)

def draw_y_line(self, x, y0, y1, color='gray'):
assert y1 > y0
self.t.color(color)
self.t.setheading(90)
self.t.up()
self.t.goto(x, y0)
self.t.down()
self.t.forward(y1 - y0)

def draw_box(self, x, y, fillcolor='', line_color='gray'):
self.t.up()
self.t.goto(x * self.unit, y * self.unit)
self.t.color(line_color)
self.t.fillcolor(fillcolor)
self.t.setheading(90)
self.t.down()
self.t.begin_fill()
for i in range(4):
self.t.forward(self.unit)
self.t.right(90)
self.t.end_fill()

def move_player(self, x, y):
self.t.up()
self.t.setheading(90)
self.t.fillcolor('red')
self.t.goto((x + 0.5) * self.unit, (y + 0.5) * self.unit)

def render(self):
if self.t == None:
self.t = turtle.Turtle()
self.wn = turtle.Screen()
self.wn.setup(self.unit * self.max_x + 100,
self.unit * self.max_y + 100)
self.wn.setworldcoordinates(0, 0, self.unit * self.max_x,
self.unit * self.max_y)
self.t.shape('circle')
self.t.width(2)
self.t.speed(0)
self.t.color('gray')
for _ in range(2):
self.t.forward(self.max_x * self.unit)
self.t.left(90)
self.t.forward(self.max_y * self.unit)
self.t.left(90)
for i in range(1, self.max_y):
self.draw_x_line(
y=i * self.unit, x0=0, x1=self.max_x * self.unit)
for i in range(1, self.max_x):
self.draw_y_line(
x=i * self.unit, y0=0, y1=self.max_y * self.unit)

for i in range(1, self.max_x - 1):
self.draw_box(i, 0, 'black')
self.draw_box(self.max_x - 1, 0, 'yellow')
self.t.shape('turtle')

x_pos = self.s % self.max_x
y_pos = self.max_y - 1 - int(self.s / self.max_x)
self.move_player(x_pos, y_pos)



class Qlearning():
def __init__(self,n_states,n_actions,args) -> None:
self.Q_table=np.zeros((n_states,n_actions))
self.epsilon_start=args.epsilon_start
self.epsilon_end=args.epsilon_end
self.epsilon_decay=args.epsilon_decay
self.choose_action_dim=np.array([0,1,2,3])
self.lr=args.lr
self.gamma=args.gamma
self.sample_count=0
def choose_action(self,state):#theta 逐渐下降的theta-贪心算法选取当前动作
self.sample_count+=1
self.epsilon=self.epsilon_end+(self.epsilon_start-self.epsilon_end)*math.exp(-1*self.sample_count/self.epsilon_decay)
if np.random.uniform(0,1)<1-self.epsilon+(self.epsilon/4):
action=np.argmax(self.Q_table[state])
else:
action=np.random.choice(self.choose_action_dim)
return action
def update(self,state,action,reward,next_state,done):
if done:
Q_target=reward
else:
Q_target=reward+self.gamma*np.max(self.Q_table[next_state])
self.Q_table[state][action]+=self.lr*(Q_target-self.Q_table[state][action])#Q-learning 更新公式
def test_action(self,state):
return np.argmax(self.Q_table[state])

class Sarsa():
def __init__(self,n_states,n_actions,args) -> None:
self.Q_table=np.zeros((n_states,n_actions))
self.epsilon_start=args.epsilon_start
self.epsilon_end=args.epsilon_end
self.epsilon_decay=args.epsilon_decay
self.choose_action_dim=np.array([0,1,2,3])
self.lr=args.lr
self.gamma=args.gamma
self.sample_count=0
def choose_action(self,state):#theta 逐渐下降的theta-贪心算法选取当前动作
self.sample_count+=1
self.epsilon=self.epsilon_end+(self.epsilon_start-self.epsilon_end)*math.exp(-1*self.sample_count/self.epsilon_decay)
if np.random.uniform(0,1)<1-self.epsilon+(self.epsilon/4):
action=np.argmax(self.Q_table[state])
else:
action=np.random.choice(self.choose_action_dim)
return action
def update(self,state,action,reward,next_state,done,next_action):
if done:
Q_target=reward
else:
Q_target=reward+self.gamma*(self.Q_table[next_state][next_action])
self.Q_table[state][action]+=self.lr*(Q_target-self.Q_table[state][action])#Sarsa 更新公式
def test_action(self,state):
return np.argmax(self.Q_table[state])



def play(type):
env=gym.make('CliffWalking-v0')
env=CliffWalkingWapper(env)
n_states=env.observation_space.n
n_actions=env.action_space.n
print('状态数:{},动作数:{}'.format(n_states,n_actions))
n_reward=[]
env.seed(0)#设置随机种子
if type=='Q-learning':
agent=Qlearning(n_states,n_actions,args)
for i in range(args.train_eps):
ep_reward=0
state=env.reset()#起点
step=0
while True:
step+=1
action=agent.choose_action(state)
if i%50==0:
env.render()
sleep(0.1)
next_state,reward,done,_=env.step(action)
agent.update(state,action,reward,next_state,done)#Q-learning无需求next_action
state=next_state
ep_reward+=reward
if done:
n_reward.append(ep_reward)
print('episode:{}:step:{},reward:{}'.format(i,step,ep_reward))
break
print('训练平均奖励:{}'.format(np.mean(n_reward)))

print(agent.Q_table)
#测试
step=0
state=env.reset()
test_reward=0
while True:
step+=1
sleep(1)
action=agent.test_action(state)
env.render()
next_state,reward,done,_=env.step(action)
state=next_state
test_reward+=reward
if done:
print('test:step:{},reward:{}'.format(step,test_reward))
break
elif type=='Sarsa':
agent=Sarsa(n_states,n_actions,args)
for i in range(args.train_eps):
ep_reward=0
state=env.reset()#起点
step=0
while True:
step+=1
action=agent.choose_action(state)
if i%100==0:
env.render()
sleep(0.1)
next_state,reward,done,_=env.step(action)
next_action=agent.choose_action(next_state)#Sarsa需求next_action
agent.update(state,action,reward,next_state,done,next_action)
state=next_state
ep_reward+=reward
if done:
n_reward.append(ep_reward)
print('episode:{}:step:{},reward:{}'.format(i,step,ep_reward))
break
print('训练平均奖励:{}'.format(np.mean(n_reward)))

print(agent.Q_table)
#测试
step=0
state=env.reset()
test_reward=0
while True:
step+=1
sleep(0.5)
action=agent.test_action(state)
env.render()
next_state,reward,done,_=env.step(action)
state=next_state
test_reward+=reward
if done:
print('test:step:{},reward:{}'.format(step,test_reward))
break
if __name__ == '__main__':
play('Sarsa')

Q-learning 效果:最后一次慢的为测试,之前为训练

4gKM7.gif

Sarsa算法效果:最后一次慢的为测试,之前为训练

4gafX.gif