实验内容
悬崖行走:从S走到G,其中灰色部分是悬崖不可到达,求可行方案
建模中,掉下悬崖的奖励是-100,G的奖励是10,原地不动的奖励-1,到达非终点位置的奖励是0(与图中的示意图不一致,不过大差不差),分别使用on-policy策略的Sarsa与off-policy策略的Q-learning算法,经过20000幕进化迭代得出safe path,optimal path,最后根据Q值来得出最终的策略,以此来对上图进行复现
算法原理
sarsa 在迭代的时候,基于 ϵ-贪婪法的策略π在当前状态 St 选择一个动作 At, 然后会进入到下一个状态 St+1,同时获得奖励 Rt+1,在新的状态 St+1 同样基于策略 π 选择一个动作 At+1,然后用它来更新价值函数,更新公式如下:
q-learning 中行为策略和目标策略是不同的。对于行为策略,是使用 ϵ− 贪心策略选取的对于目标策略,是使用贪心策略选取的
sarsa 中行为策略和目标策略是相同的,都是使用ϵ− 贪心策略选取的。
实验过程
整体代码实现了两种不同的基于表格的强化学习算法:Sarsa 和 Q-learning。这两种算法都用于解决一个简单的格子世界问题,即悬崖漫步(Cliff Walking)任务。
- CliffWalkingEnv 类:定义了一个CliffWalkingEnv环境类,用于模拟悬崖漫步的环境。这个环境由一个网格组成,智能体的目标是从起始位置移动到目标位置,同时尽量避免陷入悬崖区域。智能体可以采取的动作包括上、下、左、右移动,并会在每一步获得奖励(走到悬崖区域为负奖励,其他为常规负奖励)。当智能体到达目标或者落入悬崖时,序列结束(即到达终止状态)。
- Sarsa 类和 QLearning 类:两个对应算法的类(Sarsa 和 QLearning),它们包含了执行各自算法所需的数据和方法。这包括一个Q-table,用于存储每个状态-动作对的价值。take_action 方法用于根据epsilon-贪婪策略选择当前状态下的动作,而best_action 方法用于找到对应于最高Q值的动作。它们还包含了update方法,用于在每次环境交互后更新Q-table。
- 强化学习训练过程:对于Sarsa和Q-learning,算法会针对环境进行数次序列(episode)的迭代学习,每个序列都是从环境的起始状态开始,一直到达终止状态(或达到最大步数)。在每一步,都会根据当前策略选择动作、接收环境的反馈、并更新Q表。过程使用了tqdm库来显示进度条。每完成一定数量的序列后,会打印此时的平均回报。
- 可视化和评估:进行了可视化,展示了随着序列进行,回报如何变化。使用print_agent函数来打印最终学习到的策略。
- 计算最优路径步数:添加的calculate_optimal_path_steps 函数用于在学习结束后评估Sarsa和Q-learning的策略,计算从起始位置到目标位置的最优路径需要多少步,关键在于按照当前学习的Q表来执行动作。整个代码的目标是比较Sarsa和Q-learning在特定任务中的性能,以及具体展示它们是如何通过与环境的交互来学习策略的。通过计算最优路径步数能够给出一个直观的指标来评估学习到的策略质量。
设置初始的epsilon-贪婪策略中的参数epsilon = 0.1,学习率alpha = 0.1,折扣因子gamma = 0.9
import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm # tqdm是显示循环进度条的库
class CliffWalkingEnv:
def __init__(self, ncol, nrow):
self.nrow = nrow
self.ncol = ncol
self.x = 0 # 记录当前智能体位置的横坐标
self.y = self.nrow - 1 # 记录当前智能体位置的纵坐标
def step(self, action): # 外部调用这个函数来改变当前位置
# 4种动作, change[0]:上, change[1]:下, change[2]:左, change[3]:右。坐标系原点(0,0)
# 定义在左上角
change = [[0, -1], [0, 1], [-1, 0], [1, 0]]
self.x = min(self.ncol - 1, max(0, self.x + change[action][0]))
self.y = min(self.nrow - 1, max(0, self.y + change[action][1]))
next_state = self.y * self.ncol + self.x
reward = -1
done = False
if self.y == self.nrow - 1 and self.x > 0: # 下一个位置在悬崖或者目标
done = True
if self.x != self.ncol - 1:
reward = -100
return next_state, reward, done
def reset(self): # 回归初始状态,坐标轴原点在左上角
self.x = 0
self.y = self.nrow - 1
return self.y * self.ncol + self.x
class Sarsa:
""" Sarsa算法 """
def __init__(self, ncol, nrow, epsilon, alpha, gamma, n_action=4):
self.Q_table = np.zeros([nrow * ncol, n_action]) # 初始化Q(s,a)表格
self.n_action = n_action # 动作个数
self.alpha = alpha # 学习率
self.gamma = gamma # 折扣因子
self.epsilon = epsilon # epsilon-贪婪策略中的参数
def take_action(self, state): # 选取下一步的操作,具体实现为epsilon-贪婪
if np.random.random() < self.epsilon:
action = np.random.randint(self.n_action)
else:
action = np.argmax(self.Q_table[state])
return action
def best_action(self, state): # 用于打印策略
Q_max = np.max(self.Q_table[state])
a = [0 for _ in range(self.n_action)]
for i in range(self.n_action): # 若两个动作的价值一样,都会记录下来
if self.Q_table[state, i] == Q_max:
a[i] = 1
return a
def update(self, s0, a0, r, s1, a1):
td_error = r + self.gamma * self.Q_table[s1, a1] - self.Q_table[s0, a0]
self.Q_table[s0, a0] += self.alpha * td_error
def calculate_optimal_path_steps(agent, env, start_state, end_state):
state = start_state
steps = 0
done = False
while not done and state != end_state:
action = np.argmax(agent.Q_table[state]) # 按照最优策略选择动作
next_state, _, done = env.step(action)
state = next_state
steps += 1
if state == end_state: # 到达终点即停止
break
return steps
ncol = 12
nrow = 4
env = CliffWalkingEnv(ncol, nrow)
np.random.seed(0)
epsilon = 0.1
alpha = 0.1
gamma = 0.9
agent = Sarsa(ncol, nrow, epsilon, alpha, gamma)
num_episodes = 500 # 智能体在环境中运行的序列的数量
return_list = [] # 记录每一条序列的回报
for i in range(10): # 显示10个进度条
# tqdm的进度条功能
with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
for i_episode in range(int(num_episodes / 10)): # 每个进度条的序列数
episode_return = 0
steps = 0
state = env.reset()
action = agent.take_action(state)
done = False
while not done:
next_state, reward, done = env.step(action)
next_action = agent.take_action(next_state)
episode_return += reward # 这里回报的计算不进行折扣因子衰减
agent.update(state, action, reward, next_state, next_action)
state = next_state
action = next_action
steps += 1
return_list.append(episode_return)
if (i_episode + 1) % 10 == 0: # 每10条序列打印一下这10条序列的平均回报
pbar.set_postfix({
'episode':
'%d' % (num_episodes / 10 * i + i_episode + 1),
'return':
'%.3f' % np.mean(return_list[-10:])
})
pbar.update(1)
episodes_list = list(range(len(return_list)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Sarsa on {}'.format('Cliff Walking'))
plt.show()
start_state_sarsa = env.reset()
end_state_sarsa = nrow * ncol - 1 # 终点状态的编号
optimal_path_steps_sarsa = calculate_optimal_path_steps(agent, env, start_state_sarsa, end_state_sarsa)
print('Sarsa算法最优路径步数:', optimal_path_steps_sarsa)
def print_agent(agent, env, action_meaning, disaster=[], end=[]):
for i in range(env.nrow):
for j in range(env.ncol):
if (i * env.ncol + j) in disaster:
print('****', end=' ')
elif (i * env.ncol + j) in end:
print('EEEE', end=' ')
else:
a = agent.best_action(i * env.ncol + j)
pi_str = ''
for k in range(len(action_meaning)):
pi_str += action_meaning[k] if a[k] > 0 else 'o'
print(pi_str, end=' ')
print()
action_meaning = ['^', 'v', '<', '>']
print('Sarsa算法最终收敛得到的策略为:')
print_agent(agent, env, action_meaning, list(range(37, 47)), [47])
class QLearning:
""" Q-learning算法 """
def __init__(self, ncol, nrow, epsilon, alpha, gamma, n_action=4):
self.Q_table = np.zeros([nrow * ncol, n_action]) # 初始化Q(s,a)表格
self.n_action = n_action # 动作个数
self.alpha = alpha # 学习率
self.gamma = gamma # 折扣因子
self.epsilon = epsilon # epsilon-贪婪策略中的参数
def take_action(self, state): # 选取下一步的操作
if np.random.random() < self.epsilon:
action = np.random.randint(self.n_action)
else:
action = np.argmax(self.Q_table[state])
return action
def best_action(self, state): # 用于打印策略
Q_max = np.max(self.Q_table[state])
a = [0 for _ in range(self.n_action)]
for i in range(self.n_action):
if self.Q_table[state, i] == Q_max:
a[i] = 1
return a
def update(self, s0, a0, r, s1):
td_error = r + self.gamma * self.Q_table[s1].max(
) - self.Q_table[s0, a0]
self.Q_table[s0, a0] += self.alpha * td_error
np.random.seed(0)
epsilon = 0.1
alpha = 0.1
gamma = 0.9
agent = QLearning(ncol, nrow, epsilon, alpha, gamma)
num_episodes = 500 # 智能体在环境中运行的序列的数量
return_list = [] # 记录每一条序列的回报
for i in range(10): # 显示10个进度条
# tqdm的进度条功能
with tqdm(total=int(num_episodes / 10), desc='Iteration %d' % i) as pbar:
for i_episode in range(int(num_episodes / 10)): # 每个进度条的序列数
episode_return = 0
state = env.reset()
done = False
while not done:
action = agent.take_action(state)
next_state, reward, done = env.step(action)
episode_return += reward # 这里回报的计算不进行折扣因子衰减
agent.update(state, action, reward, next_state)
state = next_state
return_list.append(episode_return)
if (i_episode + 1) % 10 == 0: # 每10条序列打印一下这10条序列的平均回报
pbar.set_postfix({
'episode':
'%d' % (num_episodes / 10 * i + i_episode + 1),
'return':
'%.3f' % np.mean(return_list[-10:])
})
pbar.update(1)
episodes_list = list(range(len(return_list)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('Q-learning on {}'.format('Cliff Walking'))
plt.show()
action_meaning = ['^', 'v', '<', '>']
print('Q-learning算法最终收敛得到的策略为:')
print_agent(agent, env, action_meaning, list(range(37, 47)), [47])
start_state_q_learning = env.reset()
end_state_q_learning = nrow * ncol - 1 # 终点状态的编号
optimal_path_steps_q_learning = calculate_optimal_path_steps(agent, env, start_state_q_learning, end_state_q_learning)
print('Q-learning算法最优路径步数:', optimal_path_steps_q_learning)
实验结果与分析
Sarsa算法和Q-learning算法的最本质区别就在于它是on-policy的,也就是说,它更新的策略和采样的策略相同,都是用的epsilon-greedy。而Q-learning则是off-policy的,它在采样时用epsilon-greedy,而在更新时采用贪婪策略。我们要知道,所谓的算法收敛性指的是Q value的收敛,而更新Q value的方式决定了它收敛到什么值。Q-learning在更新时用贪婪策略,所以收敛到的Q值使得在贪婪策略下得到一条最优路径,即图中的optimal path。而Sarsa在更新时用epsilon-greedy,收敛到的Q值使得我们得到在epsilon-greedy策略下的最优路径,即图中的Safer path。在贪婪策略下Safer path并不是最优路径,同样在epsilon-greedy策略下Optimal path也不是最优路径。这是所谓收敛到最优策略的含义。