蒙特卡洛方法
求解价值函数和状态价值函数,可以使用蒙特卡洛方法和动态规划。首先介绍一下蒙特卡洛的方法,这个方法是统计模拟方法,基于概率统计来进行数值计算。
- 优点: 不需要知道环境模型,直接从交互中学习
- 缺点: 每一次更新都需要完整的轨迹,使用一些具有明确任务终止的情况,而且采样效率很低下。
如果用这个方法来估计策略在一个马尔可夫决策中的状态价值函数。需要采集到很多条的序列。,然后使用这些轨迹进行更新和迭代。计算方法如下:
首先采样到的序列如下:
第一条序列
[('s1', '前往s2', 0, 's2'), ('s2', '前往s3', -2, 's3'), ('s3', '前往s5', 0, 's5')]
第二条序列
[('s4', '概率前往', 1, 's4'), ('s4', '前往s5', 10, 's5')]
第五条序列
[('s2', '前往s3', -2, 's3'), ('s3', '前往s4', -2, 's4'), ('s4', '前往s5', 10, 's5')]
这里采用的是增量式计算方法:
# 对所有采样序列计算所有状态的价值
def MC(episodes, V, N, gamma):
for episode in episodes:
G = 0
for i in range(len(episode) - 1, -1, -1): #一个序列从后往前计算
(s, a, r, s_next) = episode[i]
G = r + gamma * G
N[s] = N[s] + 1
V[s] = V[s] + (G - V[s]) / N[s]
强化学习的目的:学习到一个策略,能够让agent从初始点到达终点且取到最大回报。
动态规划算法
本方法要求马尔可夫决策过程是已知的,也就是智能体交互的环境完全知道,在这种条件下,智能体不需要和环境进行交互来采样数据,直接用动态规划就可以算出来最优价值和策略。类似于监督学习任务,直接给出数据的分布公式,可以通过期望的层面来最小化模型的泛化误差更新模型。
利用马尔可夫决策过程的模型MDP, 进行求解,根据贝尔曼方程进行迭代更新,逐渐找到最优策略和值函数。
- 优点:理论基础牢固,能够找到全局最优解。在这样一个白盒环境中,不需要通过智能体和环境的大量交互来学习,可以直接用动态规划求解状态价值函数
- 缺点:需要事先了解环境的完整模型(状态转移概率和即时的奖励), 大规模问题求解比较困难。
动态规划方法主要分为两种:策略迭代 和价值迭代
策略迭代
包括两个部分:策略评估和策略提升
策略迭代是不需要和环境进行交互的,因为环境的状态状态转移概率和奖励函数都已经确定了。
- 策略评估
- 计算一下当前策略下的值函数,使用贝尔曼方程迭代更新值函数,知道值函数收敛为止。
- 策略改进
- 我门将当前的值函数选择在每一个状态上具有最大值的动作,从而更新策略为新的最优策略。策略改进也不需要与环境进行互动。只是对值函数的一种估计。
- 更新策略,让每一个状态选择最大值的动作。
- 重复评估和更新直到收敛。
问:策略选择动作的概率和状态转移概率是一样吗?
策略 π(a|s) 表示在给定状态 s 下选择动作 a 的概率,而状态转移概率 P(s’|s,a) 则表示在给定状态 s 和动作 a 后,从状态 s 转移到状态 s’ 的概率。
假设有一个网格世界,包含两个状态 s1 和 s2,以及两个动作 a1 和 a2。我们定义一个策略 π,其中在状态 s1 下选择动作 a1 的概率为 0.6,选择动作 a2 的概率为 0.4。状态转移概率如下:
P(s2|s1, a1) = 0.8 P(s1|s1, a1) = 0.2 P(s2|s1, a2) = 0.3 P(s1|s1, a2) = 0.7
时序差分方法
大部分时候环境模型是不可能获得的,这就需要智能体和环境进行交互,这类方法被成为 无模型强化学习。
首先简单理解一下时序差分方法:
回顾一下蒙特卡洛的更新方法:
这里对于价值更新是要等整个序列都结束以后,才能计算这一次回报G。在实际计算中,对于一条序列而言,从后面往前开始累加G,依次更新每一个V。时序差分方法利用了这里增量式更新的方法,可以在每一个时间步进行值函数更新。具体而言,时序差分方法用当前获得的奖励加上了下一个状态的价值估计来作为当前状态会获得的回报,即:
折扣因子后面一串就是时序差分(Temporal difference)误差,时序差分算法将步长的乘积作为状态价值的更新量。
时序差分的特点:
时序差分(Temporal Difference, TD)方法是一类强化学习算法,它结合了动态规划和蒙特卡洛方法的优点。与蒙特卡洛方法需要等待完整的回合结束后才能更新值函数不同,TD 方法可以在每个时间步对值函数进行更新。这使得 TD 方法具有以下特点:
- 增量式更新:每一个时间步都可以对值函数进行更新。
- 时序性:通过比较当前状态的值函数和下一个状态的值函数估计来更新值函数。
- 学习策略灵活:TD方法可以结合不同的策略,如贪心策略,或者softmax策略,进行值函数的更新和动作选择。
- Off-policy和On-policy:TD方法适用于离线学习和在线学习。离线学习可以直接使用历史数据进行值函数更新,不需要和环境交互。在线学习方法需要交互,根据当前策略选择动作和更新值函数。
具体到经典的算法Sarsa和Q-learning算法来理解。
Sarsa算法
Sarsa:在每个时间步,根据当前状态 s、采取的动作 a、即时奖励 r、转移到的下一个状态 s’ 以及在下一个状态选择的动作 a’。需要五元组数据,SARSA。
Q值更新公式:
Q
(
s
,
a
)
←
Q
(
s
,
a
)
+
α
∗
(
r
+
γ
∗
Q
(
s
′
,
a
′
)
−
Q
(
s
,
a
)
)
Q(s, a) ← Q(s, a) + α * (r + γ * Q(s', a') - Q(s, a))
Q(s,a)←Q(s,a)+α∗(r+γ∗Q(s′,a′)−Q(s,a))
其中 α 是学习率,γ 是折扣因子。
伪代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a7GjzcnD-1689171098747)(C:\Users\风净尘\AppData\Roaming\Typora\typora-user-images\image-
.png)]
SARSA中,对于时序差分的计算用的下一个状态采取贪心算法下动作得到的价值来计算。而Q-learning采用的式直接选取下一个状态下能够获得最大价值的动作。而不是和SARSA那样根据策略来选取的动作。
SARSA是在线学习方法,他用State-Action-Reward-State-Action的序列进行更新,并且每一个时间步都进行值函数的更新和策略的选择。
具体看看一个简单的例子来理解这个过程:
# Sarsa算法
for episode in range(num_episodes):
state = 0 # 初始状态
if np.random.rand() < epsilon:
action = np.random.randint(num_actions) # 使用ε-贪心策略随机选择动作
else:
action = np.argmax(Q[state]) # 使用当前值函数估计选择动作
while state != num_states - 1: # 当前状态不是终止状态时
# 执行动作并观察下一个状态和即时奖励
next_state = np.random.choice(np.where(maze[state] >= 0)[0]) # 随机选择下一个状态
reward = maze[state, action]
# 使用ε-贪心策略选择下一个动作
if np.random.rand() < epsilon:
next_action = np.random.randint(num_actions) # 随机选择下一个动作
else:
next_action = np.argmax(Q[next_state]) # 使用当前值函数估计选择下一个动作
# 使用Sarsa更新值函数估计
Q[state, action] += alpha * (reward + gamma * Q[next_state, next_action] - Q[state, action])
state = next_state
action = next_action # 更新当前状态和动作,Q-learning只需要更新状态即可。
Q-learning算法
Q-learning:在每个时间步,根据当前状态 s、采取的动作 a、即时奖励 r、转移到的下一个状态 s’,使用贝尔曼最优方程更新 Q 值:Q(s, a) ← Q(s, a) + α * (r + γ * max(Q(s’, a’)) - Q(s, a))。只需要一个四元组数据即可。
如何区分在线学习和离线学习
我们称采样数据的策略为行为策略(behavior policy),称用这些数据来更新的策略为目标策略(target policy)。在线策略(on-policy)算法表示行为策略和目标策略是同一个策略;而离线策略(off-policy)算法表示行为策略和目标策略不是同一个策略。Sarsa 是典型的在线策略算法,而 Q-learning 是典型的离线策略算法。判断二者类别的一个重要手段是看计算时序差分的价值目标的数据是否来自当前的策略,如图 5-1 所示。具体而言:
- 对于 Sarsa,它的更新公式必须使用来自当前策略采样得到的五元组,因此它是在线策略学习方法;
- 对于 Q-learning,它的更新公式使用的是四元组来更新当前状态动作对的价值,数据中的s和a是给定的条件,reward和s_new皆由环境采样得到,该四元组并不需要一定是当前策略采样得到的数据,也可以来自行为策略,因此它是离线策略算法。
在某些情况下,Sarsa 更加稳定,因为它考虑了当前策略下的动作选择,可以更好地控制探索和利用的权衡。Q-learning 则更倾向于选择具有最大 Q 值的动作,可能导致更快地收敛到最优策略,但也可能导致过度估计和不稳定性。
DQN深度强化Q学习
概念介绍
DQN就是用神经网络来拟合这个动作价值函数。DQN只能处理动作离散的情况,因为在动作选择中有max的操作。动作值函数更新公式如下:
Q
(
s
,
a
)
=
Q
(
s
,
a
)
+
α
∗
(
r
+
γ
∗
m
a
x
(
Q
(
s
′
,
a
′
)
)
−
Q
(
s
,
a
)
)
Q(s, a) = Q(s, a) + α * (r + γ * max(Q(s', a')) - Q(s, a))
Q(s,a)=Q(s,a)+α∗(r+γ∗max(Q(s′,a′))−Q(s,a))
其中,Q(s, a)是状态s下采取动作a的动作值估计,在DQN中,我们使用深度神经网络来估计Q值函数。网络的输入是状态s,输出是每个可能动作的Q值。网络的参数通过优化目标来不断更新,目标函数如下:
T
a
r
g
e
t
=
r
+
γ
∗
m
a
x
(
Q
(
s
′
,
a
′
)
)
Target = r + γ * max(Q(s', a'))
Target=r+γ∗max(Q(s′,a′))
其中,r是即时奖励,γ是折扣因子,s’是下一个状态,a’是在下一个状态下采取的动作。目标值表示在下一个状态下,通过选择最优的动作所能获得的最大累积奖励。
DQN算法的训练过程中,通过最小化预测Q值与目标Q值之间的均方误差来更新网络的参数。具体来说,我们使用以下损失函数:
L
o
s
s
=
(
Q
(
s
,
a
)
−
T
a
r
g
e
t
)
2
Loss = (Q(s, a) - Target)^2
Loss=(Q(s,a)−Target)2
利用神经网络的反向传播算法,更新网络参数,让预测的Q(s, a)逼近Target。
两个关键的方法:
- 经验回放
DQN属于离线学习方法,所以可以对数据加以重复利用,维护一个回放缓冲区,每次将环境中采样得到的四元组(s, a, r, s’)存放在缓冲区,在训练过程中再对缓冲区的历史数据进行随机训练。这样可以使样本满足独立假设。打破样本之间的相关性。
- 目标网络
为了让Q(s, a)逼近r + γ * max(Q(s’, a’)),神经网络更新参数的同时,目标r + γ * max(Q(s’, a’))也不断改变,容易让神经网络不太稳定,因此加入了目标网络思想,利用两套神经网络,先将目标网络的参数固定住,然后隔N步再进行更新。原来的训练网络Q(s, a)使用正常梯度下降方法来更新,每一步都要进行更新。
综合所述得到伪代码如下:
代码解析
- 首先是经验回放的类设计
class ReplayBuffer:
''' 经验回放池 '''
def __init__(self, capacity):
# collections是python标准库,有list、deque、字典等数据结构。
self.buffer = collections.deque(maxlen=capacity) # 双端队列,先进先出,一端添加,一端删除
def add(self, state, action, reward, next_state, done): # 将数据加入buffer
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size): # 从buffer中采样数据,数量为batch_size
# random.sample(population, k) 在抽样的序列中,随机抽取k个元素,返回包含元素的列表。
transitions = random.sample(self.buffer, batch_size)
# 将每一个元素的对应位置值打包为一个元组,返回以这个元组为列的迭代器。
state, action, reward, next_state, done = zip(*transitions)
return np.array(state), action, reward, np.array(next_state), done
def size(self): # 目前buffer中数据的数量
return len(self.buffer)
- 然后是Q值网络
class Qnet(torch.nn.Module):
''' 只有一层隐藏层的Q网络 '''
def __init__(self, state_dim, hidden_dim, action_dim):
super(Qnet, self).__init__()
# 输入层有状态个神经元
self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
self.fc2 = torch.nn.Linear(hidden_dim, action_dim)
def forward(self, x):
x = F.relu(self.fc1(x)) # 隐藏层使用ReLU激活函数
return self.fc2(x)
- DQN算法类实现
class DQN:
''' DQN算法 '''
def __init__(self, state_dim, hidden_dim, action_dim, learning_rate, gamma,
epsilon, target_update, device):
self.action_dim = action_dim
self.q_net = Qnet(state_dim, hidden_dim,
self.action_dim).to(device) # Q网络
# 目标网络
self.target_q_net = Qnet(state_dim, hidden_dim,
self.action_dim).to(device)
# 使用Adam优化器
self.optimizer = torch.optim.Adam(self.q_net.parameters(),
lr=learning_rate)
self.gamma = gamma # 折扣因子
self.epsilon = epsilon # epsilon-贪婪策略
self.target_update = target_update # 目标网络更新频率
self.count = 0 # 计数器,记录更新次数
self.device = device
def take_action(self, state): # epsilon-贪婪策略采取动作
if np.random.random() < self.epsilon:
action = np.random.randint(self.action_dim)
else:
state = torch.tensor([state], dtype=torch.float).to(self.device)
action = self.q_net(state).argmax().item()
return action
def update(self, transition_dict):
states = torch.tensor(transition_dict['states'],
dtype=torch.float).to(self.device)
actions = torch.tensor(transition_dict['actions']).view(-1, 1).to(
self.device)
rewards = torch.tensor(transition_dict['rewards'],
dtype=torch.float).view(-1, 1).to(self.device)
next_states = torch.tensor(transition_dict['next_states'],
dtype=torch.float).to(self.device)
dones = torch.tensor(transition_dict['dones'],
dtype=torch.float).view(-1, 1).to(self.device)
# 状态传入Q网络, 在输出的每一个状态对应的Q值中,选出actions对应的Q值,也就是类似监督学习中的yi
q_values = self.q_net(states).gather(1, actions) # Q值
# 下个状态的最大Q值,这个输出类似监督学习的y标签,也就是目标值。
max_next_q_values = self.target_q_net(next_states).max(1)[0].view(
-1, 1)
q_targets = rewards + self.gamma * max_next_q_values * (1 - dones
) # TD误差目标
dqn_loss = torch.mean(F.mse_loss(q_values, q_targets)) # 均方误差损失函数
self.optimizer.zero_grad() # PyTorch中默认梯度会累积,这里需要显式将梯度置为0
dqn_loss.backward() # 反向传播更新参数
self.optimizer.step()
if self.count % self.target_update == 0:
self.target_q_net.load_state_dict(
self.q_net.state_dict()) # 更新目标网络
self.count += 1
DQN改进算法
Double DQN网络
在传统DQN网络中,我们训练网络是用来一个Q网络进行估计动作价值函数的,并且利用贪婪策略来选择动作,这样容易到值对Q值的过高估计,从而影响到训练的稳定性和收敛性。
Double DQN网络利用了两个Q网络来解决这个问题,一个在线网络用来选择动作,另一个用来估计动作价值函数。具体来说,使用一个Q网络(称为"online"网络)选择下一个动作,然后使用另一个Q网络(称为"target"网络)估计该动作的值。通过将选择动作和估计值的过程分离,Double DQN可以更准确地估计动作值,并减少过高估计的影响。Double DQN的流程如下:
- 选择动作:从 online 网络中选择动作 a,即 a = argmax Q_online(s, a)。
- 执行动作a:获得奖励s’和r;
- 估计下一个状态动作值:先使用online网络选取s’下的动作a’ = argmax Q_online(s’, a; θ),然后利用目标网络来计算s’和a’下的Q值。
- 计算TD目标值:y = r + γ * Q_target(s’, a’; θ’)
- 计算损失函数:loss = (Q_online(s, a; θ) - y)^2
- 更新在线网络参数:使用梯度下降法根据损失函数对在线网络的参数θ进行更新。
- 更新目标网络参数:定期将在线网络的参数θ复制到目标网络的参数θ’。
这么看来,DQN与Double DQN的差异仅仅是体现在计算Q值得时候动作的选取,DQN中是直接用目标网络进行max选取动作,而Double DQN中利用在线网络选取动作,再用这个动作来在目标网络中计算Q值。
因此在代码差异上其实并不太大,DDQN代码如下:
# 下个状态的最大Q值
if self.dqn_type == 'DoubleDQN': # DQN与Double DQN的区别
max_action = self.q_net(next_states).max(1)[1].view(-1, 1)
max_next_q_values = self.target_q_net(next_states).gather(1, max_action)
else: # DQN的情况
max_next_q_values = self.target_q_net(next_states).max(1)[0].view(-1, 1)
这个环境用的是倒立摆,由于动作空间是连续的,但是DQN只能处理离散动作空间,因此这里提一嘴如何将连续动作空间离散化。
倒立摆动作空间:力矩大小[-2,2]
倒立摆的Q值:倒立摆向上直立时候能选取的最大Q值就为0,如果Q值大于0说明出现过高估计。
离散化:将动作空间离散化为11个动作。动作[0, 1, 2, 3, … 9, 10]分别代表力矩[-2, -1.6, -1.2…, 1.2 , 1.6, 2.0]
在Gym中,要step环境需要把离散得动作专户为连续的数值,代码如下:
def dis_to_con(discrete_action, env, action_dim): # 离散动作转回连续的函数 action_lowbound = env.action_space.low[0] # 连续动作的最小值 action_upbound = env.action_space.high[0] # 连续动作的最大值 return action_lowbound + (discrete_action / (action_dim - 1)) * (action_upbound - action_lowbound)