本章开始深度强化学习部分,DQN全称 Deep Q-Network ,主要贡献在于Q-learning算法的基础上引入了深度神经网络来建寺动作价值函数Q(s,a),从而可以处理高维空间。除深度网络外,还引入了例如经验回放和目标网络。
7.1 深度网络
尽管深度网络和Q表都可以近似动作价值函数,但Q表是二维表格,而神经网络是函数,可以处理连续的状态和空间。
在Q 表中我们描述状态空间的时候一般用的是状态个数,而在神经网络中我们用的是状态维度。但他们的输出都是每个动作对应的Q值,即预测,而非直接输出动作,如果要输出动作,要额外一些工作,例如结合贪心算法选择Q值最大对应的动作等,即控制过程。用神经网络代替Q所增加的额外参数θ需要用梯度下降求解。回顾一下Q-learning的跟新公式。
在DQN中,我们用需引入额外的 网络参数θ,
将差值写成损失函数的形式,用梯度下降求解。
判断终止:
从这个角度看,强化学习和深度学习训练方式一样,但不同的是,强化学习通过与环境交互,而深度学习是事先准备好的。本质上来说,强化学习解决序列决策问题。接下来将一些技巧。
7.2 经验回放
这个是所有off-policy算法都会用到的。回顾一下训练机制,强化学习通过与环境交互得到样本训练,样本包括当前状态St,当前动作at,下一时刻状态St+1,奖励rt+1,终止状态标志done(不在公式),也叫做一个转移(transition),(St,at,St+1,rt+1).
这样的方式在神经网络中会存在问题,单个迭代不稳定,,且每次迭代样本相互关联,这与梯度下降假设独立同分布不符。因此我们需要进行一些处理才能使得训练方式达到与成熟的小批量梯度下降达到同样的效果,这便是经验回放的作用。
对比来看,Q-learning算法将每次通过与环境交互的样本直接放入网络训练,但DQN中将每次交互得到的样本存储在经验池中,每次随机抽取一批来训练网络。这样就满足了独立同分布的假设。。
经验回放的容量不能太大也不能太小。因为后期产生的数据质量不太好。
7.3 目标网络
在DQN算法中还有一个重要的技巧,即使用一个每隔若干步才更新的目标网络。目标网络和当前网络结构都是相同的,都用于近似Q值,在实践中每隔若干步才把每步跟新的当前网络参数赋值给目标网络,这样做的好处是保证训练的稳定,避免Q值得估计发散。
计算损失函数时,使用目标网络来计算Q的期望值。
目标网络的作用是为存档。
7.3 实战DQN算法
7.4.1 伪代码
算法的训练过程分为交互采样和模型跟新两个步骤。交互采样的目的是与环境交互产生样本,模型跟新则是利用得到的样本跟新相关的网络参数。
在算法中我们需要定义当前网络、目标网络和经验回放等元素,这些可看作python类。
7.4.2 定义模型
定义模型即定义两个神经网络,当前网络和目标网络,由于结构相同,我们只用一个python类定义。
class MLP(nn.Module): # 所有网络必须继承 nn.Module 类,这是 PyTorch 的特性
def __init__(self, input_dim,output_dim,hidden_dim=128):
super(MLP, self).__init__()
# 定义网络的层,这里都是线性层
self.fc1 = nn.Linear(input_dim, hidden_dim) # 输入层
self.fc2 = nn.Linear(hidden_dim,hidden_dim) # 隐藏层
self.fc3 = nn.Linear(hidden_dim, output_dim) # 输出层
def forward(self, x):
# 各层对应的激活函数
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
return self.fc3(x) # 输出层不需要激活函数
三层全连接网络,输入维度就是状态数,输出维度就是动作数,中间隐藏层采用Relu激活函数,用Module类来定义网络。
7.4.3 经验回放
功能主要实现缓存样本和取出样本两个功能。
class ReplayBuffer:
def __init__(self, capacity):
self.capacity = capacity # 经验回放的容量
self.buffer = [] # 用列表存放样本
self.position = 0 # 样本下标,便于覆盖旧样本
def push(self, state, action, reward, next_state, done):
''' 缓存样本
'''
if len(self.buffer) < self.capacity: # 如果样本数小于容量
self.buffer.append(None)
self.buffer[self.position] = (state, action, reward, next_state, done)
self.position = (self.position + 1) % self.capacity
def sample(self, batch_size):
''' 取出样本,即采样
'''
batch = random.sample(self.buffer, batch_size) # 随机采出小批量转移
state, action, reward, next_state, done = zip(*batch) # 解压成状态,动作等
return state, action, reward, next_state, done
def __len__(self):
''' 返回当前样本数
'''
return len(self.buffer)
7.4.5 定义智能体
智能体即策略的载体,有时也称为策略。其主要功能时根据当前状态输出动作和更新策略,对应伪代码中的交互采样和模型更新。
两个网络为前述全连接网络,输入是状态维度,输出是动作维度。此处还定义了一个优化器用来跟新网络参数。采样动作用-greedy策略,便于在训练中探索,测试只需要检验模型性能,无需探索。只需单纯argmax,即值最大的动作。
class Agent:
def __init__(self):
# 定义当前网络
self.policy_net = MLP(state_dim,action_dim).to(device)
# 定义目标网络
self.target_net = MLP(state_dim,action_dim).to(device)
# 将当前网络参数复制到目标网络中
self.target_net.load_state_dict(self.policy_net.state_dict())
# 定义优化器
self.optimizer = optim.Adam(self.policy_net.parameters(), lr=learning_rate)
# 经验回放
self.memory = ReplayBuffer(buffer_size)
self.sample_count = 0 # 记录采样步数
def sample_action(self,state):
''' 采样动作,主要用于训练
'''
self.sample_count += 1
# epsilon 随着采样步数衰减
self.epsilon = self.epsilon_end + (self.epsilon_start - self.epsilon_end) * math.exp(-1. * self.sample_count / self.epsilon_decay)
if random.random() > self.epsilon:
with torch.no_grad(): # 不使用梯度
state = torch.tensor(np.array(state), device=self.device, dtype=torch.float32).unsqueeze(dim=0)
q_values = self.policy_net(state)
action = q_values.max(1)[1].item() # choose action corresponding to the maximum q value
else:
action = random.randrange(self.action_dim)
def predict_action(self,state):
''' 预测动作,主要用于测试
'''
with torch.no_grad():
state = torch.tensor(np.array(state), device=self.device, dtype=torch.float32).unsqueeze(dim=0)
q_values = self.policy_net(state)
action = q_values.max(1)[1].item() # choose action corresponding to the maximum q value
return action
def update(self):
pass
def update(self, share_agent=None):
# 当经验回放中样本数小于更新的批大小时,不更新算法
if len(self.memory) < self.batch_size: # when transitions in memory donot meet a batch, not update
return
# 从经验回放中采样
state_batch, action_batch, reward_batch, next_state_batch, done_batch = self.memory.sample(
self.batch_size)
# 转换成张量(便于GPU计算)
state_batch = torch.tensor(np.array(state_batch), device=self.device, dtype=torch.float)
action_batch = torch.tensor(action_batch, device=self.device).unsqueeze(1)
reward_batch = torch.tensor(reward_batch, device=self.device, dtype=torch.float).unsqueeze(1)
next_state_batch = torch.tensor(np.array(next_state_batch), device=self.device, dtype=torch.float)
done_batch = torch.tensor(np.float32(done_batch), device=self.device).unsqueeze(1)
# 计算 Q 的实际值
q_value_batch = self.policy_net(state_batch).gather(dim=1, index=action_batch) # shape(batchsize,1),requires_grad=True
# 计算 Q 的估计值,即 r+\gamma Q_max
next_max_q_value_batch = self.target_net(next_state_batch).max(1)[0].detach().unsqueeze(1)
expected_q_value_batch = reward_batch + self.gamma * next_max_q_value_batch* (1-done_batch)
# 计算损失
loss = nn.MSELoss()(q_value_batch, expected_q_value_batch)
# 梯度清零,避免在下一次反向传播时重复累加梯度而出现错误。
self.optimizer.zero_grad()
# 反向传播
loss.backward()
# clip避免梯度爆炸
for param in self.policy_net.parameters():
param.grad.data.clamp_(-1, 1)
# 更新优化器
self.optimizer.step()
# 每C(target_update)步更新目标网络
if self.sample_count % self.target_update == 0:
self.target_net.load_state_dict(self.policy_net.state_dict())
跟新时取出样本,并且转换为张量。接着计算Q值得估计值和实际值得到损失函数,然后跟新参数
7.4.6 定义环境
此处得环境为OpenAI Gym开发的推车杆游戏。目的是维持左右推动一直不倒。
环境状态数4,动作数2.有读者可能会奇怪,这不是比 算法中的 CliffWalking-v0
环境(状态数是 48, 动作数是 2)更简单吗,应该直接用 算法就能解决?实际上是不能的,因为 Cart Pole
的状态包括推车的位置(-4.8范围是 到 4.8)、速度(范围是负无穷大到正无穷大)、杆的角度(范围是 -24度 到24 度)和角速度(范围是负无穷大到正无穷大),这几个状态都是连续的值,也就是前面所说的连续状态空间,因此用 算法是很难解出来的.奖励为每个时步下能维持杆不倒就+1。设置一个环境最大步数。
7.4.7 设置参数
self.epsilon_start = 0.95 # epsilon 起始值
self.epsilon_end = 0.01 # epsilon 终止值
self.epsilon_decay = 500 # epsilon 衰减率
self.gamma = 0.95 # 折扣因子
self.lr = 0.0001 # 学习率
self.buffer_size = 100000 # 经验回放容量
self.batch_size = 64 # 批大小
self.target_update = 4 # 目标网络更新频率