连续状态空间
简介
之间讲解的都是离散装态。而在连续状态强化学习问题或连续状态马尔可夫决策过程中,问题的状态不是少数可能的离散值之一,而是一个数字向量,其中每个数字都可以取大量值中的任意一个。许多机器人控制应用,如实践实验室中的月球着陆器应用,都需要连续状态空间解决具体问题。
离散状态的例子
以火星车为例,它使用的是状态的离散集合,即简化的火星探测器只能处于六种可能位置中的一种。
连续状态的概念
大多数机器人能处于的位置不止六种,而是可以处于大量连续值位置中的任何一个。例如,火星车能在一条线上的任何地方,其位置由 0 到 6 公里间的一个数字表示,如 2.7 公里,任何处于该区间的数字都是有效的,这就是连续状态空间的一个例子。
控制汽车的例子
- 对于控制汽车或卡车的应用,如果要让自动驾驶汽车或卡车平稳行驶,其状态可能包括多个数字,如 x 位置、y 位置、方向(用希腊字母 θ 表示)、x 方向速度(用 x_dot 表示)、y 方向速度、转弯速度等。
- 汽车的状态不是像火星探测器那样只有一个数字,而是由六个数字组成的向量,每个数字都能在一定范围内取任意值,比如角度 θ 应在 0 到 360 度之间。
控制直升机的例子
- 若构建强化学习算法来控制自动直升机,描述直升机位置需包括其在南北方向的 x 位置、东西方向的 y 位置、离地面的高度 z,以及方向(通过三个额外数字来表示,即翻滚(roll)、俯仰(pitch)、偏航(yaw))。
- 控制直升机还需要知道它在 x、y、z 方向上的速度,以及翻滚、俯仰、偏航的变化速度(也称为角速度)。所以,用于控制自动直升机的状态是由 12 个数字组成的列表,这些数字作为策略的输入,策略的作用是根据这 12 个数字决定让直升机采取合适的行动。
月球着陆器模拟
应用简介
这是是一个能让用户在月球上着陆模拟飞行器的应用,它类似于一个被很多强化学习研究者使用的电子游戏。在这个应用中,用户的任务是在月球着陆器快速接近月球表面时,在适当的时间点燃推进器,使其安全降落在着陆台上。
着陆示例
展示了月球着陆器成功着陆的情况,即通过向左右两边发射火箭,使着陆器在两面黄旗之间着陆;同时也提到如果强化学习的策略表现不佳,着陆器可能会在月球表面坠毁。
操作选项
在这个应用的每个时间步,有四种可能的操作:
- 什么都不做,此时惯性和重力会将着陆器拉向月球表面。
- 发射左边的推进器,当看到左边有小红点出现时表示正在发射,这会使月球着陆器倾向于向右边移动。
- 启动底部的主引擎进行向下推进。
- 启动右边的推进器,这会将着陆器推向左边。 用户的工作就是持续选择合适的操作,让月球着陆器安全降落在着陆台上的两面旗帜之间。为了便于称呼,这些操作有时被简称为 “Nothing”(什么都不做)、“Left”(左边推进器)、“Main”(主引擎)、“Right”(右边推进器)。
状态表示
该应用的马尔可夫决策过程(MDP)的状态包括:
- 位置:水平方向的 x 坐标和垂直方向的 y 坐标,即着陆器在水平方向的位置以及离地面的高度。
- 速度:水平方向速度
和垂直方向速度
,表示着陆器在水平和垂直方向上移动的速度。
- 角度:着陆器向左或向右倾斜的角度
以及角度变化率
。
- 着陆腿状态:两个二进制变量 L 和 R,L 对应左腿是否着地(0 表示未着地,1 表示着地),R 对应右腿是否着地(0 表示未着地,1 表示着地) 。
奖励函数
- 成功着陆:如果着陆器成功到达着陆平台,会根据到达平台中心时的飞行情况获得 100 到 140 分之间的奖励。同时,靠近着陆平台会得到额外奖励,远离则会得到负奖励。
- 坠毁:如果着陆器坠毁,会得到一个巨大的负 100 奖励。
- 软着陆:如果是软着陆(即着陆但未坠毁),会得到加 100 的奖励。
- 腿着地:对于每一个成功着地的腿(左腿或右腿),会得到加 10 的奖励。
- 燃料消耗:为了鼓励不浪费燃料,避免不必要地点燃推进器,每次点燃主引擎会得到一个负面奖励,每次发射左边和右边的推进器会得到负 0.03 的奖励。 该奖励函数是中等复杂的,应用程序的设计师通过思考期望的行为并将其编码到奖励函数中,来激励期望的行为并避免不期望的行为(如坠毁)。在构建自己的强化学习应用程序时,明确期望和不期望的行为并在奖励函数中体现通常需要花费一些心思,但指定奖励函数比指定从每个状态中应采取的确切行动要容易得多。
问题目标
目标是学习一个策略,当给定一个状态s时,选择一个行动
,以最大化折扣回报总和。对于月球着陆器应用,通常会使用一个相当大的折扣因子
值,这里使用的值为 0.985,非常接近于 1。如果能够学习到这样的策略
,就意味着成功让小着陆器着陆。
深度Q网络
使用强化学习控制月球着陆器的关键在于训练一个新的网络来计算或近似状态动作值函数,从而能够选择合适的动作。
网络输入
- 对于月球着陆器,状态s由八个数字组成的列表描述,包括水平位置x、垂直位置y、水平速度
、垂直速度
、倾斜角度
、角度变化率
以及左右腿是否接地的二进制值L和R。
- 有四种可能的动作:不做任何操作(Nothing)、启动左边推进器(Left)、启动主引擎(Main)、启动右边推进器(Right)。每个动作可以用一个独热编码(one-hot encoding)的特征向量来表示,例如第一个动作编码为\([1, 0, 0, 0]\),第二个动作编码为\([0, 1, 0, 0]\)等。
- 将状态s和动作a的编码组合在一起,形成一个由 12 个数字组成的输入向量X,输入到新的神经网络中。
网络结构与任务
新网络包含两个隐藏层,每层有 64 个神经元,输出层有一个输出。其任务是根据输入的状态动作对,输出对应的状态动作值函数
的近似值。在训练过程中,以
的目标值为指导,使网络能够逐渐准确地近似该函数。
与监督学习的区别
强化学习与监督学习不同,不是输入状态让网络输出动作,而是输入状态动作对,让网络尝试输出。通过这种方式使用新网络在强化学习中是有效的。
动作选择
当月球着陆器处于某种状态s时,使用网络计算所有四个动作对应的值,选择其中值最大的动作执行。例如,如果
最大,则决定启动主引擎。
训练网络的方法
- 使用贝尔曼方程(Bellman equation)
来创建训练集。其中
是在状态s下采取动作a获得的奖励,
是折扣因子,
是采取动作a后进入的新状态,
是新状态下的可能动作。
- 通过在月球着陆器模拟器中随机采取不同的动作,观察得到许多状态s、动作a、奖励R和新状态
的元组(在 Python 代码中称为元组)。例如,某次处于状态
,采取动作
,得到奖励
并进入新状态
。
- 利用这些元组创建训练样本。对于每个元组,输入
是状态
和动作
的组合,目标值
使用贝尔曼方程的右边计算,即
。虽然一开始不知道Q函数的具体值,可以从一个完全随机的猜测开始,随着训练的进行,这个猜测会逐渐变得准确。
- 重复上述过程,收集足够多的训练样本(如 10000 个),形成训练集。
完整算法流程
- 随机初始化新网络的所有参数,将其作为对Q函数的初始随机猜测,类似于线性回归中随机初始化参数然后通过梯度下降改进的过程。
- 重复以下操作:
- 在月球着陆器中随机采取动作,得到许多状态、动作、奖励和新状态的元组。为了避免占用过多计算机内存,只记住最近的 10000 个这样的元组,这种存储最新示例的技术称为重播缓冲区(replay buffer)。
- 查看保存的 10000 个元组,创建包含 10000 个例子的训练集。每个训练样本的输入X是状态和动作的组合,目标值Y根据贝尔曼方程计算。
- 使用训练集训练新网络,使网络的输出能够更好地近似目标值Y。经过训练,新网络对状态动作值函数Q的估计会更准确,然后将当前网络更新为这个训练后的新网络。
- 不断重复上述过程,每次迭代都会使网络对Q函数的估计更准确。当运行足够长的时间后,网络对
的估计会足够好,从而可以用于为月球着陆器选择好的动作。
算法名称与效果
这种算法有时被称为深度Q网络(DQN,Deep Q-Network)算法,因为使用了深度学习和神经网络来训练模型学习Q函数。在月球着陆器上使用该算法可行,但可能需要较长时间收敛且效果不是很完美,通过对算法进行一些改进,可以使其工作得更好,后续视频会介绍这些改进内容。
简单的深度Q网络算法代码(无优化)
import gymnasium as gym
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import random
from collections import deque
# 设置随机种子以便结果可复现
torch.manual_seed(0)
np.random.seed(0)
random.seed(0)
class DQN(nn.Module):
def __init__(self, state_dim, action_dim):
super(DQN, self).__init__()
self.fc1 = nn.Linear(state_dim, 64)
self.fc2 = nn.Linear(64, 64)
self.fc3 = nn.Linear(64, action_dim)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
return self.fc3(x)
class ReplayBuffer:
def __init__(self, capacity):
self.buffer = deque(maxlen=capacity)
def add(self, state, action, reward, next_state, done):
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size):
batch = random.sample(self.buffer, batch_size)
state, action, reward, next_state, done = map(np.stack, zip(*batch))
return state, action, reward, next_state, done
def __len__(self):
return len(self.buffer)
class DQNAgent:
def __init__(self, state_dim, action_dim, lr=1e-3, gamma=0.99, epsilon=1.0, epsilon_min=0.01, epsilon_decay=0.995, buffer_size=10000):
self.state_dim = state_dim
self.action_dim = action_dim
self.gamma = gamma # 折扣因子
self.epsilon = epsilon # 探索率
self.epsilon_min = epsilon_min
self.epsilon_decay = epsilon_decay
# 创建策略网络和目标网络
self.policy_net = DQN(state_dim, action_dim)
self.target_net = DQN(state_dim, action_dim)
self.target_net.load_state_dict(self.policy_net.state_dict())
self.target_net.eval() # 目标网络设为评估模式
self.optimizer = optim.Adam(self.policy_net.parameters(), lr=lr)
self.replay_buffer = ReplayBuffer(buffer_size)
def select_action(self, state):
# epsilon-greedy策略
if random.random() > self.epsilon:
with torch.no_grad():
state = torch.FloatTensor(state).unsqueeze(0)
q_values = self.policy_net(state)
return q_values.argmax().item()
else:
return random.randrange(self.action_dim)
def update(self, batch_size):
if len(self.replay_buffer) < batch_size:
return
# 从经验回放缓冲区中采样
states, actions, rewards, next_states, dones = self.replay_buffer.sample(batch_size)
# 转换为PyTorch张量
states = torch.FloatTensor(states)
actions = torch.LongTensor(actions)
rewards = torch.FloatTensor(rewards)
next_states = torch.FloatTensor(next_states)
dones = torch.FloatTensor(dones)
# 计算当前Q值和目标Q值
q_values = self.policy_net(states).gather(1, actions.unsqueeze(1)).squeeze(1)
with torch.no_grad():
next_q_values = self.target_net(next_states).max(1)[0]
target_q_values = rewards + self.gamma * next_q_values * (1 - dones)
# 计算损失
loss = nn.MSELoss()(q_values, target_q_values)
# 优化模型
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
# 衰减探索率
self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)
def update_target_network(self):
# 软更新目标网络
self.target_net.load_state_dict(self.policy_net.state_dict())
# 训练函数
def train_dqn(env, agent, episodes=1000, batch_size=64, target_update=10):
rewards_history = []
for episode in range(episodes):
state, _ = env.reset()
total_reward = 0
done = False
while not done:
# 选择动作
action = agent.select_action(state)
# 执行动作
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
# 存储经验
agent.replay_buffer.add(state, action, reward, next_state, done)
# 更新状态和总奖励
state = next_state
total_reward += reward
# 更新网络
agent.update(batch_size)
# 定期更新目标网络
if episode % target_update == 0:
agent.update_target_network()
rewards_history.append(total_reward)
print(f"Episode {episode+1}/{episodes}, Total Reward: {total_reward:.2f}, Epsilon: {agent.epsilon:.3f}")
return rewards_history
# 测试训练好的智能体
def test_agent(env, agent, episodes=10):
for episode in range(episodes):
state, _ = env.reset()
total_reward = 0
done = False
while not done:
action = agent.select_action(state)
next_state, reward, terminated, truncated, _ = env.step(action)
done = terminated or truncated
state = next_state
total_reward += reward
# 渲染环境
env.render()
print(f"Test Episode {episode+1}/{episodes}, Total Reward: {total_reward:.2f}")
# 主函数
def main():
# 创建环境
env = gym.make('LunarLander-v2')
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
# 创建智能体
agent = DQNAgent(state_dim, action_dim)
# 训练智能体
print("开始训练...")
rewards = train_dqn(env, agent)
# 测试智能体
print("\n开始测试...")
test_agent(env, agent)
# 关闭环境
env.close()
if __name__ == "__main__":
main()
这个简化版的 DQN 实现包含了以下实现
神经网络模型:使用一个三层全连接网络近似 Q 值函数
经验回放缓冲区:存储智能体的经验以提高样本效率
目标网络:定期更新的目标网络提高训练稳定性
epsilon-greedy 策略:平衡探索与利用的动作选择策略
Q 学习更新:使用贝尔曼方程计算目标 Q 值并优化网络