前言
最近闲来无事,于是想试试用DQN网络玩一下只狼,毕竟作为只狼老玩家,不仅喜欢看人类受苦,还喜欢看机器受苦()
总之,先介绍一下DQN吧
DQN(Deep Q-Network)是一种用于强化学习的深度学习算法,通常在离散动作空间中应用较为广泛。DQN网络适合以下类型的游戏:
1. 基于状态和离散动作的游戏:DQN网络适用于那些游戏状态可以表示为向量或图像的离散动作游戏。例如,电子游戏中的经典街机游戏(如打砖块、贪吃蛇)或部分策略游戏(如卡牌游戏)。
2. 有限的动作空间:DQN网络在处理动作空间较小且离散的游戏中表现良好。由于DQN使用了Q值函数来估计每个动作的价值,较大的动作空间可能导致训练和收敛困难。
3. 可以通过观察状态进行训练的游戏:DQN网络通常使用观察到的状态来进行训练,而不需要游戏的内部信息。这使得DQN网络更适用于那些游戏状态可以通过观察屏幕截图或其他可观察数据获取的游戏。
需要注意的是,DQN网络的训练过程可能需要较长的时间,并且可能需要大量的样本数据和计算资源。此外,对于连续动作空间或需要连续时间处理的游戏,DQN可能不是最适合的选择。
总而言之,DQN网络适用于离散动作空间、基于状态观察和可以通过观察到的数据进行训练的游戏。具体选择何种算法还要根据游戏的特性和需求进行评估和选择。
所以,正如介绍里说的,DQN其实不太适合只狼这种连续动作和连续时间处理的游戏,但是,额,总之就是做着玩玩呗!
正文
好!总之先开始吧,讲起来这篇文章其实也可以看做是我自己的学习总结()
第一步
第一步先干啥呢,如果要使用神经网络进行训练的话,那输入数据的情况应该是最重要的吧,我这边一开始是打算输入图像数据,具体来说就是截取屏幕的一部分信息,最好是包括狼和boss的大部分,然后做一个边缘处理,这样就能把图像的信息尽可能的保留了,就像下面这样:
def preprocess_and_normalize_image():
gray_image = cv2.cvtColor(grabscreen.grab_screen(window_size), cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray_image, threshold1=100, threshold2=200)
normalized_image = torch.from_numpy(edges).float() / 255.0
return normalized_image
首先是灰度图,然后是提取边缘,最后是归一化处理,有一说一,我也不是很清楚这样做的效果和不做边缘提取哪个效果更好()
然后还有血量的提取,其实也是差不多的操作,代码如下:
def get_blood_data():
self_screen_gray = cv2.cvtColor(grabscreen.grab_screen(self_blood_window), cv2.COLOR_BGR2GRAY)
boss_screen_gray = cv2.cvtColor(grabscreen.grab_screen(boss_blood_window), cv2.COLOR_BGR2GRAY)
boss_blood = find_blood_location.boss_blood_count(boss_screen_gray)
self_blood = find_blood_location.self_blood_count(self_screen_gray)
return self_blood, boss_blood
# 定义一个函数,用于将当前血量信息转换为模型输入的特征向量
def preprocess_blood_data(self_blood, boss_blood):
# 在这个示例中,将敌人和自身的血量信息简单地串联成一个特征向量
# 如果需要进行更复杂的特征处理,可以在这里进行修改
feature_vector = torch.tensor([self_blood, boss_blood], dtype=torch.float32)
return feature_vector
,总之在最后,我突发奇想想要让血量和图像和韧性条一起作为数据进入模型(别问为啥)
第二步
第二步要干啥呢,我觉得大概是构建模型吧,这是我随手搞的一个简单的模型:
class DQN(nn.Module):
def __init__(self, image_channels, input_dim, output_dim):
super(DQN, self).__init__()
# 卷积层用于处理图像输入
self.conv1 = nn.Conv2d(image_channels, 32, kernel_size=8, stride=4)
self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv2 = nn.Conv2d(32, 64, kernel_size=4, stride=2)
self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv3 = nn.Conv2d(64, 128, kernel_size=3, stride=1)
self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)
self.conv4 = nn.Conv2d(128, 256, kernel_size=3, stride=1)
self.pool4 = nn.MaxPool2d(kernel_size=2, stride=2)
# 全连接层用于处理其他特征输入
self.fc1 = nn.Linear(input_dim, 512)
self.fc2 = nn.Linear(512, 256)
self.fc3 = nn.Linear(256, 128)
# 最终的全连接层输出动作的Q值
self.fc4 = nn.Linear(5632, output_dim) # 修改为新的输入维度
def forward(self, image_input, other_input):
# 处理图像输入
x_image = F.relu(self.conv1(image_input))
x_image = self.pool1(x_image)
x_image = F.relu(self.conv2(x_image))
x_image = self.pool2(x_image)
x_image = F.relu(self.conv3(x_image))
x_image = self.pool3(x_image)
x_image = F.relu(self.conv4(x_image))
x_image = self.pool4(x_image)
x_image = x_image.view(x_image.size(0), -1) # 将卷积输出扁平化为 (batch_size, num_features)
flatten_image = x_image.flatten()
# 处理其他特征输入
x_other = F.relu(self.fc1(other_input))
x_other = F.relu(self.fc2(x_other))
x_other = F.relu(self.fc3(x_other))
flatten_x_other = x_other.flatten()
# 将两个特征拼接在一起
x_combined = torch.cat((flatten_image, flatten_x_other), dim=0)
# 最终的全连接层输出Q值
q_values = self.fc4(x_combined)
return q_values
有一说一,这个模型的处理我心里都没啥底(),因为我最后粗暴地把所有数据摊平然后拼接,然后再用全连接层输出,总之就.....搞着玩的()
第三步
然后就是激动人心的训练部分啦,噢噢噢噢!
嗯,众所周知,DQN的训练中比较重要的几个部分是,奖励的设置,经验池的设置,还有软更新的设置,所以我在训练过程中,把能用的都用上了()
首先是奖励的设置,好吧,这个比较重要的地方的设置我其实有点乱来(),其实我的意图是想要鼓励进攻来着,但是好像这么设置也不太好(),嘛,总之这是代码:
def action_judge(boss_blood, next_boss_blood, self_blood, next_self_blood, stop, emergence_break):
# get action reward
# emergence_break is used to break down training
# 用于防止出现意外紧急停止训练防止错误训练数据扰乱神经网络
if next_self_blood < 3: # self dead
reward = -100
done = 1
return reward, done, stop, emergence_break
elif boss_blood - next_boss_blood > 1:
reward = 90
done = 0
return reward, done, stop, emergence_break
elif self_blood - next_self_blood <= 10:
reward = 10
done = 0
return reward, done, stop, emergence_break
elif self_blood - next_self_blood > 10:
reward = -80
done = 0
return reward, done, stop, emergence_break
elif next_boss_blood < 3:
reward = 100
done = 0
return reward, done, stop, emergence_break
else:
reward = 0
done = 0
return reward, done, stop, emergence_break
别问我问什么没单独做韧性条的判断,问就是懒()
然后是经验池,这个其实比较好理解,其实就是储存一下历次的数据,好多次使用老数据,这样提升了数据的使用率,,然后要是经验池满了,那就把老的丢了加上新的,这是代码:
# 定义经验回放缓冲区的容量
buffer_capacity = 100000
experience_buffer = []
experience = (batch_images, batch_features, action, reward, next_self_blood, next_boss_blood, done)
experience_buffer.append(experience)
if len(experience_buffer) > buffer_capacity:
experience_buffer.pop(0)
'''下面是使用经验池的数据进行训练的'''
print("经验池计算")
# 从经验池中采样一批经验
experiences = random.sample(experience_buffer, 1)
# 解压缩经验
batch_images, batch_features, actions, rewards, next_self_blood, next_boss_blood, dones = zip(*experiences)
# 将采样的经验转换为张量
batch_images = torch.stack(batch_images).to(device)
batch_features = torch.stack(batch_features).to(device)
actions = torch.tensor(actions).to(device)
rewards = torch.tensor(rewards).to(device)
next_self_blood = torch.tensor(next_self_blood).to(device)
next_boss_blood = torch.tensor(next_boss_blood).to(device)
dones = torch.tensor(dones).to(device)
# 在当前网络上计算 Q 值
q_values = current_net(batch_images, batch_features)
# 根据采样的动作索引获取预测的 Q 值
predicted_q_values = torch.max(q_values)
# 在目标网络上计算下一个状态的 Q 值
with torch.no_grad():
next_q_values = target_net(batch_images, batch_features)
next_q_max = torch.max(next_q_values)
target_q_values = rewards + gamma * next_q_max
# 计算损失和优化步骤
#print("predicted_q_values",predicted_q_values.shape)
#print("target_q_values",target_q_values.shape)
target_q_value = target_q_value.to(device)
predicted_q_values = torch.unsqueeze(predicted_q_values, 0)
predicted_q_value = predicted_q_value.to(device)
loss = loss_function(predicted_q_values, target_q_values)
optimizer.zero_grad()
loss.backward()
optimizer.step()
最后是,额是啥来着,哦,还有软更新的事,这个其实比较没有存在感(),咳咳,总之让我们看看chatgpt怎么说吧:"
软更新(Soft Update)是在强化学习中常用的一种目标网络更新策略,用于提高深度强化学习算法的稳定性和收敛性。软更新通过逐步地更新目标网络的参数,使其向当前网络的参数靠近,从而减小目标值的变动幅度,提高训练的平稳性。
在深度强化学习中,通常使用两个网络:当前网络(Current Network)和目标网络(Target Network)。当前网络用于估计动作值函数(Q值),而目标网络用于计算目标Q值。由于两个网络的参数是独立更新的,当前网络的参数更新可能会引起目标Q值的剧烈变化,从而导致训练不稳定。
软更新通过引入一个参数(通常称为τ或θ)来控制目标网络的参数更新速度,使得目标网络的参数在更新时逐渐接近当前网络的参数。具体步骤如下:
1. 定义软更新参数τ(或θ),通常取一个较小的值,如0.001。
2. 对于每个训练步骤或训练轮次,更新目标网络的参数:
目标网络参数 = (1 - τ) * 目标网络参数 + τ * 当前网络参数
这里的操作就是逐渐将当前网络的参数加权融合到目标网络的参数中,使目标网络的参数向当前网络的参数靠近。
软更新的效果在于减小目标值的变动幅度,提高了训练的稳定性。通过逐渐调整目标网络的参数,可以使得目标值的变化更加平滑,减少训练过程中的波动。这样可以帮助算法更好地学习到稳定的策略,并提高算法的收敛速度和效果。
软更新在深度强化学习算法中得到广泛应用,如DQN(Deep Q-Network)和DDPG(Deep Deterministic Policy Gradient)等算法中都采用了软更新策略。它是一种简单而有效的技术,可以提高算法的稳定性,并帮助算法更好地收敛到最优策略。
"
嘛,总之在我的代码里,就是要创建两个网络,然后时不时软更新一下,就像这样:
tau = 0.01 # 软更新的权重
current_net = DQN(image_channels, Inputsize, Outputsize)
target_net = DQN(image_channels, Inputsize, Outputsize)
soft_update(target_net, current_net, tau)
结语
好啦,虽然我似乎略过了很多玩意没讲,比如比较关键的一些地方,比如这里:
batch_images = torch.stack(buffer_images)
batch_features = torch.stack(buffer_features)
batch_images = batch_images.to(device)
batch_features = batch_features.to(device)
target_q_values = current_net(batch_images, batch_features)
buffer_images = []
buffer_features = []
current_buffer_size = 0
target_q_value = reward + gamma * torch.max(target_q_values)
predicted_q_value = q_values[action]
target_q_value = target_q_value.to(device)
predicted_q_value = predicted_q_value.to(device)
loss = loss_function(predicted_q_value, target_q_value)
但是无伤大雅(),最后的最后放一段随手录的视频吧
尝试用DQN玩只狼(pytorch框架)