DQN,顾名思义,Deep Q Learning ;在传统强化学习Q-Learning的基础之上,用深度学习的神经网络来拟合函Q值函数,从而达到更好的学习效果。
关于强化学习的基础知识,可以先看这篇文献:强化学习入门:基本思想和经典算法 - 知乎 (zhihu.com)
强化学习的主要应用范围包括:游戏,交通拥堵,能源分配,广告推送,机器人控制,组合与序列控制问题。目前我自己将要研究的是微电网电力资源分配问题,也是强化学习的一个小应用方向。关于策略,状态转移,奖励/回报,价值函数(动作价值和状态价值函数),折扣系数,学习率,目标函数(Target-value network),这些都默认大家知道了,勤动手,百度一下。
强化学习根据算法的应用环境,分为基于模型的算法和无模型算法。
Model-Free 算法,是指算法中智能体不需要学习和理解环境的信息,环境给什么就用什么,信息的结果告诉它把地球炸了,他就会想着把地球炸掉。
Model-Based算法,是指去学习和理解环境。学会用一个模型来模拟环境,通过模拟的环境来得到反馈。后者比前者多了一个模拟环境的这个环节,通过模拟环境预判接下来会发生的情况,然后选择最优动作进行执行。
一般生活中的问题,都很难用一个准确的模型进行描述,因此,主要研究“无模型算法”。
图1.1 强化学习算法分类(按照有无模型分类)
该算法是DQN算法的前身,Q-learning算法是1989年Watkins提出来的,2015年nature论文所提出的DQN就是在Q-learning的基础上修改得到的。嗯,有兴趣可以深入了解它。
论文名称:《Human-level control through deep reinforcement learning》
DQN对Q-learning的修改主要体现在以下三个方面:
(1)DQN利用深度卷积神经网络逼近值函数(作为一种逼近手段引入进去)。
(2)DQN利用了经验回放对强化学习的学习过程进行训练(经验池)。
(3)DQN独立设置了目标网络(Target)来单独处理时间差分算法中的TD偏差。它主要解决算法训练不稳定的问题。复制一个和原来Q网络结构一样的Target Q网络,用于计算Q目标值。
那接下来,就针对性的介绍这三个方面
图1.3 利用卷积神经网络逼近(Q函数)值函数
这里值函数利用神经网络进行逼近,属于非线性逼近。(中间涉及到激活函数和归一化正则化等操作,不是简单的线性逼近)。这里的值函数对应着一组参数,在神经网络里参数是每层网络的权重,我们用 seta 表示(它里边是一组数据,而非一个数据)。用公式表示的话,值函数为:Q(s,a ; seta),注意,我们这时候对值函数进行更新时,其实更新的是参数,当网络结构确定时,seta 就代表值函数。DQN所用的网络结构是3个卷积层+两个全连接层。也就是说,我们在不断的训练求解一个Q值,实际上是在更新这个网络中的各层权重参数。用卷积神经网络来拟合任意的值函数,是区别与传统Q-learning的一个重要突破(传统Q-Learning是使用的Q表,S和A稍微一多起来,计算量会呈几何倍增长,效率太低了)
利用神经网络逼近值函数的做法在强化学习领域早就存在了,可以追溯到上个世界90年代。那时,学者们发现利用神经网络,尤其是深度神经网络去逼近值函数这件事不太靠谱,因为常常出现不稳定不收敛的情况,所以这个方向一直没有突破,直到DeepMind出现。
是什么原因,才使得该方法稳定且收敛下来了呢?----------他们将认识神经科学的成果应用到了深度神经网络的训练之中!(划重点,知识迁移带来的创新)
人在睡觉的时候,海马体会把一天的记忆重放给大脑皮层。利用这个启发机制,DeepMind团队的研究人员构造了一种神经网络的训练方法:经验回放。
图1.4 DQN算法的粗略工作模型
通过经验回放为什么可以令神经网络的训练收敛且稳定?
原因是:对神经网络进行训练时,它收敛存在的假设是独立同分布。而通过强化学习采集到的经验之间存在着关联性,利用这些经验进行顺序训练,神经网络当然不稳定。经验回放可以打破经验数据间的关联。将采集的经验(原本是具有时间顺序关联的),存储到经验池中,随机抽取一批样本,进行训练,同时,随着采集经验数据的更新,也对池子中的样本库进行更新。这样子使用一个经验池存储多条经验s,a,r,s’,再从中随机抽取一批数据送去训练。解决样本关联性和利用效率的问题。
通过固定Q目标可以解决算法更新不平稳的问题,神经网络一般学习的是固定的目标值,而 Q-learning中Q同样为学习的变化量,变动太大不利于学习。所以DQN算法,使Q在一段时间内保持不变,使神经网络更易于学习,隔一段时间再去拷贝这个参数即可。
(1)这里也拿监督学习做对比,它输入x后输出预测的值y,目的是让预测值逼近这个真实值,这个真实值在监督学习中其实是稳定的:
(2)而DQN输出的预测值也是要逼近Q_target的:
(3)但是这里的Q实际上也要过一遍网络,导致这个Q也是不断变化的,这就好像在练习射箭时,拿一只会动的兔子当靶子:
图1.5 DQN中的Q网络和Q-T网络
固定Q目标做的就是让这个Q在一段时间内保持不变,隔一段时间再去拷贝这个参数即可。
图1.6 DQN的伪代码程序
先来逐行解读一下伪代码:
第1行:算法名称----带有经验回放的DQN算法。
第2行:初始化经验池以及容量N。
第3行:以一个随机权重,初始化动作-价值函数 Q。
第4行:开始一个一级循环(就是一个大循环),循环的条件是回合数,满足回合方可跳出。
第5行:初始化回合的第一个状态s1 ,预处理得到状态对应的特征输入。
第6行:从该回合的第一步出发,进行循环T步才结束。
第7行:用随机概率epsilon选择一个动作at(在代码中这里是比较变量和epsilon大小)。
第8行:否则,选择上一次的最大Q值对应的at作为本次动作。(注意,这里选最大动作时用到的值函数网络与逼近值函数所用的网络是一个网络,在代码中就是用的同一个类来实例化两个网络)上边的这两行,其实就是执行贪婪策略。
第9行:在仿真环境中执行动作at,并获取奖励rt和下一个状态xt+1(下一帧图像信息xt+1)。
第10行:更新环境状态,和权重参数等。
第11行:在经验池中存储刚才的一组数据。
第12行:从经验池中取样一个batch(这个批量的大小是可以自己设置的)
第13行:进行判断,如果还没到达最终条件,根据迭代公式计算Q值;如果到达,以最后一次奖励作为本次的Q-T网络目标值。
第14行:针对Q-Target(目标函数)和Q值函数之差的Loss,运用梯度下降策略去求解最优。一般是每隔C步更新一次TD目标网络权值。
第15行:完成一个回合之后跳出循环。
第16行:完成所有回合之后跳出循环。
图1.7 DQN算法代码实现(按照代码板块划分)
结合上边的伪代码流程,大概解释一下这个图的编程顺序和运行流程。先建立一个经验池,方便注入数据和取样数据。建立一个类,以这个类初始化Q和Q-T网络,定期更换Q-T的权重参数。再编写loss函数,取样函数等,别忘了还要初始化env(仿真环境)。
运行的流程就是:Q网络根据随机初始化的权重,进行动作价值预测,选择一个价值最大的进行执行,获得环境的反馈之后,将环境反馈的经验放到经验池中。(随着池子容量越多,经验回放的优势逐渐凸显),随机抽取经验样本,放入DQN网络(Q预测和Q-T网络)中进行计算,由于经验回放的样本是随机抽取的,因此,DQN计算的结果更加有效。由于这两个网络的参数权重没有实时同步,因此同样的经验样本喂入,会存在差值,将计算的结果和Q预测的结果做差,得到Loss函数。只要不断的用梯度下降策略,就能减小Loss函数,带来的是权重参数的改变,将这个更新后的参数放入Q网络,就可以实现Q的优化。
纸上得来终觉浅,还是需要多看视频去多思考。
#先来从头看一下DQN算法的伪代码
#1.初始化一下经验池
#2.初始化动作价值Q函数的随机权重
#3.用Q*初始化目标动作价值函数,2和3中的价值函数的差值,就是loss值,衡量算法是否优秀的标准
#4.接下来是两个大的循环,第一个循环是指需要进行多少局(回合),第二个循环是指每个循环需要经历多少步
#{第一个循环:
# 初始化状态序列
# {
# 第二个循环内容
# 1.1以随机概率epsilon选择随机a动作
# 1.2否则按照最佳Q值对应的a执行操作
# 2.1执行动作与环境进行交互,得到奖励和获得下一步状态
# 2.2进行一个状态转移,然后把学习到的经验放到经验池中
# 2.3不断从经验池中去取出经验进行运算,帮助训练,指导下一步动作
# 2.4.1游戏结束的话,就得到一个奖励
# 2.4.2游戏没有结束的话,按照TDerror 的公式 去更新Q值
# 2.5通过梯度下降,更新算法的大脑参数
# 2.6过了C步之后,将Qnetwork和Qtargetwork做一个同步。
# }
# }
###编程代码:先大致构建出伪代码,模块化编程,逐步细化###
本程序一共只有两个文件,一个是main文件,另一个是agent文件(都是.py文件)
import random
import gym
#导入游戏库
import numpy as np
import torch
import torch.nn as nn
from agent import Agent
env = gym.make("CartPole-v1")
#创建一个游戏环境
s=env.reset()#初始化游戏,返回一组观测值,s是一个向量
n_state=len(s)#这里是输入的个数
n_action=env.action_space#这里是输出的动作对应状态
EPSILON_DECAY=10000#假设可以衰减10000次
EPSILON_START=1.0#探索率从1开始衰减
EPSILON_END=0.02#探索率的最低值
n_episode=5000
#假设玩5000句
n_time_step=1000
#假设每局1000步
agent=Agent(n_input=n_state,n_output=n_action)
#实例化一个对象
TARGET_UPDATE_FREQUENCY=10
#q_target和q网络的同步频率为10,即10局更新一次
REWARD_BUFFER=np.empty(shape=n_episode)
for episode_i in range(n_episode):
for step_i in range(n_time_step):
episode_reward = 0#奖励值从0开始
epsilon = np.interp(episode_i * n_time_step+step_i,[0,EPSILON_DECAY],[EPSILON_START,EPSILON_END])#现在开始初始化探索率,探索率随着时间递减,最后保持不变
random_sample=random.random()#表示从0到1之间随机取一个数
if random_sample<=epsilon:
a=env.action_space.sample()
else:
a=agent.online_net.action(s)#TODO
s_,r,done,info = env.step(a)
agent.memeo.add_memo(s,a,r,done,s_)#TODO
s=s_
episode_reward+=r#每一个回合有一个累计奖励的过程
if done:#判断本局是否结束
s=env.reset()#如果结束,那就重置环境
REWARD_BUFFER[episode_i]=episode_reward #把这一句累加的回合奖励记录下来
break
batch_s,batch_a,batch_r,batch_done,bacth_s_=agent.memo.sample()#通过这样一个采样函数,将结果赋值给batch_S
#计算targets
target_q_values=agent.target_net(batch_s)#TODO
max_target_q_values=target_q_values.max(dim=1,keepdim=True)[0]#得到最大的target数值
targets=batch_r+agent.GAMMA *(1-batch_done)*max_target_q_values#这一步就是计算公式#TODO
#计算q_values
q_values=agent.online_net(batch_s)#给Q网络输入状态S,然后#TODO
a_q_values=torch.gather(input=q_values,dim=1,index=batch_a)#思考一下为什么是选择第二列,还有序号为batch_a?
#这一步做的是将所有的q_value给搜集起来,按顺序把各batch与对应各自的最大值q匹配起来放到一起。
#计算loss函数
loss=nn.functional.smooth_l1_loss(targets,a_q_values)#这下就求出了loss
#计算梯度下降,来更新神经网络中的各个参数
agent.optimizer.zero_grad()#TODO
loss.backward()
agent.optimizer.step()#通过这几步完成一次梯度下降#TODO
if episode_i % TARGET_UPDATE_FREQUENCY==0:
agent.target_net.load_state_dict (agent.online_net.state_dict())#这一步将online_net网络中的参数全部同步到target_net网络之中
#观测每一个回合的奖励,看看训练的咋样了
print("Episode:{}".format(episode_i))
print("Avg.Reward:{}".format(np.mean(REWARD_BUFFER[:episode_i])))
import random
import numpy as np
import torch
import torch.nn as nn #DQN中需要用到,torch中神经网络这个模块。
class Replaymemory():
def __init__(self,n_s,n_a):
self.n_s=n_s
self.n_a=n_a
self.MEMORY_SIZE=1000#经验池的大小
self.BATCH_SIZE=64#每次批量的大小
self.all_s=np.empty(shape=(self.MEMORY_SIZE,self.n_s),dtype=np.float32)
#开辟一个空白空间,形状是2维的,与MEMORY_SIZE和n_s共同相关,n_s不一定清楚,且数据类型是32位的。
self.all_a=np.random.randint(low=0,high=n_a,size=self.MEMORY_SIZE,dtype=np.uint8)
# 这个游戏中,动作只有左和右两个状态,因此可以使用0和1代替,这里使用n_a变量,在赋值时令为2就可以。
self.all_r=np.empty(self.MEMORY_SIZE,dtype=np.float32)
#只是一维的存储空间,因为存储的只是每一局导出的数据。
self.all_done = np.random.randint(low=0, high=2, size=self.MEMORY_SIZE, dtype=np.uint8)
self.all_s_=np.empty(shape=(self.MEMORY_SIZE,self.n_s),dtype=np.float32)
self.t_memo=0
self.t_max=0#在这里做一个变量的标记
#这一下子,上边就准备好了存储空间。后续直接进行调用就可以。#
def add_memo(self,s,a,r,done,s_):
#在这里对t_max进行一个判别
self.all_s[self.t_memo]=s
self.all_a[self.t_memo]=a
self.aal_r[self.t_memo]=r
self.all_done[self.t_memo]=done
self.all_s_[self.t_memo]=s_
self.t_max = max(self.t_memo, self.t_memo + 1)
self.t_memo=(self.t_memo+1)%self.MEMORY_SIZE#检查是否超过1000,如果是第1001,那么取余之后,会放在第一个位置
def sample(self):#经验池中,大于64个经验的话,随机取64个,小于64个的话,有几个取几个。
if self.t_max>self.BATCH_SIZE:
idxes = random.sample(range(0,self.t_max), self.BATCH_SIZE) # 从经验池空间的序号中,随机选择64个序号
else:
idxes=range(0,self.t_max)
batch_s=[]
batch_a=[]
batch_r=[]
batch_done=[]
batch_s_=[]
for idx in idxes:#遍历前面的随机取出的64个数据,装载到batch_s这边的5个类型数据中
batch_s.append(self.all_s[idx])
batch_a.append(self.all_a[idx])
batch_r.append(self.all_r[idx])
batch_done.append(self.all_done[idx])
batch_s_.append(self.all_s_[idx])
#torch识别不了numpy,所以需要把numpy转化成torch。
batch_s_tensor=torch.as_tensor(np.asarray(batch_s),dtype=torch.float32)
batch_a_tensor = torch.as_tensor(np.asarray(batch_a), dtype=torch.int64).unsqueeze(-1)#最后一句是升维
batch_r_tensor = torch.as_tensor(np.asarray(batch_r), dtype=torch.float32).unsqueeze(-1)
batch_done_tensor = torch.as_tensor(np.asarray(batch_done), dtype=torch.float32).unsqueeze(-1)
batch_s__tensor = torch.as_tensor(np.asarray(batch_s_), dtype=torch.float32)
return batch_s_tensor,batch_a_tensor,batch_r_tensor,batch_done_tensor,batch_s__tensor
class DQN(nn.Module):
def __init__(self,n_input,n_output):
super().init()
self.net=nn.Sequential(
nn.Linear(in_features=n_input,out_features=88)
nn.Tanh()
nn.Linear(in_features=88,out_features=n_output)
)
def forward(self,x):#前向传播函数
return self.net(x)
def act(self,obs):#得到输出的动作
obs_tensor=torch.as_tensor(obs,dtype=torch.float32)#先将数据转化成torch格式
q_value = self(obs_tensor.unsqueeze(0))#把他转化成行向量
max_q_idx=torch.argmax(input=q_value)#然后求他最大的q_value对应的序号
action = max_q_idx.detach().item()#找到这个序号对应的action
return action#把这个序号对应的操作返回给主程序,执行对应操作。
class Agent:
def __init__(self,n_input,n_output):#在这里定义一下需要的输入和输出数量
self.n_input=n_input
self.n_output=n_output
self.GAMA=0.99#这里衰减因子设置为0.99
self.learning_rate=1e-3#这里设置的学习率
self.memo=Replaymemory(self.n_input,self.n_output)#TODO,这里是要补充经验池
self.online_net= DQN(self.n_input,self.n_output) #TODO 这里是在线训练网络,这两步操作都叫做实例化
self.target_net= DQN(self.n_input,self.n_output) #TODO 这里是目标训练网络
self.optimizer= torch.optim.Adam(self.online_net.parameters(),lr=self.learning_rate)#TODO 这里是优化器,优化这个网络参数
这样子差不多就结束了,代码可以跑起来,不过要注意版本兼容的问题,最开始版本太新了,导致一直报错。觉得还是需要多实践,多看看代码。
最后,在评论区奉上一个完整的DQN详细标注的完整代码文件,pycharm打开之后可以直接运行。