sarsa和qlearning都属于时间差分法TD,是有偏估计,只用到了下一步的状态和动作估计Q。此外还有采用后续多步的TD(λ)。
以下来自对Baidu AI Studio - 人工智能学习与实训社区的强化学习7日打卡营的代码记录:
1. Sarsa
- 目的是学习特定的
state
下,特定action
的价值Q
,最终建立和优化一个Q
表格,以state
为行,action
为列,根据与环境交互得到的reward
来更新Q
表格,更新公式为:
Sarsa
在训练中为了更好的探索环境,采用ε-greedy
方式来训练,有一定概率随机选择动作输出。Agent
是和环境environment
交互的主体。predict()
方法:输入观察值observation
(或者说状态state
),输出动作值sample()
方法:再predict()
方法基础上使用ε-greedy
增加探索learn()
方法:输入训练数据,完成一轮Q
表格的更新
# agent.py
class SarsaAgent(object):
def __init__(self, obs_n, act_n, learning_rate=0.01, gamma=0.9, e_greed=0.1):
self.act_n = act_n # 动作维度,有几个动作可选
self.lr = learning_rate # 学习率
self.gamma = gamma # reward的衰减率
self.epsilon = e_greed # 按一定概率随机选动作
self.Q = np.zeros((obs_n, act_n))
#生成二维得Q表格,状态为行,动作为列,所谓状态即obs观测者,在Q表格中表示为环境格子得总数
#Q表格的行是环境格子的总数,比如CliffWalking的Q表格有4*12=48行,FrozenLake的Q表格有4*4=16行。
#Q结构是“环境格子数”*“动作数”。Q值即对应的每个格子采用某个动作所获得的终极回报
# 根据输入观察值,采样输出的动作值,带探索
def sample(self, obs):
#if条件是根据状态选择能输出最大Q值的动作,有90%的机会执行exploitation
if np.random.uniform(0, 1) < (1.0 - self.epsilon): #根据table的Q值选动作
action = self.predict(obs)
else:#有10%的机会执行exploration探索
action = np.random.choice(self.act_n) #有一定概率随机探索选取一个动作
return action
# 根据输入观察值,预测输出的动作值,输出能产生最大Q值得action
def predict(self, obs):
Q_list = self.Q[obs, :]
maxQ = np.max(Q_list)
action_list = np.where(Q_list == maxQ)[0] # maxQ可能对应多个action
action = np.random.choice(action_list)#多个action则随机选一个
return action
# 学习方法,也就是更新Q-table的方法
def learn(self, obs, action, reward, next_obs, next_action, done):
""" on-policy 即求target_Q时使用的self.Q[next_obs, next_action]是下一个obs-状态实际sample
到的action所对应的Q。即下一个obs和实际动作对应的Q
obs: 交互前的obs, s_t
action: 本次交互选择的action, a_t
reward: 本次动作获得的奖励r
next_obs: 本次交互后的obs, s_t+1
next_action: 根据当前Q表格, 针对next_obs会选择的动作, a_t+1
done: episode是否结束
"""
predict_Q = self.Q[obs, action]#获取对应单元格在动作action下的Q值
if done:
target_Q = reward # 没有下一个状态了
else:
target_Q = reward + self.gamma * self.Q[next_obs, next_action] # Sarsa
self.Q[obs, action] += self.lr * (target_Q - predict_Q) # 修正q
# 保存Q表格数据到文件
def save(self):
npy_file = './q_table.npy'
np.save(npy_file, self.Q)
print(npy_file + ' saved.')
# 从文件中读取Q值到Q表格中
def restore(self, npy_file='./q_table.npy'):
self.Q = np.load(npy_file)
print(npy_file + ' loaded.')
Training && Test(训练&&测试)
run_episode()
:agent
在一个episode
中训练的过程,使用agent.sample()
与环境交互,使用agent.learn()
训练Q
表格。test_episode()
:agent
在一个episode
中测试效果,评估目前的agent
能在一个episode
中拿到多少总reward
。
def run_episode(env, agent, render=False):
total_steps = 0 # 记录每个episode走了多少step
total_reward = 0
obs = env.reset() # 重置环境, 重新开一局(即开始新的一个episode)
action = agent.sample(obs) # 根据算法选择一个动作
while True:
next_obs, reward, done, _ = env.step(action) # 与环境进行一个交互,由action->state与策略无关,
#由环境决定,由环境的状态转移概率决定。
next_action = agent.sample(next_obs) # 根据算法选择一个动作,
#由策略决定,一般平衡exploration和exploitation
# 训练 Sarsa 算法,更新Q值表
agent.learn(obs, action, reward, next_obs, next_action, done)
action = next_action #该动作是下次实际采用动作
obs = next_obs # 存储上一个观察值
total_reward += reward
total_steps += 1 # 计算step数
if 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:
break
return total_reward
Step5 创建环境和Agent,启动训练
# 使用gym创建悬崖环境
env = gym.make("CliffWalking-v0") # 0 up, 1 right, 2 down, 3 left
# 创建一个agent实例,输入超参数
agent = SarsaAgent(
obs_n=env.observation_space.n,
act_n=env.action_space.n,
learning_rate=0.1,
gamma=0.9,
e_greed=0.1)
# 训练500个episode,打印每个episode的分数
for episode in range(500):
ep_reward, ep_steps = run_episode(env, agent, False)
print('Episode %s: steps = %s , reward = %.1f' % (episode, ep_steps, ep_reward))
# 全部训练结束,查看算法效果
test_reward = test_episode(env, agent)
print('test reward = %.1f' % (test_reward))
2.Q-learning
Q-learning
也是采用Q
表格的方式存储Q
值(状态动作价值),决策部分与Sarsa
是一样的,采用ε-greedy
方式增加探索。Q-learning
跟Sarsa
不一样的地方是更新Q
表格的方式。Sarsa
是on-policy
的更新方式,先做出动作再更新。Q-learning
是off-policy
的更新方式,更新learn()
时无需获取下一步实际做出的动作next_action
,并假设下一步动作是取最大Q
值的动作。
Q-learning
的更新公式为:
class QLearningAgent(object):
def __init__(self, obs_n, act_n, learning_rate=0.01, gamma=0.9, e_greed=0.1):
self.act_n = act_n # 动作维度,有几个动作可选
self.lr = learning_rate # 学习率
self.gamma = gamma # reward的衰减率
self.epsilon = e_greed # 按一定概率随机选动作
self.Q = np.zeros((obs_n, act_n))
# 根据输入观察值,采样输出的动作值,带探索,与sarsa同
def sample(self, obs):
if np.random.uniform(0, 1) < (1.0 - self.epsilon): #根据table的Q值选动作
action = self.predict(obs)
else:
action = np.random.choice(self.act_n) #有一定概率随机探索选取一个动作
return action
# 根据输入观察值,预测输出的动作值,与sarsa同
def predict(self, obs):
Q_list = self.Q[obs, :]
maxQ = np.max(Q_list)
action_list = np.where(Q_list == maxQ)[0] # maxQ可能对应多个action
action = np.random.choice(action_list)
return action
# 学习方法,也就是更新Q-table的方法,与sarsa不同之处是不需要next_action了来更新Q了,
#取得是下一obs状态在不同动作下的最大Q:np.max(self.Q[next_obs, :]),而实际在下一obs状态
#下采用的动作所对应的Q很可能不是这个maxQ
def learn(self, obs, action, reward, next_obs, done):
""" off-policy:更新learn()时无需获取下一步实际做出的动作next_action,
并假设下一步动作是取最大Q值的动作。但实际采用的可能不是
obs: 交互前的obs, s_t
action: 本次交互选择的action, a_t
reward: 本次动作获得的奖励r
next_obs: 本次交互后的obs, s_t+1
done: episode是否结束
"""
predict_Q = self.Q[obs, action]#即原先的Q值,用来更新
if done:
target_Q = reward # 没有下一个状态了
else:
target_Q = reward + self.gamma * np.max(self.Q[next_obs, :]) # Q-learning
self.Q[obs, action] += self.lr * (target_Q - predict_Q) # 修正q
# 把 Q表格 的数据保存到文件中
def save(self):
npy_file = './q_table.npy'
np.save(npy_file, self.Q)
print(npy_file + ' saved.')
# 从文件中读取数据到 Q表格
def restore(self, npy_file='./q_table.npy'):
self.Q = np.load(npy_file)
print(npy_file + ' loaded.')
Training && Test(训练&&测试)
def run_episode(env, agent, render=False):
total_steps = 0 # 记录每个episode走了多少step
total_reward = 0
obs = env.reset() # 重置环境, 重新开一局(即开始新的一个episode)
while True:
action = agent.sample(obs) # 根据算法选择一个动作,注意sarsa代码中这行放在while前面,
#因为sarsa后面有next_action = agent.sample(next_obs)语句,而qlearning代码没有。
next_obs, reward, done, _ = env.step(action) # 与环境进行一个交互
#与sarsa不同在于在此不需要采样next_action来训练Q
# 训练 Q-learning算法,注意没有next_action参数了
agent.learn(obs, action, reward, next_obs, done)
#通过用next_obs更新obs状态,结合while的第一条语句就采样到了next_action,更新了action
#那这个更新的action,有可能是探索到的action,也有可能是maxQ对应的action,看sample和predict方法
#所以就是计算targetQ时采用的maxQ所对应的action和实际下一步采用的action可能不一致。也就是offpolicy
#而sarsa是先通过sample到next_action之后再根据这个next_action来找到对应的Q:self.Q[next_obs, next_action]。
obs = next_obs # 存储上一个观察值
total_reward += reward
total_steps += 1 # 计算step数
if 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:
break
return total_reward
为什么最终测试时sarsa和qlearning表现不一样,sarsa是学到尽量远离危险行走而qlearning学到是贴着悬崖行走。测试阶段都一样通过采用maxQ走,而sarsa学到的maxQ是远离悬崖,qlearning学到的maxQ是贴着悬崖。因为sarsa是onpolicy在更新Q值时采用的动作和Q值是对应的,贴着悬崖的格子采用动作可能会掉到悬崖获得负的reward而Q值减小,而在远离悬崖处则不容易Q减小,越远离越不容易减小,最终学到是远离悬崖的Q最大。而qlearning则训练中采用的动作和Q值不对应,每次更新Q值都是用maxQ更新,不管行为策略,而贴着悬崖能获得maxQ。