BUAA 强化学习DQN代码及实验报告参考

DQN实验报告

一、DQN实现方式

助教给的参考代码由两个文件组成,一个是game.py,一个是train.py。game.py的内容是迷宫界面绘制和agent行走方式、奖励规则的有关代码,而train.py中是神经网络、训练DQN神经网络以及寻找最路径的代码。

我在完成作业时,由于不需要单独进行界面绘制,所以就将助教game.py中的内容和train.py中的内容写在了一起。但是总体上的内容还是不变的。大致可以分为以下几个部分,Env类、trans_torch函数、Net类、DQN类以及训练神经网络的相关代码和寻找最短路径的相关代码。

1Env

Env类分为两个部分。

一个部分为start_env方法,主要负责模拟训练过程中agent状态的初始化。另一个部分为step函数,负责对action作出响应,改变agent的状态以及返回游戏状态和当步动作的奖励值。

Trans_torch负责把当前agent的状态转化为可卷积的形式以便于卷积神经网络提取特征。

2Net

Net类则是定义了网络结构和前向传播方式。

在Net类的初始化方法中定义了网络的结构并对全连接层的权重进行了初始化。本实验中用到的网络结构由输入为三通道的有25个大小为5x5的filter的卷积神经网络,输入为7225输出为16的全连接层以及输入为16输出为4的全连接层依次相连构成。

Net类的forward方法中则固定了该网络前向计算的过程,最后返回网络前向计算的结果。

3DQN

DQN类由四个方法构成。

首先是初始化方法。初始化方法中定义了两个网络,一个是eval_net,一个是target_net。Target_net在一段时间内是保持不变的,eval_net在更新100次之后会把自己的全部参数值复制给target_net。除此之外,初始化方法中还定义了优化器和损失函数,需要注意的是这里优化器已经定义为了对eval_net的优化器。

DQN中的choose_action方法采用epsilon_greedy策略,以epsilon的概率选择贪心策略,以1-epsilon的概率选择随机策略。

Store_transitions方法定义了将新的采样数据加入记忆库的方式。

Learn方法完整描述了网络的训练过程。具体操作是,每次进行学习时,从记忆库中随机抽取32个数据。将先之间通过eval_net和动作预测当前状态下的动作价值函数得到q_eval,再通过用tatget_net和下一状态预测下一个状态的动作价值函数,然后选取最大的那个与当步收益一起近似最优收益,得到q_target。(说的太复杂了,重来。)简而言之这个就是Q_learning把q_tabel变成net之后的样子,选用两个网络可以很好的模拟进行Q_learning时,在遍历完一遍所有状态之前,前面的选择并不会改变后面状态的价值函数的这一特性。这里的网络训练目标就是最小化q_eval和q_target。(实际上就是让q_tabel尽可能收敛)具体操作就是计算两者的loss,然后用optimizer进行梯度计算,然后进行反向传递,然后更新网络中各参数的值。

4、训练网络的有关代码

训练网络的有关代码主要决定了采样的次数和学习开始的时机,我在观察了实验结果后将迭代的幕数调到了603次,并且当幕数大于400时直接采用贪心算法,以便让结果更快收敛。此处开始学习的标准是记忆库满,即样本数大于2000时开始学习。一旦开始学习后,每一次采样eval_net的参数都会更新,但是当样本数目小于2000时,eval_net是不会学习的,这一点非常关键。如果忽略这一点会造成很多问题,具体问题见第二版块。

5、寻找最短的有关代码

最后寻找最短路径就是按照eval_net给出的方案走,然后结合当前状态和动作输出下一时刻的状态即可,唯一需要注意的就是把二维坐标转换成一维的。

二、遇到的问题和解决方案

这次作业遇到的问题主要有三个,但是除了这三个之外还有一个是MC的作业中遇到的,也是导致我MC作业跑不出来结果的原因,我也觉得非常重要,所以一起列在这里。

1agent原地打转

这个是mc作业中遇到的问题,也是在听了同学的建议后获得了启发。在每一幕开始时设置一个空列表tmp,当agent第一次走到一个位置时,就把这个位置加到列表中。当agent再次走到这个位置时,将得到惩罚。为达到这一目的,tmp列表需要作为参数传到返回奖励值的函数中。

2agent走不到终点

一开始我盲目照抄参考代码但是发现出现了一个非常奇怪的问题就是agent明明到终点了但是不停下来,反而又走到了其他的地方。在一个一个打印状态后我终于发现了问题所在。

参考代码中是先修改agent的状态(agent每到达一个方块,该方块的值就会变成1,而之前agent所在方块的值则变为0),再判断是否到达终点。因为参考代码中判断终点是用方块所在的位置判断的而不是用方块的值判断的,所以在运行时是正常的。但是在我的世界的情景下,由于是空气块的地方太多,只能用方块的值判断是否是陷阱,所以在判断终点的时候我也理所当然用方块的值进行了判断。但是由于我在到达终点时先修改了agent所在的状态,把agent所在的地方的值改成了1,导致终点的值变成了1而不是初始时的2,进一步导致下一步判定是否为终点时出错,无法走到终点。

3、卷积核的大小

卷积核太大会加大学习的困难程度,导致学习不到有用的特征,并且会减慢程序的运行速度。

4、运行了1000多幕agent还在重复犯错

在运行了十几次后发现这种情况经常出现后我对神经网络的学习能力产生了深刻的怀疑。后来才发现不是神经网络的问题,而是我自己的问题。导致这个问题的原因也是盲目照抄参考代码而忽略了走迷宫和Minecraft中找最短路径的地图特征方面的差异。

走迷宫问题的障碍物极少,所以参考代码中遇到陷阱就结束这一幕开始下一幕的设定是合理的,但是MineCraft里面空气格的数量远远多于非空气格的数量,即陷阱数量远远多于非陷阱的格子的数量。如果照搬参考代码里的设定,让agent可以走到陷阱格里,并且走到陷阱格就结束这一幕开始下一幕的话,很有可能出现一千多幕都是第一步就掉到陷阱里,只采集了一个状态样本就结束了一幕。

由于eval_net必须在样本量到达了两千后才会开始学习,所以样本总量到达两千之前agent就是在随便走。因为MineCraft中agent很容易走到陷阱里,所以前一千多幕都是走一步就掉进陷阱里的现象非常有可能发生。这样不仅会使幕数增多,还会带来一个问题就是数据雷同,因为agent在经历一千多幕开始学习之后很有可能学到的只是前一两个状态怎么走,整个学习过程就会变得及其缓慢。

改进方式:让agent不要往陷阱里走,杜绝一开始就调入陷阱的情况发生。同时,为了避免初始时agent因为是随机走会一直在原地打转造成死循坏,人为设定agent在走了20步之后如果还没有到达终点就结束该幕开始下一幕。

三、实验总结

不要照搬参考代码,在不同情景下需要特别注意如何设置每一幕的终止条件以便让神经网络能够在更短时间内学习到更多内容。找不到问题时把状态和动作都打印出来,模拟agent的行走路径走一走,有时可以带来启发。还有在使用到神经网络时,要注意神经网络开始学习的触发条件;另外,可以注意观察agent在每一幕开始时选择的动作的特点(当陷阱格很多的时候),这个可以反应出神经网络的学习成果。

核心代码如下

import random
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

grid = list()
for i in range(441):
    grid.append("air")

grid[217] = "emerald_block"

grid[218] = "diamond_block"
grid[219] = "diamond_block"
grid[220] = "diamond_block"
#grid[221] = "diamond_block"
grid[238] = "diamond_block"
#grid[241] = "diamond_block"
grid[259] = "diamond_block"
grid[263] = "diamond_block"
grid[280] = "diamond_block"
grid[282] = "diamond_block"
#grid[284] = "diamond_block"
grid[301] = "diamond_block"
grid[302] = "diamond_block"

grid[303] = "redstone_block"

#参数
BATCH_SIZE = 32
LR = 0.01                   # 学习率
EPSILON = 0.9               # 最优选择动作百分比(有0.9的几率是最大选择,还有0.1是随机选择,增加网络能学到的Q值)
GAMMA = 0.9                 # 奖励递减参数(衰减作用,如果没有奖励值r=0,则衰减Q值)
TARGET_REPLACE_ITER = 100   # Q 现实网络的更新频率100次循环更新一次
MEMORY_CAPACITY = 2000      # 记忆库大小
N_ACTIONS = 4  # 棋子的动作0,1,2,3
N_STATES = 1

class Env():
    def start_env(self, grid):
        migoing = np.zeros([21, 21], dtype=np.int32)
        for i in range(21):
            for j in range(21):
                if grid[i*21 + j] == "air":
                    migoing[i][j] = 3
                elif grid[i*21 + j] == "diamond_block":
                    migoing[i][j] = 0
                elif grid[i*21 + j] == "emerald_block":
                    migoing[i][j] = 1
                    x1 = i
                    y1 = j
                elif grid[i*21 + j] == "redstone_block":
                    migoing[i][j] = 2
        self.x1 = x1
        self.y1 = y1
        self.migoing = migoing
        self.end_game = 0
        return self.migoing

    def step(self, action, tmp):    # tmp为列表,元素为元组,记录已经走过的位置
        r = 10
        self.end_game = 0
        if action[0] == 0: # 向上走
            if self.x1 == 0:
                r = -20
            else:
                self.x1 -= 1
                #print((self.x1,self.y1))
                #print(self.migoing[self.x1][self.y1])

                if self.migoing[self.x1][self.y1] == 3:   # 走到空气中
                    self.x1 += 1
                    self.end_game = 1
                    r = -40
                elif self.migoing[self.x1][self.y1] == 2: #走到终点,游戏成功
                    self.end_game = 2
                    r = 40

                if self.end_game == 0:
                    self.migoing[self.x1][self.y1] = 1
                    self.migoing[self.x1+1][self.y1] = 0

        
        if action[0] == 1: # 向下走
            if self.x1 == 20:
                r = -20
            else:
                self.x1 += 1
                #print((self.x1,self.y1))
                #print(self.migoing[self.x1][self.y1])

                if self.migoing[self.x1][self.y1] == 3:   # 走到空气中
                    self.end_game = 1
                    self.x1 -= 1
                    r = -40
                elif self.migoing[self.x1][self.y1] == 2: #走到终点,游戏成功
                    self.end_game = 2
                    r = 40

                if self.end_game == 0:
                    self.migoing[self.x1][self.y1] = 1
                    self.migoing[self.x1-1][self.y1] = 0

        if action[0] == 2: # 向左走
            if self.y1 == 0:
                r = -20
            else:
                self.y1 -= 1

                #print((self.x1,self.y1))
                #print(self.migoing[self.x1][self.y1])

                if self.migoing[self.x1][self.y1] == 3:   # 走到空气中
                    self.end_game = 1
                    self.y1 += 1
                    r = -40
                elif self.migoing[self.x1][self.y1] == 2: #走到终点,游戏成功
                    self.end_game = 2
                    r = 40

                if self.end_game == 0:
                    self.migoing[self.x1][self.y1] = 1
                    self.migoing[self.x1][self.y1+1] = 0


        
        if action[0] == 3: # 向右走
            if self.y1 == 20:
                r = -20
            else:
                self.y1 += 1

                #print((self.x1,self.y1))
                #print(self.migoing[self.x1][self.y1])

                if self.migoing[self.x1][self.y1] == 3:   # 走到空气中
                    self.end_game = 1
                    self.y1 -= 1
                    r = -40
                elif self.migoing[self.x1][self.y1] == 2: #走到终点,游戏成功
                    self.end_game = 2
                    r = 40

                if self.end_game == 0:
                    self.migoing[self.x1][self.y1] = 1
                    self.migoing[self.x1][self.y1-1] = 0

        if (self.x1, self.y1) in tmp:
            r = -20
        else:
            tmp.append((self.x1, self.y1))

        return self.end_game, r, self.migoing, tmp

def trans_torch(list1):
    list1=np.array(list1)
    l1=np.where(list1==1,1,0) # 标识agent所在位置
    l2=np.where(list1==2,1,0)   # 标识障碍物所在位置
    l3=np.where(list1==3,1,0)   # 标识终点位置 
    b=np.array([l1,l2,l3])      # 拼接得到三维目标矩阵,第一个矩阵为agent所在位置,第二个矩阵为障碍物所在位置,第三个矩阵为终点所在位置
    return b

#神经网络
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 输入为441,输出为4       
        # torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
        self.c1=nn.Conv2d(3,25,5,1,0) # 对应上方的前5个参数  25个filter每个filter的卷积结果为1,最后输出为25*1*1,相当于把二维拉平
        self.f1=nn.Linear(7225,16) # 全连接层 输入441,输出16
        self.f1.weight.data.normal_(0, 0.1) # 初始化权重
        self.f2=nn.Linear(16,4) # 隐藏层
        self.f2.weight.data.normal_(0, 0.1) # 初始化权重
    def forward(self, x):
        # Input: (N, C_{in}, H_{in}, W_{in})
        x=self.c1(x)# size: N x 441 x 1 x 1 对应 N C H W
        x=F.relu(x)
        x = x.view(x.size(0),-1) # size: N x 441
        x=self.f1(x)
        x=F.relu(x)   
        action=self.f2(x) # size: N x 4
        return action
    # 以上为前向传播网络,不涉及到学习权重的部分,相当于输入x(三维矩阵 3*21*21),输出该状态下执行四个动作的可能性

class DQN(object):
    def __init__(self):
        self.eval_net, self.target_net = Net(), Net() #DQN需要使用两个神经网络
        #eval为Q估计神经网络 target为Q现实神经网络
        self.learn_step_counter = 0 # 用于 target 更新计时,100次更新一次
        self.memory_counter = 0 # 记忆库记数
        self.memory = list(np.zeros((MEMORY_CAPACITY, 4))) # 初始化记忆库用numpy生成一个(2000,4)大小的全0矩阵,
        self.optimizer = torch.optim.Adam(self.eval_net.parameters(), lr=LR) # torch 的优化器 ,此处已经指定了优化的是eval_net
        self.loss_func = nn.MSELoss()   # 误差公式

    def choose_action(self, x):# size: 3x5x5 (batchsize = 1, channels/depth = 3, height = 21, width = 21)选动作是传入一个样本,即当前状态
        x = torch.unsqueeze(torch.FloatTensor(x), 0) # size: 1x3x21x21 (.FloatTensor将x转化为tensor类型; .unsqueeze在0位置扩展一个维度)
        # 这里只输入一个 sample,x为场景
        if np.random.uniform() < EPSILON:   # 选最优动作
            actions_value = self.eval_net.forward(x) #将场景输入Q估计神经网络
            #torch.max(input,dim)返回dim最大值并且在第二个位置返回位置比如(tensor([0.6507]), tensor([2]))
            action = torch.max(actions_value, 1)[1].data.numpy() # 返回动作最大值
        else:   # 选随机动作
            action = np.array([np.random.randint(0, N_ACTIONS)]) # 比如np.random.randint(0,2)是选择1或0
        return action

    def store_transition(self, s, a, r, s_):
        # 如果记忆库满了, 就覆盖老数据,2000次覆盖一次
        index = self.memory_counter % MEMORY_CAPACITY
        self.memory[index] = [s,a,r,s_]
        self.memory_counter += 1

    def learn(self):
        # target net 参数更新,每100次
        if self.learn_step_counter % TARGET_REPLACE_ITER == 0:
            # 将所有的eval_net里面的参数复制到target_net里面
            self.target_net.load_state_dict(self.eval_net.state_dict())
        self.learn_step_counter += 1
        # target_net是固定的
        # 抽取记忆库中的批数据
        # 从2000以内选择32个数据标签
        sample_index = np.random.choice(MEMORY_CAPACITY, BATCH_SIZE)
        b_s=[]
        b_a=[]
        b_r=[]
        b_s_=[]
        for i in sample_index:
            b_s.append(self.memory[i][0])
            b_a.append(np.array(self.memory[i][1],dtype=np.int32))
            b_r.append(np.array([self.memory[i][2]],dtype=np.int32))
            b_s_.append(self.memory[i][3])

        b_s = np.array(b_s)#取出s, b_a的每一个元素都是一个1x1的数组,所以b_a的size是32*1
        b_a = np.array(b_a) #取出a
        b_r = np.array(b_r) #取出r
        b_s_ = np.array(b_s_) #取出s_    

        #print(b_r.shape)
        #print(b_a.shape)

        b_s = torch.FloatTensor(b_s)#取出s
        b_a = torch.LongTensor(b_a) #取出a
        b_r = torch.FloatTensor(b_r) #取出r
        b_s_ = torch.FloatTensor(b_s_) #取出s_        # s是32 * 3 * 5 * 5的张量
        # 针对做过的动作b_a, 来选 q_eval 的值, (q_eval 原本有所有动作的值)
        q_eval = self.eval_net(b_s).gather(1, b_a)  # shape (batch, 1) 找到action的Q估计(关于gather使用下面有介绍)
        #a.gather(0, b) a-被提取元素的矩阵, 提取元素的维度为0, 提取元素在该维度的索引为b
        # eval_net用于计算当前状态的期望收益,target_net用于计算下一步的期望收益
        # 实际上与离散时用的Q_table是一样的,因为计算下一步的最大期望收益时,所用的数据并没有因为当前状态对应的q_table的更新而受到影响
        q_next = self.target_net(b_s_).detach()     # q_next 不进行反向传递误差, 所以 detach Q现实
        q_target = b_r + GAMMA * q_next.max(1)[0].view(BATCH_SIZE, 1)   # shape (batch, 1) DQL核心公式

        #print(b_r)
        #print(q_next.max(1)[0])
        #print(q_target.shape)

        # print(1)
        loss = self.loss_func(q_eval, q_target) #计算误差, 因为此处更新的是eval_net,所以q_target是作为标准出现
        # 计算, 更新 eval net
        self.optimizer.zero_grad() #
        loss.backward() #反向传递
        self.optimizer.step()
        return loss


###
# 训练网络
dqn = DQN() # 定义 DQN 系统
#400步
study=1
env=Env()

for i_episode in range(503):
    print(i_episode,'epoch')
    s = env.start_env(grid) # size: 
    s=trans_torch(s) # size: 3x21x21
    loss=0
    tmp = []
    if i_episode > 300:
        EPSILON = 1

    cnt = 0
    while True:
        cnt += 1
        if cnt > 20:
            break
        # env.display()   # 显示实验动画
        a = dqn.choose_action(s) #选择动作
        # 选动作, 得到环境反馈

        print((env.x1, env.y1, a))
        done,r,s_,tmp = env.step(a, tmp) 
        #print(done)
        s_=trans_torch(s_) # size: 3x21x21
        # 存记忆
        dqn.store_transition(s, a, r, s_)        
        if dqn.memory_counter > MEMORY_CAPACITY:
            loss=dqn.learn() # 记忆库满了就进行学习
            #print(loss)
        if done==2:    # 如果回合结束, 进入下回合
            # print(loss)              
            break
        s = s_
        #print(tmp)
    if done == 2:
        print("成功")
    else:
        print("失败")
    # 根据训练好的网络得到最短路径
path = []
path.append(217)
s = env.start_env(grid)
s = trans_torch(s)
EPSILON = 1
while True:
    tmp = []
    a = dqn.choose_action(s)
    done, r, s_, tmp = env.step(a, tmp)
    path.append((env.x1)*21 + (env.y1))
    s_= trans_torch(s_)
    if done==1 or done==2:    # 如果回合结束, 进入下回合
                # print(loss)
        if done==1:
            print('失败')
        if done==2:
            print('成功')                
        break
    s = s_

print(path)

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值