[Deep Q Learning] pytorch 从零开始建立一个简单的DQN--走迷宫游戏

在这里插入图片描述
关于DQN的原理我不在这里说明了,如果对DQN的原理不了解可以去看我这个帖子
https://blog.csdn.net/mahuatengmmp/article/details/100627005
对DQN的伪代码做了翻译和分析

环境:
pytorch
opencv
numpy

首先建立一个游戏环境

我是拿opencv做的显示效果,也可以不用opencv,具体因人而异

场景

首先需要建立一个游戏和网络能互相互交的场景,我使用的是一个数组
对应上面的图片:
[[1,0,0,0,0],
[0,0,0,3,0],
[0,0,0,0,0],
[0,3,0,0,0],
[0,0,0,0,2],]
1是棋子位置,2是终点位置,3是坑的位置

动作

总共有4个动作,分别为上下左右,分别用数字代表0,1,2,3

奖励值

再就是设置奖励值
我将奖励值设为棋子到达红点会获得-1的奖励值返回失败,绿点会获得1的奖励值返回胜利,碰壁会获得-0.5的奖励

返回胜利或者失败时,游戏会重置
(奖励值这一点我试过很多,发现奖励值规则设置的越复杂,那么神经网络将会学习的更加好)

代码部分

game.py

import cv2
import numpy as np
import random
import time

class Env():
    def __init__(self):
        super(Env, self).__init__()
        self.action_space = ['u', 'd', 'l', 'r']
        self.n_actions = len(self.action_space)
        
#建立虚拟环境
    def start_env(self):
        self.migong=[[1,0,0,0,0],
                     [0,0,0,3,0],
                     [0,0,0,0,0],
                     [0,3,0,0,0],
                     [0,0,0,0,2],]
        self.x1,self.y1=0,0
        self.end_game=0
        return self.migong
        
        
    def display(self):
        self.display1=np.ones((300,300,3),dtype=np.uint8)
        self.display1=np.array(np.where(self.display1==1,255,0),dtype=np.uint8)
        for i in range(5):
            cv2.line(self.display1,(i*60,0),(i*60,300),(0,0,0),1)
            cv2.line(self.display1,(0,i*60),(300,i*60),(0,0,0),1)
        for x in range(5):
            for y in range(5):
                if self.migong[y][x]==1:
                    cv2.circle(self.display1,(x*60+30,y*60+30),25,(255,0,0),-1)
                if self.migong[y][x]==2:
                    cv2.circle(self.display1,(x*60+30,y*60+30),25,(0,255,0),-1)
                if self.migong[y][x]==3:
                    cv2.circle(self.display1,(x*60+30,y*60+30),25,(0,0,255),-1)
        
        cv2.imshow('1',self.display1)
        cv2.waitKey(1)
        
        
    def step(self, action):
        r=0
        #['u'0, 'd'1, 'l'2, 'r'3]
        if action==0:
            if self.y1==0:
                r=-0.5
            else:
                self.migong[self.y1][self.x1]=0
                self.migong[self.y1-1][self.x1]=1
                self.y1-=1
                if self.y1==1 and self.x1==3:
                    self.end_game=1
                    r=-1
                elif self.y1==3 and self.x1==1:
                    self.end_game=1
                    r=-1
                elif self.y1==4 and self.x1==4:
                    self.end_game=2
                    r=1
        if action==1:
            if self.y1==4:
                r=-0.5
            else: 
                self.migong[self.y1][self.x1]=0
                self.migong[self.y1+1][self.x1]=1
                self.y1+=1
                if self.y1==1 and self.x1==3:
                    self.end_game=1
                    r=-1
                elif self.y1==3 and self.x1==1:
                    self.end_game=1
                    r=-1
                elif self.y1==4 and self.x1==4:
                    self.end_game=2
                    r=1
        if action==2:
            if self.x1==0:
                r=-0.5
            else:                
                self.migong[self.y1][self.x1]=0
                self.migong[self.y1][self.x1-1]=1
                self.x1-=1
                if self.y1==1 and self.x1==3:
                    self.end_game=1
                    r=-1
                elif self.y1==3 and self.x1==1:
                    self.end_game=1
                    r=-1
                elif self.y1==4 and self.x1==4:
                    self.end_game=2
                    r=1
        if action==3:
            if self.x1==4:
                r=-0.5
            else:
                self.migong[self.y1][self.x1]=0
                self.migong[self.y1][self.x1+1]=1
                self.x1+=1
                if self.y1==1 and self.x1==3:
                    self.end_game=1
                    r=-1
                elif self.y1==3 and self.x1==1:
                    self.end_game=1
                    r=-1
                elif self.y1==4 and self.x1==4:
                    self.end_game=2
                    r=1
        #return self.migong
        return self.end_game,r,self.migong

建立神经网络

在这里插入图片描述
这是神经网络的框架,没什么花哨的神经层,就靠1个卷积和2个全连接层就可以了

可能有人要问,你这个3x5x5的输入形状从哪来啊?
我是为了使卷积神经网络更方便的读入,所以将5x5的数组变成3x5x5的数组

每一层相当于代表一种目标的位置状态
比如将上面的数组转化为:
[[[1,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],],

[[0,0,0,0,0],
[0,0,0,1,0],
[0,0,0,0,0],
[0,1,0,0,0],
[0,0,0,0,0],],

[[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,1],]]
这样就可以方便卷积网络的输入

转化代码:

def trans_torch(list1):
    list1=np.array(list1)
    l1=np.where(list1==1,1,0)
    l2=np.where(list1==2,1,0)
    l3=np.where(list1==3,1,0)
    b=np.array([l1,l2,l3])
    return b

神经网络框架代码

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()        
        self.c1=nn.Conv2d(3,25,5,1,0)
        self.f1=nn.Linear(25,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):
        x=self.c1(x)
        x=F.relu(x)
        x = x.view(x.size(0),-1)
        x=self.f1(x)
        x=F.relu(x)   
        action=self.f2(x)
        return action

接下来就该配置DQN框架了

DQN

参数:

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

DQN框架:

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 的优化器
        self.loss_func = nn.MSELoss()   # 误差公式
    def choose_action(self, x):
        x = torch.unsqueeze(torch.FloatTensor(x), 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
        # 抽取记忆库中的批数据
        # 从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 = 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_
        # 针对做过的动作b_a, 来选 q_eval 的值, (q_eval 原本有所有动作的值)
        q_eval = self.eval_net(b_s).gather(1, b_a)  # shape (batch, 1) 找到action的Q估计(关于gather使用下面有介绍)
        q_next = self.target_net(b_s_).detach()     # q_next 不进行反向传递误差, 所以 detach Q现实
        q_target = b_r + GAMMA * q_next.max(1)[0]   # shape (batch, 1) DQL核心公式
        loss = self.loss_func(q_eval, q_target) #计算误差
        # 计算, 更新 eval net
        self.optimizer.zero_grad() #
        loss.backward() #反向传递
        self.optimizer.step()

训练部分

首先让棋子按照神经网络返回动作最大值取动作,并且有0.1的几率取随机动作(以获得更多可能),并存入经验池

当经验池达到2000后,Q估计神经网络开始从经验池中学习,并且每100步,就将Q估计的权重和偏置复制到Q现实中,同时走一步,对应经验池的位置将被覆盖

基本上按照伪代码来

dqn = DQN() # 定义 DQN 系统
study=1
win=0
lose=0
env=Env()
for i_episode in range(200):
    #print(i_episode,'epoch')
    s = env.start_env()
    s=trans_torch(s)
    while True:
        env.display()   # 显示实验动画
        a = dqn.choose_action(s) #选择动作
        # 选动作, 得到环境反馈
        done,r,s_ = env.step(a)
        
        
        s_=trans_torch(s_)
        # 存记忆
        dqn.store_transition(s, a, r, s_)        
        if dqn.memory_counter > MEMORY_CAPACITY:
            if study==1:
                print('2000经验池')
                win=0
                lose=0
                study=0
            dqn.learn() # 记忆库满了就进行学习
        if done==1 or done==2:    # 如果回合结束, 进入下回合
            #print(loss1)
            if done==1:
                lose+=1
                print('epoch',i_episode,r,'失败','总计:胜利',win,'失败',lose)
            if done==2:
                win+=1
                print('epoch',i_episode,r,'成功','总计:胜利',win,'失败',lose)                
            break
        s = s_

训练程序总代码

train.py

from game import Env
import time
import random
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
#参数
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
def trans_torch(list1):
    list1=np.array(list1)
    l1=np.where(list1==1,1,0)
    l2=np.where(list1==2,1,0)
    l3=np.where(list1==3,1,0)
    b=np.array([l1,l2,l3])
    return b
#神经网络
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()        
        self.c1=nn.Conv2d(3,25,5,1,0)
        self.f1=nn.Linear(25,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):
        x=self.c1(x)
        x=F.relu(x)
        x = x.view(x.size(0),-1)
        x=self.f1(x)
        x=F.relu(x)   
        action=self.f2(x)
        return action
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 的优化器
        self.loss_func = nn.MSELoss()   # 误差公式
    def choose_action(self, x):
        x = torch.unsqueeze(torch.FloatTensor(x), 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
        # 抽取记忆库中的批数据
        # 从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 = 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_
        # 针对做过的动作b_a, 来选 q_eval 的值, (q_eval 原本有所有动作的值)
        q_eval = self.eval_net(b_s).gather(1, b_a)  # shape (batch, 1) 找到action的Q估计(关于gather使用下面有介绍)
        q_next = self.target_net(b_s_).detach()     # q_next 不进行反向传递误差, 所以 detach Q现实
        q_target = b_r + GAMMA * q_next.max(1)[0]   # shape (batch, 1) DQL核心公式
        loss = self.loss_func(q_eval, q_target) #计算误差
        # 计算, 更新 eval net
        self.optimizer.zero_grad() #
        loss.backward() #反向传递
        self.optimizer.step()
dqn = DQN() # 定义 DQN 系统
#400步
study=1
env=Env()
for i_episode in range(100):
    #print(i_episode,'epoch')
    s = env.start_env()
    s=trans_torch(s)
    while True:
        env.display()   # 显示实验动画
        a = dqn.choose_action(s) #选择动作
        # 选动作, 得到环境反馈
        done,r,s_ = env.step(a)
        s_=trans_torch(s_)
        # 存记忆
        dqn.store_transition(s, a, r, s_)        
        if dqn.memory_counter > MEMORY_CAPACITY:
            if study==1:
                print('2000经验池')
                study=0
            dqn.learn() # 记忆库满了就进行学习
        if done==1 or done==2:    # 如果回合结束, 进入下回合
            #print(loss1)
            if done==1:
                print('epoch',i_episode,r,'失败')
            if done==2:
                print('epoch',i_episode,r,'成功')                
            break
        s = s_

训练结果:

总计:胜利 133 失败 38

总体来说还可以,我觉得可以设置更多的奖励规则,来使训练结果更好,因为我发现训练时有时会出现棋子在两个格子反复横跳的情况

发布了6 篇原创文章 · 获赞 6 · 访问量 648
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 数字20 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览