# DQN开山论文。首次提出深度 Q 网络(DQN),其核心思想是用深度神经网络来近似 Q 值函数,引入了经验回放和固定目标网络来提升训练的稳定性,解决高维状态空间(如图像)的强化学习问题。
# 无模型(model - free)、 Q-learning。
# 复现代码链接:Deep-Q-Network-AtariBreakoutGame/breakout at master · SinanGncgl/Deep-Q-Network-AtariBreakoutGame
论文链接:dqn.pdf
目录
3.2.3.1 经验回放(Experience Replay)
1. 论文概要
目标:提出一种端到端的深度强化学习方法(DQN),直接从原始像素输入学习控制策略,无需人工设计特征。
核心贡献:(1)经验回放:打破数据相关性,提升训练稳定性。
(2)目标网络:固定旧参数计算目标Q值,缓解发散问题。
(3)通用性:同一网络结构适配多款游戏,无需调参。
2. 背景知识
将深度学习与强化学习结合,解决高维视觉输入下的决策问题。
2.1 强化学习
2.1.1 马尔可夫决策过程(MDP)
2.1.2 Q-learning
(1)核心:通过估计动作价值函数Q(s, a)来指导决策, Q(s, a)表示在状态 s 执行动作 a 后,智能体预期获得的未来折扣奖励总和(详见Spinning Up in Deep RL学习记录(一)(自用)-CSDN博客)。
(2)Q - learning 的目标:找到最优 Q 函数,或者说尽可能地逼近最优 Q 函数。通过不断与环境交互学习,调整自身对 Q 值的估计,最终使得估计的 Q 值接近最优 Q 函数给出的真实动作价值。
(3)贝尔曼方程:。Q - learning 基于贝尔曼方程进行迭代更新,而贝尔曼最优方程是定义最优 Q 函数的基础。Q - learning 利用贝尔曼方程的迭代性质,逐步向最优 Q 函数的方向进行学习和更新。
(4)最优策略:在 Q - learning 过程中,当估计的 Q 值逐渐逼近最优 Q 函数时,就可以根据 Q 值来制定策略,即选择使得 Q 值最大的动作作为当前状态下的最优动作。一旦学习到了最优 Q 函数,也就意味着找到了最优策略,智能体可以依据这个策略在环境中做出最优决策。
2.2 深度学习
2.2.1 CNN
空间特征提取:CNN 通过卷积层(如 或
卷积核)对图像进行滑动窗口操作,自动提取边缘、纹理、形状等空间特征。例如,在打砖块游戏中,卷积层可识别球拍的位置、球的运动轨迹、砖块的排列等。
层次化特征学习:多层卷积与池化(如最大池化)交替,低层提取简单特征,高层组合特征形成复杂表示。如第一层卷积提取边缘,第二层组合边缘形成物体轮廓,第三层识别具体物体(球、球拍)。
权值共享:同一卷积核在图像不同位置共享权值,大幅减少参数数量,提高训练效率与泛化能力,适合处理图像这种空间平移不变性强的数据。
2.2.2 端到端
定义:直接将原始输入(如游戏画面像素)映射到决策输出(如动作选择),中间无需人工设计特征或干预。以 DQN 为例,输入 4 帧堆叠的 84×84 灰度图,经 CNN 处理后直接输出各动作的 Q 值,避免了手工特征设计的繁琐与误差,充分利用数据驱动的优势自动学习最优表示。
适应性:能根据不同任务(如不同 Atari 游戏)自动调整特征提取方式,无需为每个游戏单独设计特征工程,提高了算法的通用性与效率。
3. 论文代码复现
3.1 代码核心框架(dqn.py)
神经网络类(NeuralNetwork):定义DQN模型结构。
预处理函数(preprocessing):将原始图像转换为网络输入格式。
训练函数(train):实现经验回放、Q值更新和模型优化。
测试函数(test):使用训练好的模型进行游戏测试。
主函数(main):支持训练、测试和继续训练模式。
3.2方法对照
3.2.1 预处理与输入
(1)论文方法:RGB 转灰度 → 降采样至 84×84 → 裁剪有效区域 → 堆叠连续 4 帧(捕捉动态信息),其中:
RGB 转灰度:将彩色的 RGB 图像转换为灰度图像,减少颜色维度信息。在强化学习处理图像输入时,颜色信息并非总是必要的,灰度图像能保留图像的亮度信息,同时降低数据维度,减少计算量。例如,在打砖块游戏中,球、球拍和砖块的相对位置及运动信息比颜色更关键。
降采样至 84×84:通过降采样操作,将原始图像尺寸调整为 84×84 像素。原始游戏图像尺寸较大,降采样可进一步降低数据维度,去除一些细节信息中可能存在的噪声,同时保留对决策有用的关键特征。
裁剪有效区域:针对游戏画面,裁剪出包含主要游戏元素(如球、球拍、砖块)的有效区域,去除无关背景部分。这样可以聚焦于与智能体决策相关的区域,减少不必要的计算和干扰因素。
堆叠连续 4 帧:把连续的 4 帧图像堆叠在一起作为网络输入。这是因为在动态游戏场景中,单帧图像无法提供足够的运动信息,堆叠多帧可以捕捉到物体的运动轨迹和速度等动态信息,帮助智能体更好地理解环境变化,做出更合理的决策。例如,判断球的运动方向和速度,对于预测球的落点及决定球拍的移动方向至关重要。
通过以上步骤,可大幅降低输入到神经网络的数据维度,减轻计算负担,同时有效地保留了游戏中的时空信息,使智能体能够基于更合适的数据进行学习和决策。
(2)代码复现:
def preprocessing(image):
image = cv2.resize(image, (84, 84)) # 调整大小
image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
image[image > 0] = 255 # 二值化处理
return torch.from_numpy(image.transpose(2,0,1)) # 转换为张量
3.2.2 神经网络架构
CNN+Q-learning,CNN自动提取空间特征,实现端到端的强化学习策略。
(1)论文方法:
卷积层 1:16 个 8×8 滤波器(步长 4),ReLU 激活。
卷积层 2:32 个 4×4 滤波器(步长 2),ReLU 激活。
全连接层:256 个 ReLU 单元 → 输出层(每个动作对应 Q 值)。
优势:DQN 网络架构能够在单次前向传播过程中,计算出所有可能动作的 Q 值。这使得智能体可以快速比较不同动作的价值,从中选择最优动作,提高了决策效率,尤其适用于需要实时做出决策的强化学习任务,如 Atari 游戏这类动态交互场景。
(2)代码复现:
class NeuralNetwork(nn.Module):
def __init__(self):
self.conv1 = nn.Conv2d(4, 32, kernel_size=8, stride=4)
self.conv2 = nn.Conv2d(32, 64, 4, 2)
self.conv3 = nn.Conv2d(64, 64, 3, 1) # 额外卷积层
self.fc4 = nn.Linear(7*7*64, 512)
self.fc5 = nn.Linear(512, self.number_of_actions)
注:相比原文多了一个卷积层。
3.2.3 训练机制
使用经验回放和目标网络两大稳定机制。
3.2.3.1 经验回放(Experience Replay)
(1)论文方法
存储经验:将智能体与环境交互过程中产生的历史经验,即状态、动作
、奖励
、下一状态
组成的四元组
存储到记忆库中。记忆库可以采用队列等数据结构实现,用于保存一定数量的历史经验。
随机采样训练:在训练时,从记忆库中随机抽取小批量样本进行训练。这样做打破了样本之间的时序相关性,因为连续的交互样本往往具有较强的相关性,如果直接按顺序使用这些样本训练,可能导致模型学习到的模式存在偏差,且容易过拟合。随机采样使得训练数据更加独立同分布,符合传统机器学习算法的假设,从而提升了数据利用率,使训练更加稳定和有效。
(2)代码复现
D = deque(maxlen=replay_memory_size) # 双端队列存储经验
minibatch = random.sample(D, minibatch_size) # 随机采样
3.2.3.2 目标网络(Target Network)
(1)论文方法
独立网络计算目标 Q 值:使用一个独立于当前用于决策的网络(即评估网络)的目标网络来计算目标 Q 值。目标网络的结构与评估网络相同,但参数更新方式不同。目标网络的参数不是每一步都更新,而是定期与评估网络的参数进行同步(例如每 1000 步同步一次) 。在计算目标 Q 值时,使用目标网络的参数,这样可以使目标 Q 值在一段时间内保持相对稳定,避免因评估网络参数频繁更新而导致代码目标 Q 值波动过大,进而使训练过程更加稳定,有助于收敛到更好的解。
(2)代码复现
output_1_batch = model(state_1_batch) # 直接使用当前网络计算下一状态Q值
y_batch = reward + gamma * max(output_1_batch)
此点和论文有差异。
3.2.4 细节优化
3.2.4.1 优化器
(1)论文方法
优化器:RMSProp,学习率固定。
奖励裁剪:±1。
(2)代码复现
optimizer = optim.Adam(lr=0.0002) # 使用Adam而非RMSProp
reward = torch.clamp(reward, -1, 1) # 奖励裁剪
3.2.4.2 探索策略(ε-greedy)
(1)论文方法
ε从1.0线性衰减至0.1(前100万帧),固定为0.1。
(2)代码复现
epsilon = initial_epsilon (0.1) → final_epsilon (0.05)
epsilon -= (initial_epsilon - final_epsilon) / explore
初始ε为0.1,衰减终点为0.05。
4. 总结
使用经验回放和目标网络的训练机制,首次实现从像素到动作的端到端强化学习,推动深度RL发展;但也有长时序任务(如Q*bert需长期规划)表现不及人类,奖励裁剪可能影响策略优化,计算成本高等缺点。
5. 完整代码与项目架构
5.1 dqn.py
实现深度 Q 网络(DQN)的训练和测试逻辑。定义了神经网络的结构,使用 gameTRY.py
中提供的游戏环境进行训练和测试,通过经验回放和 epsilon-greedy 策略来学习如何在游戏中做出最优决策。
import cv2
import numpy as np
from collections import deque
import random
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.transforms as T
import torch.nn.functional as F
from gameTRY import Breakout
import os
import sys
import time
# 定义神经网络类,继承自 PyTorch 的 nn.Module
class NeuralNetwork(nn.Module):
def __init__(self):
super(NeuralNetwork, self).__init__()
# 定义动作数量,这里表示有 2 个动作可供选择
self.number_of_actions = 2
# 折扣因子,用于计算未来奖励的现值
self.gamma = 0.99
# 最终的探索率,用于 epsilon-greedy 策略
self.final_epsilon = 0.05
# 初始的探索率
self.initial_epsilon = 0.1
# 训练的总迭代次数
self.number_of_iterations = 2000000
# 经验回放缓冲区的大小
self.replay_memory_size = 750000
# 每次训练时从经验回放缓冲区中采样的小批量大小
self.minibatch_size = 32
# 从初始探索率到最终探索率所需的时间步长
self.explore = 3000000
# 定义卷积层
# 输入通道数为 4,输出通道数为 32,卷积核大小为 8,步长为 4
self.conv1 = nn.Conv2d(4, 32, kernel_size=8, stride=4)
# 输入通道数为 32,输出通道数为 64,卷积核大小为 4,步长为 2
self.conv2 = nn.Conv2d(32, 64, 4, 2)
# 输入通道数为 64,输出通道数为 64,卷积核大小为 3,步长为 1
self.conv3 = nn.Conv2d(64, 64, 3, 1)
# 定义全连接层
# 输入特征数为 7 * 7 * 64,输出特征数为 512
self.fc4 = nn.Linear(7 * 7 * 64, 512)
# 输入特征数为 512,输出特征数为动作数量
self.fc5 = nn.Linear(512, self.number_of_actions)
def forward(self, x):
# 对输入数据依次通过卷积层和激活函数 ReLU
x = F.relu(self.conv1(x))
x = F.relu(self.conv2(x))
x = F.relu(self.conv3(x))
# 将卷积层的输出展平,以便输入到全连接层
x = F.relu(self.fc4(x.view(x.size(0), -1)))
# 输出每个动作的 Q 值
return self.fc5(x)
# 图像预处理函数,将输入的图像转换为适合神经网络输入的格式
def preprocessing(image):
# 将图像转换为灰度图,并调整大小为 84x84
image_data = cv2.cvtColor(cv2.resize(image, (84, 84)), cv2.COLOR_BGR2GRAY)
# 将灰度图中大于 0 的像素值设置为 255
image_data[image_data > 0] = 255
# 调整图像的形状为 (84, 84, 1)
image_data = np.reshape(image_data, (84, 84, 1))
# 交换维度,将通道维度放在前面
image_tensor = image_data.transpose(2, 0, 1)
# 将数据类型转换为 float32
image_tensor = image_tensor.astype(np.float32)
# 将 NumPy 数组转换为 PyTorch 张量
image_tensor = torch.from_numpy(image_tensor)
return image_tensor
# 初始化神经网络的权重
def init_weights(m):
if type(m) == nn.Conv2d or type(m) == nn.Linear:
# 对卷积层和全连接层的权重进行均匀初始化,范围在 -0.01 到 0.01 之间
torch.nn.init.uniform(m.weight, -0.01, 0.01)
# 将偏置项初始化为 0.01
m.bias.data.fill_(0.01)
# 训练函数,用于训练神经网络
def train(model, start):
# 定义 Adam 优化器,用于更新神经网络的参数
optimizer = optim.Adam(model.parameters(), lr=0.0002)
# 定义均方误差损失函数
criterion = nn.MSELoss()
# 创建游戏环境实例
game_state = Breakout()
# 初始化经验回放缓冲区
D = deque()
# 初始动作设置为不做任何动作
action = torch.zeros([model.number_of_actions], dtype=torch.float32)
action[0] = 0
# 执行初始动作,获取图像数据、奖励和游戏是否结束的标志
image_data, reward, terminal = game_state.take_action(action)
# 对图像数据进行预处理
image_data = preprocessing(image_data)
# 将四张相同的预处理图像拼接在一起,作为初始状态
state = torch.cat((image_data, image_data, image_data, image_data)).unsqueeze(0)
# 初始化探索率
epsilon = model.initial_epsilon
# 初始化迭代次数
iteration = 0
# 主训练循环,直到达到最大迭代次数
while iteration < model.number_of_iterations:
# 通过神经网络获取当前状态下每个动作的 Q 值
output = model(state)[0]
# 初始化动作向量
action = torch.zeros([model.number_of_actions], dtype=torch.float32)
# epsilon-greedy 探索策略,以一定概率随机选择动作
random_action = random.random() <= epsilon
if random_action:
print("Random action!")
# 根据 epsilon-greedy 策略选择动作
action_index = [torch.randint(model.number_of_actions, torch.Size([]), dtype=torch.int)
if random_action
else torch.argmax(output)][0]
# 将选择的动作对应的位置置为 1
action[action_index] = 1
# 逐渐降低探索率
if epsilon > model.final_epsilon:
epsilon -= (model.initial_epsilon - model.final_epsilon) / model.explore
# 执行选择的动作,获取下一个状态的图像数据、奖励和游戏是否结束的标志
image_data_1, reward, terminal = game_state.take_action(action)
# 对下一个状态的图像数据进行预处理
image_data_1 = preprocessing(image_data_1)
# 更新状态,将当前状态的后三张图像和新的图像拼接在一起
state_1 = torch.cat((state.squeeze(0)[1:, :, :], image_data_1)).unsqueeze(0)
# 调整动作和奖励的形状
action = action.unsqueeze(0)
reward = torch.from_numpy(np.array([reward], dtype=np.float32)).unsqueeze(0)
# 将当前状态、动作、奖励、下一个状态和游戏是否结束的标志保存到经验回放缓冲区
D.append((state, action, reward, state_1, terminal))
# 如果经验回放缓冲区已满,移除最早的经验
if len(D) > model.replay_memory_size:
D.popleft()
# 从经验回放缓冲区中随机采样一个小批量的经验
minibatch = random.sample(D, min(len(D), model.minibatch_size))
# 解包小批量的经验
state_batch = torch.cat(tuple(d[0] for d in minibatch))
action_batch = torch.cat(tuple(d[1] for d in minibatch))
reward_batch = torch.cat(tuple(d[2] for d in minibatch))
state_1_batch = torch.cat(tuple(d[3] for d in minibatch))
# 获取下一个状态的 Q 值
output_1_batch = model(state_1_batch)
# 根据 Bellman 方程计算目标 Q 值
y_batch = torch.cat(tuple(reward_batch[i] if minibatch[i][4]
else reward_batch[i] + model.gamma * torch.max(output_1_batch[i])
for i in range(len(minibatch))))
# 计算当前状态下选择动作的 Q 值
q_value = torch.sum(model(state_batch) * action_batch, dim=1)
# 清空优化器的梯度
optimizer.zero_grad()
# 分离目标 Q 值,避免梯度传播
y_batch = y_batch.detach()
# 计算损失
loss = criterion(q_value, y_batch)
# 反向传播计算梯度
loss.backward()
# 更新神经网络的参数
optimizer.step()
# 更新当前状态为下一个状态
state = state_1
# 迭代次数加 1
iteration += 1
# 每迭代 10000 次,保存一次模型
if iteration % 10000 == 0:
torch.save(model, "trained_model/current_model_" + str(iteration) + ".pth")
# 打印训练信息
print("total iteration: {} Elapsed time: {:.2f} epsilon: {:.5f}"
" action: {} Reward: {:.1f}".format(iteration, ((time.time() - start) / 60), epsilon,
action_index.cpu().detach().numpy(), reward.numpy()[0][0]))
# 测试函数,用于测试训练好的模型
def test(model):
# 创建游戏环境实例
game_state = Breakout()
# 初始动作设置为不做任何动作
action = torch.zeros([model.number_of_actions], dtype=torch.float32)
action[0] = 1
# 执行初始动作,获取图像数据、奖励和游戏是否结束的标志
image_data, reward, terminal = game_state.take_action(action)
# 对图像数据进行预处理
image_data = preprocessing(image_data)
# 将四张相同的预处理图像拼接在一起,作为初始状态
state = torch.cat((image_data, image_data, image_data, image_data)).unsqueeze(0)
# 无限循环进行测试
while True:
# 通过神经网络获取当前状态下每个动作的 Q 值
output = model(state)[0]
# 初始化动作向量
action = torch.zeros([model.number_of_actions], dtype=torch.float32)
# 选择 Q 值最大的动作
action_index = torch.argmax(output)
# 将选择的动作对应的位置置为 1
action[action_index] = 1
# 执行选择的动作,获取下一个状态的图像数据、奖励和游戏是否结束的标志
image_data_1, reward, terminal = game_state.take_action(action)
# 对下一个状态的图像数据进行预处理
image_data_1 = preprocessing(image_data_1)
# 更新状态,将当前状态的后三张图像和新的图像拼接在一起
state_1 = torch.cat((state.squeeze(0)[1:, :, :], image_data_1)).unsqueeze(0)
# 更新当前状态为下一个状态
state = state_1
# 主函数,根据输入的模式选择训练、测试或继续训练
def main(mode):
if mode == 'test':
# 加载训练好的模型,并将其设置为评估模式
model = torch.load('trained_model/current_model_420000.pth', map_location='cpu', weights_only = False).eval()
# 调用测试函数进行测试
test(model)
elif mode == 'train':
# 如果保存模型的目录不存在,则创建该目录
if not os.path.exists('trained_model/'):
os.mkdir('trained_model/')
# 创建神经网络模型实例
model = NeuralNetwork()
# 初始化模型的权重
model.apply(init_weights)
# 记录训练开始时间
start = time.time()
# 调用训练函数进行训练
train(model, start)
elif mode == 'continue':
# 加载训练好的模型,并将其设置为评估模式
model = torch.load('trained_model/current_model_50000.pth', map_location='cpu', weights_only = False).eval()
# 记录训练开始时间
start = time.time()
# 调用训练函数继续训练
train(model, start)
if __name__ == "__main__":
# 根据命令行参数调用主函数
main(sys.argv[1])
5.2 gameTRY.py
负责实现游戏的具体逻辑,包括游戏元素(砖块、球拍、球)的初始化、绘制、移动,以及碰撞检测和游戏状态的更新。提供了一个游戏环境,智能体可以与这个环境进行交互,获取环境的状态和奖励。
import pygame
import math
import random
# 定义屏幕尺寸常量
SIZE_OF_THE_SCREEN = 424, 430
# 砖块的高度和宽度
HEIGHT_OF_BRICK = 13
WIDTH_OF_BRICK = 32
# 球拍的高度、宽度和Y坐标
HEIGH_OF_PADDLE = 8
PADDLE_WIDTH = 50
PADDLE_Y = SIZE_OF_THE_SCREEN[1] - HEIGH_OF_PADDLE - 10
# 球的直径、半径以及X坐标的最大限制
BALL_DIAMETER = 12
BALL_RADIUS = BALL_DIAMETER // 2
MAX_PADDLE_X = SIZE_OF_THE_SCREEN[0] - PADDLE_WIDTH
MAX_BALL_X = SIZE_OF_THE_SCREEN[0] - BALL_DIAMETER
MAX_BALL_Y = SIZE_OF_THE_SCREEN[1] - BALL_DIAMETER
# 颜色常量定义
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
BLUE = (0, 0, 255)
COLOR_OF_BRICK = (153, 76, 0)
PADDLE_COLOR = (204, 0, 0)
FPS = 60
FPSCLOCK = pygame.time.Clock()
pygame.init() # 初始化pygame模块
screen = pygame.display.set_mode(SIZE_OF_THE_SCREEN)
pygame.display.set_caption(" BREAKOUT")
clock = pygame.time.Clock()
class Breakout:
def __init__(self):
"""初始化游戏状态:包括球的速度、球拍和球的矩形对象,调用方法创建砖块"""
self.capture = 0
# 球的速度(x方向,y方向),初始设置为[12, -12]用于训练
self.ball_vel = [12, -12]
# 定义球拍的矩形(左x坐标,上y坐标,宽度,高度)
self.paddle = pygame.Rect(215, PADDLE_Y, PADDLE_WIDTH, HEIGH_OF_PADDLE)
# 定义球的矩形(左x坐标,上y坐标,直径,直径)
self.ball = pygame.Rect(225, PADDLE_Y - BALL_DIAMETER, BALL_DIAMETER, BALL_DIAMETER)
self.create_bricks() # 调用方法创建砖块
def create_bricks(self):
"""创建砖块布局:通过循环生成多个砖块矩形对象并添加到bricks列表中"""
y_ofs = 20
self.bricks = []
for i in range(11):
x_ofs = 15
for j in range(12):
# 创建砖块矩形(左x坐标,上y坐标,宽度,高度)并添加到列表
self.bricks.append(pygame.Rect(x_ofs, y_ofs, WIDTH_OF_BRICK, HEIGHT_OF_BRICK))
x_ofs += WIDTH_OF_BRICK + 1 # 计算下一个砖块的x坐标
y_ofs += HEIGHT_OF_BRICK + 1 # 计算下一行砖块的y坐标
def draw_bricks(self):
"""绘制所有砖块:遍历bricks列表,使用pygame绘制矩形"""
for brick in self.bricks:
pygame.draw.rect(screen, COLOR_OF_BRICK, brick)
def draw_paddle(self):
"""绘制球拍:使用pygame绘制球拍矩形"""
pygame.draw.rect(screen, PADDLE_COLOR, self.paddle)
def draw_ball(self):
"""绘制球:使用pygame绘制圆形表示球"""
pygame.draw.circle(screen, WHITE, (self.ball.left + BALL_RADIUS, self.ball.top + BALL_RADIUS), BALL_RADIUS)
def check_input(self, input_action):
"""根据输入动作移动球拍:0表示左移,1表示右移"""
# 处理向左移动
if input_action[0] == 1:
self.paddle.left -= 12 # 左移12像素(训练时的速度)
if self.paddle.left < 0:
self.paddle.left = 0 # 确保不超出左边界
# 处理向右移动
if input_action[1] == 1:
self.paddle.left += 12 # 右移12像素(训练时的速度)
if self.paddle.left > MAX_PADDLE_X:
self.paddle.left = MAX_PADDLE_X # 确保不超出右边界
def move_ball(self):
"""移动球并处理边界碰撞:更新球的位置,根据边界反弹"""
self.ball.left += self.ball_vel[0] # 按x方向速度移动球
self.ball.top += self.ball_vel[1] # 按y方向速度移动球
# 处理左边界碰撞
if self.ball.left <= 0:
self.ball.left = 0
self.ball_vel[0] = -self.ball_vel[0] # 反转x方向速度
# 处理右边界碰撞
elif self.ball.left >= MAX_BALL_X:
self.ball.left = MAX_BALL_X
self.ball_vel[0] = -self.ball_vel[0] # 反转x方向速度
# 处理上边界碰撞
if self.ball.top < 0:
self.ball.top = 0
self.ball_vel[1] = -self.ball_vel[1] # 反转y方向速度
# 处理下边界碰撞(这里实际是球落到底部的情况,后续逻辑会重置)
elif self.ball.top >= MAX_BALL_Y:
self.ball.top = MAX_BALL_Y
self.ball_vel[1] = -self.ball_vel[1] # 反转y方向速度
def take_action(self, input_action):
"""执行一帧的游戏逻辑:处理输入、移动、碰撞检测、绘制并返回数据"""
pygame.event.pump() # 处理事件泵
reward = 0.1 # 初始奖励值
terminal = False # 是否结束标志
randNum = random.randint(0, 1) # 随机数(未明确具体用途,代码中似未充分使用)
# 处理退出事件
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
screen.fill(BLACK) # 填充屏幕为黑色
self.check_input(input_action) # 处理输入移动球拍
self.move_ball() # 移动球
# 处理球与砖块的碰撞
for brick in self.bricks:
if self.ball.colliderect(brick): # 检测球与砖块碰撞
reward = 2 # 碰撞到砖块的奖励
self.ball_vel[1] = -self.ball_vel[1] # 反转球的y方向速度
self.bricks.remove(brick) # 从列表中移除被碰撞的砖块
break
# 所有砖块被消除时重置游戏(可视为一局结束)
if len(self.bricks) == 0:
self.terminal = True
self.__init__() # 重新初始化游戏状态(似有问题,__init__会重新创建所有砖块,但代码逻辑如此)
# 处理球与球拍的碰撞
if self.ball.colliderect(self.paddle):
self.ball.top = PADDLE_Y - BALL_DIAMETER # 重置球的y坐标到球拍上方
self.ball_vel[1] = -self.ball_vel[1] # 反转球的y方向速度
# 球落到底部(未碰到球拍),游戏结束
elif self.ball.top > self.paddle.top:
terminal = True
self.__init__() # 重新初始化游戏(可能用于新一局开始)
reward = -2 # 球掉落的惩罚奖励
self.draw_bricks() # 绘制砖块
self.draw_ball() # 绘制球
self.draw_paddle() # 绘制球拍
# 捕获屏幕图像数据(转换为三维数组)
image_data = pygame.surfarray.array3d(pygame.display.get_surface())
pygame.display.update() # 更新屏幕显示
FPSCLOCK.tick(FPS) # 控制帧率
return image_data, reward, terminal # 返回屏幕图像数据、奖励值和是否结束标志
5.3 架构
从头训练模型:
python dqn.py train
中断后接着训练模型:
python dqn.py continue
测试模型:
python dqn.py test