原文:
annas-archive.org/md5/e0caa69bfbd246ee6119f0157bdca923
译者:飞龙
第十章:10. 使用深度递归 Q 网络玩 Atari 游戏
引言
在本章中,我们将介绍 深度递归 Q 网络(DRQNs)及其变种。你将使用 卷积神经网络(CNNs)和 递归神经网络(RNNs)训练 深度 Q 网络(DQN)模型。你将获得使用 OpenAI Gym 包训练强化学习代理玩 Atari 游戏的实践经验。你还将学习如何使用注意力机制分析长时间序列的输入和输出数据。在本章结束时,你将对 DRQNs 有一个清晰的理解,并能够使用 TensorFlow 实现它们。
引言
在上一章中,我们了解到 DQNs 相比传统强化学习技术取得了更高的性能。视频游戏是 DQN 模型表现优异的典型例子。训练一个代理来玩视频游戏对于传统的强化学习代理来说非常困难,因为在训练过程中需要处理和分析大量可能的状态、动作和 Q 值组合。
深度学习算法以处理高维张量而闻名。一些研究人员将 Q 学习技术与深度学习模型相结合,克服了这一局限性,并提出了 DQNs。DQN 模型包含一个深度学习模型,作为 Q 值的函数逼近。此技术在强化学习领域取得了重大突破,因为它有助于处理比传统模型更大的状态空间和动作空间。
从那时起,进一步的研究已展开,设计了不同类型的 DQN 模型,如 DRQNs 或 深度注意力递归 Q 网络(DARQNs)。在本章中,我们将看到 DQN 模型如何从 CNN 和 RNN 模型中受益,这些模型在计算机视觉和自然语言处理领域取得了惊人的成果。我们将在下一节中介绍如何训练这些模型来玩著名的 Atari 游戏《打砖块》。
理解《打砖块》环境
在本章中,我们将训练不同的深度强化学习代理来玩《打砖块》游戏。在深入之前,先了解一下这款游戏。
《打砖块》是一款由 Atari 在 1976 年设计并发布的街机游戏。苹果公司联合创始人 Steve Wozniak 是设计和开发团队的一员。这款游戏在当时非常受欢迎,随着时间的推移,多个版本被开发出来。
游戏的目标是用一个球打破位于屏幕顶部的所有砖块(由于该游戏于 1974 年开发,屏幕分辨率较低,因此球由像素表示,在以下截图中它的形状看起来像一个矩形),而不让球掉下来。玩家可以在屏幕底部水平移动一个挡板,在球掉下之前将其击打回来,并将球弹回砖块。同时,球在撞击侧墙或天花板后会反弹。游戏结束时,如果球掉落(此时玩家失败),或者当所有砖块都被打破,玩家获胜并可进入下一阶段:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_10_01.jpg
图 10.1: 《打砖块》游戏截图
OpenAI 的gym
包提供了一个模拟此游戏的环境,允许深度强化学习智能体在其上进行训练和游戏。我们将使用的环境名称是BreakoutDeterministic-v4
。下面是该环境的一些基本代码实现。
在能够训练智能体玩这个游戏之前,你需要从gym
包中加载《打砖块》环境。为此,我们将使用以下代码片段:
import gym
env = gym.make('BreakoutDeterministic-v4')
这是一个确定性游戏,智能体选择的动作每次都会按预期发生,并且具有4
的帧跳跃率。帧跳跃指的是在执行新动作之前,一个动作会被重复多少帧。
该游戏包括四个确定性的动作,如以下代码所示:
env.action_space
以下是代码的输出结果:
Discrete(4)
观察空间是一个大小为210
x 160
的彩色图像(包含3
个通道):
env.observation_space
以下是代码的输出结果:
Box(210, 160, 3)
要初始化游戏并获取第一个初始状态,我们需要调用.reset()
方法,代码如下所示:
state = env.reset()
从动作空间中采样一个动作(即从所有可能的动作中随机选择一个),我们可以使用.sample()
方法:
action = env.action_space.sample()
最后,要执行一个动作并获取其从环境中返回的结果,我们需要调用.step()
方法:
new_state, reward, is_done, info = env.step(action)
以下截图展示的是执行一个动作后的环境状态的new_state
结果:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_10_02.jpg
图 10.2: 执行动作后的新状态结果
.step()
方法返回四个不同的对象:
-
由前一个动作产生的新环境状态。
-
与前一个动作相关的奖励。
-
一个标志,指示在前一个动作之后游戏是否已经结束(无论是胜利还是游戏结束)。
-
来自环境的其他信息。正如 OpenAI 的说明所述,这些信息不能用于训练智能体。
在完成了一些关于 OpenAI 中《打砖块》游戏的基本代码实现后,接下来我们将进行第一次练习,让我们的智能体来玩这个游戏。
练习 10.01: 使用随机智能体玩《打砖块》
在本练习中,我们将实现一些用于玩 Breakout 游戏的函数,这些函数将在本章剩余部分中非常有用。我们还将创建一个随机动作的智能体:
-
打开一个新的 Jupyter Notebook 文件并导入
gym
库:import gym
-
创建一个名为
RandomAgent
的类,该类接收一个名为env
的输入参数,即游戏环境。该类将拥有一个名为get_action()
的方法,该方法将从环境中返回一个随机动作:class RandomAgent(): def __init__(self, env): self.env = env def get_action(self, state): return self.env.action_space.sample()
-
创建一个名为
initialize_env()
的函数,该函数将返回给定输入环境的初始状态,一个对应于完成标志初始值的False
值,以及作为初始奖励的0
:def initialize_env(env): initial_state = env.reset() initial_done_flag = False initial_rewards = 0 return initial_state, initial_done_flag, initial_rewards
-
创建一个名为
play_game()
的函数,该函数接收一个智能体、一个状态、一个完成标志和一个奖励列表作为输入。该函数将返回收到的总奖励。play_game()
函数将在完成标志为True
之前进行迭代。在每次迭代中,它将执行以下操作:从智能体获取一个动作,在环境中执行该动作,累计收到的奖励,并为下一状态做准备:def play_game(agent, state, done, rewards): while not done: action = agent.get_action(state) next_state, reward, done, _ = env.step(action) state = next_state rewards += reward return rewards
-
创建一个名为
train_agent()
的函数,该函数接收一个环境、一个游戏轮数和一个智能体作为输入。该函数将从collections
包中创建一个deque
对象,并根据提供的轮数进行迭代。在每次迭代中,它将执行以下操作:使用initialize_env()
初始化环境,使用play_game()
玩游戏,并将收到的奖励追加到deque
对象中。最后,它将打印游戏的平均得分:def train_agent(env, episodes, agent): from collections import deque import numpy as np scores = deque(maxlen=100) for episode in range(episodes) state, done, rewards = initialize_env(env) rewards = play_game(agent, state, done, rewards) scores.append(rewards) print(f"Average Score: {np.mean(scores)}")
-
使用
gym.make()
函数实例化一个名为env
的 Breakout 环境:env = gym.make('BreakoutDeterministic-v4')
-
实例化一个名为
agent
的RandomAgent
对象:agent = RandomAgent(env)
-
创建一个名为
episodes
的变量,并将其值设置为10
:episodes = 10
-
通过提供
env
、轮次和智能体来调用train_agent
函数:train_agent(env, episodes, agent)
在训练完智能体后,你将期望达到以下分数(由于游戏的随机性,你的分数可能略有不同):
Average Score: 0.6
随机智能体在 10 轮游戏后取得了较低的分数,即 0.6。我们会认为当智能体的得分超过 10 时,它已经学会了玩这个游戏。然而,由于我们使用的游戏轮数较少,我们还没有达到得分超过 10 的阶段。然而,在这一阶段,我们已经创建了一些玩 Breakout 游戏的函数,接下来我们将重用并更新这些函数。
注意
要访问此部分的源代码,请参考packt.live/30CfVeH
。
你也可以在网上运行此示例,网址为packt.live/3hi12nU
。
在下一节中,我们将学习 CNN 模型以及如何在 TensorFlow 中构建它们。
TensorFlow 中的 CNN
CNN 是一种深度学习架构,在计算机视觉任务中取得了惊人的成果,如图像分类、目标检测和图像分割。自动驾驶汽车是这种技术的实际应用示例。
CNN 的主要元素是卷积操作,其中通过将滤波器应用于图像的不同部分来检测特定的模式,并生成特征图。特征图可以看作是一个突出显示检测到的模式的图像,如下例所示:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_10_03.jpg
图 10.3:垂直边缘特征图示例
CNN 由多个卷积层组成,每个卷积层使用不同的滤波器进行卷积操作。CNN 的最后几层通常是一个或多个全连接层,负责为给定数据集做出正确的预测。例如,训练用于预测数字图像的 CNN 的最后一层将是一个包含 10 个神经元的全连接层。每个神经元将负责预测每个数字(0 到 9)的发生概率:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_10_04.jpg
图 10.4:用于分类数字图像的 CNN 架构示例
使用 TensorFlow 构建 CNN 模型非常简单,这要归功于 Keras API。要定义一个卷积层,我们只需要使用Conv2D()
类,如以下代码所示:
from tensorflow.keras.layers import Conv2D
Conv2D(128, kernel_size=(3, 3), activation="relu")
在前面的例子中,我们创建了一个具有128
个3x3
大小的滤波器(或内核)的卷积层,并使用relu
作为激活函数。
注意
在本章中,我们将使用 ReLU 激活函数来构建 CNN 模型,因为它是最具性能的激活函数之一。
要定义一个全连接层,我们将使用Dense()
类:
from tensorflow.keras.layers import Dense
Dense(units=10, activation='softmax')
在 Keras 中,我们可以使用Sequential()
类来创建一个多层 CNN:
import tensorflow as tf
from tensorflow.keras.layers import Conv2D, Dense
model = tf.keras.Sequential()
model.add(Conv2D(128, kernel_size=(3, 3), activation="relu"), \
input_shape=(100, 100, 3))
model.add(Conv2D(128, kernel_size=(3, 3), activation="relu"))
model.add(Dense(units=100, activation="relu"))
model.add(Dense(units=10, activation="softmax"))
请注意,您只需要为第一个卷积层提供输入图像的维度。定义完模型的各层后,您还需要通过提供损失函数、优化器和要显示的度量标准来编译模型:
model.compile(loss='sparse_categorical_crossentropy', \
optimizer="adam", metrics=['accuracy'])
最后,最后一步是用训练集和指定数量的epochs
来训练 CNN:
model.fit(features_train, label_train, epochs=5)
TensorFlow 中的另一个有用方法是tf.image.rgb_to_grayscale()
,它用于将彩色图像转换为灰度图像:
img = tf.image.rgb_to_grayscale(img)
要调整输入图像的大小,我们将使用tf.image.resize()
方法:
img = tf.image.resize(img, [50, 50])
现在我们知道如何构建 CNN 模型,接下来让我们在以下练习中将其付诸实践。
练习 10.02:使用 TensorFlow 设计 CNN 模型
在本次练习中,我们将使用 TensorFlow 设计一个 CNN 模型。该模型将用于我们在活动 10.01中使用的 DQN 代理,使用 CNN 训练 DQN 玩打砖块游戏,我们将在其中训练这个模型玩打砖块游戏。执行以下步骤来实现这个练习:
-
打开一个新的 Jupyter Notebook 文件并导入
tensorflow
包:import tensorflow as tf
-
从
tensorflow.keras.models
导入Sequential
类:from tensorflow.keras.models import Sequential
-
实例化一个顺序模型并将其保存到变量
model
中:model = Sequential()
-
从
tensorflow.keras.layers
导入Conv2D
类:from tensorflow.keras.layers import Conv2D
-
使用
Conv2D
实例化一个卷积层,设置32
个大小为8
的滤波器,步幅为 4x4,激活函数为 relu,输入形状为(84
,84
,1
)。这些维度与 Breakout 游戏屏幕的大小有关。将其保存到变量conv1
中:conv1 = Conv2D(32, 8, (4,4), activation='relu', \ padding='valid', input_shape=(84, 84, 1))
-
使用
Conv2D
实例化第二个卷积层,设置64
个大小为4
的滤波器,步幅为 2x2,激活函数为relu
。将其保存到变量conv2
中:conv2 = Conv2D(64, 4, (2,2), activation='relu', \ padding='valid')
-
使用
Conv2D
实例化第三个卷积层,设置64
个大小为3
的滤波器,步幅为 1x1,激活函数为relu
。将其保存到变量conv3
中:conv3 = Conv2D(64, 3, (1,1), activation='relu', padding='valid')
-
通过
add()
方法将三个卷积层添加到模型中:model.add(conv1) model.add(conv2) model.add(conv3)
-
从
tensorflow.keras.layers
导入Flatten
类。这个类将调整卷积层输出的大小,转化为一维向量:from tensorflow.keras.layers import Flatten
-
通过
add()
方法将一个实例化的Flatten
层添加到模型中:model.add(Flatten())
-
从
tensorflow.keras.layers
导入Dense
类:from tensorflow.keras.layers import Dense
-
使用
256
个单元实例化一个全连接层,并将激活函数设置为relu
:fc1 = Dense(256, activation='relu')
-
使用
4
个单元实例化一个全连接层,这与 Breakout 游戏中可能的操作数量相对应:fc2 = Dense(4)
-
通过
add()
方法将两个全连接层添加到模型中:model.add(fc1) model.add(fc2)
-
从
tensorflow.keras.optimizers
导入RMSprop
类:from tensorflow.keras.optimizers import RMSprop
-
使用
0.00025
作为学习率实例化一个RMSprop
优化器:optimizer=RMSprop(lr=0.00025)
-
通过在
compile
方法中指定mse
作为损失函数,RMSprop
作为优化器,accuracy
作为训练期间显示的指标,来编译模型:model.compile(loss='mse', optimizer=optimizer, \ metrics=['accuracy'])
-
使用
summary
方法打印模型的摘要:model.summary()
以下是代码的输出:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_10_05.jpg
图 10.5:CNN 模型摘要
输出显示了我们刚刚构建的模型的架构,包括不同的层以及在模型训练过程中使用的参数数量。
注意
要访问此特定部分的源代码,请参考packt.live/2YrqiiZ
。
你也可以在packt.live/3fiNMxE
上在线运行这个示例。
我们已经设计了一个包含三个卷积层的 CNN 模型。在接下来的部分,我们将看到如何将这个模型与 DQN 代理结合使用。
将 DQN 与 CNN 结合
人类通过视觉玩视频游戏。他们观察屏幕,分析情况,并决定最合适的行动。在视频游戏中,屏幕上可能会发生许多事情,因此能够看到所有这些模式可以在游戏中提供显著的优势。将 DQN 与 CNN 结合,可以帮助强化学习智能体根据特定情况学习采取正确的行动。
不仅仅使用全连接层,DQN 模型还可以通过卷积层作为输入来扩展。模型将能够分析输入图像,找到相关模式,并将它们输入到负责预测 Q 值的全连接层,如下所示:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_10_06.jpg
图 10.6:普通 DQN 与结合卷积层的 DQN 之间的区别
添加卷积层有助于智能体更好地理解环境。我们将在接下来的活动中构建的 DQN 智能体将使用练习 10.02中的 CNN 模型,使用 TensorFlow 设计 CNN 模型,以输出给定状态的 Q 值。但我们将使用两个模型,而不是单一模型。这两个模型将共享完全相同的架构。
第一个模型将负责预测玩游戏时的 Q 值,而第二个模型(称为目标模型)将负责学习应当是什么样的最优 Q 值。这种技术帮助目标模型更快地收敛到最优解。
活动 10.01:训练 DQN 与 CNN 一起玩 Breakout
在本活动中,我们将构建一个带有额外卷积层的 DQN,并训练它使用 CNN 玩 Breakout 游戏。我们将为智能体添加经验回放。我们需要预处理图像,以便为 Breakout 游戏创建四张图像的序列。
以下指令将帮助你完成此任务:
-
导入相关的包(
gym
、tensorflow
、numpy
)。 -
对训练集和测试集进行重塑。
-
创建一个包含
build_model()
方法的 DQN 类,该方法将实例化一个由get_action()
方法组成的 CNN 模型,get_action()
方法将应用 epsilon-greedy 算法选择要执行的动作,add_experience()
方法将存储通过玩游戏获得的经验,replay()
方法将执行经验回放,通过从记忆中抽样经验并训练 DQN 模型,update_epsilon()
方法将逐渐减少 epsilon 值以适应 epsilon-greedy 算法。 -
使用
initialize_env()
函数通过返回初始状态、False
表示任务未完成标志、以及0
作为初始奖励来初始化环境。 -
创建一个名为
preprocess_state()
的函数,该函数将对图像执行以下预处理:裁剪图像以去除不必要的部分,将图像转换为灰度图像,并将图像调整为正方形。 -
创建一个名为
play_game()
的函数,该函数将在游戏结束前持续进行游戏,然后存储经验和累积的奖励。 -
创建一个名为
train_agent()
的函数,该函数将通过多个回合进行迭代,代理将在每回合中玩游戏并进行经验回放。 -
实例化一个 Breakout 环境并训练一个 DQN 代理进行
50
个回合的游戏。请注意,由于我们正在训练较大的模型,这一步骤可能需要更长时间才能完成。预期输出将接近这里显示的结果。由于游戏的随机性以及 epsilon-greedy 算法选择执行动作时的随机性,您可能会看到略有不同的值:
[Episode 0] - Average Score: 3.0 Average Score: 0.59
注意
本次活动的解答可以在第 752 页找到。
在下一节中,我们将看到如何通过另一种深度学习架构来扩展这个模型:RNN。
TensorFlow 中的 RNN
在上一节中,我们展示了如何将卷积神经网络(CNN)集成到深度 Q 网络(DQN)模型中,以提高强化学习代理的性能。我们添加了一些卷积层,作为 DQN 模型的全连接层的输入。这些卷积层帮助模型分析游戏环境中的视觉模式,并做出更好的决策。
然而,使用传统的 CNN 方法有一个局限性。CNN 只能分析单张图像。而在玩像 Breakout 这样的电子游戏时,分析图像序列要比分析单张图像更有力,因为它有助于理解球的运动轨迹。这就是 RNN 发挥作用的地方:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_10_07.jpg
图 10.7:RNN 的序列化
RNN 是神经网络的一种特定架构,它处理一系列输入。它们在自然语言处理领域非常流行,用于处理语料库中的文本,例如语音识别、聊天机器人或文本翻译。文本可以被定义为一系列相互关联的单词。仅凭单个单词很难判断一个句子或段落的主题。你必须查看多个单词的序列,才能做出猜测。
有不同类型的 RNN 模型,其中最流行的是门控循环单元(GRU)和长短期记忆(LSTM)。这两种模型都有记忆功能,可以记录模型已经处理过的不同输入(例如,句子的前五个单词),并将它们与新的输入(如句子的第六个单词)结合起来。
在 TensorFlow 中,我们可以按照如下方式构建一个包含10
个单元的LSTM
层:
from tensorflow.keras.layers import LSTM
LSTM(10, activation='tanh', recurrent_activation='sigmoid')
Sigmoid 激活函数是 RNN 模型中最常用的激活函数。
定义GRU
层的语法与此非常相似:
from tensorflow.keras.layers import GRU
GRU(10, activation='tanh', recurrent_activation='sigmoid')
在 Keras 中,我们可以使用 Sequential()
类来创建一个多层 LSTM:
import tensorflow as tf
from tensorflow.keras.layers import LSTM, Dense
model = tf.keras.Sequential()
model.add(LSTM(128, activation='tanh', \
recurrent_activation='sigmoid'))
model.add(Dense(units=100, activation="relu")))
model.add(Dense(units=10, activation="softmax"))
在拟合模型之前,你需要通过提供损失函数、优化器和要显示的度量标准来编译它:
model.compile(loss='sparse_categorical_crossentropy', \
optimizer="adam", metrics=['accuracy'])
我们之前已经看到如何定义 LSTM 层,但为了将其与 CNN 模型结合使用,我们需要在 TensorFlow 中使用一个名为 TimeDistributed()
的封装类。该类用于将相同的指定层应用到输入张量的每个时间步,如下所示:
TimeDistributed(Dense(10))
在前面的示例中,完全连接的层被应用到接收到的每个时间步。在我们的案例中,我们希望在将图像序列输入到 LSTM 模型之前,先对每个图像应用卷积层。为了构建这样的序列,我们需要将多个图像堆叠在一起,以便 RNN 模型可以将其作为输入。现在,让我们进行一个练习,设计一个 CNN 和 RNN 模型的组合。
练习 10.03:设计一个结合 CNN 和 RNN 模型的 TensorFlow 组合
在这个练习中,我们将设计一个结合了 CNN 和 RNN 的模型,该模型将被我们的 DRQN 代理用于 活动 10.02,训练 DRQN 玩 Breakout,以玩 Breakout 游戏:
-
打开一个新的 Jupyter Notebook 并导入
tensorflow
包:import tensorflow as tf
-
从
tensorflow.keras.models
导入Sequential
类:from tensorflow.keras.models import Sequential
-
实例化一个
sequential
模型,并将其保存到名为model
的变量中:model = Sequential()
-
从
tensorflow.keras.layers
导入Conv2D
类:from tensorflow.keras.layers import Conv2D
-
使用
Conv2D
实例化一个卷积层,该层具有32
个大小为8
的滤波器,步长为4
x4
,激活函数为relu
。并将其保存到名为conv1
的变量中:conv1 = Conv2D(32, 8, (4,4), activation='relu', \ padding='valid', input_shape=(84, 84, 1))
-
使用
Conv2D
实例化第二个卷积层,该层具有64
个大小为4
的滤波器,步长为2
x2
,激活函数为relu
。并将其保存到名为conv2
的变量中:conv2 = Conv2D(64, 4, (2,2), activation='relu', \ padding='valid')
-
使用
Conv2D
实例化第三个卷积层,该层具有64
个大小为3
的滤波器,步长为1
x1
,激活函数为relu
。并将其保存到名为conv3
的变量中:conv3 = Conv2D(64, 3, (1,1), activation='relu', \ padding='valid')
-
从
tensorflow.keras.layers
导入TimeDistributed
类:from tensorflow.keras.layers import TimeDistributed
-
实例化一个时间分布层,该层将
conv1
作为输入,输入形状为 (4
,84
,84
,1
)。并将其保存到一个名为time_conv1
的变量中:time_conv1 = TimeDistributed(conv1, input_shape=(4, 84, 84, 1))
-
实例化第二个时间分布层,该层将
conv2
作为输入,并将其保存到名为time_conv2
的变量中:time_conv2 = TimeDistributed(conv2)
-
实例化第三个时间分布层,该层将
conv3
作为输入,并将其保存到名为time_conv3
的变量中:time_conv3 = TimeDistributed(conv3)
-
使用
add()
方法将三个时间分布层添加到模型中:model.add(time_conv1) model.add(time_conv2) model.add(time_conv3)
-
从
tensorflow.keras.layers
导入Flatten
类:from tensorflow.keras.layers import Flatten
-
实例化一个时间分布层,该层将
Flatten()
层作为输入,并将其保存到名为time_flatten
的变量中:time_flatten = TimeDistributed(Flatten())
-
使用
add()
方法将time_flatten
层添加到模型中:model.add(time_flatten)
-
从
tensorflow.keras.layers
导入LSTM
类:from tensorflow.keras.layers import LSTM
-
实例化一个具有
512
单元的 LSTM 层,并将其保存到名为lstm
的变量中:lstm = LSTM(512)
-
使用
add()
方法将 LSTM 层添加到模型中:model.add(lstm)
-
从
tensorflow.keras.layers
导入Dense
类:from tensorflow.keras.layers import Dense
-
实例化一个包含
128
个单元且激活函数为relu
的全连接层:fc1 = Dense(128, activation='relu')
-
使用
4
个单元实例化一个全连接层:fc2 = Dense(4)
-
使用
add()
方法将两个全连接层添加到模型中:model.add(fc1) model.add(fc2)
-
从
tensorflow.keras.optimizers
导入RMSprop
类:from tensorflow.keras.optimizers import RMSprop
-
使用学习率为
0.00025
的RMSprop
实例:optimizer=RMSprop(lr=0.00025)
-
通过在
compile
方法中指定mse
作为损失函数,RMSprop
作为优化器,以及accuracy
作为在训练期间显示的度量,来编译模型:model.compile(loss='mse', optimizer=optimizer, \ metrics=['accuracy'])
-
使用
summary
方法打印模型摘要:model.summary()
以下是代码输出:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_10_08.jpg
图 10.8:CNN+RNN 模型摘要
我们已经成功地将 CNN 模型与 RNN 模型结合。前面的输出展示了我们刚刚构建的模型架构,其中包含不同的层和在训练过程中使用的参数数量。该模型以四张图像的序列作为输入,并将其传递给 RNN,RNN 会分析它们之间的关系,然后将结果传递给全连接层,全连接层将负责预测 Q 值。
注意
要查看该部分的源代码,请访问 packt.live/2UDB3h4
。
你也可以在线运行这个示例,访问 packt.live/3dVrf9T
。
现在我们知道如何构建一个 RNN,我们可以将这个技术与 DQN 模型结合。这样的模型被称为 DRQN,我们将在下一节中探讨这个模型。
构建 DRQN
DQN 可以从 RNN 模型中受益,RNN 可以帮助处理序列图像。这种架构被称为 深度递归 Q 网络 (DRQN)。将 GRU 或 LSTM 模型与 CNN 模型结合,将使强化学习代理能够理解球的运动。为了实现这一点,我们只需在卷积层和全连接层之间添加一个 LSTM(或 GRU)层,如下图所示:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_10_09.jpg
图 10.9:DRQN 架构
为了将图像序列输入到 RNN 模型中,我们需要将多张图像堆叠在一起。对于 Breakout 游戏,在初始化环境之后,我们需要获取第一张图像并将其复制多次,以形成第一组初始图像序列。完成后,在每次动作后,我们可以将最新的图像附加到序列中,并移除最旧的图像,从而保持序列大小不变(例如,最大四张图像的序列)。
活动 10.02:训练 DRQN 玩 Breakout 游戏
在本活动中,我们将通过替换活动 10.01中的 DQN 模型来构建一个 DRQN 模型,使用 CNN 训练 DQN 玩 Breakout 游戏。然后,我们将训练 DRQN 模型来玩 Breakout 游戏,并分析智能体的性能。以下说明将帮助您完成本活动:
-
导入相关的包(
gym
、tensorflow
、numpy
)。 -
重塑训练集和测试集。
-
创建
DRQN
类,并包含以下方法:build_model()
方法用于实例化一个结合 CNN 和 RNN 的模型,get_action()
方法用于应用 epsilon-greedy 算法选择要执行的动作,add_experience()
方法用于将游戏过程中获得的经验存储在记忆中,replay()
方法通过从记忆中采样经验进行经验回放,并每两轮保存一次模型,update_epsilon()
方法用于逐渐减少 epsilon-greedy 中的 epsilon 值。 -
使用
initialize_env()
函数来训练智能体,该函数通过返回初始状态、False
的 done 标志和0
作为初始奖励来初始化环境。 -
创建一个名为
preprocess_state()
的函数,对图像进行以下预处理:裁剪图像以去除不必要的部分,将图像转换为灰度图像,然后将图像调整为方形。 -
创建一个名为
combine_images()
的函数,用于堆叠一系列图像。 -
创建一个名为
play_game()
的函数,该函数将玩一局游戏直到结束,然后存储经验和累积奖励。 -
创建一个名为
train_agent()
的函数,该函数将通过多轮训练让智能体玩游戏并执行经验回放。 -
实例化一个 Breakout 环境,并训练一个
DRQN
智能体进行200
轮游戏。注意
我们建议训练 200 轮(或 400 轮),以便正确训练模型并获得良好的性能,但这可能需要几个小时,具体取决于系统配置。或者,您可以减少训练轮数,这会减少训练时间,但会影响智能体的性能。
预期的输出结果将接近此处显示的内容。由于游戏的随机性和 epsilon-greedy 算法在选择动作时的随机性,您可能会得到稍微不同的值:
[Episode 0] - Average Score: 0.0
[Episode 50] - Average Score: 0.43137254901960786
[Episode 100] - Average Score: 0.4
[Episode 150] - Average: 0.54
Average Score: 0.53
注意
本活动的解决方案可以在第 756 页找到。
在接下来的章节中,我们将看到如何通过将注意力机制添加到 DRQN 中来提高模型的性能,并构建 DARQN 模型。
注意力机制和 DARQN 介绍
在前一部分,我们看到将 RNN 模型添加到 DQN 中有助于提高其性能。RNN 因处理序列数据(如时间信息)而闻名。在我们的案例中,我们使用了 CNN 和 RNN 的组合,帮助我们的强化学习智能体更好地理解来自游戏的图像序列。
然而,RNN 模型在分析长序列输入或输出数据时确实存在一些局限性。为了解决这一问题,研究人员提出了一种叫做注意力机制的技术,这也是深度注意力递归 Q 网络(DARQN)的核心技术。DARQN 模型与 DRQN 模型相同,只是增加了一个注意力机制。为了更好地理解这个概念,我们将通过一个应用实例:神经翻译。神经翻译是将文本从一种语言翻译成另一种语言的领域,例如将莎士比亚的戏剧(原文为英语)翻译成法语。
序列到序列模型最适合此类任务。它们包括两个组件:编码器和解码器。它们都是 RNN 模型,如 LSTM 或 GRU 模型。编码器负责处理输入数据中的一系列词语(在我们之前的例子中,这将是一个英语单词的句子),并生成一个被称为上下文向量的编码版本。解码器将这个上下文向量作为输入,并预测相关的输出序列(在我们的例子中是法语单词的句子):
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_10_10.jpg
图 10.10: 序列到序列模型
上下文向量的大小是固定的。它是输入序列的编码版本,只包含相关信息。你可以将它视为输入数据的总结。然而,这个向量的固定大小限制了模型从长序列中保留足够相关信息的能力。它往往会“遗忘”序列中的早期元素。但在翻译的情况下,句子的开头通常包含非常重要的信息,例如其主语。
注意力机制不仅为解码器提供上下文向量,还提供编码器的前一个状态。这使得解码器能够找到前一个状态、上下文向量和所需输出之间的相关关系。在我们的例子中,这有助于理解输入序列中两个远离彼此的元素之间的关系:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_10_11.jpg
图 10.11: 带有注意力机制的序列到序列模型
TensorFlow 提供了一个Attention
类。它的输入是一个形状为[output, states]
的张量。最好通过使用函数式 API 来使用它,其中每个层作为一个函数接受输入并提供输出结果。在这种情况下,我们可以简单地从 GRU 层提取输出和状态,并将它们作为输入提供给注意力层:
from tensorflow.keras.layers import GRU, Attention
out, states = GRU(512, return_sequences=True, \
return_state=True)(input)
att = Attention()([out, states])
要构建 DARQN 模型,我们只需要将注意力机制添加到 DRQN 模型中。
让我们将这个注意力机制添加到我们之前的 DRQN 代理(在活动 10.02,训练 DRQN 玩 Breakout中),并在下一个活动中构建 DARQN 模型。
活动 10.03:训练 DARQN 玩 Breakout
在本次活动中,我们将通过向之前的 DRQN 中添加一个注意力机制来构建 DARQN 模型(来自活动 10.02,训练 DRQN 玩 Breakout)。然后,我们将训练该模型来玩 Breakout 游戏,并分析代理的表现。以下说明将帮助你完成此活动:
-
导入相关的包(
gym
、tensorflow
和numpy
)。 -
重塑训练集和测试集。
-
创建一个
DARQN
类,包含以下方法:build_model()
方法,它将实例化一个结合了 CNN 和 RNN 的模型(类似于练习 10.03,使用 TensorFlow 设计 CNN 和 RNN 模型的组合);get_action()
方法,它将应用 epsilon-greedy 算法选择要执行的动作;add_experience()
方法,用于将游戏中获得的经验存储到内存中;replay()
方法,它将通过从内存中采样经验并训练 DARQN 模型来执行经验重放,并在每两次回合后保存模型;以及update_epsilon()
方法,用于逐渐减少 epsilon 值以进行 epsilon-greedy。 -
使用
initialize_env()
函数初始化环境,返回初始状态,False
作为 done 标志,以及0
作为初始奖励。 -
使用
preprocess_state()
函数对图像进行以下预处理:裁剪图像以去除不必要的部分,转换为灰度图像,并将图像调整为正方形。 -
创建一个名为
combine_images()
的函数,用于堆叠一系列图像。 -
使用
play_game()
函数进行游戏直到结束,然后存储经验和累积奖励。 -
通过若干回合进行迭代,代理将进行游戏并使用
train_agent()
函数执行经验重放。 -
实例化一个 Breakout 环境并训练一个
DARQN
代理玩这个游戏,共进行400
个回合。注意
我们建议训练 400 个回合,以便正确训练模型并获得良好的性能,但这可能会根据系统配置花费几个小时。或者,你可以减少回合数,这将减少训练时间,但会影响代理的表现。
输出结果将接近你看到的这里。由于游戏的随机性以及 epsilon-greedy 算法在选择行动时的随机性,你可能会看到略有不同的值:
[Episode 0] - Average Score: 1.0
[Episode 50] - Average Score: 2.4901960784313726
[Episode 100] - Average Score: 3.92
[Episode 150] - Average Score: 7.37
[Episode 200] - Average Score: 7.76
[Episode 250] - Average Score: 7.91
[Episode 300] - Average Score: 10.33
[Episode 350] - Average Score: 10.94
Average Score: 10.83
注意
此活动的解答可以在第 761 页找到。
摘要
在本章中,我们学习了如何将深度学习技术与 DQN 模型相结合,并训练它来玩 Atari 游戏《Breakout》。我们首先探讨了如何为智能体添加卷积层,以处理来自游戏的截图。这帮助智能体更好地理解游戏环境。
我们进一步改进了模型,在 CNN 模型的输出上添加了一个 RNN。我们创建了一系列图像并将其输入到 LSTM 层。这种顺序模型使得 DQN 智能体能够“可视化”球的方向。这种模型被称为 DRQN。
最后,我们使用了注意力机制并训练了一个 DARQN 模型来玩《Breakout》游戏。该机制帮助模型更好地理解之前相关的状态,并显著提高了其表现。随着新的深度学习技术和模型的设计,该领域仍在不断发展,这些新技术在不断超越上一代模型的表现。
在下一章,你将接触到基于策略的方法和演员-评论员模型,该模型由多个子模型组成,负责根据状态计算行动并计算 Q 值。
第十一章:11. 基于策略的强化学习方法
概述
在本章中,我们将实现不同的基于策略的强化学习方法(RL),如策略梯度法、深度确定性策略梯度(DDPGs)、信任区域策略优化(TRPO)和近端策略优化(PPO)。你将了解一些算法背后的数学原理,还将学习如何在 OpenAI Gym 环境中为 RL 智能体编写策略代码。在本章结束时,你不仅将对基于策略的强化学习方法有一个基础的理解,而且还将能够使用前面提到的基于策略的 RL 方法创建完整的工作原型。
介绍
本章的重点是基于策略的强化学习方法(RL)。然而,在正式介绍基于策略的强化学习方法之前,我们先花些时间理解它们背后的动机。让我们回到几百年前,那时地球大部分地方还未被探索,地图也不完整。那个时候,勇敢的水手们凭借坚定的勇气和不屈的好奇心航行在广阔的海洋上。但是,他们在辽阔的海洋中并非完全盲目。他们仰望夜空寻找方向。夜空中的星星和行星引导着他们走向目的地。不同时间和地点看到的夜空是不同的。正是这些信息,加上精确的夜空地图,指引着这些勇敢的探险家们到达目的地,有时甚至是未知的、未标记的土地。
现在,你可能会问这个故事与强化学习有什么关系。那些水手们并非总是能够获得夜空的地图。这些地图是由环球旅行者、水手、天文爱好者和天文学家们经过数百年创造的。水手们实际上曾经一度在盲目中航行。他们在夜间观察星星,每次转弯时,他们都会标记自己相对于星星的位置。当到达目的地时,他们会评估每个转弯,并找出哪些转弯在航行过程中更为有效。每一艘驶向相同目的地的船只也可以做同样的事情。随着时间的推移,他们对哪些转弯在相对于船只在海上的位置,结合夜空中星星的位置,能更有效地到达目的地有了较为清晰的评估。你可以把它看作是在计算价值函数,通过这种方式,你能知道最优的即时动作。但一旦水手们拥有了完整的夜空地图,他们就可以简单地推导出一套策略,带领他们到达目的地。
你可以将大海和夜空视为环境,而将水手视为其中的智能体。在几百年的时间里,我们的智能体(水手)建立了对环境的模型,从而能够得出一个价值函数(计算船只相对位置),进而引导他们采取最佳的即时行动步骤(即时航行步骤),并帮助他们建立了最佳策略(完整的航行路线)。
在上一章中,你学习了深度递归 Q 网络(DRQN)及其相较于简单深度 Q 网络的优势。你还为非常流行的雅达利电子游戏Breakout建模了一个 DRQN 网络。在本章中,你将学习基于策略的强化学习方法。
我们还将学习策略梯度,它将帮助你实时学习模型。接着,我们将了解一种名为 DDPG 的策略梯度技术,以便理解连续动作空间。在这里,我们还将学习如何编写月球着陆模拟(Lunar Lander)代码,使用OUActionNoise
类、ReplayBuffer
类、ActorNetwork
类和CriticNetwork
类等类来理解 DDPG。我们将在本章后面详细了解这些类。最后,我们将学习如何通过使用 TRPO、PPO 和优势演员评论家(A2C)技术来改进策略梯度方法。这些技术将帮助我们减少训练模型的运行成本,从而改进策略梯度技术。
让我们从以下小节开始,学习一些基本概念,如基于值的强化学习(RL)、基于模型的强化学习(RL)、演员-评论家方法、动作空间等。
值基和模型基强化学习简介
虽然拥有一个良好的环境模型有助于预测某个特定动作相对于其他可能动作是否更好,但你仍然需要评估每一个可能状态下的所有可能动作,以便制定出最佳策略。这是一个非平凡的问题,如果我们的环境是一个仿真,且智能体是人工智能(AI),那么计算开销也非常大。将基于模型的学习方法应用到仿真中时,可以呈现如下情景。
以乒乓(图 11.1)为例。(乒乓——发布于 1972 年——是雅达利公司制造的第一批街机电子游戏之一。)现在,让我们看看基于模型的学习方法如何有助于为乒乓制定最佳策略,并探讨其可能的缺点。那么,假设我们的智能体通过观察游戏环境(即,观察每一帧的黑白像素)学会了如何玩乒乓。接下来,我们可以要求智能体根据游戏环境中的某一帧黑白像素来预测下一个可能的状态。但如果环境中有任何背景噪音(例如,背景中播放着一个随机的、无关的视频),我们的智能体也会将其考虑在内。
现在,在大多数情况下,这些背景噪声对我们的规划没有帮助——也就是说,确定最优策略——但仍然会消耗我们的计算资源。以下是Pong游戏的截图:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_01.jpg
图 11.1:雅达利 Pong 游戏
基于值的方法优于基于模型的方法,因为在执行从一个状态到另一个状态的转换时,基于值的方法只关心每个动作的价值,基于我们为每个动作预测的累积奖励。它会认为任何背景噪声大多是无关紧要的。基于值的方法非常适合推导最优策略。假设你已经学会了一个动作-值函数——一个 Q 函数。那么,你可以简单地查看每个状态下的最高值,这样就能得出最优策略。然而,基于值的函数仍然可能效率低下。让我用一个例子来解释为什么。在从欧洲到北美,或者从南非到印度南部海岸的旅行中,我们的探索船的最优策略可能只是直接前进。然而,船可能会遇到冰山、小岛或洋流,这些可能会暂时偏离航道。它仍然可能是船只前进的最优策略,但值函数可能会任意变化。所以,在这种情况下,基于值的方法会尝试逼近所有这些任意值,而基于策略的方法可以是盲目的,因此在计算成本上可能更高效。因此,在很多情况下,基于值的函数计算最优策略可能效率较低。
演员-评论员模型简介
所以,我们简要解释了基于值方法和基于模型方法之间的权衡。现在,我们能否以某种方式结合这两者的优点,创建一个它们的混合模型呢?演员-评论员模型将帮助我们实现这一点。如果我们画出一个维恩图(图 11.2),我们会发现演员-评论员模型位于基于值和基于策略的强化学习方法的交集处。它们基本上可以同时学习值函数和策略。我们将在接下来的章节中进一步讨论演员-评论员模型。
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_02.jpg
图 11.2:不同强化学习方法之间的关系
在实践中,大多数时候,我们尝试基于价值函数所产生的值来学习策略,但实际上我们是同时学习策略和值的。为了结束这部分关于演员-评论家方法的介绍,我想分享一句 Bertrand Russell 的名言。Russell 在他的书《哲学问题》中说:“我们可以不通过实例推导一般命题,而仅凭一般命题的意义就能理解它,尽管一些实例通常是必需的,用以帮助我们弄清楚一般命题的含义。” 这句话值得我们思考。关于如何实现演员-评论家模型的代码将在本章后面介绍。接下来,我们将学习动作空间的内容,我们已经在第一章《强化学习导论》中涉及了它的基本概念。
在前面的章节中,我们已经介绍了动作空间的基本定义和类型。在这里,我们将快速回顾一下动作空间的概念。动作空间定义了游戏环境的特性。让我们看一下以下图示来理解这些类型:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_03.jpg
图 11.3:动作空间
动作空间有两种类型——离散和连续。离散动作空间允许离散的输入——例如,游戏手柄上的按钮。这些离散动作可以向左或向右移动,向上或向下移动,向前或向后移动,等等。
另一方面,连续动作空间允许连续输入——例如,方向盘或摇杆的输入。在接下来的章节中,我们将学习如何将策略梯度应用于连续动作空间。
策略梯度
既然我们已经通过上一节中的导航示例阐明了偏好基于策略的方法而非基于价值的方法的动机,那么让我们正式介绍策略梯度。与使用存储缓冲区存储过去经验的 Q 学习不同,策略梯度方法是实时学习的(即它们从最新的经验或动作中学习)。策略梯度的学习是由智能体在环境中遇到的任何情况驱动的。每次梯度更新后,经验都会被丢弃,策略继续前进。让我们看一下我们刚才学到的内容的图示表示:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_04.jpg
图 11.4:策略梯度方法的图示解释
一个立即引起我们注意的事情是,策略梯度方法通常比 Q 学习效率低,因为每次梯度更新后,经验都会被丢弃。策略梯度估计器的数学表示如下:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_05.jpg
图 11.5:策略梯度估计器的数学表示
在这个公式中,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_05a.png 是随机策略,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_05b.png 是我们在时间点 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_05c.png 上的优势估计函数——即所选动作的相对价值估计。期望值,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_05d.png,表示我们算法中有限样本批次的平均值,在其中我们交替进行采样和优化。这里,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_05e.png 是梯度估计器。https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_05f.png 和 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_05g.png 变量定义了在时间间隔 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_05h.png 时的动作和状态。
最后,策略梯度损失定义如下:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_06.jpg
图 11.6:策略梯度损失定义
为了计算优势函数,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_06a.png,我们需要 折扣奖励 和 基准估计。折扣奖励也称为 回报,它是我们智能体在当前回合中获得的所有奖励的加权和。之所以称为折扣奖励,是因为它关联了一个折扣因子,优先考虑即时奖励而非长期奖励。https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_06b.png 本质上是折扣奖励和基准估计之间的差值。
请注意,如果你在理解这个概念时仍然有问题,那也不是大问题。只需尝试抓住整体概念,最终你会理解完整的概念。话虽如此,我还将向你介绍一个简化版的基础策略梯度算法。
我们首先初始化策略参数,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_06c.png,以及基准值,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_06d.png:
for iteration=1, 2, 3, … do
Execute the current policy and collect a set of trajectories
At each timestep in each trajectory, compute
the return Rt and the advantage estimate .
Refit the baseline minimizing ,
summed over all trajectories and timesteps.
Update the policy using the policy gradient estimate
end for
一个建议是反复浏览算法,并配合初步解释,来更好地理解策略梯度的概念。但再说一次,首先掌握整体的理解才是最重要的。
在实现实际元素之前,请通过 PyPI 安装 OpenAI Gym 和 Box2D 环境(包括如 Lunar Lander 等环境)。要进行安装,请在终端/命令提示符中输入以下命令:
pip install torch==0.4.1
pip install pillow
pip install gym "gym[box2d]"
现在,让我们使用策略梯度方法来实现一个练习。
练习 11.01:使用策略梯度和演员-评论员方法将航天器着陆在月球表面
在这个练习中,我们将处理一个玩具问题(OpenAI Lunar Lander),并使用基础策略梯度和演员-评论员方法帮助将月球着陆器着陆到 OpenAI Gym Lunar Lander 环境中。以下是实现此练习的步骤:
-
打开一个新的 Jupyter Notebook,导入所有必要的库(
gym
,torch
和numpy
):import gym import torch as T import numpy as np
-
定义
ActorCritic
类:class ActorCritic(T.nn.Module): def __init__(self): super(ActorCritic, self).__init__() self.transform = T.nn.Linear(8, 128) self.act_layer = T.nn.Linear(128, 4) # Action layer self.val_layer = T.nn.Linear(128, 1) # Value layer self.log_probs = [] self.state_vals = [] self.rewards = []
因此,在前面代码中初始化
ActorCritic
类时,我们正在创建我们的动作和价值网络。我们还创建了空数组来存储对数概率、状态值和奖励。 -
接下来,创建一个函数,将我们的状态通过各个层并命名为
forward
:def forward(self, state): state = T.from_numpy(state).float() state = T.nn.functional.relu(self.transform(state)) state_value = self.val_layer(state) act_probs = T.nn.functional.softmax\ (self.act_layer(state)) act_dist = T.distributions.Categorical(act_probs) action = act_dist.sample() self.log_probs.append(act_dist.log_prob(action)) self.state_vals.append(state_value) return action.item()
在这里,我们将状态传递通过值层,经过 ReLU 转换后得到状态值。类似地,我们将状态通过动作层,然后使用 softmax 函数得到动作概率。接着,我们将概率转换为离散值以供采样。最后,我们将对数概率和状态值添加到各自的数组中并返回一个动作项。
-
创建
computeLoss
函数,首先计算折扣奖励。这有助于优先考虑即时奖励。然后,我们将按照策略梯度损失方程来计算损失:def computeLoss(self, gamma=0.99): rewards = [] discounted_reward = 0 for reward in self.rewards[::-1]: discounted_reward = reward + gamma \ * discounted_reward rewards.insert(0, discounted_reward) rewards = T.tensor(rewards) rewards = (rewards – rewards.mean()) / (rewards.std()) loss = 0 for log_probability, value, reward in zip\ (self.log_probs, self.state_vals, rewards): advantage = reward – value.item() act_loss = -log_probability * advantage val_loss = T.nn.functional.smooth_l1_loss\ (value, reward) loss += (act_loss + val_loss) return loss
-
接下来,创建一个
clear
方法,用于在每回合后清除存储对数概率、状态值和奖励的数组:def clear(self): del self.log_probs[:] del self.state_vals[:] del self.rewards[:]
-
现在,让我们开始编写主代码,这将帮助我们调用之前在练习中定义的类。我们首先分配一个随机种子:
np.random.seed(0)
-
然后,我们需要设置我们的环境并初始化我们的策略:
env = gym.make(""LunarLander-v2"") policy = ActorCritic() optimizer = T.optim.Adam(policy.parameters(), \ lr=0.02, betas=(0.9, 0.999))
-
最后,我们迭代至少
10000
次以确保适当收敛。在每次迭代中,我们采样一个动作并获取该动作的状态和奖励。然后,我们基于该动作更新我们的策略,并清除我们的观察数据:render = True np.random.seed(0) running_reward = 0 for i in np.arange(0, 10000): state = env.reset() for t in range(10000): action = policy(state) state, reward, done, _ = env.step(action) policy.rewards.append(reward) running_reward += reward if render and i > 1000: env.render() if done: break print("Episode {}\tReward: {}".format(i, running_reward)) # Updating the policy optimizer.zero_grad() loss = policy.computeLoss(0.99) loss.backward() optimizer.step() policy.clear() if i % 20 == 0: running_reward = running_reward / 20 running_reward = 0
现在,当你运行代码时,你将看到每个回合的运行奖励。以下是前 20 个回合的奖励,总代码为 10,000 回合:
Episode 0 Reward: -320.65657506841114 Episode 1 Reward: -425.64874914703705 Episode 2 Reward: -671.2867424162646 Episode 3 Reward: -1032.281198268248 Episode 4 Reward: -1224.3354097571892 Episode 5 Reward: -1543.1792365484055 Episode 6 Reward: -1927.4910808775028 Episode 7 Reward: -2023.4599189797761 Episode 8 Reward: -2361.9002491621986 Episode 9 Reward: -2677.470775357419 Episode 10 Reward: -2932.068423127369 Episode 11 Reward: -3204.4024449864355 Episode 12 Reward: -3449.3136628102934 Episode 13 Reward: -3465.3763860613317 Episode 14 Reward: -3617.162199366013 Episode 15 Reward: -3736.83983321837 Episode 16 Reward: -3883.140249551331 Episode 17 Reward: -4100.137703945375 Episode 18 Reward: -4303.308164747067 Episode 19 Reward: -4569.71587308837 Episode 20 Reward: -4716.304224574078
注意
为了方便展示,这里仅展示了前 20 个回合的输出。
要访问该特定部分的源代码,请参阅
packt.live/3hDibst
。本部分目前没有在线互动示例,需要在本地运行。
这个输出表示我们的智能体——月球着陆器,已经开始采取行动。负奖励表明一开始,智能体不够聪明,无法采取正确的行动,因此它采取了随机行动,并因此受到了负面奖励。负奖励是惩罚。随着时间的推移,智能体将开始获得正面奖励,因为它开始学习。很快,你会看到游戏窗口弹出,展示月球着陆器的实时进展,如下图所示:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_07.jpg
图 11.7:月球着陆器的实时进展
在下一部分,我们将研究 DDPG,它扩展了策略梯度的思想。
深度确定性策略梯度
在本节中,我们将应用 DDPG 技术以理解连续动作空间。此外,我们还将学习如何编写月球着陆模拟程序来理解 DDPG。
注意
我们建议你将本节中给出的所有代码输入到你的 Jupyter 笔记本中,因为我们将在后续的练习 11.02中使用它,创建学习智能体。
我们将在这里使用 OpenAI Gym 的 Lunar Lander 环境来处理连续动作空间。让我们首先导入必要的内容:
import os
import gym
import torch as T
import numpy as np
现在,我们将学习如何定义一些类,如OUActionNoise
类、ReplayBuffer
类、ActorNetwork
类和CriticNetwork
类,这些将帮助我们实现 DDPG 技术。在本节结束时,你将获得一个完整的代码库,可以在我们的 OpenAI Gym 游戏环境中应用 DDPG。
Ornstein-Uhlenbeck 噪声
首先,我们将定义一个类,提供一种被称为 Ornstein-Uhlenbeck 噪声的东西。物理学中的 Ornstein–Uhlenbeck 过程用于模拟在摩擦力作用下布朗运动粒子的速度。如你所知,布朗运动是指悬浮在液体或气体中的粒子,由于与同一流体中其他粒子的碰撞而产生的随机运动。Ornstein–Uhlenbeck 噪声提供的是一种具有时间相关性的噪声,并且其均值为 0。由于智能体对模型的了解为零,因此训练它变得很困难。在这种情况下,Ornstein–Uhlenbeck 噪声可以用作生成这种知识的样本。让我们来看一下这个类的代码实现:
class OUActionNoise(object):
def __init__(self, mu, sigma=0.15, theta=.2, dt=1e-2, x0=None):
self.theta = theta
self.mu = mu
self.sigma = sigma
self.dt = dt
self.x0 = x0
self.reset()
def __call__(self):
x = self.x_previous
dx = self.theta * (self.mu –- x) * self.dt + self.sigma \
* np.sqrt(self.dt) * np.random.normal\
(size=self.mu.shape)
self.x_previous = x + dx
return x
def reset(self):
self.x_previous = self.x0 if self.x0 is not None \
else np.zeros_like(self.mu)
在前面的代码中,我们定义了三个不同的函数——即_init_()
、_call()_
和reset()
。在接下来的部分,我们将学习如何实现ReplayBuffer
类来存储智能体的过去学习记录。
ReplayBuffer 类
Replay buffer 是我们从 Q-learning 中借用的一个概念。这个缓冲区本质上是一个存储所有智能体过去学习记录的空间,它将帮助我们更好地训练模型。我们将通过定义状态、动作和奖励的记忆大小来初始化该类。所以,初始化看起来会像这样:
class ReplayBuffer(object):
def __init__(self, max_size, inp_shape, nb_actions):
self.memory_size = max_size
self.memory_counter = 0
self.memory_state = np.zeros\
((self.memory_size, *inp_shape))
self.new_memory_state = np.zeros\
((self.memory_size, *inp_shape))
self.memory_action = np.zeros\
((self.memory_size, nb_actions))
self.memory_reward = np.zeros(self.memory_size)
self.memory_terminal = np.zeros(self.memory_size, \
dtype=np.float32)
接下来,我们需要定义store_transition
方法。这个方法接受状态、动作、奖励和新状态作为参数,并存储从一个状态到另一个状态的过渡。这里还有一个done
标志,用于指示智能体的终止状态。请注意,这里的索引只是一个计数器,我们之前已初始化,它从0
开始,当其值等于最大内存大小时:
def store_transition(self, state, action, \
reward, state_new, done):
index = self.memory_counter % self.memory_size
self.memory_state[index] = state
self.new_memory_state[index] = state_new
self.memory_action[index] = action
self.memory_reward[index] = reward
self.memory_terminal[index] = 1 - done
self.memory_counter += 1
最后,我们需要sample_buffer
方法,它将用于随机抽样缓冲区:
def sample_buffer(self, bs):
max_memory = min(self.memory_counter, self.memory_size)
batch = np.random.choice(max_memory, bs)
states = self.memory_state[batch]
actions = self.memory_action[batch]
rewards = self.memory_reward[batch]
states_ = self.new_memory_state[batch]
terminal = self.memory_terminal[batch]
return states, actions, rewards, states_, terminal
所以,整个类一目了然,应该是这样的:
DDPG_Example.ipynb
class ReplayBuffer(object):
def __init__(self, max_size, inp_shape, nb_actions):
self.memory_size = max_size
self.memory_counter = 0
self.memory_state = np.zeros((self.memory_size, *inp_shape))
self.new_memory_state = np.zeros\
((self.memory_size, *inp_shape))
self.memory_action = np.zeros\
((self.memory_size, nb_actions))
self.memory_reward = np.zeros(self.memory_size)
self.memory_terminal = np.zeros(self.memory_size, \
dtype=np.float32)
The complete code for this example can be found at https://packt.live/2YNL2BO.
在这一部分,我们学习了如何存储智能体的过去学习记录以更好地训练模型。接下来,我们将更详细地学习我们在本章简介中简要解释过的 actor-critic 模型。
Actor-Critic 模型
接下来,在 DDPG 技术中,我们将定义演员和评论家网络。现在,我们已经介绍了演员-评论家模型,但我们还没有详细讨论过它。可以将演员视为当前的策略,评论家则是价值函数。你可以将演员-评论家模型概念化为一个引导策略。我们将使用全连接神经网络来定义我们的演员-评论家模型。
CriticNetwork
类从初始化开始。首先,我们将解释参数。Linear
层,我们将使用输入和输出维度对其进行初始化。接下来是全连接层的权重和偏置的初始化。此初始化将权重和偏置的值限制在参数空间的一个非常窄的范围内,我们在-f1
到f1
之间采样,如下代码所示。这有助于我们的网络更好地收敛。我们的初始层后面跟着一个批量归一化层,这同样有助于更好地收敛网络。我们将对第二个全连接层重复相同的过程。CriticNetwork
类还会获取一个动作值。最后,输出是一个标量值,我们接下来将其初始化为常数初始化。
我们将使用学习率为 beta 的Adam
优化器来优化我们的CriticNetwork
类:
class CriticNetwork(T.nn.Module):
def __init__(self, beta, inp_dimensions,\
fc1_dimensions, fc2_dimensions,\
nb_actions):
super(CriticNetwork, self).__init__()
self.inp_dimensions = inp_dimensions
self.fc1_dimensions = fc1_dimensions
self.fc2_dimensions = fc2_dimensions
self.nb_actions = nb_actions
self.fc1 = T.nn.Linear(*self.inp_dimensions, \
self.fc1_dimensions)
f1 = 1./np.sqrt(self.fc1.weight.data.size()[0])
T.nn.init.uniform_(self.fc1.weight.data, -f1, f1)
T.nn.init.uniform_(self.fc1.bias.data, -f1, f1)
self.bn1 = T.nn.LayerNorm(self.fc1_dimensions)
self.fc2 = T.nn.Linear(self.fc1_dimensions, \
self.fc2_dimensions)
f2 = 1./np.sqrt(self.fc2.weight.data.size()[0])
T.nn.init.uniform_(self.fc2.weight.data, -f2, f2)
T.nn.init.uniform_(self.fc2.bias.data, -f2, f2)
self.bn2 = T.nn.LayerNorm(self.fc2_dimensions)
self.action_value = T.nn.Linear(self.nb_actions, \
self.fc2_dimensions)
f3 = 0.003
self.q = T.nn.Linear(self.fc2_dimensions, 1)
T.nn.init.uniform_(self.q.weight.data, -f3, f3)
T.nn.init.uniform_(self.q.bias.data, -f3, f3)
self.optimizer = T.optim.Adam(self.parameters(), lr=beta)
self.device = T.device(""gpu"" if T.cuda.is_available() \
else ""cpu"")
self.to(self.device)
现在,我们必须为我们的网络编写forward
函数。该函数接受一个状态和一个动作作为输入。我们通过这个方法获得状态-动作值。因此,我们的状态经过第一个全连接层,接着是批量归一化和 ReLU 激活函数。激活值通过第二个全连接层,然后是另一个批量归一化层,在最终激活之前,我们考虑动作值。请注意,我们将状态和动作值相加,形成状态-动作值。然后,状态-动作值通过最后一层,最终得到我们的输出:
def forward(self, state, action):
state_value = self.fc1(state)
state_value = self.bn1(state_value)
state_value = T.nn.functional.relu(state_value)
state_value = self.fc2(state_value)
state_value = self.bn2(state_value)
action_value = T.nn.functional.relu(self.action_value(action))
state_action_value = T.nn.functional.relu\
(T.add(state_value, action_value))
state_action_value = self.q(state_action_value)
return state_action_value
所以,最终,CriticNetwork
类的结构如下所示:
DDPG_Example.ipynb
class CriticNetwork(T.nn.Module):
def __init__(self, beta, inp_dimensions,\
fc1_dimensions, fc2_dimensions,\
nb_actions):
super(CriticNetwork, self).__init__()
self.inp_dimensions = inp_dimensions
self.fc1_dimensions = fc1_dimensions
self.fc2_dimensions = fc2_dimensions
self.nb_actions = nb_actions
The complete code for this example can be found at https://packt.live/2YNL2BO.
接下来,我们将定义ActorNetwork
。它与CriticNetwork
类大致相同,但有一些细微而重要的变化。让我们先编写代码,然后再解释:
class ActorNetwork(T.nn.Module):
def __init__(self, alpha, inp_dimensions,\
fc1_dimensions, fc2_dimensions, nb_actions):
super(ActorNetwork, self).__init__()
self.inp_dimensions = inp_dimensions
self.fc1_dimensions = fc1_dimensions
self.fc2_dimensions = fc2_dimensions
self.nb_actions = nb_actions
self.fc1 = T.nn.Linear(*self.inp_dimensions, \
self.fc1_dimensions)
f1 = 1./np.sqrt(self.fc1.weight.data.size()[0])
T.nn.init.uniform_(self.fc1.weight.data, -f1, f1)
T.nn.init.uniform_(self.fc1.bias.data, -f1, f1)
self.bn1 = T.nn.LayerNorm(self.fc1_dimensions)
self.fc2 = T.nn.Linear(self.fc1_dimensions, \
self.fc2_dimensions)
f2 = 1./np.sqrt(self.fc2.weight.data.size()[0])
T.nn.init.uniform_(self.fc2.weight.data, -f2, f2)
T.nn.init.uniform_(self.fc2.bias.data, -f2, f2)
self.bn2 = T.nn.LayerNorm(self.fc2_dimensions)
f3 = 0.003
self.mu = T.nn.Linear(self.fc2_dimensions, \
self.nb_actions)
T.nn.init.uniform_(self.mu.weight.data, -f3, f3)
T.nn.init.uniform_(self.mu.bias.data, -f3, f3)
self.optimizer = T.optim.Adam(self.parameters(), lr=alpha)
self.device = T.device("gpu" if T.cuda.is_available() \
else "cpu")
self.to(self.device)
def forward(self, state):
x = self.fc1(state)
x = self.bn1(x)
x = T.nn.functional.relu(x)
x = self.fc2(x)
x = self.bn2(x)
x = T.nn.functional.relu(x)
x = T.tanh(self.mu(x))
return x
如你所见,这与我们的CriticNetwork
类类似。这里的主要区别是,我们没有动作值,并且我们以稍微不同的方式编写了forward
函数。请注意,forward
函数的最终输出是一个tanh
函数,它将我们的输出限制在0
和1
之间。这对于我们将要处理的环境是必要的。让我们实现一个练习,帮助我们创建一个学习代理。
练习 11.02:创建一个学习代理
在这个练习中,我们将编写我们的Agent
类。我们已经熟悉学习智能体的概念,那么让我们看看如何实现一个。这个练习将完成我们一直在构建的 DDPG 示例。在开始练习之前,请确保已经运行了本节中的所有示例代码。
注意
我们假设你已经将前一节中呈现的代码输入到新的笔记本中。具体来说,我们假设你已经在笔记本中编写了导入必要库的代码,并创建了OUActionNoise
、ReplayBuffer
、CriticNetwork
和ActorNetwork
类。本练习开始时会创建Agent
类。
为了方便起见,本练习的完整代码,包括示例中的代码,可以在packt.live/37Jwhnq
找到。
以下是实现此练习的步骤:
-
首先使用
__init__
方法,传入 alpha 和 beta,它们分别是我们演员和评论家网络的学习率。然后,传入输入维度和一个名为tau
的参数,我们稍后会解释它。接着,我们希望传入环境,它是我们的连续动作空间,gamma 是智能体的折扣因子,之前我们已经讨论过。然后,传入动作数量、记忆的最大大小、两层的大小和批处理大小。接着,初始化我们的演员和评论家。最后,我们将引入噪声和update_params
函数:class Agent(object): def __init__(self, alpha, beta, inp_dimensions, \ tau, env, gamma=0.99, nb_actions=2, \ max_size=1000000, l1_size=400, \ l2_size=300, bs=64): self.gamma = gamma self.tau = tau self.memory = ReplayBuffer(max_size, inp_dimensions, \ nb_actions) self.bs = bs self.actor = ActorNetwork(alpha, inp_dimensions, \ l1_size, l2_size, \ nb_actions=nb_actions) self.critic = CriticNetwork(beta, inp_dimensions, \ l1_size, l2_size, \ nb_actions=nb_actions) self.target_actor = ActorNetwork(alpha, inp_dimensions, \ l1_size, l2_size, \ nb_actions=nb_actions) self.target_critic = CriticNetwork(beta, inp_dimensions, \ l1_size, l2_size, \ nb_actions=nb_actions) self.noise = OUActionNoise(mu=np.zeros(nb_actions)) self.update_params(tau=1)
update_params
函数更新我们的参数,但有一个关键问题。我们基本上有一个动态目标。这意味着我们使用相同的网络来同时计算动作和动作的值,同时在每个回合中更新估计值。由于我们为两者使用相同的参数,这可能会导致发散。为了解决这个问题,我们使用目标网络,它学习值和动作的组合,另一个网络则用于学习策略。我们将定期用评估网络的参数更新目标网络的参数。 -
接下来,我们有
select_action
方法。在这里,我们从演员(actor)获取观察结果,并将其通过前馈网络传递。这里的mu_prime
基本上是我们添加到网络中的噪声,也称为探索噪声。最后,我们调用actor.train()
并返回mu_prime
的numpy
值:def select_action(self, observation): self.actor.eval() observation = T.tensor(observation, dtype=T.float)\ .to(self.actor.device) mu = self.actor.forward(observation).to(self.actor.device) mu_prime = mu + T.tensor(self.noise(),\ dtype=T.float).to(self.actor.device) self.actor.train() return mu_prime.cpu().detach().numpy()
-
接下来是我们的
remember
函数,它不言自明。这个函数接收state
、action
、reward
、new_state
和done
标志,以便将它们存储到记忆中:def remember(self, state, action, reward, new_state, done): self.memory.store_transition(state, action, reward, \ new_state, done)
-
接下来,我们将定义
learn
函数:def learn(self): if self.memory.memory_counter < self.bs: return state, action, reward, new_state, done = self.memory\ .sample_buffer\ (self.bs) reward = T.tensor(reward, dtype=T.float)\ .to(self.critic.device) done = T.tensor(done).to(self.critic.device) new_state = T.tensor(new_state, dtype=T.float)\ .to(self.critic.device) action = T.tensor(action, dtype=T.float).to(self.critic.device) state = T.tensor(state, dtype=T.float).to(self.critic.device) self.target_actor.eval() self.target_critic.eval() self.critic.eval() target_actions = self.target_actor.forward(new_state) critic_value_new = self.target_critic.forward\ (new_state, target_actions) critic_value = self.critic.forward(state, action) target = [] for j in range(self.bs): target.append(reward[j] + self.gamma\ *critic_value_new[j]*done[j]) target = T.tensor(target).to(self.critic.device) target = target.view(self.bs, 1) self.critic.train() self.critic.optimizer.zero_grad() critic_loss = T.nn.functional.mse_loss(target, critic_value) critic_loss.backward() self.critic.optimizer.step() self.critic.eval() self.actor.optimizer.zero_grad() mu = self.actor.forward(state) self.actor.train() actor_loss = -self.critic.forward(state, mu) actor_loss = T.mean(actor_loss) actor_loss.backward() self.actor.optimizer.step() self.update_params()
在这里,我们首先检查我们是否在内存缓冲区中有足够的样本用于学习。因此,如果我们的内存计数器小于批次大小——意味着我们在内存缓冲区中没有足够的样本——我们将直接返回该值。否则,我们将从内存缓冲区中抽取
state
(状态)、action
(动作)、reward
(奖励)、new_state
(新状态)和done
(结束)标志。一旦采样,我们必须将所有这些标志转换为张量以进行实现。接下来,我们需要计算目标动作,然后使用目标动作状态和新状态计算新的批评者值。然后,我们计算批评者值,这是我们在当前回放缓冲区中遇到的状态和动作的值。之后,我们计算目标。请注意,在我们将gamma
与新的批评者值相乘时,如果done
标志为0
,结果会变为0
。这基本上意味着当回合结束时,我们只考虑当前状态的奖励。然后,目标被转换为张量并重塑,以便实现目的。现在,我们可以计算并反向传播我们的批评者损失。接下来,我们对执行者网络做同样的事。最后,我们更新目标执行者和目标批评者网络的参数。 -
接下来,定义
update_params
函数:def update_params(self, tau=None): if tau is None: tau = self.tau # tau is 1 actor_params = self.actor.named_parameters() critic_params = self.critic.named_parameters() target_actor_params = self.target_actor.named_parameters() target_critic_params = self.target_critic.named_parameters() critic_state_dict = dict(critic_params) actor_state_dict = dict(actor_params) target_critic_dict = dict(target_critic_params) target_actor_dict = dict(target_actor_params) for name in critic_state_dict: critic_state_dict[name] = tau*critic_state_dict[name]\ .clone() + (1-tau)\ *target_critic_dict[name]\ .clone() self.target_critic.load_state_dict(critic_state_dict) for name in actor_state_dict: actor_state_dict[name] = tau*actor_state_dict[name]\ .clone() + (1-tau)\ *target_actor_dict[name]\ .clone() self.target_actor.load_state_dict(actor_state_dict)
在这里,
update_params
函数接受一个tau
值,它基本上允许我们以非常小的步骤更新目标网络。tau
的值通常非常小,远小于1
。需要注意的是,我们从tau
等于1
开始,但后来将其值减少到一个更小的数字。该函数的作用是首先获取批评者、执行者、目标批评者和目标执行者的所有参数名称。然后,它使用目标批评者和目标执行者更新这些参数。现在,我们可以创建 Python 代码的主要部分。 -
如果你已经正确创建了
Agent
类,那么,结合前面的示例代码,你可以使用以下代码初始化我们的学习智能体:env = gym.make("LunarLanderContinuous-v2") agent = Agent(alpha=0.000025, beta=0.00025, \ inp_dimensions=[8], tau=0.001, \ env=env, bs=64, l1_size=400, \ l2_size=300, nb_actions=2) for i in np.arange(100): observation = env.reset() action = agent.select_action(observation) state_new, reward, _, _ = env.step(action) observation = state_new env.render() print("Episode {}\tReward: {}".format(i, reward))
对于输出,你将看到每一回合的奖励。以下是前 10 回合的输出:
Episode 0 Reward: -0.2911892911560017 Episode 1 Reward: -0.4945150137594737 Episode 2 Reward: 0.5150667951556557 Episode 3 Reward: -1.33324749569461 Episode 4 Reward: -0.9969126433110092 Episode 5 Reward: -1.8466220765944854 Episode 6 Reward: -1.6207456680346013 Episode 7 Reward: -0.4027838988393455 Episode 8 Reward: 0.42631743995534066 Episode 9 Reward: -1.1961709218053898 Episode 10 Reward: -1.0679394471159185
你在前面的输出中看到的奖励在负数和正数之间波动。这是因为到目前为止,我们的智能体一直在从它可以采取的所有动作中随机选择。
注意
要访问这一部分的源代码,请参考packt.live/37Jwhnq
。
本节目前没有在线互动示例,需要在本地运行。
在接下来的活动中,我们将让智能体记住它过去的学习内容,并从中学习。以下是游戏环境的样子:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_08.jpg
图 11.8:显示 Lunar Lander 在游戏环境中悬停的输出窗口
然而,你会发现 Lander 并没有尝试着陆,而是悬停在我们的游戏环境中的月球表面。这是因为我们还没有让代理学习。我们将在接下来的活动中做到这一点。
在下一个活动中,我们将创建一个代理,帮助学习一个使用 DDPG 的模型。
活动 11.01:创建一个通过 DDPG 学习模型的代理
在这个活动中,我们将实现本节所学,并创建一个通过 DDPG 学习的代理。
注意
我们已经为实际的 DDPG 实现创建了一个 Python 文件,可以通过 from ddpg import *
作为模块导入。该模块和活动代码可以从 GitHub 下载,网址是 packt.live/2YksdXX
。
以下是执行此活动的步骤:
-
导入必要的库(
os
、gym
和ddpg
)。 -
首先,我们像之前一样创建我们的 Gym 环境(
LunarLanderContinuous-v2
)。 -
使用一些合理的超参数初始化代理,参考 练习 11.02,创建学习代理。
-
设置随机种子,以便我们的实验可重复。
-
创建一个空数组来存储分数;你可以将其命名为
history
。至少迭代1000
次,每次迭代时,将运行分数变量设置为0
,并将done
标志设置为False
,然后重置环境。然后,当done
标志不为True
时,执行以下步骤。 -
从观察中选择一个动作,并获取新的
state
、reward
和done
标志。保存observation
、action
、reward
、state_new
和done
标志。调用代理的learn
函数,并将当前奖励添加到运行分数中。将新的状态设置为观察,最后,当done
标志为True
时,将score
附加到history
。注意
要观察奖励,我们只需添加一个
print
语句。奖励值将类似于前一个练习中的奖励。以下是预期的仿真输出:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_09.jpg
图 11.9:训练 1000 轮后的环境截图
注意
本活动的解决方案可以在第 766 页找到。
在下一节中,我们将看到如何改善刚刚实现的策略梯度方法。
改善策略梯度
在本节中,我们将学习一些有助于改善我们在前一节中学习的策略梯度方法的各种方法。我们将学习诸如 TRPO 和 PPO 等技术。
我们还将简要了解 A2C 技术。让我们在下一节中理解 TRPO 优化技术。
信任域策略优化
在大多数情况下,强化学习对权重初始化非常敏感。例如,学习率。如果我们的学习率太高,那么可能发生的情况是,我们的策略更新将我们的策略网络推向参数空间的一个区域,在该区域中,下一个批次收集到的数据会遇到一个非常差的策略。这可能导致我们的网络再也无法恢复。现在,我们将讨论一些新方法,这些方法试图消除这个问题。但在此之前,让我们快速回顾一下我们已经涵盖的内容。
在 策略梯度 部分,我们定义了优势函数的估计器,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_9a.png,作为折扣奖励与基线估计之间的差异。从直观上讲,优势估计器量化了在某一状态下,代理所采取的行动相较于该状态下通常发生的情况有多好。优势函数的一个问题是,如果我们仅仅根据一批样本使用梯度下降不断更新权重,那么我们的参数更新可能会偏离数据采样的范围。这可能导致优势函数的估计不准确。简而言之,如果我们持续在一批经验上运行梯度下降,我们可能会破坏我们的策略。
确保这一问题不发生的一种方法是确保更新后的策略与旧策略差异不大。这基本上是 TRPO 的核心要点。
我们已经理解了普通策略梯度的梯度估计器是如何工作的:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_10.jpg
图 11.10:普通策略梯度方法
下面是 TRPO 的效果:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_11.jpg
图 11.11:TRPO 的数学表示
这里唯一的变化是,前面公式中的对数运算符被除以 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_11a.png 代替。这就是所谓的 TRPO 目标,优化它将得到与普通策略梯度相同的结果。为了确保新更新的策略与旧策略差异不大,TRPO 引入了一个叫做 KL 约束的限制。
用简单的话来说,这个约束确保我们的新策略不会偏离旧策略太远。需要注意的是,实际的 TRPO 策略提出的是一个惩罚,而不是一个约束。
近端策略优化(PPO)
对于 TRPO,看起来一切都很好,但引入 KL 约束会为我们的策略增加额外的操作成本。为了解决这个问题,并基本上一次性解决普通策略梯度的问题,OpenAI 的研究人员引入了 PPO,我们现在来探讨这个方法。
PPO 背后的主要动机如下:
-
实现的便捷性
-
参数调优的便捷性
-
高效的采样
需要注意的一点是,PPO 方法不使用重放缓冲区来存储过去的经验,而是直接从代理在环境中遇到的情况中进行学习。这也被称为在线
学习方法,而前者——使用重放缓冲区存储过去的经验——被称为离线
学习方法。
PPO 的作者定义了一个概率比率,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_11b.png,它基本上是新旧策略之间的概率比率。因此,我们得到以下内容:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_12.jpg
图 11.12:旧策略和新策略之间的概率比率
当提供一批采样的动作和状态时,如果该动作在当前策略下比在旧策略下更有可能,那么这个比率将大于1
。否则,它将保持在0
和1
之间。现在,PPO 的最终目标写下来是这样的:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_13.jpg
图 11.13:PPO 的最终目标
让我们解释一下。像传统的策略梯度一样,PPO 试图优化期望,因此我们对一批轨迹计算期望算子。现在,这是我们在 TRPO 中看到的修改后的策略梯度目标的最小值,第二部分是它的截断版本。截断操作保持策略梯度目标在https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_13a.png和https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_13b.png之间。这里的https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_13c.png是一个超参数,通常等于0.2
。
虽然这个函数乍一看很简单,但它的巧妙之处非常显著。https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_13d.png可以是负数也可以是正数,分别表示负优势估计和正优势估计。这种优势估计器的行为决定了min
操作符的工作方式。以下是实际 PPO 论文中clip
参数的插图:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_14.jpg
图 11.14:clip 参数插图
左侧的图表示优势为正。这意味着我们的动作产生的结果好于预期。右侧的图表示我们的动作产生的结果低于预期回报。
请注意,在左侧的图中,当r
过高时,损失趋于平坦。这可能发生在当前策略下的动作比旧策略下的动作更有可能时。在这种情况下,目标函数会在这里被截断,从而确保梯度更新不会超过某个限制。
另一方面,当目标函数为负时,当 r
接近 0
时,损失会趋于平缓。这与在当前策略下比旧策略更不可能执行的动作相关。现在你可能已经能理解截断操作如何将网络参数的更新保持在一个理想的范围内。通过实现 PPO 技术来学习它会是一个更好的方法。所以,让我们从一个练习开始,使用 PPO 来降低我们策略的操作成本。
练习 11.03:使用 PPO 改进月球着陆器示例
在本练习中,我们将使用 PPO 实现月球着陆器示例。我们将遵循几乎与之前相同的结构,如果你已经完成了之前的练习和示例,你将能够轻松跟进这个练习:
-
打开一个新的 Jupyter Notebook,并导入必要的库(
gym
、torch
和numpy
):import gym import torch as T import numpy as np
-
按照在 DDPG 示例中的做法设置我们的设备:
device = T.device("cuda:0" if T.cuda.is_available() else "cpu")
-
接下来,我们将创建
ReplayBuffer
类。在这里,我们将创建数组来存储动作、状态、对数概率、奖励和终止状态:class ReplayBuffer: def __init__(self): self.memory_actions = [] self.memory_states = [] self.memory_log_probs = [] self.memory_rewards = [] self.is_terminals = [] def clear_memory(self): del self.memory_actions[:] del self.memory_states[:] del self.memory_log_probs[:] del self.memory_rewards[:] del self.is_terminals[:]
-
现在,我们将定义我们的
ActorCritic
类。我们将首先定义action
和value
层:class ActorCritic(T.nn.Module): def __init__(self, state_dimension, action_dimension, \ nb_latent_variables): super(ActorCritic, self).__init__() self.action_layer = T.nn.Sequential\ (T.nn.Linear(state_dimension, \ nb_latent_variables),\ T.nn.Tanh(),\ T.nn.Linear(nb_latent_variables, \ nb_latent_variables),\ T.nn.Tanh(),\ T.nn.Linear(nb_latent_variables, \ action_dimension),\ T.nn.Softmax(dim=-1)) self.value_layer = T.nn.Sequential\ (T.nn.Linear(state_dimension, \ nb_latent_variables),\ T.nn.Tanh(), \ T.nn.Linear(nb_latent_variables, \ nb_latent_variables),\ T.nn.Tanh(),\ T.nn.Linear(nb_latent_variables, 1))
-
现在,我们将定义方法,从动作空间中采样并评估动作的对数概率、状态值和分布的熵:
# Sample from the action space def act(self, state, memory): state = T.from_numpy(state).float().to(device) action_probs = self.action_layer(state) dist = T.distributions.Categorical(action_probs) action = dist.sample() memory.memory_states.append(state) memory.memory_actions.append(action) memory.memory_log_probs.append(dist.log_prob(action)) return action.item() # Evaluate log probabilities def evaluate(self, state, action): action_probs = self.action_layer(state) dist = T.distributions.Categorical(action_probs) action_log_probs = dist.log_prob(action) dist_entropy = dist.entropy() state_value = self.value_layer(state) return action_log_probs, \ T.squeeze(state_value), dist_entropy
最后,
ActorCritic
类看起来像这样:Exercise11_03.ipynb class ActorCritic(T.nn.Module): def __init__(self, state_dimension, \ action_dimension, nb_latent_variables): super(ActorCritic, self).__init__() self.action_layer = T.nn.Sequential(T.nn.Linear\ (state_dimension, \ nb_latent_variables),\ T.nn.Tanh(), \ T.nn.Linear(nb_latent_variables, \ nb_latent_variables),\ T.nn.Tanh(),\ T.nn.Linear(nb_latent_variables, \ action_dimension),\ T.nn.Softmax(dim=-1)) The complete code for this example can be found at https://packt.live/2zM1Z6Z.
-
现在,我们将使用
__init__()
和update()
函数定义我们的Agent
类。首先让我们定义__init__()
函数:class Agent: def __init__(self, state_dimension, action_dimension, \ nb_latent_variables, lr, betas, gamma, \ K_epochs, eps_clip): self.lr = lr self.betas = betas self.gamma = gamma self.eps_clip = eps_clip self.K_epochs = K_epochs self.policy = ActorCritic(state_dimension,\ action_dimension,\ nb_latent_variables)\ .to(device) self.optimizer = T.optim.Adam\ (self.policy.parameters(), \ lr=lr, betas=betas) self.policy_old = ActorCritic(state_dimension,\ action_dimension,\ nb_latent_variables)\ .to(device) self.policy_old.load_state_dict(self.policy.state_dict()) self.MseLoss = T.nn.MSELoss()
-
现在让我们定义
update
函数:def update(self, memory): # Monte Carlo estimate rewards = [] discounted_reward = 0 for reward, is_terminal in \ zip(reversed(memory.memory_rewards), \ reversed(memory.is_terminals)): if is_terminal: discounted_reward = 0 discounted_reward = reward + \ (self.gamma * discounted_reward) rewards.insert(0, discounted_reward)
-
接下来,规范化奖励并将其转换为张量:
rewards = T.tensor(rewards).to(device) rewards = (rewards - rewards.mean()) \ / (rewards.std() + 1e-5) # Convert to Tensor old_states = T.stack(memory.memory_states)\ .to(device).detach() old_actions = T.stack(memory.memory_actions)\ .to(device).detach() old_log_probs = T.stack(memory.memory_log_probs)\ .to(device).detach() # Policy Optimization for _ in range(self.K_epochs): log_probs, state_values, dist_entropy = \ self.policy.evaluate(old_states, old_actions)
-
接下来,找到概率比,找到损失并向后传播我们的损失:
# Finding ratio: pi_theta / pi_theta__old ratios = T.exp(log_probs - old_log_probs.detach()) # Surrogate Loss advantages = rewards - state_values.detach() surr1 = ratios * advantages surr2 = T.clamp(ratios, 1-self.eps_clip, \ 1+self.eps_clip) * advantages loss = -T.min(surr1, surr2) \ + 0.5*self.MseLoss(state_values, rewards) \ - 0.01*dist_entropy # Backpropagation self.optimizer.zero_grad() loss.mean().backward() self.optimizer.step()
-
使用新权重更新旧策略:
# New weights to old policy self.policy_old.load_state_dict(self.policy.state_dict())
所以,在本练习的步骤 6-10中,我们通过初始化我们的策略、优化器和旧策略来定义一个智能体。然后,在
update
函数中,我们首先采用状态奖励的蒙特卡洛估计。奖励规范化后,我们将其转换为张量。然后,我们进行
K_epochs
次策略优化。在这里,我们需要找到概率比,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_14a.png,即新策略和旧策略之间的概率比,如前所述。之后,我们找到损失,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_14b.png,并向后传播我们的损失。最后,我们使用新权重更新旧策略。
-
现在,我们可以像在上一个练习中一样运行模拟,并保存策略以供将来使用:
Exercise11_03.ipynb
env = gym.make("LunarLander-v2")
render = False
solved_reward = 230
logging_interval = 20
update_timestep = 2000
np.random.seed(0)
memory = ReplayBuffer()
agent = Agent(state_dimension=env.observation_space.shape[0],\
action_dimension=4, nb_latent_variables=64, \
lr=0.002, betas=(0.9, 0.999), gamma=0.99,\
K_epochs=4, eps_clip=0.2)
current_reward = 0
avg_length = 0
timestep = 0
for i_ep in range(50000):
state = env.reset()
for t in range(300):
timestep += 1
The complete code for this example can be found at https://packt.live/2zM1Z6Z.
以下是输出的前 10 行:
Episode 0, reward: -8
Episode 20, reward: -182
Episode 40, reward: -154
Episode 60, reward: -175
Episode 80, reward: -136
Episode 100, reward: -178
Episode 120, reward: -128
Episode 140, reward: -137
Episode 160, reward: -140
Episode 180, reward: -150
请注意,我们会在一定间隔保存我们的策略。如果你想稍后加载该策略并从那里运行模拟,这会非常有用。此练习的模拟输出将与图 11.9相同,唯一不同的是这里操作成本已被降低。
在这里,如果你观察奖励之间的差异,由于我们使用了 PPO 技术,每个连续回合中给予的积分要小得多。这意味着学习并没有像在练习 11.01、使用策略梯度和演员-评论员方法将航天器降落在月球表面中那样失控,因为在那个例子中,奖励之间的差异更大。
注
要访问本节的源代码,请参考packt.live/2zM1Z6Z
。
本节目前没有在线互动示例,需要在本地运行。
我们几乎涵盖了与基于策略的强化学习相关的所有重要主题。所以,接下来我们将讨论最后一个话题——A2C 方法。
优势演员-评论员方法
我们已经在介绍中学习了演员-评论员方法及其使用原因,并且在我们的编码示例中也见过它的应用。简单回顾一下——演员-评论员方法位于基于价值和基于策略方法的交集处,我们同时更新我们的策略和我们的价值,后者充当着衡量我们策略实际效果的评判标准。
接下来,我们将学习 A2C 是如何工作的:
-
我们首先通过随机权重初始化策略参数,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_14c.png。
-
接下来,我们使用当前策略进行N步的操作,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_14d.png,并存储状态、动作、奖励和转移。
-
如果我们到达状态的最终回合,我们将奖励设置为
0
;否则,我们将奖励设置为当前状态的值。 -
然后,我们通过从最后一个回合反向循环,计算折扣奖励、策略损失和价值损失。
-
最后,我们应用随机梯度下降(SGD),使用每个批次的平均策略和值损失。
-
从步骤 2开始,重复执行直到达到收敛。
注
我们在这里仅简要介绍了 A2C。该方法的详细描述和实现超出了本书的范围。
我们讨论的第一个编码示例(练习 11.01、使用策略梯度和演员-评论员方法将航天器降落在月球表面)遵循了基本的 A2C 方法。然而,还有另一种技术,叫做异步优势演员-评论员(A3C)方法。记住,我们的策略梯度方法是在线工作的。也就是说,我们只在使用当前策略获得的数据上进行训练,并且不跟踪过去的经验。然而,为了保持我们的数据是独立同分布的,我们需要一个大的转移缓冲区。A3C 提供的解决方案是并行运行多个训练环境,以获取大量训练数据。借助 Python 中的多进程,这实际上在实践中非常快速。
在下一个活动中,我们将编写代码来运行我们在 练习 11.03 中学习的月球着陆器仿真,使用 PPO 改进月球着陆器示例。我们还将渲染环境以查看月球着陆器。为此,我们需要导入 PIL 库。渲染图像的代码如下:
if render:
env.render()
img = env.render(mode = "rgb_array")
img = Image.fromarray(img)
image_dir = "./gif"
if not os.path.exists(image_dir):
os.makedirs(image_dir)
img.save(os.path.join(image_dir, "{}.jpg".format(t)))
让我们从最后一个活动的实现开始。
活动 11.02:加载已保存的策略以运行月球着陆器仿真
在这个活动中,我们将结合之前章节中解释的 RL 多个方面。我们将利用在 练习 11.03 中学到的知识,使用 PPO 改进月球着陆器示例,编写简单代码来加载已保存的策略。这个活动结合了构建工作 RL 原型的所有基本组件——在我们的案例中,就是月球着陆器仿真。
需要执行的步骤如下:
-
打开 Jupyter,并在新笔记本中导入必要的 Python 库,包括 PIL 库来保存图像。
-
使用
device
参数设置你的设备。 -
定义
ReplayBuffer
、ActorCritic
和Agent
类。我们已经在前面的练习中定义过这些类。 -
创建月球着陆器环境。初始化随机种子。
-
创建内存缓冲区并初始化代理与超参数,就像在前一个练习中一样。
-
将已保存的策略加载为旧策略。
-
最后,循环遍历你希望的回合数。在每次迭代开始时,将回合奖励初始化为
0
。不要忘记重置状态。再执行一个循环,指定max
时间戳。获取每个动作所采取的state
、reward
和done
标志,并将奖励加入回合奖励中。 -
渲染环境以查看你的月球着陆器运行情况。
以下是预期的输出:
Episode: 0, Reward: 272 Episode: 1, Reward: 148 Episode: 2, Reward: 249 Episode: 3, Reward: 169 Episode: 4, Reward: 35
以下屏幕截图展示了某些阶段的仿真输出:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_15.jpg
](https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_11_15.jpg)
图 11.15:展示月球着陆器仿真环境
完整的仿真输出可以在 packt.live/3ehPaAj
以图像形式找到。
注意
该活动的解决方案可以在第 769 页找到。
总结
在本章中,我们学习了基于策略的方法,主要是值方法(如 Q 学习)的缺点,这些缺点促使了策略梯度的使用。我们讨论了 RL 中基于策略的方法的目的,以及其他 RL 方法的权衡。
你已经了解了帮助模型在实时环境中学习的策略梯度。接下来,我们学习了如何使用演员-评论员模型、ReplayBuffer
类以及奥恩斯坦-乌伦贝克噪声实现 DDPG,以理解连续动作空间。我们还学习了如何通过使用 TRPO 和 PPO 等技术来改进策略梯度。最后,我们简要讨论了 A2C 方法,它是演员-评论员模型的一个高级版本。
此外,在本章中,我们还在 OpenAI Gym 中的月球着陆器环境中进行了一些实验——包括连续和离散动作空间——并编写了我们讨论过的多种基于策略的强化学习方法。
在下一章中,我们将学习一种无梯度的优化方法,用于优化神经网络和基于强化学习的算法。随后,我们将讨论基于梯度的方法的局限性。本章通过遗传算法提出了一种替代梯度方法的优化方案,因为它们能够确保全局最优收敛。我们还将学习使用遗传算法解决复杂问题的混合神经网络。
第十二章:12. 强化学习的进化策略
概述
本章中,我们将识别梯度方法的局限性以及进化策略的动机。我们将分解遗传算法的组成部分,并将其应用于强化学习(RL)。在本章结束时,你将能够将进化策略与传统机器学习方法结合,特别是在选择神经网络超参数时,同时识别这些进化方法的局限性。
引言
在上一章中,我们讨论了各种基于策略的方法及其优势。在本章中,我们将学习无梯度方法,即遗传算法;逐步开发这些算法,并利用它们优化神经网络和基于 RL 的算法。本章讨论了梯度方法的局限性,如在局部最优解处停滞,以及在处理噪声输入时收敛速度较慢。本章通过遗传算法提供了梯度方法的替代优化解决方案,因为遗传算法确保全局最优收敛。你将研究并实现遗传算法的结构,并通过神经网络的超参数选择和网络拓扑结构的演化来实现它们,同时将其与 RL 结合用于平衡车杆活动。使用遗传算法的混合神经网络用于解决复杂问题,如建模等离子体化学反应器、设计分形频率选择性表面或优化生产过程。在接下来的部分中,你将审视梯度方法带来的问题。
梯度方法的问题
在本节中,你将了解基于价值的方法与基于策略的方法的区别,以及在策略搜索算法中使用梯度方法。你将进一步分析在基于策略的方法中使用梯度方法的优缺点,并通过 TensorFlow 实现随机梯度下降,以解决带有两个未知数的立方函数。
强化学习有两种方法:基于价值的方法和基于策略的方法。这些方法用于解决与马尔可夫决策过程(MDPs)和部分可观察马尔可夫决策过程(POMDPs)相关的复杂决策问题。基于价值的方法依赖于通过识别最优价值函数来确定和推导最优策略。像 Q-learning 或 SARSA(λ)这样的算法属于这一类,对于涉及查找表的任务,它们的实现能导致全局最优的回报收敛。由于这些算法依赖于已知的环境模型,因此对于部分可观察或连续空间,使用这些价值搜索方法时无法保证收敛到最优解。
相反,基于策略的方法不是依赖于价值函数来最大化回报,而是使用梯度方法(随机优化)来探索策略空间。基于梯度的方法或策略梯度方法通过使用损失函数将参数化空间(环境)映射到策略空间,从而使 RL 代理能够直接探索整个策略空间或其一部分。其中最常用的方法(将在本节中实现)是梯度下降。
注意
有关梯度下降的进一步阅读,请参考Marbach, 2001的技术论文,链接如下:https://link.springer.com/article/10.1023/A:1022145020786。
梯度方法(随机梯度下降或上升法)的优点是它们适用于 POMDP 或非 MDP 问题,尤其是在解决具有多个约束的机器人问题时。然而,采用基于梯度的方法也有几个缺点。最显著的一个缺点是像REINFORCE和DPG这样的算法只能确定期望回报的局部最优解。当局部最优解被找到时,RL 代理不会进行全局搜索。例如,解决迷宫问题的机器人可能会被困在一个角落,并且会不断尝试在同一位置移动。此外,在处理高回报方差或噪声输入数据时,算法的性能会受到影响,因为它们的收敛速度较慢。例如,当一个机器人臂被编程为捡起并放置一个蓝色部件到托盘中,但桌面颜色带有蓝色调,导致传感器(如摄像头)无法正确识别部件时,算法的表现就会受到干扰。
注意
有关REINFORCE算法的进一步阅读,请参考Williams, 1992的技术论文,链接如下:https://link.springer.com/article/10.1007/BF00992696。
同样,请阅读Silvester, 2014的DPG算法,链接如下:http://proceedings.mlr.press/v32/silver14.pdf。
基于梯度的方法的替代方案是使用无梯度方法,这些方法依赖于进化算法来实现回报的全局最优解。
以下练习将帮助你理解梯度方法在收敛到最优解过程中的潜力以及在方法逐步寻找最优解时所需的漫长过程。你将面对一个数学函数(损失函数),它将输入值,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_00a.png,映射到输出值,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_00b.png。目标是确定输入的最优值,以使输出值达到最低;然而,这个过程依赖于每一步,并且存在停留在局部最优解的风险。我们将使用GradientTape()
函数来计算梯度,这实际上就是求导的解决方案。这将帮助你理解这种优化策略的局限性。
练习 12.01:使用随机梯度下降法进行优化
本练习旨在使你能够应用梯度方法,最著名的 随机梯度下降法 (SGD),并通过遵循所需步骤收敛到最优解。
以下损失函数有两个未知数, https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_00c.png:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_01.jpg
图 12.1:示例损失函数
在 100 步内,使用学习率 0.1
查找 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_01a.png 的最优值。
执行以下步骤以完成练习:
-
创建一个新的 Jupyter Notebook。
-
将
tensorflow
包导入为tf
:import tensorflow as tf
-
定义一个输出 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_01b.png 的函数:
def funct(x,y): return x**2-8*x+y**2+3*y
-
定义一个初始化
x
和y
变量的函数,并将它们初始化为值5
和10
:def initialize(): x = tf.Variable(5.0) y = tf.Variable(10.0) return x, y x, y= initialize()
-
在上述代码片段中,我们使用了十进制格式为
x
和y
分配初始值,以启动优化过程,因为Variable()
构造函数需要具有float32
类型的张量。 -
通过选择 TensorFlow 中
keras
的SGD
来实例化优化器,并输入学习率 0.1:optimizer = tf.keras.optimizers.SGD(learning_rate = 0.1)
-
设置一个
100
步的循环,其中你计算损失,使用GradientTape()
函数进行自动微分,并处理梯度:for i in range(100): with tf.GradientTape() as tape: # Calculate loss function using x and y values loss= funct(x,y) # Get gradient values gradients = tape.gradient(loss, [x, y]) # Save gradients in array without altering them p_gradients = [grad for grad in gradients]
在前面的代码中,我们使用了 TensorFlow 的
GradientTape()
来计算梯度(本质上是微分解)。我们创建了一个损失参数,当调用该函数时,存储了 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_01c.png 值。GradientTape()
在调用gradient()
方法时激活,后者用于在单次计算中计算多个梯度。梯度被存储在p_gradients
数组中。 -
使用
zip()
函数将梯度与值聚合:ag = zip(p_gradients, [x,y])
-
打印步骤和 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_01d.png 的值:
print('Step={:.1f} , z ={:.1f},x={:.1f},y={:.1f}'\ .format(i, loss.numpy(), x.numpy(), y.numpy()))
-
使用已处理的梯度应用优化器:
optimizer.apply_gradients(ag)
-
运行应用程序。
你将得到以下输出:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_02.jpg
图 12.2:使用 SGD 逐步优化
你可以在输出中看到,从 Step=25
开始, https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_02a.png 的值没有变化;因此,它们被认为是相应损失函数的最优值。
通过打印输入和输出的步骤和值,你可以观察到算法在 100 步之前就收敛到最优值 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_02a.png。然而,你可以观察到问题是与步骤相关的:如果优化在全局最优收敛之前停止,得到的解将是次优解。
注意
要访问此特定部分的源代码,请参考 packt.live/2C10rXD
。
你也可以在 packt.live/2DIWSqc
在线运行此示例。
这项练习帮助你理解并应用 SGD 来求解损失函数,提升了你的分析能力以及使用 TensorFlow 编程的技能。这将有助于你选择优化算法,让你理解基于梯度方法的局限性。
在本节中,我们探讨了梯度方法在强化学习(RL)算法中的优缺点,识别了它们在决策过程中的适用问题类型。示例展示了梯度下降法的简单应用,通过使用 SGD 优化算法在 TensorFlow 中找到了两个未知数的最优解。在下一节中,我们将探讨一种不依赖梯度的优化方法:遗传算法。
遗传算法简介
由于梯度方法的一个问题是解决方案可能会卡在某个局部最优点,其他方法如不依赖梯度的算法可以作为替代方案。在本节中,你将学习关于不依赖梯度的方法,特别是进化算法(例如遗传算法)。本节概述了遗传算法实现的步骤,并通过练习教你如何实现进化算法来求解上一节给出的损失函数。
当存在多个局部最优解或需要进行函数优化时,推荐使用不依赖梯度的方法。这些方法包括进化算法和粒子群优化。此类方法的特点是依赖于一组优化解,这些解通常被称为种群。方法通过迭代搜索找到一个良好的解或分布,从而解决问题或数学函数。寻找最优解的模式基于达尔文的自然选择范式以及遗传进化的生物学现象。进化算法从生物进化模式中汲取灵感,如突变、繁殖、重组和选择。粒子群算法受到群体社会行为的启发,比如蜜蜂巢或蚁群,在这些群体中,单一的解被称为粒子,能够随着时间演化。
自然选择的前提是遗传物质(染色体)以某种方式编码了物种的生存。物种的进化依赖于它如何适应外部环境以及父母传递给子代的信息。在遗传物质中,不同代之间会发生变异(突变),这些变异可能导致物种成功或不成功地适应环境(尤其在恶劣环境下)。因此,遗传算法有三个步骤:选择、繁殖(交叉)和突变。
演化算法通过创建一个原始解的种群、选择一个子集,并使用重组或突变来获得不同的解来进行处理。这一新解集可以部分或完全替代原始解集。为了实现替代,这些解会经历一个基于适应度分析的选择过程。这增加了更适合用于开发新解集的解的机会。
除了解的开发外,演化算法还可以通过使用概率分布来进行参数适应。仍然会生成种群;然而,使用适应度方法来选择分布的参数,而不是实际的解。在确定新参数后,新的分布将用于生成新的解集。以下是一些参数选择的策略:
-
在估算出原始种群的参数梯度后,使用自然梯度上升,也称为自然进化策略(NESes)。
-
选择具有特定参数的解,并使用该子集的均值来寻找新的分布均值,这被称为交叉熵优化(CEO)。
-
根据每个解的适应度赋予权重,使用加权平均值作为新的分布均值 —— 协方差矩阵适应进化策略(CMAESes)。
演化策略中发现的一个主要问题是,实现解的适应度可能会在计算上昂贵且噪声较大。
遗传算法(GAs)保持解种群,并通过多个方向进行搜索(通过染色体),进一步促进这些方向上的信息交换。算法最常见的实现是字符串处理,字符串可以是二进制或基于字符的。主要的两个操作是突变和交叉。后代的选择基于解与目标(目标函数)的接近程度,这表示它们的适应度。
总体而言,遗传算法(GAs)有以下几个步骤:
-
种群创建。
-
适应度得分的创建并分配给种群中的每个解。
-
选择两个父代进行繁殖,依据适应度得分(可能是表现最好的解)。
-
通过结合和重新组织两个父代的代码,创建两个子代解。
-
应用随机突变。
-
孩子代的生成会重复进行,直到达到新的种群规模,并为种群分配权重(适应度得分)。
-
该过程将重复进行,直到达到最大代数或实现目标性能。
我们将在本章中进一步详细查看这些步骤。
在梯度算法和遗传算法之间有许多差异,其中一个差异是开发过程。基于梯度的算法依赖于微分,而遗传算法则使用选择、繁殖和变异等遗传过程。以下练习将使你能够实现遗传算法并评估其性能。你将使用一个简单的遗传算法在 TensorFlow 中进行优化,找到tensorflow_probability
包的最佳解决方案。
练习 12.02:使用遗传算法实现固定值和均匀分布优化
在本练习中,你仍然需要像前一个练习那样求解以下函数:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_03.jpg
图 12.3:样本损失函数
找到种群大小为 100 时的https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_03a.png的最优值,初始值为https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_03b.png,然后扩展到从类似于基于梯度方法的分布中随机抽样。
本练习的目标是让你分析应用遗传算法(GAs)和梯度下降方法的差异,从一对变量和多种潜在解决方案开始。该算法通过应用选择、交叉和变异来帮助优化问题,达到最优或接近最优的解决方案。此外,你将从一个均匀分布中抽样 100 个样本的值https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_00c.png。在本练习结束时,你将评估从固定变量开始和从分布中抽样之间的差异:
-
创建一个新的 Jupyter Notebook。
-
导入
tensorflow
包,并下载和导入tensorflow_probability
:import tensorflow as tf import tensorflow_probability as tfp
-
定义一个函数,输出https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_03d.png:
def funct(x,y): return x**2-8*x+y**2+3*y
-
通过定义值为 5 和 10 的https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_03e.png变量来确定初始步长:
initial_position = (tf.Variable(5.0), tf.Variable(10.0))
-
通过选择名为
differential_evolution_minimize
的tensorflow_probability
优化器来实例化优化器:optimizer1 = tfp.optimizer.differential_evolution_minimize\ (funct, initial_position = initial_position, \ population_size = 100, \ population_stddev = 1.5, seed = 879879)
-
使用
objective_value
和position
函数打印https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_03f.png的最终值:print('Final solution: z={:.1f}, x={:.1f}, y={:.1f}'\ .format(optimizer1.objective_value.numpy(),\ optimizer1.position[0].numpy(), \ optimizer1.position[1].numpy()))
-
运行应用程序。你将获得以下输出。你可以观察到最终的值与图 12.2中
Step=25.0
的值是相同的:Final solution: z=-18.2, x=4.0, y=-1.5
在本练习中,将显示最终的最优解。不需要额外的优化步骤来达到与基于梯度的方法相同的解决方案。你可以看到,你使用的代码行数更少,并且算法收敛所需的时间更短。
对于均匀优化,修改代码的步骤如下:
-
导入
random
包:import random
-
初始化种群大小,并通过从种群大小的随机均匀分布中抽样https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_03h.png变量来创建初始种群:
size = 100 initial_population = (tf.random.uniform([size]), \ tf.random.uniform([size]))
-
使用相同的优化器,将
initial_position
参数更改为initial_population
;使用相同的种子:optimizer2 = tfp.optimizer.differential_evolution_minimize\ (funct, initial_population= initial_population,\ seed=879879)
-
使用
objective_value
和position
函数打印最终值!12:print('Final solution: z={:.1f}, x={:.1f}, y={:.1f}'\ .format(optimizer2.objective_value.numpy(),\ optimizer2.position[0].numpy(),\ optimizer2.position[1].numpy()))
输出将如下所示:
Final solution: z=-18.2, x=4.0, y=-1.5
尽管值有所不同,你仍然会得到相同的结果。这意味着我们可以随机采样或选择一组特定的初始值,遗传算法仍然会更快地收敛到最优解,这意味着我们可以通过使用比梯度法更少的代码行来改进我们的代码。
注意
若要访问该特定部分的源代码,请参考packt.live/2MQmlPr
。
你也可以在线运行这个例子,访问packt.live/2zpH6hJ
。
该解将收敛到最优值,无论初始起点如何,无论是使用固定的输入值还是随机采样的染色体种群。
本节提供了进化算法的概述,解释了进化策略和遗传算法(GA)之间的区别。你有机会使用tensorflow_probabilities
包实现差分进化,以优化损失函数的解法,分析了两种不同技术的实现:从固定输入值开始和使用输入值的随机采样。你还可以评估遗传算法与梯度下降方法的实施。遗传算法可以使用独立的起始值,并且其收敛到全局最优解的速度更快,不容易受到梯度下降方法的干扰,而梯度下降方法是逐步依赖的,并且对输入变量更敏感。
在接下来的部分中,我们将基于开发遗传算法的原则,首先从种群创建的角度开始。
组件:种群创建
在上一节中,你已经了解了用于函数优化的进化方法。在本节中,我们将重点讨论种群创建、适应度得分创建以及创建遗传算法的任务。
种群,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_03g.png,被识别为一组个体或染色体:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_04.jpg
图 12.4:种群表达式
这里,s
代表染色体的总数(种群大小),i
是迭代次数。每个染色体是以抽象形式表示的、对所提出问题的可能解决方案。对于二元问题,种群可以是一个随机生成的包含一和零的矩阵。
染色体是输入变量(基因)的组合:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_05.jpg
图 12.5:染色体表达式
这里,m
是基因(或变量)的最大数量。
转换为代码后,种群创建可以如下所示:
population = np.zeros((no_chromosomes, no_genes))
for i in range(no_chromosomes):
ones = random.randint(0, no_genes)
population[i, 0:ones] = 1
np.random.shuffle(population[i])
然后,每个染色体将通过适应度函数进行比较:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_06.jpg
图 12.6:适应度函数
适应度函数可以通过如下方式转化为代码:
identical_to_target = population == target
函数的输出是一个分数,表示染色体与目标(最优解)之间的接近程度。目标通过最大化适应度函数来表示。有些优化问题依赖于最小化一个成本函数。这个函数可以是数学函数、热力学模型,或者计算机游戏。这可以通过考虑权重较低(分数较低)的染色体,或者将成本函数转化为适应度函数来完成。
一旦适应度函数被识别并定义,进化过程就可以开始。初始种群被生成。初始种群的一个特点是多样性。为了提供这种多样性,元素可以是随机生成的。为了使种群进化,迭代过程从选择适应度最佳的父代开始,进而启动繁殖过程。
练习 12.03:种群创建
在这个练习中,我们将创建一个原始的二进制染色体种群,长度为 5。每个染色体应该包含八个基因。我们将定义一个目标解,并输出每个染色体与目标的相似度。这个练习的目的是让你设计并建立 GA 的第一组步骤,并找到适应目标的二进制解。这个练习类似于将一个控制系统的输出与目标匹配:
-
创建一个新的 Jupyter Notebook。导入
random
和numpy
库:import random import numpy as np
-
创建一个生成随机种群的函数:
# create function for random population def original_population(chromosomes, genes): #initialize the population with zeroes population = np.zeros((chromosomes, genes)) #loop through each chromosome for i in range(chromosomes): #get random no. of ones to be created ones = random.randint(0, genes) #change zeroes to ones population[i, 0:ones] = 1 #shuffle rows np.random.shuffle(population[i]) return population
-
定义一个创建目标解的函数:
def create_target_solution(gene): #assume that there is an equal number of ones and zeroes counting_ones = int(gene/2) # build array with equal no. of ones and zeros target = np.zeros(gene) target[0:counting_ones] = 1 # shuffle the array to mix zeroes and ones np.random.shuffle(target) return target
-
定义一个函数来计算每个染色体的适应度权重:
def fitness_function(target,population): #create an array of true/false compared to the reference identical_to_target = population == target #sum no. of genes that are identical fitness_weights = identical_to_target.sum(axis = 1) return fitness_weights
在前面的代码中,你正在将种群中的每个染色体与目标进行比较,并将相似度以布尔值的形式记录下来——如果相似则为
True
,如果不同则为False
,这些值保存在名为identical_to_target
的矩阵中。统计所有为True
的元素,并将它们作为权重输出。 -
初始化种群,包含
5
个染色体和8
个基因,并计算weights
:#population of 5 chromosomes, each having 8 genes population = original_population(5,8) target = create_target_solution(8) weights = fitness_function(target,population)
在前面的代码中,我们根据三个开发的函数计算
population
、target
和weights
。 -
使用
for
循环打印目标解、染色体的索引、染色体和权重:print('\n target:', target) for i in range(len(population)): print('Index:', i, '\n chromosome:', population[i],\ '\n similarity to target:', weights[i])
-
运行程序,你将得到类似如下的输出,因为种群元素是随机化的:
target: [0\. 0\. 1\. 1\. 1\. 0\. 0\. 1.] Index: 0 chromosome: [1\. 1\. 1\. 1\. 1\. 0\. 1\. 1.] similarity to target: 5 Index: 1 chromosome: [1\. 0\. 1\. 1\. 1\. 0\. 0\. 0.] similarity to target: 6 Index: 2 chromosome: [1\. 0\. 0\. 0\. 0\. 0\. 0\. 0.] similarity to target: 3 Index: 3 chromosome: [0\. 0\. 0\. 1\. 1\. 0\. 1\. 0.] similarity to target: 5 Index: 4 chromosome: [1\. 0\. 0\. 1\. 1\. 1\. 0\. 1.] similarity to target: 5
你会注意到,每个染色体都会与目标进行比较,并且相似度(基于适应度函数)会被打印出来。
注意
要访问此特定部分的源代码,请参阅packt.live/2zrjadT
。
你也可以在线运行这个例子,网址是packt.live/2BSSeEG
。
本节展示了遗传算法开发的第一步:生成随机种群、为种群中的每个元素(染色体)分配适应度分数,以及获得与目标最匹配的元素数量(在此情况下与最优解的相似度最高)。接下来的章节将扩展代码生成,直到达到最优解。为了实现这一点,在下一节中,你将探索用于繁殖过程的父代选择。
组成部分:父代选择
前一节展示了种群的概念;我们讨论了创建目标解并将其与种群中的元素(染色体)进行比较。这些概念已在一个练习中实现,接下来将在本节继续。在本节中,你将探索选择的概念,并实现两种选择策略。
对于繁殖过程(这是遗传算法的核心部分,因为它依赖于创建更强的后代染色体),有三个步骤:
-
父代选择
-
混合父代以创建新的子代(交叉)
-
用子代替代种群中的父代
选择本质上是选择两个或更多父代进行混合过程。一旦选择了适应度标准,就需要决定如何进行父代选择,以及将从父代中产生多少子代。选择是进行遗传进化的重要步骤,因为它涉及确定适应度最高的子代。选择最佳个体的最常见方法是通过“适者生存”。这意味着算法将逐步改进种群。遗传算法的收敛性依赖于选择更高适应度的染色体的程度。因此,收敛速度高度依赖于染色体的成功选择。如果优先选择适应度最高的染色体,可能会找到一个次优解;如果候选染色体的适应度始终较低,那么收敛将非常缓慢。
可用的选择方法如下:
-
自上而下配对:这是指创建一个染色体列表,并将其两两配对。奇数索引的染色体与偶数索引的染色体配对,从而生成母-父对。列表顶部的染色体会被选中。
-
随机选择:这涉及使用均匀分布的数字生成器来选择父代。
-
随机加权选择或轮盘赌法:这涉及到计算染色体相对于整个种群的适应度概率。父代的选择是随机进行的。概率(权重)可以通过排名或适应度来确定。第一种方法(见图 12.7)依赖于染色体的排名 (https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_06a.png),它可以作为染色体在种群列表中的索引,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_06b.png 表示所需的染色体数量(父代):https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_07.jpg
图 12.7:基于排名的概率
第二种方法(见图 12.8)依赖于染色体的适应度 (https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_07a.png) 与整个种群适应度之和的比较(https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_07b.png):
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_08.jpg
图 12.8:基于染色体适应度的概率
另外,概率(见图 12.9)也可以基于染色体的适应度 (https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_08a.png) 与种群中最高适应度的比较来计算 (https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_08b.png)。在所有这些情况下,概率将与随机选择的数字进行比较,以识别具有最佳权重的父代:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_09.jpg
图 12.9:基于种群中最高适应度的概率
- 锦标赛选择法:该方法基于从染色体子集中随机选择染色体,其中适应度最高的染色体被选为父代。这个过程会重复,直到确定所需数量的父代。
轮盘赌和锦标赛技术是遗传算法中最常用的选择方法,因为它们受到生物过程的启发。轮盘赌方法的问题是它可能会有噪音,而且根据所使用的选择类型,收敛速度可能会受到影响。锦标赛方法的一个优点是它可以处理大规模种群,从而实现更平滑的收敛。轮盘赌方法用于在种群中加入随机元素,而当你希望识别与目标最相似的父代时,则使用锦标赛方法。以下练习将帮助你实现锦标赛和轮盘赌技术,并评估你对它们的理解。
练习 12.04:实现锦标赛和轮盘赌选择技术
在本练习中,你将实现锦标赛选择和轮盘选择方法,针对的是 Exercise 12.02, Implementing Fixed Value and Uniform Distribution Optimization Using GAs 中的二进制染色体种群。每个染色体应该包含八个基因。我们将定义一个目标解决方案,并打印出两组父母:一组基于锦标赛方法,另一组基于轮盘选择,从剩余种群中选出。每次选择父母后,适应度排名都将设为最小值:
-
创建一个新的 Jupyter Notebook,导入
random
和numpy
库:import random import numpy as np
-
创建一个用于生成随机种群的函数:
# create function for random population def original_population(chromosomes, genes): #initialize the population with zeroes population = np.zeros((chromosomes, genes)) #loop through each chromosome for i in range(chromosomes): #get random no. of ones to be created ones = random.randint(0, genes) #change zeroes to ones population[i, 0:ones] = 1 #shuffle rows np.random.shuffle(population[i]) return population
-
定义一个函数,用于创建目标解决方案:
def create_target_solution(gene): #assume that there is an equal number of ones and zeroes counting_ones = int(gene/2) # build array with equal no. of ones and zeros target = np.zeros(gene) target[0:counting_ones] = 1 # shuffle the array to mix zeroes and ones np.random.shuffle(target) return target
-
定义一个函数,用于计算每个染色体的适应度权重:
def fitness_function(target,population): #create an array of true/false compared to the reference identical_to_target = population == target #sum no. of genes that are identical fitness_weights = identical_to_target.sum(axis = 1) return fitness_weights
-
定义一个函数,用于选择权重最高(适应度评分最高)的父母对。由于种群规模缩小,染色体之间的竞争更加激烈。这个方法也被称为锦标赛选择:
# select the best parents def select_parents(population, weights): #identify the parent with the highest weight parent1 = population[np.argmax(weights)] #replace weight with the minimum number weights[np.argmax(weights)] = 0 #identify the parent with the second-highest weight parent2 = population[np.argmax(weights)] return parent1, parent2
-
创建一个轮盘函数,通过从均匀分布中选择一个随机数来实现:
def choice_by_roulette(sorted_population, fitness): normalised_fitness_sum = 0 #get a random draw probability draw = random.uniform(0,1) prob = []
-
在函数中,计算所有适应度评分的总和:
for i in range(len(fitness)): normalised_fitness_sum += fitness[i]
-
计算染色体的适应度概率,与所有适应度评分的总和以及与适应度最高的染色体相比:
ma = 0 n = 0 # calculate the probability of the fitness selection for i in range(len(sorted_population)): probability = fitness[i]/normalised_fitness_sum #compare fitness to the maximum fitness and track it prob_max = fitness[i]/np.argmax(fitness) prob.append(probability) if ma < prob_max: ma = prob_max n = i
-
遍历所有染色体,选择适应度概率更高的父母,条件是其适应度评分总和高于
draw
,或者其适应度评分比最大适应度评分的父母概率更高:for i in range(len(sorted_population)): if draw <= prob[i]: fitness[i] = 0 return sorted_population[i], fitness else: fitness[n] = 0 return sorted_population[n], fitness
-
初始化
population
,计算target
和适应度评分,并打印出评分和target
:population = original_population(5,8) target = create_target_solution(8) weights = fitness_function(target,population) print(weights) print('\n target:', target)
你将会得到类似于这样的输出:
[5 1 5 3 4]
-
应用第一个选择方法,并打印出父母和新的评分:
print('\n target:', target) parents = select_parents(population,weights) print('Parent 1:', parents[0],'\nParent 2:', parents[1]) print(weights)
你将会看到锦标赛选择过程的类似输出:
target: [0\. 1\. 1\. 1\. 1\. 0\. 0\. 0.] Parent 1: [1\. 1\. 1\. 1\. 1\. 0\. 1\. 1.] Parent 2: [1\. 1\. 1\. 1\. 1\. 1\. 1\. 0.] [0 1 5 3 4]
你可以观察到,对于父母 1,分数已被替换为
0
。对于父母 2,分数保持不变。 -
使用轮盘函数选择下一个父母对,并打印出父母和权重:
parent3, weights = choice_by_roulette(population, weights) print('Parent 3:', parent3, 'Weights:', weights) parent4, weights = choice_by_roulette(population, weights) print('Parent 4:', parent4,'Weights:', weights)
你将会看到类似于这样的输出:
0.8568696148662779 [0.0, 0.07692307692307693, 0.38461538461538464, 0.23076923076923078, 0.3076923076923077] Parent 3: [1\. 1\. 1\. 1\. 1\. 1\. 1\. 0.] Weights: [0 1 0 3 4] 0.4710306341255527 [0.0, 0.125, 0.0, 0.375, 0.5] Parent 4: [0\. 0\. 1\. 0\. 1\. 1\. 1\. 0.] Weights: [0 1 0 3 0]
你可以看到父母 2 和父母 3 是相同的。这一次,父母的权重被修改为 0。此外,父母 4 被选中,并且它的权重也被改为 0。
注意
若要访问该部分的源代码,请参考 packt.live/2MTsKJO
。
你也可以在线运行这个示例,网址是 packt.live/2YrwMhP
。
通过这个练习,你实现了一种类似锦标赛的方法,通过选择得分最高的父代,以及轮盘选择技术。同时,你还开发了一种避免重复选择相同染色体的方法。第一组父代是使用第一种方法选择的,而第二种方法用于选择第二组父代。我们还发现需要一种替换索引的方法,以避免重复选择相同的染色体,这是选择过程中可能出现的陷阱之一。这帮助你理解了这两种方法之间的差异,并使你能够将与遗传算法相关的方法从种群生成到选择实际运用。
组件:交叉应用
本节扩展了通过交叉将父代的遗传代码重组到子代中的方法(即通过结合和重新组织两个父代的代码创建两个子代解决方案)。可以使用各种技术来创建新的解决方案,从而生成新的种群。机器学习中两个有效解的二进制信息可以通过一种称为交叉的过程重新组合,这类似于生物遗传交换,其中遗传信息从父代传递到子代。交叉确保了解决方案的遗传物质传递到下一代。
交叉是最常见的繁殖技术或交配方式。在父代(选定染色体)的第一个和最后一个基因之间,交叉点表示二进制代码的分裂点,这些代码将传递给子代(后代):第一个父代交叉点左侧的部分将由第一个子代继承,而第二个父代交叉点右侧的部分将成为第一个子代的一部分。第二个父代的左侧部分与第一个父代的右侧部分结合,形成第二个子代:
child1 = np.hstack((parent1[0:p],parent2[p:]))
child2 = np.hstack((parent2[0:p], parent1[p:]))
有多种交叉技术,如下所示:
-
单点交叉(你可以在前面的代码中看到)涉及在一个点上分割父代的遗传代码,并将第一部分传递给第一个子代,第二部分传递给第二个子代。传统遗传算法使用这种方法;交叉点对于两个染色体是相同的,并且是随机选择的。
-
两点交叉涉及两个交叉点,影响两个父代之间的基因交换。引入更多的交叉点可能会降低遗传算法的性能,因为遗传信息会丧失。然而,采用两点交叉可以更好地探索状态或参数空间。
-
多点交叉涉及多个分裂。如果分裂次数是偶数,那么分裂点是随机选择的,染色体中的各部分会交换。如果次数是奇数,则交换的部分是交替进行的。
-
均匀交叉涉及随机选择(如同抛硬币一样)一个父代,提供染色体(基因)中的某个元素。
-
三父代交叉涉及对比两个父代的每个基因。如果它们的值相同,子代继承该基因;如果不同,子代从第三个父代继承该基因。
请参考以下代码示例:
def crossover_reproduction(parents, population):
#define parents separately
parent1 = parents[0]
parent2 = parents[1]
#randomly assign a point for cross-over
p = random.randrange(0, len(population))
print("Crossover point:", p)
#create children by joining the parents at the cross-over point
child1 = np.hstack((parent1[0:p],parent2[p:]))
child2 = np.hstack((parent2[0:p], parent1[p:]))
return child1, child2
在前面的代码中,我们定义了两个父代之间的交叉函数。我们分别定义了父代,然后随机指定一个交叉点。接着,我们定义了通过在定义的交叉点将父代结合起来创建子代。
在接下来的练习中,你将继续实现遗传算法的组件,创建子代染色体。
练习 12.05:为新一代实施交叉
在这个练习中,我们将实现两个父代之间的交叉,以生成新的子代。按照练习 12.04:实现锦标赛和轮盘赌中的步骤,并使用权重最高的染色体,我们将应用单点交叉来创建第一组新的子代:
-
创建一个新的 Jupyter Notebook。导入
random
和numpy
库:import random import numpy as np
-
创建一个随机种群的函数:
def original_population(chromosomes, genes): #initialize the population with zeroes population = np.zeros((chromosomes, genes)) #loop through each chromosome for i in range(chromosomes): #get random no. of ones to be created ones = random.randint(0, genes) #change zeroes to ones population[i, 0:ones] = 1 #shuffle rows np.random.shuffle(population[i]) return population
如你在前面的代码中所见,我们已经创建了一个
population
函数。 -
定义一个函数来创建目标解:
def create_target_solution(gene): #assume that there is an equal number of ones and zeroes counting_ones = int(gene/2) # build array with equal no. of ones and zeros target = np.zeros(gene) target[0:counting_ones] = 1 # shuffle the array to mix zeroes and ones np.random.shuffle(target) return target
-
定义一个函数来计算每个染色体的适应度权重:
def fitness_function(target,population): #create an array of true/false compared to the reference identical_to_target = population == target #sum no. of genes that are identical fitness_weights = identical_to_target.sum(axis = 1) return fitness_weights
-
定义一个函数来选择具有最高权重(最高适应度得分)的父代对。由于种群较小,染色体之间的竞争更为激烈。此方法也被称为锦标赛选择:
# select the best parents def select_parents(population, weights): #identify the parent with the highest weight parent1 = population[np.argmax(weights)] #replace weight with the minimum number weights[np.argmax(weights)] = 0 #identify the parent with the second-highest weight parent2 = population[np.argmax(weights)] return parent1, parent2
-
定义一个使用随机选择的交叉点的交叉函数:
def crossover_reproduction(parents, population): #define parents separately parent1 = parents[0] parent2 = parents[1] #randomly assign a point for cross-over p = random.randrange(0, len(population)) print("Crossover point:", p) #create children by joining the parents at the cross-over point child1 = np.hstack((parent1[0:p],parent2[p:])) child2 = np.hstack((parent2[0:p], parent1[p:])) return child1, child2
-
初始化种群,设置
5
个染色体和8
个基因,并计算权重
:population = original_population(5,8) target = create_target_solution(8) weights = fitness_function(target,population)
-
打印
目标
解:print('\n target:', target)
输出结果如下:
target: [1\. 0\. 0\. 1\. 1\. 0\. 1\. 0.]
-
选择权重最高的父代并打印最终选择:
parents = select_parents(population,weights) print('Parent 1:', parents[0],'\nParent 2:', parents[1])
输出结果如下:
Parent 1: [1\. 0\. 1\. 1\. 1\. 0\. 1\. 1.] Parent 2: [1\. 0\. 0\. 0\. 0\. 0\. 0\. 0.]
-
应用
crossover
函数并打印子代:children = crossover_reproduction(parents,population) print('Child 1:', children[0],'\nChild 2:', children[1])
输出结果如下:
Crossover point: 4 Child 1: [1\. 0\. 1\. 1\. 0\. 0\. 0\. 0.] Child 2: [1\. 0\. 0\. 0\. 1\. 0\. 1\. 1.]
-
运行应用程序。
你将获得与以下代码片段相似的输出。正如你所见,种群元素是随机化的。检查
Child 1
和Child 2
的元素是否与Parent 1
和Parent 2
的元素相同:target: [1\. 0\. 1\. 1\. 0\. 0\. 1\. 0.] . . . Parent 1: [1\. 0\. 1\. 1\. 1\. 0\. 1\. 1.] Parent 2: [0\. 0\. 1\. 1\. 0\. 1\. 0\. 0.] . . . Crossover point: 1 Child 1: [1\. 0\. 1\. 1\. 0\. 1\. 0\. 0.] Child 2: [0\. 0\. 1\. 1\. 1\. 0\. 1\. 1.]. . .
你可以检查Child 1
的交叉点之后的元素与Parent 2
的数组元素是否相同,且Child 2
的元素与Parent 1
的数组元素相同。
注意
要访问此特定部分的源代码,请参考packt.live/30zHbup
。
你也可以在网上运行这个示例:packt.live/3fueZxx
。
在本节中,我们识别了称为交叉的重新组合技术的各种策略。展示了随机生成交叉点的单点交叉的基本实现。在接下来的章节中,我们将讨论遗传算法设计的最后一个元素:群体变异。
组件:群体变异
在前面的章节中,你已经实现了群体生成、父代选择和交叉繁殖。本节将集中讨论随机变异的应用,以及重复生成子代直到达到新的群体大小,并为遗传算法群体分配权重(适应度评分)。本节将包括对变异技术的解释。接下来将介绍可用的变异技术,并讨论群体替换。最后,将提供一个实施变异技术的练习。
梯度方法的一个警告是算法可能会停留在局部最优解。为了防止这种情况发生,可以向解决方案群体引入变异。变异通常发生在交叉过程之后。变异依靠随机分配二进制信息,可以是在染色体集合中,也可以是在整个群体中。变异通过引入群体中的随机变化来提供问题空间的探索途径。这种技术防止了快速收敛,并鼓励探索新的解决方案。在最后几代(最后的世代)或者达到最优解时,变异不再被应用。
有各种变异技术,如下:
-
单点变异(翻转)涉及随机选择不同染色体的基因,并将它们的二进制值更改为它们的相反值(从 0 到 1,从 1 到 0)。
-
交换涉及选择一个父代染色体的两个部分并交换它们,从而生成一个新的子代。
-
你还可以在父代或染色体群体中随机选择一个段落进行反转,所有的二进制值都会变成它们的相反值。
变异的发生由其概率决定。概率定义了在群体内发生变异的频率。如果概率为 0%,那么在交叉后,子代不变;如果发生变异,染色体或整个群体的一部分将被改变。如果概率为 100%,则整个染色体都将被改变。
变异过程发生后,计算新子代的适应度,并将它们包含在群体中。这导致了群体的新一代。根据使用的策略,适应度最低的父代被丢弃,以给新生成的子代腾出位置。
练习 12.06:使用变异开发新的子代
在这个练习中,我们将专注于新一代的开发。我们将再次创建一个新种群,选择两个父代染色体,并使用交叉操作来生成两个子代。然后我们将这两个新染色体添加到种群中,并以 0.05 的概率对整个种群进行突变:
-
创建一个新的 Jupyter Notebook。导入
random
和numpy
库:import random import numpy as np
-
创建一个用于生成随机种群的函数:
def original_population(chromosomes, genes): #initialize the population with zeroes population = np.zeros((chromosomes, genes)) #loop through each chromosome for i in range(chromosomes): #get random no. of ones to be created ones = random.randint(0, genes) #change zeroes to ones population[i, 0:ones] = 1 #shuffle rows np.random.shuffle(population[i]) return population
-
定义一个创建目标解的函数:
def create_target_solution(gene): #assume that there is an equal number of ones and zeroes counting_ones = int(gene/2) # build array with equal no. of ones and zeros target = np.zeros(gene) target[0:counting_ones] = 1 # shuffle the array to mix zeroes and ones np.random.shuffle(target) return target
-
定义一个函数来计算每个染色体的适应度权重:
def fitness_function(target,population): #create an array of true/false compared to the reference identical_to_target = population == target #sum no. of genes that are identical fitness_weights = identical_to_target.sum(axis = 1) return fitness_weights
-
定义一个函数来选择具有最高权重(最高适应度得分)的父代对。由于种群较小,染色体之间的竞争更为激烈。这种方法也称为锦标赛选择:
# select the best parents def select_parents(population, weights): #identify the parent with the highest weight parent1 = population[np.argmax(weights)] #replace weight with the minimum number weights[np.argmax(weights)] = 0 #identify the parent with the second-highest weight parent2 = population[np.argmax(weights)] return parent1, parent2
-
定义一个使用随机选择交叉点的交叉函数:
def crossover_reproduction(parents, population): #define parents separately parent1 = parents[0] parent2 = parents[1] #randomly assign a point for cross-over p = random.randrange(0, len(population)) print("Crossover point:", p) #create children by joining the parents at the cross-over point child1 = np.hstack((parent1[0:p],parent2[p:])) child2 = np.hstack((parent2[0:p], parent1[p:])) return child1, child2
-
定义一个突变函数,使用概率和种群作为输入:
def mutate_population(population, mutation_probability): #create array of random mutations that uses the population mutation_array = np.random.random(size = (population.shape)) """ compare elements of the array with the probability and put the results into an array """ mutation_boolean = mutation_array \ >= mutation_probability """ convert boolean into binary and store to create a new array for the population """ population[mutation_boolean] = np.logical_not\ (population[mutation_boolean]) return population
在前面的代码片段中,设置突变选择的条件是检查数组中的每个元素是否大于突变概率,该概率作为阈值。如果元素大于阈值,则应用突变。
-
将
children
数组附加到原始种群中,创建一个新的交叉population
,并使用print()
函数显示它:population = original_population(5,8) target = create_target_solution(8) weights = fitness_function(target,population) parents = select_parents(population,weights) children = crossover_reproduction(parents,population)
输出将如下所示:
Crossover point: 3
-
接下来,将
population
与children
合并:population_crossover = np.append(population, children, axis= 0) print('\nPopulation after the cross-over:\n', \ population_crossover)
种群将如下所示:
Population after the cross-over: [[0\. 1\. 0\. 0\. 0\. 0\. 1\. 0.] [0\. 0\. 0\. 0\. 0\. 1\. 0\. 0.] [1\. 1\. 1\. 1\. 1\. 0\. 0\. 1.] [1\. 1\. 1\. 0\. 1\. 1\. 1\. 1.] [0\. 1\. 1\. 1\. 1\. 0\. 0\. 0.] [1\. 1\. 1\. 1\. 1\. 0\. 0\. 1.] [1\. 1\. 1\. 0\. 1\. 1\. 1\. 1.]]
-
使用交叉种群和突变概率
0.05
来创建一个新种群,并显示突变后的种群:mutation_probability = 0.05 new_population = mutate_population\ (population_crossover,mutation_probability) print('\nNext generation of the population:\n',\ new_population)
如你所见,阈值(mutation_probability)是 0.05。因此,如果元素大于这个阈值,它们将发生突变(所以基因发生突变的几率是 95%)。
输出将如下所示:
Next generation of the population: [[1\. 0\. 1\. 1\. 1\. 1\. 0\. 1.] [1\. 0\. 1\. 1\. 1\. 0\. 1\. 1.] [1\. 0\. 0\. 0\. 0\. 1\. 1\. 0.] [0\. 0\. 0\. 1\. 0\. 0\. 0\. 0.] [1\. 0\. 0\. 0\. 1\. 1\. 1\. 1.] [0\. 0\. 0\. 0\. 0\. 1\. 1\. 0.] [0\. 0\. 0\. 1\. 0\. 1\. 0\. 1.]]
你将得到一个类似的输出,因为种群元素是随机化的。你可以看到交叉操作生成的染色体被添加到原始种群中,且在突变后,种群的染色体数量相同,但基因不同。交叉和突变步骤可以通过循环函数重复,直到达到目标解。这些循环也被称为代。
注意
要访问此特定部分的源代码,请参考 packt.live/3dXaBqi
。
你还可以在网上运行这个示例,网址是 packt.live/2Ysc5Cl
。
在本节中,描述了突变的概念。突变的好处在于它为染色体引入了随机变异,促进了探索,并帮助避免局部最优。介绍了不同的突变技术。我们使用的示例展示了通过在交叉过程完成后对种群实施反向突变来观察突变概率的影响。
应用于超参数选择
在本节中,我们将探讨遗传算法(GAs)在参数选择中的应用,尤其是在使用神经网络时。遗传算法广泛应用于生产调度和铁路管理中的优化问题。这类问题的解决方案依赖于将神经网络与遗传算法结合,作为函数优化器。
本节的练习提供了一个平台,用于调整神经网络的超参数,以预测风流模式。您将应用一个简单的遗传算法来优化用于训练神经网络的超参数值。
人工神经网络(ANNs)模拟了大脑中神经元的生物学过程和结构。人工神经网络中的神经元依赖于输入信息(参数)和权重的组合。该乘积(加上偏置)通过传递函数,这是一组并行排列的神经元,形成一个层。
对于权重和偏置优化,人工神经网络使用梯度下降法进行训练和反向传播过程。这影响了神经网络的发展,因为在训练开始之前,神经网络拓扑结构需要完全设计。由于设计是预设的,某些神经元在训练过程中可能没有被使用,但它们可能仍然处于活跃状态,因此变得冗余。此外,使用梯度方法的神经网络可能会陷入局部最优,因此需要依赖其他方法来帮助其继续处理,如正则化、岭回归或套索回归。人工神经网络广泛应用于语音识别、特征检测(无论是图像、拓扑还是信号处理)和疾病检测。
为了防止这些问题并增强神经网络的训练,遗传算法可以被实现。遗传算法用于函数优化,而交叉和变异技术则有助于问题空间的探索。最初,遗传算法被用于优化神经网络的权重和节点数。为此,遗传算法的染色体编码了可能的权重和节点变动。神经网络生成的适应度函数依赖于潜在值与参数的实际值之间的均方误差。
然而,研究已经扩展到递归神经网络(RNNs)的实现,并将其与强化学习(RL)结合,旨在提高多处理器性能。递归神经网络是一种人工神经网络(ANN),其输出不仅是输入加权过程的结果,还包含了一个包含先前输入和输出的向量。这使得神经网络能够保持对先前训练实例的知识。
遗传算法有助于扩展神经网络的拓扑结构,超越权重调整。一例是 EDEN,其中编码在染色体内进行,并且网络架构和学习率在多个 TensorFlow 数据集上实现了高精度。训练神经网络时,最具挑战性的问题之一是馈送给网络的特征(或输入超参数)的质量。如果参数不合适,输入和输出的映射将会错误。因此,遗传算法可以作为人工神经网络的替代方法,通过优化特征选择来发挥作用。
以下练习将教你如何应用简单的遗传算法来识别 RNN 的最佳参数(窗口大小和单元数量)。所实现的遗传算法使用 deap
包,通过 eaSimple()
函数,可以使用基于工具箱的代码创建一个简单的遗传算法,包括种群创建、通过 selRandom()
函数进行选择、通过 cxTwoPoint()
函数进行交叉和通过 mutFlipBit()
函数进行变异。为了进行比较和超参数选择,使用 selBest()
函数。
练习 12.07:为 RNN 训练实现遗传算法超参数优化
本次练习的目标是通过简单的遗传算法识别 RNN 使用的最佳超参数。在本次练习中,我们使用的是一个 2012 年天气预报挑战赛中的数据集。训练和验证参数时仅使用一个特征 wp2
。使用的两个超参数是单元数量和窗口大小。这些超参数代表染色体的遗传物质:
注意
数据集可以在以下 GitHub 仓库中找到:https://packt.live/2Ajjz2F。
原始数据集可以在以下链接找到:https://www.kaggle.com/c/GEF2012-wind-forecasting/data。
-
创建一个新的 Jupyter Notebook。导入
pandas
和numpy
库及其函数:import numpy as np import pandas as pd from sklearn.metrics import mean_squared_error from sklearn.model_selection import train_test_split as split from tensorflow.keras.layers import SimpleRNN, Input, Dense from tensorflow.keras.models import Model from deap import base, creator, tools, algorithms from scipy.stats import bernoulli from bitstring import BitArray
从
sklearn
包中导入mean_squared_error
和train_test_split
。同时,从tensorflow
和keras
包中导入SimpleRNN
、Input
、Dense
(来自layers
文件夹)和模型(来自Model
类)。为了创建遗传算法,必须从deap
包中调用base
、creator
、tools
和algorithms
。对于统计学,我们使用的是伯努利方程;因此,我们将从scipy.stats
包中调用bernoulli
。从bitstrings
中,我们将调用BitArray
。 -
使用随机种子进行模型开发;
998
是种子的初始化数字:np.random.seed(998)
-
从
train.csv
文件加载数据,使用np.reshape()
将数据修改为只包含wp2
列的数组,并选择前 1,501 个元素:#read data from csv data = pd.read_csv('../Dataset/train.csv') #use column wp2 data = np.reshape(np.array(data['wp2']), (len(data['wp2']), 1)) data = data[0:1500]
-
定义一个函数,根据窗口大小划分数据集:
def format_dataset(data, w_size): #initialize as empty array X, Y = np.empty((0, w_size)), np.empty(0) """ depending on the window size the data is separated in 2 arrays containing each of the sizes """ for i in range(len(data)-w_size-1): X = np.vstack([X,data[i:(i+w_size),0]]) Y = np.append(Y, data[i+w_size,0]) X = np.reshape(X,(len(X),w_size,1)) Y = np.reshape(Y,(len(Y), 1)) return X, Y
-
定义一个函数,通过简单的遗传算法训练 RNN,识别最佳超参数:
def training_hyperparameters(ga_optimization): """ decode GA solution to integer window size and number of units """ w_size_bit = BitArray(ga_optimization[0:6]) n_units_bit = BitArray(ga_optimization[6:]) w_size = w_size_bit.uint n_units = n_units_bit.uint print('\nWindow Size: ', w_size, \ '\nNumber of units: ',n_units) """ return fitness score of 100 if the size or the units are 0 """ if w_size == 0 or n_units == 0: return 100 """ segment train data on the window size splitting it into 90 train, 10 validation """ X,Y = format_dataset(data, w_size) X_train, X_validate, Y_train, Y_validate = \ split(X, Y, test_size= 0.10, random_state= 998)
第一阶段是识别与窗口大小和单元数量相关的染色体部分。接下来,如果没有窗口大小或单元数量,则返回一个极高的适应度得分。将两个数组按 90:10 的比例分割为训练数组和验证数组。
-
初始化输入特征,并使用训练数据集训练
SimpleRNN
模型。为了优化,使用 Adam 算法,并将均方误差作为损失函数。为了训练模型,使用fit
函数,设置epochs
为5
,批次大小为4
。要生成预测值,使用存储在X_validate
中的输入值,在模型的predict
函数中进行预测。计算RMSE
:input_features = Input(shape=(w_size,1)) x = SimpleRNN(n_units,input_shape=(w_size,1))(input_features) output = Dense(1, activation='linear')(x) rnnmodel = Model(inputs=input_features, outputs = output) rnnmodel.compile(optimizer='adam', \ loss = 'mean_squared_error') rnnmodel.fit(X_train, Y_train, epochs=5, \ batch_size=4, shuffle = True) Y_predict = rnnmodel.predict(X_validate) # calculate RMSE score as fitness score for GA RMSE = np.sqrt(mean_squared_error(Y_validate, Y_predict)) print('Validation RMSE: ', RMSE, '\n') return RMSE,
-
实例化种群大小、遗传算法使用的代数和基因长度,分别设置为
4
、5
和10
:population_size = 4 generations = 5 gene = 10
-
使用
deap
包中的工具箱实例化遗传算法,eaSimple()
。为此,使用创建器工具将适应度函数实例化为RMSE
:creator.create('FitnessMax', base.Fitness, weights= (-1.0,)) creator.create('Individual', list, fitness = creator.FitnessMax) toolbox = base.Toolbox() toolbox.register('bernoulli', bernoulli.rvs, 0.5) toolbox.register('chromosome', tools.initRepeat, \ creator.Individual, toolbox.bernoulli, n = gene) toolbox.register('population', tools.initRepeat, \ list, toolbox.chromosome) toolbox.register('mate', tools.cxTwoPoint) toolbox.register('mutate', tools.mutFlipBit, indpb = 0.6) toolbox.register('select', tools.selRandom) toolbox.register('evaluate', training_hyperparameters) population = toolbox.population(n = population_size) algo = algorithms.eaSimple(population,toolbox,cxpb=0.4, \ mutpb=0.1, ngen=generations, \ verbose=False)
输出的最后几行如下所示:
Window Size: 48 Number of units: 15 Train on 1305 samples Epoch 1/5 1305/1305 [==============================] - 3s 2ms/sample - loss: 0.0106 Epoch 2/5 1305/1305 [==============================] - 3s 2ms/sample - loss: 0.0066 Epoch 3/5 1305/1305 [==============================] - 3s 2ms/sample - loss: 0.0057 Epoch 4/5 1305/1305 [==============================] - 3s 2ms/sample - loss: 0.0051 Epoch 5/5 1305/1305 [==============================] - 3s 2ms/sample - loss: 0.0049 Validation RMSE: 0.05564985152918074
RMSE
值越低,超参数越好。伯努利分布用于随机初始化染色体基因。基于染色体,初始化种群。在工具箱中,创建新种群有四个步骤:cxTwoPoint()
表示父代在两个点交叉信息(交叉),mutFlipBit()
会以0.6
的概率仅突变染色体的一个元素,selRandom()
函数,evaluate(这使用来自第 6 步和第 7 步的 RNN 训练函数)。 -
使用
selBest()
函数选择单一最佳解,k=1
,比较解的适应度函数,选择与最优解最相似的那个。为了获得最佳窗口大小和单元数量,遍历染色体,将比特值转换为无符号整数,并打印最优超参数:optimal_chromosome = tools.selBest(population, k = 1) optimal_w_size = None optimal_n_units = None for op in optimal_chromosome: w_size_bit = BitArray(op[0:6]) n_units_bit = BitArray(op[6:]) optimal_w_size = w_size_bit.uint optimal_n_units = n_units_bit.uint print('\nOptimal window size:', optimal_w_size, \ '\n Optimal number of units:', optimal_n_units)
输出将如下所示:
Optimal window size: 48 Optimal number of units: 15
-
运行应用程序,你将得到类似的输出。窗口大小和单元数量的初始值将显示。遗传算法将使用 RNN 运行指定的代数。在每个代的结束时,
RMSE
值会显示出来。一旦所有代数完成,最佳值将显示出来:https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_10.jpg
图 12.10:使用遗传算法优化窗口大小和单元数量
我们从初始窗口大小51
和15
个单元开始;最佳窗口大小减少为28
,单元数量减少到4
。根据RMSE
计算,参数之间的差异减少至0.05
。
注意
要访问该特定部分的源代码,请参考packt.live/37sgQA6
。
你也可以在packt.live/30AOKRK
在线运行此示例。
本节内容已经涵盖了将遗传算法与神经网络结合,作为替代梯度下降方法的方案。遗传算法主要用于优化神经网络的神经元数量和权重,但通过混合方法,其应用可以扩展到优化网络结构和超参数选择。本次练习测试了你应用遗传算法来寻找与天气预测问题相关的两个特征的最佳值的能力。这些特征被用来训练一个递归神经网络(RNN),利用 RMSE 值估计风流。在接下来的部分中,你将扩展对整个神经网络架构优化的混合技术的知识,使用 NEAT 方法。
NEAT 与其他形式
神经进化是指使用遗传算法(GA)进化神经网络的术语。这一机器学习分支在各种问题中已被证明优于强化学习(RL),并且可以与强化学习结合使用,因为它是一种无监督学习方法。如前一节所述,神经进化系统专注于改变人工神经网络(ANN)的权重、神经元数量(在隐藏层中)和拓扑结构。
增强拓扑的神经进化(NEAT)专注于人工神经网络(ANN)的拓扑进化。它涉及训练一个简单的 ANN 结构,该结构由输入和输出神经元以及表示偏置的单元组成,但没有隐藏层。每个 ANN 结构都在一个染色体中编码,其中包含节点基因和连接基因(即两个节点基因之间的映射或连接)。每个连接指定输入、输出、权重节点、连接的激活以及创新编号,这个编号作为基因交叉过程中的链接。
突变与连接的权重或整个系统的结构相关。结构突变可以通过在两个未连接的节点之间加入连接,或者通过在已有连接上增加一个新节点,从而产生两个新的连接(一个是在现有的节点对之间,另一个是包含新创建节点的连接)。
交叉过程涉及识别种群中不同染色体之间的共同基因。这依赖于关于基因派生的历史信息,使用全球创新编号。由突变产生的基因会从其突变的基因获得递增编号,而通过交叉产生的基因保持原来的编号。这项技术有助于解决因基因匹配问题而导致的神经网络拓扑结构问题。没有相同创新编号的基因从具有最高适应度的父本中选择。如果两个父本具有相同的适应度,基因将从每个父本中随机选择。
拥有相似拓扑的染色体根据它们之间的距离进行分组 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_10a.png;因此,个体根据与平均基因数 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_10e.png 的差异,以及不同的基因 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_10b.png、附加基因 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_10c.png 和权重差异 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_10d.png 进行评估。每个系数 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_10f.png 作为一个权重,强调每个参数的重要性:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_11.jpg
图 12.11:拓扑距离计算
为了将染色体分类到不同物种中,比较距离 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_11a.png 与阈值 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_11b.png。如果 https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_11c.png,那么染色体属于满足此条件的第一个物种。为了防止物种主导,物种中的所有元素需要具有相同的适应度水平,该水平是根据物种中的成员数量计算的。物种的进化(包括多少新染色体被包含,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_11d.png)取决于物种适应度与种群平均适应度之间的比较,https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_11e.png:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_12.jpg
图 12.12:新染色体数量的计算
NEAT 的优势在于,与那些具有随机拓扑参数的神经进化算法不同,它从最简单的神经网络拓扑形式开始,并逐步进化以寻找最优解,从而显著减少了所使用的代数数量。
进化拓扑算法被分类为 权重进化人工神经网络 (TWEANNs),包括 EDEN、细胞编码 (CE)、强制子群体 (SE) —— 这是一种固定拓扑系统(其中 NEAT 在 CartPole 上优于后两者)—— 并行分布式遗传编程 (PDGP),和 广义递归链接获取 (GNARL).
现在我们将通过一个练习,展示如何应用 NEAT 来解决一个简单的 XNOR 门问题,XNOR 门是一种具有二进制输出的逻辑门。二进制输入和输出通过真值表进行量化,真值表是布尔逻辑表达式功能值集合的表示,展示了逻辑值的组合。
练习 12.08:使用 NEAT 实现 XNOR 门功能
在练习中,您将看到 NEAT 在解决简单布尔代数问题中的影响。该问题涉及实现 NEAT 算法以识别用于再现互斥非(XNOR)门的二进制输出的最佳神经网络拓扑结构。这是一种逻辑门,当两个输入信号相同时(即 0 或 1 - 分别等同于关闭和打开),逻辑门的输出将为 1(打开),而当一个输入为高(1)而另一个输入为低(0)时,输出将为 0(关闭)。
我们有以下 XNOR 逻辑门的真值表:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_13.jpg
图 12.13:XNOR 门的真值表
使用 NEAT 算法创建一个前馈神经网络,可以模拟 XNOR 门的输出。
执行以下步骤完成练习:
-
在您的 Anaconda 环境中执行以下命令:
conda install neat
-
创建一个新的 Jupyter Notebook。
-
从
__future__
文件中导入print_function
,并导入neat
和os
包:from __future__ import print_function import os import neat
-
根据真值表初始化 XNOR 门的输入和输出:
xnor_inputs = [(0.0, 0.0), (0.0, 1.0), (1.0, 0.0), (1.0, 1.0)] xnor_output = [(1.0,),(0.0,),(0.0,),(1.0,)]
-
创建一个适应性函数,该函数使用实际输出和使用 NEAT 的前馈神经网络输出之间的平方差:
def fitness_function(chromosomes, configuration): for ch_id, chromosome in chromosomes: chromosome.fitness = 4.0 neural_net = neat.nn.FeedForwardNetwork.create\ (chromosome, configuration) for xnor_i,xnor_o in zip(xnor_inputs, xnor_output): output = neural_net.activate(xnor_i) squared_diff = (output[0] - xnor_o[0])**2 chromosome.fitness -= squared_diff
-
创建一个名为
config-feedforward-xnor
的新文本文件。在文件中包含以下 NEAT 算法的参数。对于适应性函数,选择最大值,阈值接近4
,人口大小为200
:[NEAT] fitness_criterion = max fitness_threshold = 3.9 pop_size = 200 reset_on_extinction = False
-
在同一
config-feedforward-xnor
文件中,包括使用0.01
的变异率的节点激活的sigmoid
函数。聚合选项主要是添加值,聚合的变异率为 0:[DefaultGenome] # activation options of the nodes activation_default = sigmoid activation_mutate_rate = 0.01 activation_options = sigmoid # aggregation options for the node aggregation_default = sum aggregation_mutate_rate = 0.0 aggregation_options = sum
-
为算法设置
bias
参数:# bias options for the node bias_init_mean = 0.0 bias_init_stdev = 0.05 bias_max_value = 30.0 bias_min_value = -30.0 bias_mutate_power = 0.5 bias_mutate_rate = 0.8 bias_replace_rate = 0.1
对于偏置,最小值和最大值分别为
-30
和30
。将初始标准差设置为0.05
,尽可能低,幂为0.5
,变异率为0.8
,替换率为0.1
。这些值对实施遗传算法优化至关重要。 -
定义系数https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_13a.png,因为我们仅考虑基因之间的差异(它们的不一致性)和权重之间的差异:
# compatibility options for the genes in the chromosome compatibility_disjoint_coefficient = 1.0 compatibility_weight_coefficient = 0.5
-
包括关于拓扑、连接以及与节点的包含或移除相关的参数信息:
# add/remove rates for connections between nodes conn_add_prob = 0.5 conn_delete_prob = 0.5 # connection enable options enabled_default = True enabled_mutate_rate = 0.01 feed_forward = True initial_connection = full # add/remove rates for nodes node_add_prob = 0.2 node_delete_prob = 0.2
-
从一个没有任何隐藏层的简单网络开始,并设置节点和连接的响应参数:
# network parameters num_hidden = 0 num_inputs = 2 num_outputs = 1 # node response options response_init_mean = 1.0 response_init_stdev = 0.0 response_max_value = 30.0 response_min_value = -30.0 response_mutate_power = 0.0 response_mutate_rate = 0.0 response_replace_rate = 0.0 # connection weight options weight_init_mean = 0.0 weight_init_stdev = 1.0 weight_max_value = 30 weight_min_value = -30 weight_mutate_power = 0.5 weight_mutate_rate = 0.9 weight_replace_rate = 0.15
-
选择距离阈值、物种适应性函数和父代选择的默认参数。这是要包含在
config-feedforward-xnor
文件中的最终参数集:[DefaultSpeciesSet] compatibility_threshold = 3.0 [DefaultStagnation] species_fitness_func = max max_stagnation = 20 species_elitism = 2 [DefaultReproduction] Elitism = 2 survival_threshold = 0.2
-
现在,在主代码文件中,使用
config-feedforward-xnor
文件配置神经网络的 NEAT 公式,并输出网络的每个配置在Exercise 12.08
内:#load configuration configuration = neat.Config(neat.DefaultGenome, \ neat.DefaultReproduction, \ neat.DefaultSpeciesSet, \ neat.DefaultStagnation,\ "../Dataset/config-feedforward-xnor") print("Output of file configuration:", configuration)
输出如下:
Output of file configuration: <neat.config.Config object at 0x0000017618944AC8>
-
根据 NEAT 算法的配置获取种群,并将进度输出到终端,以监控统计差异:
#load the population size pop = neat.Population(configuration) #add output for progress in terminal pop.add_reporter(neat.StdOutReporter(True)) statistics = neat.StatisticsReporter() pop.add_reporter(statistics) pop.add_reporter(neat.Checkpointer(5))
-
运行算法
200
代,并为神经网络拓扑选择最佳解决方案:#run for 200 generations using best = pop.run(fitness_function, 200) #display the best chromosome print('\n Best chromosome:\n{!s}'.format(best))
输出将类似于以下内容:
****** Running generation 0 ****** Population's average fitness: 2.45675 stdev: 0.36807 Best fitness: 2.99412 - size: (1, 2) - species 1 - id 28 Average adjusted fitness: 0.585 Mean genetic distance 0.949, standard deviation 0.386 Population of 200 members in 1 species: ID age size fitness adj fit stag ==== === ==== ======= ======= ==== 1 0 200 3.0 0.585 0 Total extinctions: 0 Generation time: 0.030 sec ****** Running generation 1 ****** Population's average fitness: 2.42136 stdev: 0.28774 Best fitness: 2.99412 - size: (1, 2) - species 1 - id 28 Average adjusted fitness: 0.589 Mean genetic distance 1.074, standard deviation 0.462 Population of 200 members in 1 species: ID age size fitness adj fit stag ==== === ==== ======= ======= ==== 1 1 200 3.0 0.589 1 Total extinctions: 0 Generation time: 0.032 sec (0.031 average)
-
使用函数将神经网络的输出与期望输出进行比较:
#show output of the most fit chromosome against the data print('\n Output:') best_network = neat.nn.FeedForwardNetwork.create\ (best, configuration) for xnor_i, xnor_o in zip(xnor_inputs, xnor_output): output = best_network.activate(xnor_i) print("input{!r}, expected output {!r}, got: {:.1f}"\ .format(xnor_i,xnor_o,output[0]))
输出将如下所示:
Output: input(0.0, 0.0), expected output (1.0,), got: 0.9 input(0.0, 1.0), expected output (0.0,), got: 0.0 input(1.0, 0.0), expected output (0.0,), got: 0.2 input(1.0, 1.0), expected output (1.0,), got: 0.9
-
运行代码后,你将得到类似于此处所见的输出。由于染色体是随机生成的,算法将在不同的代数中收敛到一个接近最优的解:
****** Running generation 41 ****** Population's average fitness: 2.50036 stdev: 0.52561 Best fitness: 3.97351 - size: (8, 16) - species 2 - id 8095 Best individual in generation 41 meets fitness threshold \ - complexity: (8, 16) Best chromosome: Key: 8095 Fitness: 3.9735119749933214 Nodes: 0 DefaultNodeGene(key=0, bias=-0.02623087593563278, \ response=1.0, activation=sigmoid, \ aggregation=sum) 107 DefaultNodeGene(key=107, bias=-1.5209385195946818, \ response=1.0, activation=sigmoid, \ aggregation=sum)[…] Connections: DefaultConnectionGene(key=(-2, 107), \ weight=1.8280370376000628, \ enabled=True) DefaultConnectionGene(key=(-2, 128), \ weight=0.08641968818530771, \ enabled=True) DefaultConnectionGene(key=(-2, 321), \ weight=1.2366021868005421, \ enabled=True)[…]
通过运行这个实验,你可以看到转换到接近最优解的过程发生在小于最大代数(200
)的情况下。前馈神经网络的输出几乎是最优的,因为它的值是整数。它们的值接近 1 和 0。你还可以观察到,从一个没有隐藏层的神经网络开始,ANN 进化成了具有 1149
个节点和各种连接的网络。
注意
若要访问此特定部分的源代码,请参考 packt.live/2XTBs0M
。
本节目前没有在线互动示例,需在本地运行。
在本节中,介绍了 NEAT 算法,这是一种变异神经网络拓扑的神经进化算法。NEAT 算法与其他 TWEANNs(拓扑进化神经网络)的不同之处在于变异、交叉和选择的方式,这些操作优化神经网络的结构,从一个没有隐藏层的简单网络开始,并演化成一个更复杂的网络,节点和连接的数量增加。
这个练习涉及实现 NEAT 来重现 XNOR 逻辑门的输出,使你能够理解 NEAT 算法的结构,并分析将神经进化技术应用于简单电子问题的好处和意义。在下一节中,你将通过解决小车摆杆问题来测试你的编程能力和遗传算法(GA)的知识。
活动 12.01:小车摆杆活动
自动控制是一项挑战,尤其是在使用机械臂或小车运输车间设备时。这个问题通常被概括为小车摆杆问题。你将编写程序控制一个自动化小车以保持一根杆子平衡。目标是最大化杆子保持平衡的时间。为了解决这个问题,代理可以使用神经网络进行状态-动作映射。挑战在于确定神经网络的结构,并为神经网络每一层确定最优的权重、偏差和神经元数量的解决方案。我们将使用遗传算法(GA)来确定这些参数的最佳值。
该活动的目标是实现一个遗传算法,用于选择人工神经网络(ANN)的参数,经过 20 代之后,可以在 500 次试验中获得高的平均分数。你将输出每代和每集的平均分数,并通过调整神经网络的参数,使用遗传算法监控收敛到最优策略的过程,以图形方式呈现。此活动旨在通过实现前几章和本章的概念,测试你的编程能力。以下是实现此活动所需的步骤:
-
创建一个 Jupyter Notebook 文件并导入适当的包,如下所示:
import gym import numpy as np import math import tensorflow as tf from matplotlib import pyplot as plt from random import randint from statistics import median, mean
-
初始化环境以及状态和动作空间的形状。
-
创建一个函数,用于生成随机选择的初始网络参数。
-
创建一个函数,使用一组参数生成神经网络。
-
创建一个函数,获取使用神经网络时 300 步的总奖励。
-
创建一个函数,在运行初始随机选择时,获取种群中每个元素的适应度分数。
-
创建一个突变函数。
-
创建一个单点交叉函数。
-
创建一个函数,通过选择奖励最高的对来生成下一代。
-
在函数中选择参数,构建神经网络并添加这些参数。
-
使用识别的参数构建神经网络,并根据构建的神经网络获得新的奖励。
-
创建一个函数,用于输出收敛图。
-
创建一个遗传算法函数,根据最高的平均奖励输出神经网络的参数。
-
创建一个函数,将参数数组解码为每个神经网络参数。
-
设置代数为 50,试验次数为 15,步骤数和试验数为 500。你将得到类似以下的输出(这里只显示前几行):
Generation:1, max reward:11.0 Generation:2, max reward:11.0 Generation:3, max reward:10.0 Generation:4, max reward:10.0 Generation:5, max reward:11.0 Generation:6, max reward:10.0 Generation:7, max reward:10.0 Generation:8, max reward:10.0 Generation:9, max reward:11.0 Generation:10, max reward:10.0 Generation:11, max reward:10.0 Generation:12, max reward:10.0 Generation:13, max reward:10.0 Generation:14, max reward:10.0 Generation:15, max reward:10.0 Generation:16, max reward:10.0 Generation:17, max reward:10.0 Generation:18, max reward:10.0 Generation:19, max reward:11.0 Generation:20, max reward:11.0
奖励与代数的关系图将类似于以下内容:
https://github.com/OpenDocCN/freelearn-dl-pt7-zh/raw/master/docs/rl-ws/img/B16182_12_14.jpg
图 12.14:代数中获得的奖励
平均奖励的输出(这里只显示最后几行)将类似于以下内容:
Trial:486, total reward:8.0
Trial:487, total reward:9.0
Trial:488, total reward:10.0
Trial:489, total reward:10.0
Trial:490, total reward:8.0
Trial:491, total reward:9.0
Trial:492, total reward:9.0
Trial:493, total reward:10.0
Trial:494, total reward:10.0
Trial:495, total reward:9.0
Trial:496, total reward:10.0
Trial:497, total reward:9.0
Trial:498, total reward:10.0
Trial:499, total reward:9.0
Average reward: 9.384
注意
该活动的解决方案可以在第 774 页找到。
总结
本章中,你探讨了基于梯度和非基于梯度的算法优化方法,重点介绍了进化算法的潜力——特别是遗传算法——通过模仿自然的方式解决优化问题,比如亚优解。遗传算法由特定元素组成,如种群生成、父代选择、父代重组或交叉、以及最终突变发生,利用这些元素生成二进制最优解。
接着,探索了遗传算法(GAs)在神经网络的超参数调优和选择中的应用,帮助我们找到最合适的窗口大小和单元数。我们看到了结合深度神经网络和进化策略的最先进算法的实现,例如用于 XNOR 输出估计的 NEAT。最后,你有机会通过 OpenAI Gym 的平衡杆模拟来实现本章所学的内容,在这个模拟中,我们研究了使用深度神经网络进行动作选择时,遗传算法在参数调优中的应用。
强化学习(RL)系统中混合方法的发展是最近的优化发展之一。你已经开发并实现了适用于无模型 RL 系统的优化方法。在附加章节中(该章节可以通过互动版本的研讨会在 courses.packtpub.com 上访问),你将探索基于模型的 RL 方法以及深度 RL 在控制系统中的最新进展,这些进展可以应用于机器人技术、制造业和交通领域。
现在,你已经能够利用本书中学到的概念,使用各种编码技术和模型来进一步提升你的专业领域,并可能带来新的变化和进步。你的旅程才刚刚开始——你已经迈出了破解强化学习(RL)世界的第一步,并且你现在拥有了提升 Python 编程技能的工具,所有这些你都可以独立应用。