DQN(Deep Q-Network)算法是一种结合了深度学习和强化学习的技术,用于解决具有高维观测空间的强化学习问题。DQN算法通过使用深度神经网络来近似Q函数(也称为动作价值函数),在许多领域取得了突破性的成果,特别是在游戏和机器人控制方面。以下是DQN算法的关键特点:
-
深度Q网络:DQN使用一个深度神经网络来学习状态-动作对的价值函数,即 Q(s,a)Q(s,a)。
-
经验回放:DQN算法使用一个经验回放缓冲区来存储过去的转换(状态、动作、奖励、下一个状态),这有助于提高数据的利用率并减少训练过程中的方差。
-
目标网络:DQN使用两个相同的神经网络,一个用于预测 QQ 值(主网络),另一个作为目标网络。目标网络的参数定期从主网络复制过来,以稳定训练过程。
-
Bellman方程:DQN算法通过最小化Bellman方程的误差来更新网络权重。
-
损失函数:DQN的损失函数通常使用均方误差(MSE)来衡量预测的 QQ 值和目标 QQ 值之间的差异。
-
探索与利用:DQN通常使用ε-贪心策略来平衡探索和利用。
-
训练过程:
- 从环境中获取状态 ss 并选择动作 aa。
- 执行动作并观察奖励 rr 和下一个状态 s′s′。
- 使用 s,a,r,s′s,a,r,s′ 更新经验回放缓冲区。
- 从缓冲区中随机采样一批数据来计算损失并更新网络。
-
稳定性:DQN算法通过目标网络和经验回放来提高训练过程的稳定性。
-
应用:DQN算法已被成功应用于各种游戏和模拟环境中,例如Atari 2600游戏和一些3D游戏。
-
变体:DQN算法有多种变体,如Double DQN、Dueling DQN和Prioritized Experience Replay,这些变体在不同方面改进了原始DQN算法的性能。
import gym
from matplotlib import pyplot as plt
#创建环境
env = gym.make('CartPole-v0')
env.reset()
#打印游戏
def show():
plt.imshow(env.render(mode='rgb_array'))
plt.show()
show()
#测试游戏环境
def test_env():
state = env.reset()
print('state=',state)
print('env.action_space=',env.action_space)
print('随机一个动作')
action = env.action_space.sample()
print('action',action)
print('执行一个动作,得到下一个状态,奖励,是否结束')
state,reward,over,_=env.step(action)
print('state=',state)
print('reward',reward)
print('over',over)
test_env()
#创建神经网络模型,动作模型,经验模型
import torch
#计算动作的模型,也是真正要用的模型
model = torch.nn.Sequential(
torch.nn.Linear(4,128),
torch.nn.ReLu(),
torch.nn.Linear(128,2),
)
#经验网络,用于评估一个状态的分数
next_model = torch.nn.Sequential(
torch.nn.Linear(4,128),
torch.nn.ReLU(),
torch.nn.Linear(128,2),
)
#把model的参数复制给next_model
next_model.load_state_dict(model.state_dict())#load_state_dict() 方法用于加载模型的参数。当你想要将一个模型的参数复制给另一个模型时,可以使用这个方法。
model,next_model
import random
#得到一个动作
def get_action(state):
if random.random()<0.01:
return random.choice([0,1])
#走神经网络,得到一个动作
state = torch.FloatTensor(state).reshape(1,4)
return model(state).argmax().item()
get_action([0.0012=3847,-0.01194451,0.04260966,0.00688801])
-
探索概率:
if random.random() < 0.01
:这行代码检查一个随机数是否小于0.01,如果是,智能体将进行探索。0.01是探索的概率阈值。
-
随机选择动作:
return random.choice([0, 1])
:如果智能体选择探索,它将随机选择一个动作,这里的动作集合是[0, 1]
。这意味着动作空间被限制为两个可能的动作。
-
利用策略:
- 如果随机数大于或等于0.01,智能体将利用其当前的知识来选择最佳动作。
-
状态转换:
state = torch.FloatTensor(state).reshape(1, 4)
:将当前状态转换为一个 PyTorch 浮点张量,并重塑为尺寸为(1, 4)
的张量。这通常是为了匹配神经网络的输入尺寸。
-
神经网络预测:
return model(state).argmax().item()
:将状态输入到神经网络model
中,获取每个可能动作的预测值,然后选择具有最高预测值的动作。argmax().item()
返回具有最大值的动作索引。
#更新数据样本池函数,责备离线学习
#样本池
datas=[]
#向样本池添加N条数据,删除M条最古老的数据
def update_date():
old_count = len(datas)
#玩到新增了N个数据为止
while len(datas) - old_count<200:
#初始化游戏
state = env.reset()
#玩到游戏结束为止
over = False
while not over:
#根据当前状态得到一个动作
action = get_action(state)
#执行动作,得到反馈
next_state,reward,over,_=env.step(action)
#记录数据样本
datas.append((state,action,reward,next_state,over))
#更新游戏状态,开始下一个动作
state = next_state
update_count = len(datas)-old_count
drop_count = max(len(datas)-10000,0)
#数据上限,超出时从最古老的开始删除
while len(datas)>10000:
datas.pop(0)
return update_count,drop_count
#数据采样函数
#获取一批数据样本
#get_sample 函数是一个用于从数据集 datas 中随机抽取样本的函数,并将这些样本转换成适合深度学习模型输入的 PyTorch 张量格式
def get_sample():
samples = random.sample(datas,64)
#[b,4]
state = torch.FloatTensor([i[0]for i in samples]).reshape(-1,4)
#[b,1]
action = torch.LongTensor([i[1]for i in samples]).reshape(-1,1)
#[b,1]
reward = torch.FloatTensor([i[2]for i in samples]).reshape(-1,1)
#[b,4]
next_state = torch.FloatTensor([i[3]for i in samples]).reshape(-1,4)
#[b,1]
over = torch.LongTensor([i[4]for i in samples]).reshape(-1,1)
return state,action,reward,next_state,over
state,action,reward,next_state,over=get_sample()
state,action,reward,next_state,over
#计算value函数
def get_value(state,action):
#使用状态计算出动作的logits
#[b,4]->[b,2]
value = model(state)
#根据实际使用的action取出每一个值
#这个值就是模型评估的在该状态下,执行动作的分数
#在执行动作前,显然并不知道会得到的反馈和next_state
#所以这里不能也不需要考虑next_state和reward
#[b,2]->[b,1]
value = value.gather(dim = 1,index=action)
return value
get_value(state,action).shape
-
模型预测:
value = model(state)
:使用模型对状态state
进行预测,得到一个包含每个可能动作的值的张量。假设模型输出的尺寸是[b, 2]
,其中b
是批量大小,2
是动作的数量。
-
索引选择:
value = value.gather(dim=1, index=action)
:使用gather
方法根据action
索引从模型输出中选择对应的值。这里假设action
是一个长整型张量,其尺寸为[b, 1]
。
#计算target函数
def get_target(reward,next_state,over):
#上面已经把模型认为的状态下执行动作的分数给评估出来了
#下面使用next_state和reward计算真实的分数
#针对一个状态,它到底应该多少分,可以使用以往模型积累的经验评估
#这也是没办法的办法,因为显然没有精确解,这里使用延迟更新的next_model评估
#使用next_state计算下一个状态的分数
#[b,4}->[b,2]
with torch.no_grad():
target = next_model(next_state)
#取所有动作中分数最大的
#[b,2]-[b,1]
target = target.max(dim=1)[0]
target = target.reshape(-1,1)
#下一个状态的分数乘以一个系数,相当于权重
target*=0.98
#如果next_state已经游戏结束,则next_state分数是0
#因为如果下一步已经游戏结束,显然不需要再继续下去,也就不需要考虑next_state了
#[b,1]*[b,1]->[b,1]
target *=(1-over)
#加上reward就是最终的分数
#[b,1]+[b,1]->[b,1]
target+=reward
return target
-
使用
next_model
预测下一个状态的值:target = next_model(next_state)
:使用next_model
对下一个状态next_state
进行预测,得到每个可能动作的值。
-
选择最佳动作的值:
target = target.max(dim=1)[0]
:从预测的值中选择每个状态的最大动作值。max(dim=1)
返回最大值和索引,[0]
选择最大值。
-
重塑目标张量:
target = target.reshape(-1, 1)
:将目标张量重塑为二维张量,每行一个值。
-
应用折扣因子:
target *= 0.98
:将目标值乘以折扣因子0.98
,这是强化学习中的常见做法,用于计算未来奖励的当前价值。
-
处理游戏结束的情况:
target *= (1 - over)
:如果over
是一个表示游戏是否结束的二进制张量(1 表示结束,0 表示未结束),这行代码将结束状态的目标值设置为 0。
-
计算最终的目标值:
target += reward
:将即时奖励加到目标值上,得到最终的TD目标。
-
返回值:
- 函数返回
target
,这是每个样本的TD目标值。
- 函数返回
测试函数
from IPython import display
def test(play):
#初始化游戏
state = env.reset()
#记录反馈值的和,这个值越大越好
reward_sum = 0
#玩到游戏结束为止
over = False
while not over:
#根据当前状态得到一个动作
action = get_action(state)
#执行动作,得到反馈
state,reward,over,_ env.step(action)
reward_sum+=reward
#打印动画
if play and random.random()<0.1:#跳帧
display.clear_output(wait = True)
show()
return reward_sum
训练函数
def train():
model.train()
optimizer = torch.optim.Adam(model.parameters(),lr = 2e-3)
loss_fn = torch.nn.MSELoss()
#训练N次
for epoch in range(500):
#更新N条数据
update_count,drop_count = update_data()
#每次更新过数据后,学习N次
for i in range(200):
#采样一批数据
state, action,reward,next_state,over = get_sample()
#计算一批样本的value和target
value = get_value(state,action)
target = get_target(reward,next_state,over)
#更新参数
loss = loss_fn(value,target)
optimizer.zero_grad()
loss.backward()
optimizer.step()
#把model参数复制给next_model
if(i+1)%10==0:
next_model.load_state_dict(model.state_dict())
if epoch % 50==0:
test_result =sum([test(play = False)for _ in range(20)])/20
print(spoch.len(datas),update_count,drop_count,test_result)
DQN改进算法doubleDQN
def get_target(reward,next_state,over):
#上面已经把模型认为的状态下执行动作的分数给评估出来了
#下面使用next_state和reward计算真实的分数
#针对一个状态,它到底应该多少分,可以使用以往模型积累的经验评估
#这也是没办法的办法,因为显然没有精确解,这里使用延迟更新的next_model评估
#使用next_state计算下一个状态的分数
#[b,4}->[b,2]
with torch.no_grad():
target = next_model(next_state)
"""以下是主要的DoubleDQN和DQN的区别"""
#取所有动作中分数最大的
#[b,11]-[b]
#target = target.max(dim=1)[0]
#使用model计算下一个状态的分数
#[b,3]->[b,11]
with torch.no_grad():
model_target = model(next_state)
#取分数最高的下标
#[b,11]->[b,1]
model_target = model_target.max(dim=1)[1]
model_target = model_target.reshape(-1,1)
#以这个下标取next_value当中的值
#[b,11]->[b]
target = target.gather(dim=1,index=model_target)
"""以上是区别"""
#下一个状态的分数乘以一个系数,相当于权重
target*=0.98
#如果next_state已经游戏结束,则next_state分数是0
#因为如果下一步已经游戏结束,显然不需要再继续下去,也就不需要考虑next_state了
#[b,1]*[b,1]->[b,1]
target *=(1-over)
#加上reward就是最终的分数
#[b,1]+[b,1]->[b,1]
target+=reward
return target
DuelingDQN改进
class Vent(torch.nn.Model):
def __init__(self):
super().__init__()
self.fc = torch.nn.Sequential(
torch.nn.Linear(3,128),
torch.nn.ReLU(),
)
self.fc_A = torch.nn.Linear(128,11)
self.fc_V = torch.nn.Linear(128,1)
def forward(self,x):
#[5,11]->[5,128]->[5,11]
A = self.fc_A(self.fc(x))
#[5,11]->[5,128]->[5,1]
V = self.fc_V(self.fc(x))
#[5,11]->[5]->[5,1]
A_mean = A.mean(dim=1).reshape(-1,1)
#[5,11]->[5,128]->[5,11]
A -=A_mean
#Q值由V值计算得到
#[5,11]+[5,1]=[5,11]
Q = A+V
return Q
-
初始化 (
__init__
方法):- 调用基类的初始化方法。
- 定义一个顺序模型
self.fc
,包含一个线性层和ReLU激活函数,用于特征提取。
-
动作价值层 (
self.fc_A
):- 一个线性层,将输入特征从128维映射到11维,对应每个动作的价值。
-
状态价值层 (
self.fc_V
):- 一个线性层,将输入特征从128维映射到1维,对应当前状态的全局价值。
-
前向传播 (
forward
方法):- 输入
x
通过self.fc
进行特征提取,然后分别通过self.fc_A
和self.fc_V
得到动作价值和状态价值。
- 输入
-
动作价值的均值 (
A_mean
):- 计算
self.fc_A
输出的均值,并重塑以匹配原始形状。
- 计算
-
中心化动作价值:
- 从原始动作价值中减去均值,进行中心化处理。
-
计算Q值:
- 将中心化后的动作价值与状态价值相加,得到最终的Q值。