(零基础可以看懂)深度强化学习之DQN类算法之第1篇-2013年NeurIPS版本的DQN(含代码)-《强化学习系列专栏第4篇》

本文介绍了2013年DeepMind在NeurIPS发表的《Playing Atari with Deep Reinforcement Learning》论文,探讨DQN如何将神经网络应用于Q-Learning,实现游戏自主学习。内容包括DQN的关键部分解释,如replay memory、状态预处理、神经网络结构等,并提供了代码复现和详细讲解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(零基础可以看懂)深度强化学习之DQN类算法之第1篇-2013年NeurIPS版本的DQN(含代码)-《强化学习系列专栏第4篇》

背景

    DQN是由Deep Q-Learning缩写而来。从名字中可以看出,其本质上还是一种Q-Learning算法,只不过结合了深度学习。2013年的时候,位于伦敦的DeepMind(现在已经被谷歌收购了,也就是Alpha Go的父母)在NeurIPS发表了一篇名为《Playing Atari with Deep Reinforcement Learning》的论文,它出人不意的将神经网络引进到了Q-Learning算法中。该模型可以通过训练后,让其自主的玩那种我们小时候玩的小霸王的那种游戏(暴露了我的年龄,哈哈)。下面,我开始讲解这篇论文,并将我自己复现的代码放上来。

论文原文链接

《Playing Atari with Deep Reinforcement Learning》

介绍

    读懂这篇论文还需要知道一些前置的知识,前置的知识我已经讲解过了,在我的博客上,我附一下链接。

第1篇:(零基础可以看懂)强化学习中的动态规划(贝尔曼方程)(含代码)-《强化学习系列专栏第1篇》
第2篇:(零基础可以看懂)强化学习中的蒙特卡罗应用(贝尔曼方程)(含代码)-《强化学习系列专栏第2篇》
第3篇:(零基础可以看懂)强化学习中的时间差分算法(含代码)-《强化学习系列专栏第3篇》

    与Q-Learning的区别在于,DQN使用了神经网络当作其Q函数(Q table),由于神经网络的连续及非线性的特征,使得神经网络可以表示“无限多”的状态。

模型关键部分解释

我们这里以打乒乓这个游戏举例子,在gym里面名称为“Pong-v0”。游戏如下图,
在这里插入图片描述

①算法中使用了一个名为replay memory的变量D(代码中实际上是一个队列)去存储情形(当前的状态,当前的行为,当前行为的奖励,下一个状态),然后训练的时候,从变量D中随机选取batch_size个样本去训练神经网络部分。这样做的目的是为了防止训练数据的连续性导致模型的不够泛化(因为是随机抽取的,所以可以保证每次抽取出来的训练数据不是连续的)。这个replay memory是有最大存储限制的,论文中是设置了D的最大长度是100万。

②为了减少状态的数量,论文中对图片进行了预处理,由于图片是210160(高宽)尺寸的,并且有128种颜色,也就是说图片有3个通道。于是论文中,首先将图片转为灰度图,转换完之后,图片就只有1个通道了。接着,再对该灰度图进行下采样到11084的尺寸。最后,再将图片的中间区域裁剪出来,裁剪成8484尺寸大小的图片。只有经过这样预处理过后的图片,才可以放到神经网络里面进行前向传播。经过预处理后的图片如下图所示,
在这里插入图片描述
③由于如果只放一张图片到神经网络里面去,神经网络并不会捕捉到动态的信息,比如球到底是从左边往右边飞,还是从右边往左边飞(玩游戏是一个类似视频一样有前后动态关系的一种场景),因此,要想让神经网络知道这是一个动态的状态,我们将此时此刻这1帧图片,再加前3帧图片,组成4个通道,放到神经网络里,这样一来,神经网络就知道“前因后果”中的“前因”了,它就知道球从哪个方向飞过来了。

④神经网络部分其实是对图片进行一个特征提取,然后映射到可行的动作的数量的维度上。神经网络的结构是这样的:
    第1层:首先,网络的输入是4幅 84 ∗ 84 84*84 8484大小的图片,4幅图片组成4个通道。所以,网络的输入数据的shape是[batch_size, 4, 84, 84],接着,使用16个 8 ∗ 8 8*8 88大小,上下左右步长均为4的卷积核,进行卷积操作,然后使用ReLU进行激活。此时输出的shape为[batch_size, 16, 20, 20]。
    第2层:使用32个 4 ∗ 4 4*4 44,上下左右步长均为步长为2的卷积核,进行卷积操作,然后使用ReLU进行激活。此时输出的shape为[batch_size, 32, 9, 9]。接着将其压平,变成[batch_size, 2592]的shape。
    第3层:使用 2592 ∗ 256 2592*256 2592256大小的全连接层进行全连接操作。然后使用ReLU激活函数激活,此时输出的shape为[batch_size, 256]。
    第4层:使用 256 ∗ 动作数量 256*动作数量 256动作数量大小的全连接层进行全连接操作。此时输出的shape为[batch_size, 动作数量]。在打乒乓球的游戏中,动作数量为6。

我们讲完了关键的要点,我们直接将算法的伪代码放上来。
在这里插入图片描述
伪代码中需要注意的点如下:
①capacity N就是所设置的D的最大长度。
Φ 1 Φ_1 Φ1其实就是对图像所作的预处理的操作。
③该算法和Q-Learning一样,使用了ε-greedy的方法。
④store transition ( Φ t , a t , r t , Φ t + 1 ) (Φ_t, a_t, r_t, Φ_{t+1}) (Φt,at,rt,Φt+1) in D这句话要注意,存储是当前的状态(也就是当前帧的图片),当前的行为,当前行为所获得的奖励,下一个状态(也就是下一帧图片)。
⑤标签值 y j y_j yj是会变化的,如果下一个状态游戏已经结束,那么 y j = r j y_j=r_j yj=rj,否则还要加上一项上述图中的公式。
⑥使用的是MSE作为损失函数。

代码

# encoding=utf-8
'''
Author: Haitaifantuan
Create Date: 2020-09-27 23:23:52
Author Email: 47970915@qq.com
Description: Should you have any question, do not hesitate to contact me via E-mail.
'''
import gym
import torch.nn as nn
import torch
from torchvision import transforms
import atari_py
import random
import time
from PIL import Image
import matplotlib.pyplot as plt
from collections import deque
import copy
import os


class Preprocessing(nn.Module):
    def __init__(self):
        super(Preprocessing, self).__init__()

        self.preprocessing = transforms.Compose([
            # 按照论文步骤
            # 先转换为灰度图
            transforms.Grayscale(1),
            # 再下采样到110*84的大小
            transforms.Resize((110, 84)),
            # 转换为Tensor()输入到网络
            transforms.ToTensor()
        ]
        )

    def forward(self, input):
        # 由于传进来是torch.Tensor()
        # 所以我们要将其转换为PIL.Image才能预处理
        input = Image.fromarray(input)
        # 最后输出的就是论文所说的84*84的灰度图像了
        output = self.preprocessing(input)  # 这个时候output是[1, 84, 84]
        # 将多余的维度压缩掉,最后返回的是[84, 84]的形状
        output = torch.squeeze(output)
        # 然后再裁剪到84*84的大小的游戏区域
        output = output[17:101, :]  # 这个区域是游戏的区域
        # plt.imshow(output, cmap='gray')
        # plt.show()
        return output


class Deep_Q_Network(nn.Module):
    def __init__(self, action_nums):
        super(Deep_Q_Network, self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(4, 16, (8, 8), 4),
            nn.ReLU(),  # 论文中使用的不一定是这个激活函数,这里是为了简化使用ReLU
            nn.Conv2d(16, 32, (4, 4), 2),
            nn.ReLU()  # 论文中使用的不一定是这个激活函数,这里是为了简化使用ReLU
        )

        self.classifier = nn.Sequential(
            nn.Linear(2592, 256),
            nn.Linear(256, action_nums)
        )

    def forward(self, input):
        output = self.features(input)
        output = output.view(-1, 2592)
        output = self.classifier(output)
        output = torch.squeeze(output)
        return output

    def initialization(self):
        for m in self.modules():
            if isinstance(m, nn.Linear):
                nn.init.kaiming_normal_(m.weight.data)
            if isinstance(m, nn.Linear):
                nn.init.kaiming_normal_(m.weight.data)


class Agent(object):
    def __init__(self):
        # 模型保存的路径
        self.model_path = './2013_NIPS_DQN_cpu_trained_model_save_reward_loss/'
        if not os.path.exists(self.model_path):
            os.mkdir(self.model_path)
        self.save_model_path = self.model_path + '/model'

        self.lr = 0.001

        # 我们玩“乒乓球游戏”,这里搭建下环境
        self.env = gym.make('Pong-v0')
        self.env = self.env.unwrapped
        # 这个是游戏的valid的动作
        self.action_space = self.env.action_space.n
        self.action_nums = self.env.action_space.n
        # 构建图像预处理对象
        self.preprocessing = Preprocessing()
        # 构建deep q-network网络
        self.deep_q_network = Deep_Q_Network(self.action_nums)
        # 初始化网络
        if os.path.exists(self.save_model_path):
            state_dict = torch.load(self.save_model_path)
            self.deep_q_network.load_state_dict(state_dict)
            print("从已训练好的模型中加载模型成功")
        else:
            self.deep_q_network.initialization()
            print("初始化模型所有参数成功")
        # 构建损失函数
        self.loss_func = nn.MSELoss()
        # 构建优化器
        self.opti = torch.optim.SGD(self.deep_q_network.parameters(), lr=self.lr, momentum=0.9)
        # 每次训练的样本数量,论文中是32
        self.batch_size = 32
        # 创建一个缓存,超过大小后,最新的放进去,老的扔掉
        self.replay_memory_size = 200000  # 30000的话,2080Ti显存11G不够  10万需要20个G内存左右
        self.replay_memory = deque()
        # 当memory_size达到多少后,开始训练
        self.begin_to_train_memory_size = 50000

        self.alpha = 0.9
        self.gamma = 0.9
        self.init_epsilon = 1  # 论文为1
        self.final_epsilon = 0.1  # 论文为0.1
        self.epsilon_decay_frames = 1000000  # 论文1000000

        self.train_times = 0

        # 论文中是每4帧,agent进行一次动作的选择。
        self.select_action_every_k_time = 4

        # 记录reward变化的变量
        self.reward_change = []
        # 记录loss变化的变量
        self.loss_change = []

    def four_img_list_to_Q_net_input(self, four_img_list):
        stacked = torch.stack(list(four_img_list))
        return stacked

    def generate_initial_4_frames(self, current_state_single_frame):
        '''
        由于环境一开始,four_img_list的长度是小于4的
        因此我们需要让其长度达到4后,再继续后面的记录操作
        在前4步,我们都使用模型选择动作
        :param current_state_single_frame:
        :return: 返回一个队列,里面存放了第1、2、3、4帧游戏画面的对应的Tensor数值
        '''
        four_img_list = deque()
        # 由于一开始并没有4张图片可以使用
        # 因此,我们根据当前的状态,复制出另外3张图片
        # 然后随着step的进行,我们一张图片一张图片的放进去
        four_img_list.extend([current_state_single_frame, current_state_single_frame,
                              current_state_single_frame, current_state_single_frame])

        for _ in range(3):
            # 渲染环境
            self.env.render()
            # 这里将4帧图片变成4个通道放到网络里
            current_state_4_frames_stacked_result = self.four_img_list_to_Q_net_input(four_img_list)
            # 放到网络里需要再添加一个Batch_size部分的维度
            current_state_4_frames_stacked_result = torch.unsqueeze(current_state_4_frames_stacked_result, dim=0)
            action_value = self.deep_q_network(current_state_4_frames_stacked_result)
            action = torch.argmax(action_value)
            next_state, reward, done, info = self.env.step(action)
            next_state_to_tensor = self.preprocessing(next_state)

            four_img_list.append(next_state_to_tensor)
            four_img_list.popleft()

        return four_img_list

    def train(self):
        # 原始论文:如果达到了replay_memory的最大值,那就开始从replay_memory中随机选取样本进行训练
        # if len(self.replay_memory) > (self.replay_memory_size - 1):
        if len(self.replay_memory) > self.begin_to_train_memory_size:
            batch_data = random.choices(self.replay_memory, k=32)

            # 拿到训练数据后,将他们进行解包
            current_state_4_frames_stacked_result_list = [each[0] for each in batch_data]
            current_state_action_list = torch.LongTensor([[each[1]] for each in batch_data])
            reward_list = torch.FloatTensor([[each[2]] for each in batch_data])
            next_state_4_frames_stacked_result_list = [each[3] for each in batch_data]
            done_list = [[each[4]] for each in batch_data]

            # 将训练数据放到模型里进行前向传播
            y_pre = self.deep_q_network(torch.stack(current_state_4_frames_stacked_result_list).squeeze()).gather(dim=1,
                        index=current_state_action_list)

            # 根据公式,构建标签值
            q_net_result = self.deep_q_network(torch.stack(next_state_4_frames_stacked_result_list, dim=0)).detach()
            y_target = reward_list + self.gamma * torch.max(q_net_result, dim=1)[0].reshape(self.batch_size, -1)

            self.loss = self.loss_func(y_pre, y_target)

            self.opti.zero_grad()
            self.loss.backward()
            self.opti.step()
            self.train_times += 1

    def close_env(self):
        self.env.close()

    def save_model(self):
        torch.save(self.deep_q_network.state_dict(), self.save_model_path)

    def fire_in_the_hole(self):
        frame_count = 0
        self.current_epsilon = self.init_epsilon
        self.begin_time = time.time()
        for self.episode in range(100000):
            # 一个episode结束后,重新设置下环境,返回到随机的一个初始状态
            current_state_single_frame = self.env.reset()
            # 将current_state()预处理一下然后转换为Tensor
            current_state = self.preprocessing(current_state_single_frame)

            # 这个方法返回的four_img_list里面就存放了第1、2、3、4帧画面的Tensor()形式
            four_img_list = self.generate_initial_4_frames(current_state)
            current_state_4_frames_stacked_result = self.four_img_list_to_Q_net_input(four_img_list)

            # 记录一下当前这一盘总的reward
            self.current_episode_reward = 0
            self.select_action_count = 0
            while True:
                # 渲染环境
                self.env.render()

                # 论文每4帧才根据ε-greedy方法做一个动作
                # 其他3帧时间的动作选取上一轮选择的动作
                if self.select_action_count == 0 or self.select_action_count == self.select_action_every_k_time:
                    # 根据ε-greedy方法,走一步,看看
                    if random.random() < self.current_epsilon:
                        current_state_action = self.env.action_space.sample()
                    else:
                        # 根据Q函数找到最优的动作
                        # 放到网络里需要再添加一个Batch_size部分的维度
                        action_value = self.deep_q_network(
                            torch.unsqueeze(current_state_4_frames_stacked_result, dim=0))
                        current_state_action = torch.argmax(action_value)

                    self.select_action_count = 0

                next_state, reward, done, info = self.env.step(current_state_action)
                next_state_to_tensor = self.preprocessing(next_state)

                self.current_episode_reward += reward

                four_img_list.append(next_state_to_tensor)
                four_img_list.popleft()

                next_state_4_frames_stacked_result = self.four_img_list_to_Q_net_input(four_img_list)

                # (将当前的状态以及前三幅图片组成的图片,当前的行为,当前获得的奖励,下一个状态,游戏是否结束)添加到replay_memory中
                self.replay_memory.append((current_state_4_frames_stacked_result, current_state_action,
                                           reward, next_state_4_frames_stacked_result, done))
                if len(self.replay_memory) > self.replay_memory_size:
                    self.replay_memory.popleft()

                # 判断当前这一盘游戏是否结束
                if done:
                    self.end_time = time.time()
                    self.minute = int((self.end_time - self.begin_time) / 60)
                    self.hour = int(self.minute / 60)
                    self.day = int(self.hour / 24)

                    if len(self.replay_memory) < self.begin_to_train_memory_size:
                        self.loss = torch.tensor(0)
                    break

                current_state_4_frames_stacked_result = next_state_4_frames_stacked_result
                self.select_action_count += 1

                frame_count += 1
                if frame_count <= self.epsilon_decay_frames:
                    self.current_epsilon = self.init_epsilon - (
                            self.init_epsilon - self.final_epsilon) * frame_count / self.epsilon_decay_frames

                # 执行训练网络的操作,里面会判断reply_memory的长度是否达到最大值了
                self.train()

            self.reward_change.append(self.current_episode_reward)
            self.loss_change.append(self.loss.data.item())

            print(
                "当前已训练{}-{}小时-{}分钟===当前为第{}个Episode===当前episode共获得{}reward===总共已训练{}===当前的loss为\
                :{}===当前的epsilon值为:{}===当前reply_memory的长度为:{}".format(
                    self.day, self.hour, self.minute, self.episode, self.current_episode_reward, self.train_times,
                    self.loss,
                    self.current_epsilon, len(self.replay_memory)))

            if self.episode % 10 == 0:
                # 保存模型
                self.save_model()
                # 将当前的self.reward_change列表保存下来,以覆盖的方式保存下来。
                with open(self.model_path + '/reward_change.txt', 'w', encoding='utf-8') as file:
                    file.write(str(self.reward_change))

                # 将当前的self.loss_change保存下来
                with open(self.model_path + '/loss_change.txt', 'w', encoding='utf-8') as file:
                    file.write(str(self.loss_change))

        # 关闭游戏环境
        self.close_env()


agent = Agent()
agent.fire_in_the_hole()

代码复现、详细讲解及我的Github地址

完整代码地址:https://github.com/haitaifantuan/reinforcement_leanring

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值