Deep Q Network算法
文章目录
选择方法前先搞清楚状态和动作空间是连续的还是离散的!!
1.DQN理论:连续状态-离散动作
参考论文:Mnih, V., Kavukcuoglu, K., Silver, D. et al. Human-level control through deep reinforcement learning. Nature 518, 529–533 (2015). https://doi.org/10.1038/nature14236
表格形式Q-table存储的Q(s,a),状态数太复杂太多,使用网络拟合Q-table,此为近似方法。(函数拟合)
DQN适用于离散动作空间,是因为函数Q的更新过程涉及 m a x a max_a maxa的操作。
1.1 Q-网络
使用神经网络拟合Q表。DQN即采用离散动作的第二种方式。
Q
w
(
s
,
a
)
=
N
N
w
(
s
)
L
o
s
s
=
1
2
N
∑
i
=
1
N
[
Q
w
(
s
i
,
a
i
)
−
(
r
i
+
γ
m
a
x
a
′
∈
A
Q
(
s
i
′
,
a
′
)
]
2
参数:
w
Q_w(\bold{s},\bold{a})=NN_w(\bold{s})\\ Loss = \frac{1}{2N}\sum_{i=1}^{N}[Q_w(s_i,a_i)-(r_i+\gamma max_{a' \in A}Q(s_i',a')]^2\\ 参数:w
Qw(s,a)=NNw(s)Loss=2N1i=1∑N[Qw(si,ai)−(ri+γmaxa′∈AQ(si′,a′)]2参数:w
-
当动作空间是连续时:
-
当动作空间是离散时:
1.2 经验回放
深度学习假设样本数据是独立同分布的,但是强化学习中采样的数据是强相关的。DQN使用经验回放机制,将每次从环境中采样得到的四元组数据(状态、动作、奖励、下一状态)存储到回放缓存区。再通过随机采样若干数据进行学习,以打破数据之间相关性,并且可提高训练效率。
1.3 训练网络和目标网络
神经网络的训练目标是使得 Q w ( s , a ) Q_w(\bold{s},\bold{a}) Qw(s,a)不断逼近 r + γ m a x a ′ Q w ( s ′ , a ′ ) r+\gamma max_{\bold{a'} }Q_w(\bold{s'},\bold{a'}) r+γmaxa′Qw(s′,a′) 。由于神经网络的训练目标包含神经网络的输入,因此在更新参数的同时目标也在变化,这使得神经网络的训练过程不稳定。因此提出两张网络:训练网络和目标网络(两张网络结构完全一致)。
-
训练网络 Q w ( s , a ) Q_w(\bold{s},\bold{a}) Qw(s,a)
计算损失函数中的 1 2 [ Q w ( s , a ) − ( r + γ m a x a ′ Q w − ( s ′ , a ′ ) ] 2 \frac{1}{2}[Q_w(\bold{s},\bold{a})-(r+\gamma max_{\bold{a'} }Q_{w^-}(\bold{s'},\bold{a'})]^2 21[Qw(s,a)−(r+γmaxa′Qw−(s′,a′)]2 中的 Q w ( s , a ) Q_w(\bold{s},\bold{a}) Qw(s,a) ,并使用正常的梯度下降法实时更新参数 w w w。
-
目标网络
计算损失函数中的 1 2 [ Q w ( s , a ) − ( r + γ m a x a ′ Q w − ( s ′ , a ′ ) ] 2 \frac{1}{2}[Q_w(\bold{s},\bold{a})-(r+\gamma max_{\bold{a'} }Q_{w^-}(\bold{s'},\bold{a'})]^2 21[Qw(s,a)−(r+γmaxa′Qw−(s′,a′)]2 中的 ( r + γ m a x a ′ Q w − ( s ′ , a ′ ) ) (r+\gamma max_{\bold{a'} }Q_{w^-}(\bold{s'},\bold{a'})) (r+γmaxa′Qw−(s′,a′)) ,目标网络使用训练网络的旧参数,目标网络的参数 w − w_- w−每隔 C C C步才会与训练网络同步一次,即 w − < − w w_-<-w w−<−w。
使得 C C C步内,神经网络的训练目标是不变的。只有训练网络是参与训练的。
1.4 程度框图和伪代码
DQN伪代码:
2.案例-CartPole
官网:https://gymnasium.farama.org/environments/classic_control/cart_pole/
源码:https://github.com/openai/gym/blob/master/gym/envs/classic_control/cartpole.py
2.1 理论
2.1.1 环境
gym:https://gymnasium.farama.org/api/env/
安装:
pip3 install swig
pip3 install box2d
pip3 install gymnasium
pip3 install gymnasium[box2d]
Tensor.detach — PyTorch 2.1 文档
2.1.2 状态
智能体的状态是一个维数为 4 的向量,每一维都是连续的。
2.1.3 动作
2.1.4 奖励
在游戏中每坚持一帧,智能体能获得分数为 1 的奖励,坚持时间越长,则最后的分数越高。(对于CartPole-v1,最大奖励为坚持500帧,即500)
2.1.5 终止条件
- 对应env.step()的输出:
s_, r, Termination, Truncation, info = env.step(a)
注意:在目前V1版本的源码中,无论episode长度是否大于500,其Truncation输出均为False,没有官网API说的时间截断功能。
2.1.6 DQN-回放经验池
此案例使用np.array
作为数据存储形式,定义二维数组为回访经验池。
- 一个epoch即为一次试验,在一个epoch中,智能体可行走多步(step),直至达到该试验终止条件。
- 前期:智能体每走一个step,即向记忆库写入一条数据 (探索);
- 后期:当记忆库被填满后进入后期,在每个epoch中,每走一个step,重头开始向记忆库中写入一条数据 (探索);同时在记忆库中抽取BATCH_SIZE(N)条数据训练Q函数网络 (利用)。
2.1.7 进度条库Tdqm
Python tqdm 进度条库的基本使用_tqdm库用法-CSDN博客
GitHub - tqdm/tqdm: ⚡️ A Fast, Extensible Progress Bar for Python and CLI
from tqdm import tqdm
#设置一个pbar即只有一个不断更新的进度条
with tqdm(total = int(400)) as pbar:
for i_episode
pbar.desc = "epoch %d " % (i_episode )
pbar.set_postfix({
'avg_loss':
'%0.3f' % np.mean(episode_loss),
'return':
'%0.3f' % episode_r
})
pbar.update(1) #循环一次更新一次
2.2 代码
2.2.1 结果
(修正后的奖励):
2.2.2 code
基于pytorch的DQN算法
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import gymnasium as gym
import matplotlib.pyplot as plt
from tqdm import tqdm
# 超参数
BATCH_SIZE = 32
LR = 0.01 # learning rate
EPSILON = 0.1 # 贪心策略:最优选择动作百分比
GAMMA = 0.9 # 奖励递减参数
TARGET_REPLACE_ITER = 100 # Q目标网络的更新频率C
MEMORY_CAPACITY = 200 # 记忆库大小
#输出环境
env = gym.make('CartPole-v1', render_mode="human")
env = env.unwrapped #环境重置
N_ACTIONS = env.action_space.n # 杆子能做的动作
N_STATES = env.observation_space.shape[0] # 杆子能获取的环境信息数
N_NEURON = 128 #单层神经元个数
#当有多个相同结构网络时,使用这种方式固定网络结构
class Net(nn.Module):
def __init__(self,n_feature,n_hidden,n_output):
super(Net, self).__init__()
self.fc1 = nn.Linear(n_feature, n_hidden)
self.fc1.weight.data.normal_(0, 0.1) # 初始化网络权重
self.out = nn.Linear(n_hidden, n_output)
self.out.weight.data.normal_(0, 0.1) # 初始化网络权重
def forward(self, x):
x = self.fc1(x)
x = F.relu(x)
actions_value = self.out(x)
return actions_value
class DQN(object):
def __init__(self):
#建立训练网络和目标网络,以及存储区 可在大类中传递
self.eval_net = Net(n_feature=N_STATES, n_hidden=N_NEURON, n_output=N_ACTIONS)
self.target_net = Net(n_feature=N_STATES, n_hidden=N_NEURON, n_output=N_ACTIONS)
self.learn_step_counter = 0 # 用于 target 更新计时
self.memory_counter = 0 # 记忆库记数
self.memory = np.zeros((MEMORY_CAPACITY, N_STATES * 2 + 2)) # 初始化记忆库-np数组
self.optimizer = torch.optim.Adam(self.eval_net.parameters(), lr=LR) # torch 的优化器
self.loss_func = nn.MSELoss() # 误差公式
def choose_action(self, x):
#x必须为可梯度下降数据
x = torch.unsqueeze(torch.FloatTensor(x), 0)
#贪心算法 以epsilon的概率选择低价值动作
if np.random.uniform() > EPSILON:
actions_value = self.eval_net.forward(x)
action = torch.max(actions_value, 1)[1].data.numpy()[0]
else: #随机
action = np.random.randint(0, N_ACTIONS) #np.int
return action
def store_transition(self, s, a, r, s_): #回放缓存区
transition = np.hstack((s, [a, r], s_)) #以列为单位合并,合并成一行
#存储数据
index = self.memory_counter % MEMORY_CAPACITY
self.memory[index,:] = transition
self.memory_counter += 1
def learn(self):
#检测是否更新目标网络
if self.learn_step_counter % TARGET_REPLACE_ITER == 0:
self.target_net.load_state_dict(self.eval_net.state_dict())
self.learn_step_counter += 1
#更新训练网络
sample_index = np.random.choice(MEMORY_CAPACITY, BATCH_SIZE)#随机抽取数据
b_memory = self.memory[sample_index,:]
b_s = torch.FloatTensor(b_memory[:,:N_STATES])
b_a = torch.LongTensor(b_memory[:,N_STATES:N_STATES + 1])
b_r = torch.FloatTensor(b_memory[:,N_STATES+1:N_STATES+2])
b_s_ = torch.FloatTensor(b_memory[:, -N_STATES:])
Q = self.eval_net(b_s).gather(1,b_a) # shape=[BATCH_SIZE,1]依据当时的动作选取价值
Q_ = self.target_net(b_s_).detach() #下一状态的Q_且不更新
Q_target = b_r + GAMMA*Q_.max(1)[0].view(BATCH_SIZE,1)
loss = self.loss_func(Q, Q_target)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
#print(loss.data)
return loss.data
dqn = DQN()
r_list = []
with tqdm(total = int(100)) as pbar:
for i_episode in range(100):
episode_r = 0
episode_loss = []
s = env.reset() #环境重置
s = np.array(s[0])
while True:
env.render() # 显示实验动画
a = dqn.choose_action(s)
# 选动作, 得到环境反馈
s_, r, Termination, _, info = env.step(a)
#修改 reward, 使 DQN 快速学习 杆子越偏,车位置越偏,奖励越小
x, x_dot, theta, theta_dot = s_
r1 = (env.x_threshold - abs(x)) / env.x_threshold - 0.8
r2 = (env.theta_threshold_radians - abs(theta)) / env.theta_threshold_radians - 0.5
r = r1 + r2 #每一步的奖励不超过0.7,最多500步
episode_r +=r
# 存记忆
dqn.store_transition(s, a, r, s_)
if dqn.memory_counter > MEMORY_CAPACITY:
step_loss = dqn.learn() # 记忆库满了就进行学习
else:
step_loss = 10000
episode_loss.append(step_loss) #记忆智能体每一步后训练网络的Loss
if Termination | (len(episode_loss) >= 500): # 如果回合结束, 进入下回合
break
s = s_
r_list.append(episode_r)
pbar.desc = "epoch %d " % (i_episode )
pbar.set_postfix({
'avg_loss':
'%0.3f' % np.mean(episode_loss),
'return':
'%0.3f' % episode_r,
'epoch_step': #每个epoch的步数step
'%d' % len(episode_loss)
})
pbar.update(1)
episodes_list = list(range(len(r_list)))
plt.plot(episodes_list, r_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('DQN on {}'.format('CartPole-v1'))
plt.show()