DQN 算法通过贪婪法直接获得目标 Q 值,贪婪法通过最大化方式使 Q 值快速向可能的优化目标收敛,但易导致过估计Q 值的问题,使模型具有较大的偏差。
即:
对于DQN模型, 损失函数使用的
Q(state) = reward + Q(nextState)max
Q(state)
由训练网络生成, Q(nextState)max
由目标网络生成
这种损失函数会存在问题,即当Q(nextState)max
总是大于0时,那么Q(state)
总是在不停的增大,同时Q(nextState)max
也在不断的增大, 即Q(state)
存在被高估的情况。
作者采用 Double DQN 算法解耦动作的选择和目标 Q 值的计算,以解决过估计 Q 值的问题。
Double DQN 算法结构如下。在 Double DQN 框架中存在两个神经网络模型,分别是训练网络与目标网络。这两个神经网络模型的结构完全相同,但是权重参数不同;每训练一段之间后,训练网络的权重参数才会复制给目标网络。训练时,训练网络用于估计当前的 ,而目标网络用于估计 ,这样就能保证真实值 的估计不会随着训练网络的不断自更新而变化过快。此外,DQN 还是一种支持离线学习的框架,即通过构建经验池的方式离线学习过去的经验。将均方误差 MSE(Q_{train}, Q_{target}) 作为训练模型的损失函数,通过梯度下降法进行反向传播,对训练模型进行更新;若干轮经验池采样后,再将训练模型的权重赋给目标模型,以此进行 Double DQN 框架下的模型自学习。
目标 Q 值的计算公式如下所示:
y j = r j + γ max a ′ Q ( s j + 1 , a ′ ; θ ′ ) y_j=r_j+\gamma \max _{a^{\prime}} Q\left(s_{j+1}, a^{\prime} ; \theta^{\prime}\right) yj=rj+γa′maxQ(sj+1,a′;θ′)
Double DQN 算法不直接通过最大化的方式选取目标网络计算的所有可能 Q Q Q 值,而是首先通过估计网络选取最大 Q Q Q 值对应的动作,公式表示如下:
a max = argmax a Q ( s t + 1 , a ; θ ) a_{\max }=\operatorname{argmax}_a Q\left(s_{t+1}, a ; \theta\right) amax=argmaxaQ(st+1,a;θ)
然后目标网络根据 a max a_{\max } amax 计算目标 Q 值,公式表示如下:
y j = r j + γ Q ( s j + 1 , a max ; θ ′ ) y_j=r_j+\gamma Q\left(s_{j+1}, a_{\max } ; \theta^{\prime}\right) yj=rj+γQ(sj+1,amax;θ′)
最后将上面两个公式结合,目标 Q Q Q 值的最终表示形式如下:
y j = r j + γ Q ( s j + 1 , argmax a Q ( s t + 1 , a ; θ ) ; θ ′ ) y_j=r_j+\gamma Q\left(s_{j+1}, \operatorname{argmax}_a Q\left(s_{t+1, a ; \theta}\right) ; \theta^{\prime}\right) yj=rj+γQ(sj+1,argmaxaQ(st+1,a;θ);θ′)
目标是最小化目标函数,即最小化估计 Q Q Q 值和目标 Q Q Q 值的差值,公式如下:
δ = ∣ Q ( s t , a t ) − y t ∣ = ∣ Q ( s t , a t ; θ ) − ( r t + γ Q ( S t + 1 , argmax a Q ( s t + 1 , a ; θ ) ; θ ′ ) ) ∣ \begin{aligned} & \delta=\left|Q\left(s_t, a_t\right)-y_t\right|=\mid Q\left(s_t, a_t ; \theta\right)-\left(r_t+\right. \\ & \left.\gamma Q\left(S_{t+1}, \operatorname{argmax}_a Q\left(s_{t+1}, a ; \theta\right) ; \theta^{\prime}\right)\right) \mid \end{aligned} δ=∣Q(st,at)−yt∣=∣Q(st,at;θ)−(rt+γQ(St+1,argmaxaQ(st+1,a;θ);θ′))∣
结合目标函数,损失函数定义如下:
loss = { 1 2 δ 2 for ∣ δ ∣ ⩽ 1 ∣ δ ∣ − 1 2 otherwize } \text { loss }=\left\{\begin{array}{cl} \frac{1}{2} \delta^2 & \text { for }|\delta| \leqslant 1 \\ |\delta|-\frac{1}{2} & \text { otherwize } \end{array}\right\} loss ={21δ2∣δ∣−21 for ∣δ∣⩽1 otherwize }
import gym
#定义环境
class MyWrapper(gym.Wrapper):
def __init__(self):
env = gym.make('CartPole-v1', render_mode='rgb_array')
super().__init__(env)
self.env = env
self.step_n = 0
def reset(self):
state, _ = self.env.reset()
self.step_n = 0
return state
def step(self, action):
state, reward, terminated, truncated, info = self.env.step(action)
over = terminated or truncated
#限制最大步数
self.step_n += 1
if self.step_n >= 200:
over = True
#没坚持到最后,扣分
if over and self.step_n < 200:
reward = -1000
return state, reward, over
#打印游戏图像
def show(self):
from matplotlib import pyplot as plt
plt.figure(figsize=(3, 3))
plt.imshow(self.env.render())
plt.show()
env = MyWrapper()
env.reset()
env.show()
import torch
#定义模型,评估状态下每个动作的价值
model = torch.nn.Sequential(
torch.nn.Linear(4, 64),
torch.nn.ReLU(),
torch.nn.Linear(64, 64),
torch.nn.ReLU(),
torch.nn.Linear(64, 2),
)
#延迟更新的模型,用于计算target
model_delay = torch.nn.Sequential(
torch.nn.Linear(4, 64),
torch.nn.ReLU(),
torch.nn.Linear(64, 64),
torch.nn.ReLU(),
torch.nn.Linear(64, 2),
)
#复制参数
model_delay.load_state_dict(model.state_dict())
model, model_delay
from IPython import display
import random
#玩一局游戏并记录数据
def play(show=False):
data = []
reward_sum = 0
state = env.reset()
over = False
while not over:
action = model(torch.FloatTensor(state).reshape(1, 4)).argmax().item()
if random.random() < 0.1:
action = env.action_space.sample()
next_state, reward, over = env.step(action)
data.append((state, action, reward, next_state, over))
reward_sum += reward
state = next_state
if show:
display.clear_output(wait=True)
env.show()
return data, reward_sum
play()[-1]
#数据池
class Pool:
def __init__(self):
self.pool = []
def __len__(self):
return len(self.pool)
def __getitem__(self, i):
return self.pool[i]
#更新动作池
def update(self):
#每次更新不少于N条新数据
old_len = len(self.pool)
while len(pool) - old_len < 200:
self.pool.extend(play()[0])
#只保留最新的N条数据
self.pool = self.pool[-2_0000:]
#获取一批数据样本
def sample(self):
data = random.sample(self.pool, 64)
state = torch.FloatTensor([i[0] for i in data]).reshape(-1, 4)
action = torch.LongTensor([i[1] for i in data]).reshape(-1, 1)
reward = torch.FloatTensor([i[2] for i in data]).reshape(-1, 1)
next_state = torch.FloatTensor([i[3] for i in data]).reshape(-1, 4)
over = torch.LongTensor([i[4] for i in data]).reshape(-1, 1)
return state, action, reward, next_state, over
pool = Pool()
pool.update()
pool.sample()
len(pool), pool[0]
#训练
def train():
model.train()
optimizer = torch.optim.Adam(model.parameters(), lr=2e-4)
loss_fn = torch.nn.MSELoss()
#共更新N轮数据
for epoch in range(1000):
pool.update()
#每次更新数据后,训练N次
for i in range(200):
#采样N条数据
state, action, reward, next_state, over = pool.sample()
#计算value
value = model(state).gather(dim=1, index=action)
#计算target
with torch.no_grad():
target = model_delay(next_state)
target = target.max(dim=1)[0].reshape(-1, 1)
target = target * 0.99 * (1 - over) + reward
loss = loss_fn(value, target)
loss.backward()
optimizer.step()
optimizer.zero_grad()
#复制参数
if (epoch + 1) % 5 == 0:
model_delay.load_state_dict(model.state_dict())
if epoch % 100 == 0:
test_result = sum([play()[-1] for _ in range(20)]) / 20
print(epoch, len(pool), test_result)
train()