六、多臂老虎机问题
在前面的章节中,我们学习了强化学习(RL)的基本概念和几种 RL 算法,以及如何将 RL 问题建模为马尔可夫决策过程(MDP)。 我们还看到了用于解决 MDP 的不同的基于模型和无模型的算法。 在本章中,我们将看到 RL 中的经典问题之一,称为多臂老虎机(MAB)问题。 我们将看到什么是 MAB 问题,以及如何使用不同的算法解决该问题,然后了解如何使用 MAB 识别将接收大部分点击的正确广告横幅。 我们还将学习广泛用于构建推荐系统的上下文老虎机。
在本章中,您将了解以下内容:
- MAB 问题
ε
贪婪算法- softmax 探索算法
- 置信区间上界算法
- 汤普森采样算法
- MAB 的应用
- 使用 MAB 识别正确的广告横幅
- 情境老虎机
MAB 问题
MAB 问题是 RL 中的经典问题之一。 MAB 实际上是一台老虎机,是一种在赌场玩的赌博游戏,您可以拉动手臂(杠杆)并根据随机生成的概率分布获得支出(奖励)。 一台老虎机称为单臂老虎机,当有多台老虎机时,称为多臂老虎机或 k 臂老虎机。
MAB 如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L0CYqlg8-1681653750828)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00143.jpeg)]
由于每台老虎机都通过自己的概率分布为我们提供奖励,因此我们的目标是找出哪台老虎机将在一段时间内为我们提供最大的累积奖励。 因此,智能体在每个时间步t
上执行动作a[t]
,即从投币游戏机中拉出一条手臂并获得奖励R[t]
和,我们智能体的目标是使累积奖励最大化。
我们将手臂Q(a)
的值定义为通过拉动手臂获得的平均奖励:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JxtAiMpC-1681653750829)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00144.jpeg)]
因此,最优臂就是为我们提供最大累积奖励的臂,即:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cRMmAuLp-1681653750830)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00145.jpeg)]
我们智能体的目标是找到最佳手臂,并最大程度地减少后悔,这可以定义为了解k
哪个手臂是最佳手臂的代价。 现在,我们如何找到最好的手臂? 我们应该探索所有武器还是选择已经给予我们最大累积奖励的武器? 这就是探索与利用的困境。 现在,我们将看到如何使用以下各种探索策略来解决这个难题:
- ε 贪婪策略
- Softmax 探索
- 置信区间上界算法
- 汤姆森采样技术
在继续之前,让我们在 OpenAI Gym 中安装bandit
环境; 您可以通过在终端中键入以下命令来安装bandit
环境:
git clone https://github.c[om/JKCoop](https://www.google.com/url?q=https://github.com/JKCooper2/gym-bandits.git&sa=D&ust=1529836954889000&usg=AFQjCNFpMNcU8k-62v6Bb0UZSngaldPxeg)[er2/gym-ba](https://www.google.com/url?q=https://github.com/JKCooper2/gym-bandits.git&sa=D&ust=1529836954889000&usg=AFQjCNFpMNcU8k-62v6Bb0UZSngaldPxeg)[ndits.g](https://www.google.com/url?q=https://github.com/JKCooper2/gym-bandits.git&sa=D&ust=1529836954889000&usg=AFQjCNFpMNcU8k-62v6Bb0UZSngaldPxeg)[it](https://www.google.com/url?q=https://github.com/JKCooper2/gym-bandits.git&sa=D&ust=1529836954889000&usg=AFQjCNFpMNcU8k-62v6Bb0UZSngaldPxeg)
cd gym-bandits
pip install -e .
安装后,让我们导入gym
和gym_bandits
:
import gym_bandits
import gym
现在我们将初始化环境; 我们使用带有十个臂的 MAB:
代码语言:javascript复制env = gym.make("BanditTenArmedGaussian-v0")
我们的行动空间将是 10,因为我们有 10 条手臂:
代码语言:javascript复制env.action_space
输出如下:
代码语言:javascript复制10
贪婪策略
我们已经学到了很多关于ε贪婪策略的知识。 在ε
贪婪策略中,或者我们选择概率为 1 epsilon
的最佳手臂,或者我们随机选择概率为epsilon
的手臂:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hAb6U2Dl-1681653750830)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00146.gif)]
现在,我们将看到如何使用ε
贪婪策略选择最佳手臂:
- 首先,让我们初始化所有变量:
# number of rounds (iterations)
num_rounds = 20000
# Count of number of times an arm was pulled
count = np.zeros(10)
# Sum of rewards of each arm
sum_rewards = np.zeros(10)
# Q value which is the average reward
Q = np.zeros(10)
- 现在我们定义
epsilon_greedy
函数:
def epsilon_greedy(epsilon):
rand = np.random.random()
if rand < epsilon:
action = env.action_space.sample()
else:
action = np.argmax(Q)
return action
- 开始拉动手臂:
for i in range(num_rounds):
# Select the arm using epsilon greedy
arm = epsilon_greedy(0.5)
# Get the reward
observation, reward, done, info = env.step(arm)
# update the count of that arm
count[arm] = 1
# Sum the rewards obtained from the arm
sum_rewards[arm] =reward
# calculate Q value which is the average rewards of the arm
Q[arm] = sum_rewards[arm]/count[arm]
print( 'The optimal arm is {}'.format(np.argmax(Q)))
以下是输出:
代码语言:javascript复制The optimal arm is 3
softmax 探索算法
Softmax 探索(也称为玻尔兹曼探索)是用于找到最佳老虎机的另一种策略。 在ε
贪婪策略中,我们等效地考虑所有非最佳分支,但是在 softmax 探索中,我们根据来自玻尔兹曼分布的概率选择一个分支。 选择手臂的概率由下式给出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I9hpjSsI-1681653750830)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00147.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JQYuxJBD-1681653750830)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00148.jpeg)]称为温度因子,它指定我们可以探索多少个随机臂。 当τ
高时,将平等地探索所有手臂,但是当τ
低时,将选择高回报手臂。 请看以下步骤:
- 首先,初始化变量:
# number of rounds (iterations)
num_rounds = 20000
# Count of number of times an arm was pulled
count = np.zeros(10)
# Sum of rewards of each arm
sum_rewards = np.zeros(10)
# Q value which is the average reward
Q = np.zeros(10)
- 现在我们定义
softmax
函数:
def softmax(tau):
total = sum([math.exp(val/tau) for val in Q])
probs = [math.exp(val/tau)/total for val in Q]
threshold = random.random()
cumulative_prob = 0.0
for i in range(len(probs)):
cumulative_prob = probs[i]
if (cumulative_prob > threshold):
return i
return np.argmax(probs)
- 开始拉动手臂:
for i in range(num_rounds):
# Select the arm using softmax
arm = softmax(0.5)
# Get the reward
observation, reward, done, info = env.step(arm)
# update the count of that arm
count[arm] = 1
# Sum the rewards obtained from the arm
sum_rewards[arm] =reward
# calculate Q value which is the average rewards of the arm
Q[arm] = sum_rewards[arm]/count[arm]
print( 'The optimal arm is {}'.format(np.argmax(Q)))
以下是输出:
代码语言:javascript复制The optimal arm is 3
置信区间上限算法
通过ε
贪婪和 softmax 探索,我们以概率探索了随机动作。 随机动作对于探索各种武器很有用,但也可能导致我们尝试无法给我们带来丰厚回报的动作。 我们也不想错过实际上是好的武器,但在最初的回合中却给出了差的奖励。 因此,我们使用一种称为上置信界上限(UCB)的新算法。 它基于面对不确定性时称为乐观的原则。
UCB 算法可帮助我们根据置信区间选择最佳分支。 好的,置信区间是多少? 让我们说我们有两条手臂。 我们拉开这两个手臂,发现第一手臂给了我们 0.3 奖励,第二手臂给了我们 0.8 奖励。 但是,随着一轮拉动手臂,我们不应该得出这样的结论:第二臂将给我们最好的回报。 我们必须尝试几次拉动手臂,取每个手臂获得的奖励平均值,然后选择平均值最高的手臂。 但是,我们如何才能找到每个臂的正确平均值? 这是置信区间进入图片的位置。 置信区间指定武器平均奖励值所在的区间。 如果手臂 1 的置信区间为[0.2, 0.9]
,则表示手臂 1 的平均值在此区间 0.2 至 0.9 内。 0.2 称为下置信界,而 0.9 称为 UCB。 UCB 选择具有较高 UCB 的机器进行探索。
假设我们有三台老虎机,并且每台老虎机都玩了十次。 下图显示了这三个老虎机的置信区间:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GY68VpOF-1681653750831)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00151.gif)]
我们可以看到老虎机 3 具有较高的 UCB。 但是我们不应该得出这样的结论:老虎机 3 只需拉 10 次就能给我们带来丰厚的回报。 一旦我们几次拉起手臂,我们的置信区间就会准确。 因此,随着时间的流逝,置信区间会变窄并缩小为实际值,如下图所示。 因此,现在,我们可以选择具有较高 UCB 的老虎机 2 :
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZJfy9fKj-1681653750831)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00152.gif)]
UCB 背后的想法非常简单:
- 选择具有较高平均奖励和较高置信度上限的动作(手臂)
- 拉手臂并获得奖励
- 更新手臂的奖励和置信范围
但是,我们如何计算 UCB?
我们可以使用公式√(2log(t) / N(a))
计算 UCB,其中N(a)
是拉动手臂的次数,t
是回合的总数。
因此,在 UCB 中,我们选择具有以下公式的手臂:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9042izX4-1681653750832)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00154.jpeg)]
首先,初始化变量:
代码语言:javascript复制# number of rounds (iterations)
num_rounds = 20000
# Count of number of times an arm was pulled
count = np.zeros(10)
# Sum of rewards of each arm
sum_rewards = np.zeros(10)
# Q value which is the average reward
Q = np.zeros(10)
现在,让我们定义我们的UCB
函数:
def UCB(iters):
ucb = np.zeros(10)
#explore all the arms
if iters < 10:
return i
else:
for arm in range(10):
# calculate upper bound
upper_bound = math.sqrt((2*math.log(sum(count))) / count[arm])
# add upper bound to the Q value
ucb[arm] = Q[arm] upper_bound
# return the arm which has maximum value
return (np.argmax(ucb))
让我们开始武装起来:
代码语言:javascript复制for i in range(num_rounds):
# Select the arm using UCB
arm = UCB(i)
# Get the reward
observation, reward, done, info = env.step(arm)
# update the count of that arm
count[arm] = 1
# Sum the rewards obtained from the arm
sum_rewards[arm] =reward
# calculate Q value which is the average rewards of the arm
Q[arm] = sum_rewards[arm]/count[arm]
print( 'The optimal arm is {}'.format(np.argmax(Q)))
输出如下:
代码语言:javascript复制The optimal arm is 1
汤普森采样算法
汤普森采样(TS)是另一种广泛使用的算法,可克服探索利用难题。 它是一种概率算法,基于先验分布。 TS 背后的策略非常简单:首先,我们先计算每个k
武器的平均回报,也就是说,我们从K
个手臂的每个中提取n
个样本,并计算k
个分布。 这些初始分布将与真实分布不同,因此我们将其称为先验分布:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JEkFyqxu-1681653750832)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00155.gif)]
由于我们有伯努利奖赏,因此我们使用 beta 分布来计算先验。 beta 分布[alpha, beta]
的值在[0, 1]
区间内。 Alpha 代表我们获得正面奖励的次数,而 Beta 代表我们获得负面奖励的次数。
现在,我们将看到 TS 如何帮助我们选择最佳手臂。 TS 中涉及的步骤如下:
- 从每个
k
分布中采样一个值,并将该值用作先验平均值。 - 选择具有最高先验均值的手臂并观察奖励。
- 使用观察到的奖励来修改先前的分布。
因此,经过几轮之后,先前的分布将开始类似于真实的分布:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hZkX5HyG-1681653750832)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00156.gif)]
我们将通过在 Python 中实现 TS 来更好地理解 TS。 首先,让我们初始化变量:
代码语言:javascript复制# number of rounds (iterations)
num_rounds = 20000
# Count of number of times an arm was pulled
count = np.zeros(10)
# Sum of rewards of each arm
sum_rewards = np.zeros(10)
# Q value which is the average reward
Q = np.zeros(10)
# initialize alpha and beta values
alpha = np.ones(10)
beta = np.ones(10)
定义我们的thompson_sampling
函数:
def thompson_sampling(alpha,beta):
samples = [np.random.beta(alpha[i] 1,beta[i] 1) for i in range(10)]
return np.argmax(samples)
使用 TS 开始与老虎机一起玩:
代码语言:javascript复制for i in range(num_rounds):
# Select the arm using thompson sampling
arm = thompson_sampling(alpha,beta)
# Get the reward
observation, reward, done, info = env.step(arm)
# update the count of that arm
count[arm] = 1
# Sum the rewards obtained from the arm
sum_rewards[arm] =reward
# calculate Q value which is the average rewards of the arm
Q[arm] = sum_rewards[arm]/count[arm]
# If it is a positive reward increment alpha
if reward >0:
alpha[arm] = 1
# If it is a negative reward increment beta
else:
beta[arm] = 1
print( 'The optimal arm is {}'.format(np.argmax(Q)))
输出如下:
代码语言:javascript复制The optimal arm is 3
MAB 的应用
到目前为止,我们已经研究了 MAB 问题以及如何使用各种探索策略来解决它。 但是老虎机不仅仅用来玩老虎机,还可以用来玩老虎机。 他们有很多应用。
老虎机被用来代替 AB 测试。 AB 测试是常用的经典测试方法之一。 假设您有网站登录页面的两个版本。 您怎么知道大多数用户喜欢哪个版本? 您进行 AB 测试以了解用户最喜欢哪个版本。
在 AB 测试中,我们分配一个单独的探索时间和一个单独的开采时间。 也就是说,它有两个不同的专用时间,仅用于探索和利用。 但是这种方法的问题在于,这将引起很多遗憾。 因此,我们可以使用解决 MAB 的各种探索策略来最大程度地减少后悔。 与其与老虎机分别进行完整的探索和利用,不如以适应性方式同时进行探索和利用。
老虎机广泛用于网站优化,最大化转化率,在线广告,广告序列等。 考虑您正在运行一个短期活动。 如果在这里进行 AB 测试,那么几乎所有的时间都将花费在探索和利用上,因此在这种情况下,使用老虎机将非常有用。
使用 MAB 识别正确的广告横幅
假设您正在运营一个网站,并且同一广告有五个不同的横幅,并且您想知道哪个横幅吸引了用户。 我们将此问题陈述建模为老虎机问题。 假设这五个横幅是老虎机的五个武器,如果用户点击广告,我们将奖励 1 分;如果用户未点击广告,我们将奖励 0 分。
在正常的 AB 测试中,我们将对这五个横幅广告进行完整的探索,然后再确定哪个横幅广告是最好的。 但这将耗费我们大量的精力和时间。 取而代之的是,我们将使用良好的探索策略来确定哪个横幅广告将为我们带来最多的回报(最多的点击次数)。
首先,让我们导入必要的库:
代码语言:javascript复制import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
让我们模拟一个形状为5 x 10,000
的数据集,其中列为Banner_type
广告,行为0
或1
,即单击了广告(1
)或没有被用户点击(0
):
df = pd.DataFrame()
df['Banner_type_0'] = np.random.randint(0,2,100000)
df['Banner_type_1'] = np.random.randint(0,2,100000)
df['Banner_type_2'] = np.random.randint(0,2,100000)
df['Banner_type_3'] = np.random.randint(0,2,100000)
df['Banner_type_4'] = np.random.randint(0,2,100000)
让我们查看几行数据:
代码语言:javascript复制df.head()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rWzq8q6E-1681653750832)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00157.gif)]
代码语言:javascript复制num_banner = 5
no_of_iterations = 100000
banner_selected = []
count = np.zeros(num_banner)
Q = np.zeros(num_banner)
sum_rewards = np.zeros(num_banner)
定义ε
贪婪策略:
def epsilon_greedy(epsilon):
random_value = np.random.random()
choose_random = random_value < epsilon
if choose_random:
action = np.random.choice(num_banner)
else:
action = np.argmax(Q)
return action
代码语言:javascript复制for i in range(no_of_iterations):
banner = epsilon_greedy(0.5)
reward = df.values[i, banner]
count[banner] = 1
sum_rewards[banner] =reward
Q[banner] = sum_rewards[banner]/count[banner]
banner_selected.append(banner)
我们可以绘制结果并查看哪个横幅广告为我们带来了最大的点击次数:
代码语言:javascript复制sns.distplot(banner_selected)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rVo3h3kg-1681653750833)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00158.jpeg)]
情境老虎机
我们刚刚看到了老虎机如何用于向用户推荐正确的广告横幅。 但是横幅首选项因用户而异。 用户 A 喜欢横幅广告类型 1,但是用户 B 喜欢横幅广告类型 3。因此,我们必须根据用户行为来个性化广告横幅广告。 我们该怎么做? 我们引入了一种新的老虎机类型,称为情境老虎机。
在正常的 MAB 问题中,我们执行操作并获得奖励。 但是对于情境老虎机,我们不仅要单独采取行动,还要采取环境状态。 国家拥有环境。 在这里,状态指定了用户的行为,因此我们将根据状态(用户行为)采取行动(展示广告),从而获得最大的回报(广告点击)。 因此,上下文老虎机被广泛用于根据用户的偏好行为来个性化内容。 它们用于解决推荐系统中面临的冷启动问题。 Netflix 使用上下文盗贼根据用户行为来个性化电视节目的插图。
总结
在本章中,我们了解了 MAB 问题以及如何将其应用于不同的应用。 我们了解了解决探索-利用困境的几种方法。 首先,我们查看了ε
贪婪策略,在其中我们以概率epsilon
进行了探索,并以概率 1 epsilon
进行了探索。 我们查看了 UCB 算法,在该算法中我们选择了具有最大上限值的最佳操作,其次是 TS 算法,在此我们通过 beta 分布获得了最佳操作。
在接下来的章节中,我们将学习深度学习以及如何使用深度学习解决 RL 问题。
问题
问题列表如下:
- 什么是 MAB 问题?
- 什么是探索利用困境?
epsilon
在ε
贪婪策略中有何意义?- 我们如何解决探索与利用的困境?
- 什么是 UCB 算法?
- 汤普森采样与 UCB 算法有何不同?
进一步阅读
您还可以参考以下链接:
- 个性化的上下文老虎机
- Netflix 如何使用情境老虎机
- 使用 MAB 的协同过滤
七、深度学习基础
到目前为止,我们已经了解了强化学习(RL)的工作原理。 在接下来的章节中,我们将学习深度强化学习(DRL),它是深度学习和 RL 的结合。 DRL 在 RL 社区引起了很多关注,并且对解决许多 RL 任务产生了严重影响。 要了解 DRL,我们需要在深度学习方面有坚实的基础。 深度学习实际上是机器学习的一个子集,并且全都与神经网络有关。 深度学习已经存在了十年,但是之所以现在如此流行,是因为计算的进步和海量数据的可用性。 拥有如此庞大的数据量,深度学习算法将胜过所有经典的机器学习算法。 因此,在本章中,我们将学习几种深度学习算法,例如循环神经网络(RNN),长短期记忆(LSTM)和卷积神经网络(CNN)算法及其应用。
在本章中,您将了解以下内容:
- 人工神经元
- 人工神经网络(ANN)
- 建立神经网络对手写数字进行分类
- RNN
- LSTM
- 使用 LSTM 生成歌曲歌词
- CNN
- 使用 CNN 对时尚产品分类
人工神经元
在了解 ANN 之前,首先让我们了解什么是神经元以及大脑中神经元的实际工作方式。 神经元可以定义为人脑的基本计算单元。 我们的大脑包含大约 1000 亿个神经元。 每个神经元通过突触连接。 神经元通过称为树突的分支状结构从外部环境,感觉器官或其他神经元接收输入,如下图所示。 这些输入被增强或减弱,也就是说,根据它们的重要性对其进行加权,然后将它们加在一起在体(细胞体)中。 然后,这些合计的输入从细胞体中被处理并穿过轴突,然后被发送到其他神经元。 下图显示了基本的单个生物神经元:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WLfsWv7U-1681653750833)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00159.gif)]
现在,人工神经元如何工作? 假设我们有三个输入, x1
,x2
和x3
,预测输出y
。 这些输入乘以权重,即w[1]
,w[2]
和w[3]
,即x1 * w[1] w2 * w[2] x3 * w[3]
。 但是,为什么我们将这些输入乘以权重? 因为在计算输出y
时,所有输入都不是同等重要的。 假设x2
在计算输出方面比其他两个输入更为重要。 然后,为w[2]
分配较高的值,而不是为其他两个权重分配较高的值。 因此,将权重乘以输入后, x2
的值将比其他两个输入值高。 在将输入与权重相乘后,我们将它们求和,然后添加一个称为偏差b
的值。 因此,z = (x1 * w1 x2 * w2 x3 * w3) b
,即:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FSVaOAPg-1681653750833)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00160.jpeg)]
z
看起来不像线性回归方程吗? 不仅仅是直线方程吗?z = mx b
。
其中m
是权重(系数),x
是输入,b
是偏差(截距)。 嗯,是。 那么神经元和线性回归有什么区别? 在神经元中,我们通过应用称为激活或传递函数的函数f()
向结果z
引入非线性。 因此,我们的输出为y = f(z)
。 下图显示了一个人工神经元:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bpgtt4UK-1681653750834)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00161.gif)]
在神经元中,我们将输入x
乘以权重w
,并在应用激活函数f(z)
之前加上偏差b
。得出此结果,并预测输出y
。
人工神经网络
神经元很酷,对吗? 但是单个神经元不能执行复杂的任务,这就是为什么我们的大脑拥有数十亿个神经元,这些神经元分层组织,形成一个网络。 类似地,人工神经元分层排列。 每层都将以信息从一层传递到另一层的方式连接。 典型的 ANN 由以下几层组成:
- 输入层
- 隐藏层
- 输出层
每层都有一个神经元集合,一层中的神经元与另一层中的所有神经元相互作用。 但是,同一层中的神经元不会互相影响。 下图显示了典型的 ANN:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UX3uTKN4-1681653750834)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00162.gif)]
输入层
输入层是我们向网络提供输入的地方。 输入层中神经元的数量就是我们馈送到网络的输入数量。 每个输入都会对预测输出产生一定的影响,并将其乘以权重,同时将添加偏差并将其传递给下一层。
隐藏层
输入层和输出层之间的任何层都称为隐藏层。 它处理从输入层接收的输入。 隐藏层负责推导输入和输出之间的复杂关系。 即,隐藏层标识数据集中的模式。 可以有任意数量的隐藏层,但是我们必须根据我们的问题选择许多隐藏层。 对于一个非常简单的问题,我们只能使用一个隐藏层,但是在执行诸如图像识别之类的复杂任务时,我们使用许多隐藏层,其中每个层都负责提取图像的重要特征,以便我们可以轻松识别图像。 当我们使用许多隐藏层时,该网络称为深度神经网络。
输出层
处理输入后,隐藏层将其结果发送到输出层。 顾名思义,输出层发出输出。 输出层中神经元的数量与我们希望网络解决的问题类型有关。 如果是二分类,则输出层中神经元的数量会告诉我们输入所属的类。 如果是五类的多类分类,并且如果我们要获取每个类作为输出的概率,则输出层中的神经元数为五,每个神经元发出该概率。 如果是回归问题,则输出层中只有一个神经元。
激活函数
激活函数用于在神经网络中引入非线性。 我们将激活函数应用于权重乘以并加到偏差上的输入,即f(z)
,其中z = 输入 * 权重 偏差
。 激活函数有以下几种:
- Sigmoid 函数:Sigmoid 函数是最常用的激活函数之一。 它在
0
和1
之间缩放值。 可以将 Sigmoid 函数定义为f(z) = 1 / (1 e^z)
。 当我们将此函数应用于z
时,值将在 0 到1
的范围内缩放。 这也称为逻辑函数。 它是 Sigmoid,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R2x5R4B1-1681653750834)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00164.gif)]
- 双曲正切函数:与 Sigmoid 函数不同,双曲正切函数在
-1
和1
之间缩放值。 双曲正切函数可以定义为f(z) = (e^(2z) - 1) / (e^(2z) 1)
。 当我们将此函数应用于z
时,这些值将在-1
至1
的范围内缩放。 它也是 S 形,但居中为零,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pWXVp2Bn-1681653750834)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00166.gif)]
- ReLU 函数:ReLU 也称为整流线性单元。 它是使用最广泛的激活函数之一。 ReLU 函数可以定义为
f(z) = max(0, z)
,即当z
小于 0 且f(z)
为 0。 当z
大于或等于0
时,等于z
:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gTxnGnY3-1681653750834)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00168.gif)]
- Softmax 函数:softmax 函数实际上是 Sigmoid 函数的推广。 它通常应用于网络的最后一层,同时执行多类分类任务。 它给出了每个类别作为输出的概率,因此 softmax 值的总和将始终等于
1
。 可以定义为σ(z[i]) = e^z[i] / Σ[j] e^z[j]
。
深入研究 ANN
我们知道,在人工神经元中,我们将输入乘以权重,对其施加偏倚,然后应用激活函数来产生输出。 现在,我们将看到在神经元分层放置的神经网络环境中如何发生这种情况。 网络中的层数等于隐藏层数加输出层数。 我们不考虑输入层。 考虑一个具有一个输入层,一个隐藏层和一个输出层的两层神经网络,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dVhYSEKL-1681653750835)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00170.gif)]
假设我们有两个输入, x1
和x2
,我们必须预测输出y
]。 由于我们有两个输入,因此输入层中的神经元数将为两个。 现在,这些输入将乘以权重,然后添加偏差并将结果值传播到将应用激活函数的隐藏层。 因此,首先我们需要初始化权重矩阵。 在现实世界中,我们不知道哪个输入真正重要,因此需要权重较高才能计算输出。 因此,我们将随机初始化权重和偏差值。 我们可以将输入层和隐藏层之间的权重和偏差分别表示为w[xh]
和 b[h]
。 权重矩阵的大小如何? 权重矩阵的大小必须为[当前层中的神经元数量 * 下一层中的神经元数量]
。 这是为什么? 因为这是基本的矩阵乘法规则。 要乘以任意两个矩阵AB
,矩阵A
中的列数必须等于矩阵B
中的行数。因此,权重矩阵w[xh]
的大小应为[输入层中的神经元数量 * 隐藏层中的神经元数量]
,即2 x 4
:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PA4wr32O-1681653750840)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00171.jpeg)]
即,z[1] = 输入 * 权重 偏差
。 现在,这将传递到隐藏层。 在隐藏层中,我们将激活函数应用于z[1]
。 让我们考虑以下 Sigmoid 激活函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YURuopsu-1681653750840)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00172.jpeg)]
应用激活函数后,我们再次将结果a[1]
乘以新的权重矩阵,并添加在隐藏层和输出层之间流动的新偏差值。 我们可以将该权重矩阵和偏差分别表示为w[hy]
和b[y]
。 该权重矩阵w[hy]
的大小将为[隐藏层中的神经元数量 * 输出层中的神经元数量]
。 由于我们在隐藏层有四个神经元,在输出层有一个神经元,因此w[hy]
矩阵大小为4 x 1
。 因此,我们将a[1]
乘以权重矩阵w[hy]
,然后加上偏差b[y]
并将结果传递到下一层,即输出层:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XktFlbNR-1681653750840)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00173.jpeg)]
现在,在输出层中,我们将 Sigmoid 函数应用于z[2]
,这将产生输出值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q3fAQmhY-1681653750840)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00174.jpeg)]
从输入层到输出层的整个过程称为正向传播,如下所示:
代码语言:javascript复制 def forwardProp():
z1 = np.dot(x,wxh) bh
a1 = sigmoid(z1)
z2 = np.dot(a1,why) by
yHat = sigmoid(z2)
前向传播很酷,不是吗? 但是我们如何知道神经网络生成的输出是否正确? 我们必须定义一个新函数,称为成本函数J
,也称为损失函数,它告诉我们神经网络的表现如何。 有许多不同的成本函数。 我们将均方误差用作成本函数,可以将其定义为实际值(y)
与预测值(y_hat)
之间的均方差:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rnhYsHl2-1681653750841)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00177.jpeg)]
我们的目标是最小化成本函数,以便我们的神经网络预测会更好。 如何使成本函数最小化? 我们可以通过更改正向传播中的某些值来最小化成本函数。 我们可以改变哪些值? 显然,我们不能更改输入和输出。 现在,我们剩下了权重和偏差值。 我们只是随机初始化权重矩阵和偏差,所以它不是完美的。 现在,我们将调整神经网络的权重矩阵(w[xh]
和w[hy]
) 结果。 我们如何调整这些权重矩阵? 这是一种称为梯度下降的新技术。
梯度下降
由于正向传播,我们处于输出层。 因此,现在,我们将网络从输出层反向传播到输入层,并通过计算成本函数相对于权重的梯度来最小化误差,从而更新权重。 听起来令人困惑,对吧? 让我们从一个类比开始。 假设您位于山顶上,如下图所示,并且您想到达山顶的最低点。 您将必须在山上向下走一步,这会导致您到达最低点(即,您从山上下降到最低点)。 可能有许多地区看起来像山上的最低点,但我们必须到达最低点,实际上这是最低点。 也就是说,当存在全局最低点时,您不应被认为是最低点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vJ8Hgt5W-1681653750841)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00178.gif)]
同样,我们可以表示成本函数,如下所示。 这是成本与权重的关系图。 我们的目标是使成本函数最小化。 也就是说,我们必须达到成本最低的最低点。 该点显示了我们的初始权重(即我们在山上的位置)。 如果将这一点向下移动,则可以到达误差最小的位置,即成本函数上的最低点(山上的最低点):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OTHHpe9c-1681653750841)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00179.gif)]
我们如何向下移动该点(初始权重)? 我们如何下降并到达最低点? 我们可以通过计算成本函数相对于该点的梯度来移动该点(初始权重)。 梯度是导数,实际上是切线的斜率,如下图所示。 因此,通过计算梯度,我们下降(向下移动)并到达最低点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jBYHPme4-1681653750841)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00180.gif)]
计算梯度后,我们通过权重更新规则更新旧权重:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WKwo7piI-1681653750842)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00181.jpeg)]
什么是α? 它被称为学习率。 如果学习率很小,那么我们会向下走一小步,并且梯度下降会很慢。 如果学习率很高,那么我们将迈出一大步,梯度下降将很快,但是我们可能无法达到全局最小值,而陷入局部最小值。 因此,学习率应选择最佳,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4YYBiiBK-1681653750842)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00182.gif)]
现在,让我们从数学上看一下。 现在,我们将讨论很多有趣的数学运算,因此戴上微积分帽并按照以下步骤操作。 因此,我们有两个权重,一个是w[xh]
,对输入权重是隐藏的,另一个是w[hy]
, 隐藏以输出权重。 我们需要根据我们的权重更新规则来更新这些权重。 为此,首先,我们需要计算成本函数相对于权重的导数。
由于我们正在反向传播,也就是说,从输出层到输入层,我们的第一权重将是w[hy]
。 因此,现在我们需要计算J
相对于w[hy]
的导数。 我们如何计算导数? 回想一下我们的成本函数J = 1 / 2(y - y_hat)
。 我们无法直接计算导数,因为J
中没有w[hy]
项。
回顾如下给出的前向传播方程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zdkKilDQ-1681653750842)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00184.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3g4cPVQM-1681653750843)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00185.jpeg)]
首先,我们将计算相对于y_hat
的偏导数,然后从y_hat
计算相对于z[2]
的偏导数。 从z[2]
,我们可以直接计算我们的导数w[hy]
。 这实际上是连锁规则。
因此,我们的等式变为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-864Bvi1U-1681653750843)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00191.jpeg)] ----(1)
我们将计算以下每一项:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qz3fQf4v-1681653750843)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00192.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7lVmsi4c-1681653750844)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00193.jpeg)]
其中σ'
是我们的 Sigmoid 激活函数的导数。 我们知道 Sigmoid 函数是σ = 1 / (1 e^(-z))
,所以 Sigmoid 函数的导数将是σ' = e^(-z) / (1 e^(-z))^2
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OxlPe5Y8-1681653750844)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00197.jpeg)]
我们将所有这些替换为第一个方程式(1)
。
现在,我们需要针对下一个权重w[xh]
来计算J
的导数。 同样,我们无法直接根据J
计算w[xh]
的导数,因为我们在J
中的没有任何w[xh]
项。 因此,我们需要使用链式规则; 再次回顾我们的前向传播步骤:*
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tsUc7mKk-1681653750844)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00184.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8nQqHoIB-1681653750844)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00185.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ToFPHstg-1681653750845)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00198.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v4K40j04-1681653750845)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00199.jpeg)]
现在,权重w[xh]
的梯度计算将变为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ViabYAim-1681653750845)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00200.jpeg)] ----(2)
我们将计算以下每一项:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fpn95J7Z-1681653750845)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00201.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4XW8Dxzt-1681653750845)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00202.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tuoH7j86-1681653750846)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00203.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l6hwmKPO-1681653750846)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00204.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lGd5IZ6R-1681653750846)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00205.jpeg)]
一旦我们计算了两个权重的梯度,就将根据权重更新规则更新以前的权重。
现在,让我们做一些编码。 看等式(1)
和(2)
。 我们在两个方程式中都有∂J/∂y_hat
和∂J/∂z[2]
,因此我们不必一遍又一遍地进行计算。 我们将其定义为delta3
:
delta3 = np.multiply(-(y-yHat),sigmoidPrime(z2))
现在,我们计算w[hy]
的梯度为:
dJ_dWhy = np.dot(a1.T,delta3)
我们将w[xh]
的梯度计算为:
delta2 = np.dot(delta3,Why.T)*sigmoidPrime(z1)
dJ_dWxh = np.dot(X.T,delta2)
我们将根据权重更新规则将权重更新为:
代码语言:javascript复制Wxh = -alpha * dJ_dWhy
Why = -alpha * dJ_dWxh
此反向传播的完整代码如下:
代码语言:javascript复制 def backProp():
delta3 = np.multiply(-(y-yHat),sigmoidPrime(z2))
dJdW2 = np.dot(a1.T, delta3)
delta2 = np.dot(delta3,Why.T)*sigmoidPrime(z1)
dJdW1 = np.dot(X.T, delta2)
Wxh = -alpha * dJdW1
Why = -alpha * dJdW2
在继续之前,让我们熟悉一下神经网络中一些常用的术语:
- 正向传播:正向传播意味着从输入层到输出层的正向传播。
- 反向传播:反向传播表示从输出层向输入层的反向传播。
- 周期:周期指定了神经网络看到我们整个训练数据的次数。 因此,对于所有训练样本,我们可以说一个周期等于一个向前通过和一个向后通过。
- 批量大小:批量大小指定了我们在一次向前和向后一次通过中使用的训练样本数。
- 迭代次数:迭代次数表示通过次数,其中
一遍 = 一次正向 一次反向
。
假设我们有 12,000 个训练样本,而我们的批量大小为 6,000。 我们将需要两个迭代来完成一个周期。 也就是说,在第一次迭代中,我们传递了前 6,000 个样本,并执行了前向遍历和后向遍历; 在第二次迭代中,我们传递接下来的 6,000 个样本,并执行正向传播和反向传递。 经过两次迭代,我们的神经网络将看到整个 12,000 个训练样本,这使它成为一个周期。
TensorFlow 中的神经网络
现在,我们将看到如何使用 TensorFlow 构建基本的神经网络,该网络可以预测手写数字。 我们将使用流行的 MNIST 数据集,该数据集具有标记的手写图像的集合以进行训练。
首先,我们必须导入 TensorFlow 并从tensorflow.examples.tutorial.mnist
加载数据集:
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("/tmp/data/", one_hot=True)
现在,我们将看到数据中包含的内容:
代码语言:javascript复制print("No of images in training set {}".format(mnist.train.images.shape))
print("No of labels in training set {}".format(mnist.train.labels.shape))
print("No of images in test set {}".format(mnist.test.images.shape))
print("No of labels in test set {}".format(mnist.test.labels.shape))
它将打印以下内容:
代码语言:javascript复制No of images in training set (55000, 784)
No of labels in training set (55000, 10)
No of images in test set (10000, 784)
No of labels in test set (10000, 10)
training set
中有55000
个图像,每个图像的大小均为784
。 我们也有10
标签,它们实际上是0
至9
。 类似地,我们在test set
中有10000
图像。
现在,我们绘制输入图像以查看其外观:
代码语言:javascript复制img1 = mnist.train.images[41].reshape(28,28)
plt.imshow(img1, cmap='Greys')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dhCrxOaA-1681653750846)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00208.gif)]
让我们开始建立我们的网络。 我们将构建一个包含一个输入层,一个隐藏层和一个预测手写数字的输出层的两层神经网络。
首先,我们为输入和输出定义占位符。 由于我们的输入数据形状为784
,因此我们可以将输入占位符定义为:
x = tf.placeholder(tf.float32, [None, 784])
None
表示什么? None
指定传递的样本数(批大小),这将在运行时动态确定。
由于我们将10
类作为输出,因此可以将placeholder
输出定义为:
y = tf.placeholder(tf.float32, [None, 10]
接下来,我们初始化超参数:
代码语言:javascript复制learning_rate = 0.1
epochs = 10
batch_size = 100
然后,我们将隐藏层的输入之间的权重和偏差分别定义为w_xh
和b_h
。 我们用值初始化权重矩阵,从正态分布中随机抽取标准差为0.03
的值:
w_xh = tf.Variable(tf.random_normal([784, 300], stddev=0.03), name='w_xh')
b_h = tf.Variable(tf.random_normal([300]), name='b_h')
接下来,我们将隐藏层与输出层之间的权重和偏差分别定义为w_hy
和b_y
:
w_hy = tf.Variable(tf.random_normal([300, 10], stddev=0.03), name='w_hy')
b_y = tf.Variable(tf.random_normal([10]), name='b_y')
现在开始进行正向传播。 回想一下我们在前向传播中执行的步骤:
代码语言:javascript复制z1 = tf.add(tf.matmul(x, w_xh), b_h)
a1 = tf.nn.relu(z1)
z2 = tf.add(tf.matmul(a1, w_hy), b_y)
yhat = tf.nn.softmax(z2)
我们将成本函数定义为交叉熵损失。 交叉熵损失也称为对数损失,可以定义如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QhEtUO6v-1681653750846)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00209.jpeg)]
其中y[i]
是实际值,y_hat[i]
是预测值:
cross_entropy = tf.reduce_mean(-tf.reduce_sum(y * tf.log(yhat), reduction_indices=[1]))
我们的目标是使成本函数最小化。 我们可以通过向后传播网络并执行梯度下降来最小化成本函数。 使用 TensorFlow,我们不必手动计算梯度; 我们可以使用 TensorFlow 的内置梯度下降优化器功能,如下所示:
代码语言:javascript复制optimiser = tf.train.GradientDescentOptimizer(learning_rate=learning_rate).minimize(cross_entropy)
为了评估我们的模型,我们将计算精度如下:
代码语言:javascript复制correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(yhat, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
我们知道 TensorFlow 是通过构建计算图来运行的,到目前为止,我们所写的任何内容实际上只有在启动 TensorFlow 会话后才能运行。 所以,让我们这样做。
首先,初始化 TensorFlow 变量:
代码语言:javascript复制init_op = tf.global_variables_initializer()
现在,开始 TensorFlow 会话并开始训练模型:
代码语言:javascript复制with tf.Session() as sess:
sess.run(init_op)
total_batch = int(len(mnist.train.labels) / batch_size)
for epoch in range(epochs):
avg_cost = 0
for i in range(total_batch):
batch_x, batch_y = mnist.train.next_batch(batch_size=batch_size)
_, c = sess.run([optimiser, cross_entropy],
feed_dict={x: batch_x, y: batch_y})
avg_cost = c / total_batch
print("Epoch:", (epoch 1), "cost =""{:.3f}".format(avg_cost))
print(sess.run(accuracy, feed_dict={x: mnist.test.images, y: mnist.test.labels}))
RNN
鸟儿在 ____ 中飞翔。 如果我要求您预测空白,则可能会预测“天空”。 您如何预测“天空”一词会很好地填补这一空白? 因为您阅读了整个句子并根据理解句子的上下文,预测“天空”是正确的词。 如果我们要求正常的神经网络为该空格预测正确的单词,它将无法预测正确的单词。 这是因为正常神经网络的输出仅基于当前输入。 因此,神经网络的输入将只是前一个单词the
。 也就是说,在正常的神经网络中,每个输入都彼此独立。 因此,在我们必须记住输入序列以预测下一个序列的情况下,它将不能很好地执行。
我们如何使我们的网络记住整个句子以正确预测下一个单词? 这是 RNN 发挥作用的地方。 RNN 不仅基于当前输入,而且还基于先前的隐藏状态来预测输出。 您可能想知道为什么 RNN 必须基于当前输入和先前的隐藏状态来预测输出,以及为什么它不能仅使用当前输入和先前的输入而不是当前输入和先前的隐藏状态来预测输出 。 这是因为前一个输入将存储有关前一个单词的信息,而前一个隐藏状态将捕获有关整个句子的信息,也就是说,前一个隐藏状态将存储上下文。 因此,基于当前输入和先前的隐藏状态而不是仅基于当前输入和先前的输入来预测输出非常有用。
RNN 是一种特殊类型的神经网络,广泛应用于顺序数据。 换句话说,它适用于排序重要的数据。 简而言之,RNN 有一个存储先前信息的存储器。 它广泛应用于各种自然语言处理(NLP)任务,例如机器翻译,情感分析等。 它也适用于时间序列数据,例如股票市场数据。 仍然不清楚 RNN 到底是什么? 查看下图,该图显示了正常神经网络和 RNN 的比较:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TUWfSPUe-1681653750847)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00212.gif)]
您是否注意到 RNN 与我们在上一主题中看到的普通神经网络有何不同? 是。 区别在于隐藏状态中存在一个循环,这意味着如何使用以前的隐藏状态来计算输出。
还是令人困惑? 查看以下 RNN 的展开版本:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lUGZCgyI-1681653750847)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00213.gif)]
如您所见,根据当前输入x1
,当前隐藏状态*预测输出y1, h[1]
以及先前的隐藏状态h[0]
。 类似地,看看如何计算输出y[2]
。 它采用当前输入x[2]
和当前隐藏状态 h[2]
以及先前的隐藏状态h[1]
。 这就是 RNN 的工作方式; 它需要当前输入和先前的隐藏状态来预测输出。 我们可以将这些隐藏状态称为内存,因为它们保存了到目前为止已经看到的信息。
现在,我们将看到一些数学运算:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kHcxgVvP-1681653750847)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00214.gif)]
在上图中:
U
代表输入到隐藏状态的权重矩阵W
代表隐藏状态到隐藏状态的权重矩阵V
表示隐藏状态到输出的权重矩阵
因此,在前向传递中,我们计算以下内容:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LdCWD8tg-1681653750847)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00215.jpeg)]
即,时间 t 的隐藏状态 = tanh([输入到隐藏权重的矩阵 * 输入] [隐藏权重到隐藏权重的矩阵 *时间 t-1 的先前隐藏状态])
:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iAbu4Fjc-1681653750847)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00216.jpeg)]
即,时间的 t 输出 = Sigmoid(隐藏权重到输出的矩阵 * 时间 t 的隐藏状态)
。
我们还可以将损失函数定义为交叉熵损失,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VFNOAUpb-1681653750848)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00217.jpeg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MpW098gO-1681653750848)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00218.jpeg)]
在前面的示例中,y[t]
是时间t
时的实际单词,y_hat[t]
是时间t
时的预测单词。 由于我们将整个序列作为训练样本,因此总损失将是每个时间步的损失总和。
时间上的反向传播
现在,我们如何训练 RNN? 就像我们训练了正常的神经网络一样,我们可以使用反向传播来训练 RNN。 但是在 RNN 中,由于所有时间步长都具有依赖性,因此每个输出的梯度将不仅取决于当前时间步长,还取决于先前时间步长。 我们将此沿时间的反向传播(BPTT)。 它与反向传播基本相同,不同之处在于它应用了 RNN。 要查看它如何在 RNN 中发生,让我们考虑一下 RNN 的展开版本,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6q1Bf50s-1681653750848)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00221.gif)]
在上图中,L[1]
,L[2]
和L[3]
是每个时间步的损失。 现在,我们需要在每个时间步长计算相对于我们的权重矩阵U
,V
和W
的损失梯度。 就像我们之前通过对每个时间步长求和来计算总损失一样,我们用每个时间步长的梯度之和来更新权重矩阵:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bkbxaX2E-1681653750848)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00222.jpeg)]
但是,此方法存在问题。 梯度计算涉及计算关于激活函数的梯度。 当我们计算相对于 Sigmoid/tanh 函数的梯度时,该梯度将变得非常小。 当我们在许多时间步长上进一步传播网络并乘以梯度时,梯度将趋于变得越来越小。 这称为消失梯度问题。 那么,由于这个问题会发生什么呢? 由于梯度会随着时间消失,因此我们无法了解有关长期依赖关系的信息,也就是说,RNN 无法将信息在内存中保留更长的时间。
逐渐消失的梯度不仅会出现在 RNN 中,还会发生在其他深层网络中,在这些深层网络中,当我们使用 Sigmoid/tanh 函数时会出现许多隐藏层。 还有一个称为爆炸梯度的问题,其中梯度值变得大于 1,当我们将这些梯度相乘时,它将导致很大的数字。
一种解决方案是将 ReLU 用作激活函数。 但是,我们有一个称为 LSTM 的 RNN 变体,它可以有效解决消失的梯度问题。 我们将在接下来的部分中看到它的工作原理。
长短期记忆 RNN
RNN 非常酷,对吧? 但是我们在训练 RNN 时遇到了一个问题,即消失梯度问题。 让我们来探讨一下。 天空是 __ 的。 RNN 可以根据所看到的信息轻松地将最后一个单词预测为“蓝色”。 但是 RNN 无法涵盖长期依赖关系。 这意味着什么? 假设 Archie 在中国生活了 20 年。 他喜欢听好音乐。 他是一个非常大的漫画迷。 他的 __ 很流利。 现在,您将预测空白为中文。 您是如何预测的? 因为您了解 Archie 在中国生活了 20 年,所以您认为他可能会说流利的中文。 但是 RNN 不能在记忆中保留所有这些信息以说 Archie 能够说流利的中文。 由于消失的梯度问题,它无法长时间在内存中重新收集/记住信息。 我们该如何解决?
LSTM 来了!!!!
LSTM 是 RNN 的一种变体,可以解决梯度消失的问题。 LSTM 会在需要时将信息保留在内存中。 因此,基本上,RNN 单元将替换为 LSTM。 LSTM 如何实现这一目标?
下图显示了一个典型的 LSTM 单元:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hIRdyaOy-1681653750848)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00223.gif)]
LSTM 单元称为内存,它们负责存储信息。 但是信息必须在存储器中保留多长时间? 我们什么时候可以删除旧信息并用新信息更新单元格? 所有这些决定将由以下三个特殊部门做出:
- 遗忘门
- 输入门
- 输出门
如果查看 LSTM 单元,则顶部水平线C[t]
被称为单元状态。 这是信息流向的地方。 LSTM 门将不断更新有关单元状态的信息。 现在,我们将看到这些门的功能:
- 遗忘门:遗忘门负责确定哪些信息不应处于单元状态。 看下面的陈述: “Harry 是一位好歌手。 他住在纽约。 Zayn 也是一位出色的歌手。” 一旦我们开始谈论 Zayn,网络就会了解到主题已从 Harry 更改为 Zayn,并且不再需要有关 Harry 的信息。 现在,“遗忘门”将从单元状态中删除/忘记关于哈利的信息。
- 输入门:输入门负责确定应在存储器中存储哪些信息。 让我们考虑相同的示例: “Harry 是一位出色的歌手。 他住在纽约。 Zayn 也是一位出色的歌手。” 因此,在“遗忘门”从单元状态中删除信息之后,输入门将确定存储器中必须包含哪些信息。 在此,由于通过遗忘门从单元状态中删除了关于哈利的信息,因此输入门决定用关于 Zayn 的信息来更新单元状态。
- 输出门:该输出门负责确定一次从单元状态
t
应该显示什么信息。 现在,考虑以下句子: “Zayn 的首张专辑取得了巨大的成功。 恭喜 ____。” 在这里,“恭喜”是用于形容名词的形容词。 输出层将预测Zayn
(名词),以填补空白。
使用 LSTM RNN 生成歌曲歌词
现在,我们将看到如何使用 LSTM 网络生成 Zayn Malik 的歌曲歌词。 可以从此处下载数据集,其中包含 Zayn 的歌词集。
首先,我们将导入必要的库:
代码语言:javascript复制import tensorflow as tf
import numpy as np
现在,我们将读取包含歌词的文件:
代码语言:javascript复制with open("Zayn_Lyrics.txt","r") as f:
data=f.read()
data=data.replace('n','')
data = data.lower()
让我们看看数据中包含的内容:
代码语言:javascript复制data[:50]
"now i'm on the edge can't find my way it's inside "
然后,我们将所有字符存储在all_chars
变量中:
all_chars=list(set(data))
我们将唯一字符数存储在unique_chars
中:
unique_chars = len(all_chars)
我们还将字符总数存储在total_chars
中:
total_chars =len(data)
现在,我们将在每个字符与其索引之间创建一个映射。 char_to_ix
将具有字符到索引的映射,而ix_to_char
将具有字符到索引的映射:
char_to_ix = { ch:i for i,ch in enumerate(all_chars) }
ix_to_char = { i:ch for i,ch in enumerate(all_chars) }
也就是说,例如:
代码语言:javascript复制char_to_ix['e']
9
代码语言:javascript复制ix_to_char[9]
e
接下来,我们定义一个generate_batch
函数,该函数将生成输入值和目标值。 目标值只是i
乘以输入值的偏移。
例如:如果input = [12,13,24]
的偏移值为1
,则目标将为[13,24]
:
def generate_batch(seq_length,i):
inputs = [char_to_ix[ch] for ch in data[i:i seq_length]]
targets = [char_to_ix[ch] for ch in data[i 1:i seq_length 1]]
inputs=np.array(inputs).reshape(seq_length,1)
targets=np.array(targets).reshape(seq_length,1)
return inputs,targets
我们将定义序列长度,学习率和节点数,即神经元数:
代码语言:javascript复制seq_length = 25
learning_rate = 0.1
num_nodes = 300
让我们构建我们的 LSTM RNN。 TensorFlow 为我们提供了用于构建 LSTM 单元的BasicLSTMCell()
函数,我们需要指定 LSTM 单元中的单元数以及希望使用的激活函数的类型。
因此,我们将创建一个 LSTM 单元,然后使用tf.nn.dynamic_rnn()
函数使用该单元构建 RNN,它将返回输出和状态值:
def build_rnn(x):
cell= tf.contrib.rnn.BasicLSTMCell(num_units=num_nodes, activation=tf.nn.relu)
outputs, states = tf.nn.dynamic_rnn(cell, x, dtype=tf.float32)
return outputs,states
现在,我们将为输入X
和目标Y
创建一个占位符:
X=tf.placeholder(tf.float32,[None,1])
Y=tf.placeholder(tf.float32,[None,1])
将X
和Y
转换为int
:
X=tf.cast(X,tf.int32)
Y=tf.cast(Y,tf.int32)
我们还将为X
和Y
创建onehot
表示形式,如下所示:
X_onehot=tf.one_hot(X,unique_chars)
Y_onehot=tf.one_hot(Y,unique_chars)
通过调用build_rnn
函数从 RNN 获取输出和状态:
outputs,states=build_rnn(X_onehot)
转置输出:
代码语言:javascript复制outputs=tf.transpose(outputs,perm=[1,0,2])
初始化权重和偏差:
代码语言:javascript复制W=tf.Variable(tf.random_normal((num_nodes,unique_chars),stddev=0.001))
B=tf.Variable(tf.zeros((1,unique_chars)))
我们将通过将输出乘以权重并加上偏差来计算输出:
代码语言:javascript复制Ys=tf.matmul(outputs[0],W) B
接下来,执行 softmax 激活并获得概率:
代码语言:javascript复制prediction = tf.nn.softmax(Ys)
我们将计算cross_entropy
损失为:
cross_entropy=tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=Y_onehot,logits=Ys))
我们的目标是使损失最小化,因此我们将反向传播网络并执行梯度下降:
代码语言:javascript复制optimiser = tf.train.GradientDescentOptimizer(learning_rate=learning_rate).minimize(cross_entropy)
现在,我们将定义名为predict
的辅助函数,该函数根据我们的 RNN 模型得出下一个预测字符的索引:
def predict(seed,i):
x=np.zeros((1,1))
x[0][0]= seed
indices=[]
for t in range(i):
p=sess.run(prediction,{X:x})
index = np.random.choice(range(unique_chars), p=p.ravel())
x[0][0]=index
indices.append(index)
return indices
我们设置batch_size
,批数和epochs
的数量以及shift
值以生成批量:
batch_size=100
total_batch=int(total_chars//batch_size)
epochs=1000
shift=0
最后,我们将开始 TensorFlow 会话并构建模型:
代码语言:javascript复制init=tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init)
for epoch in range(epoch):
print("Epoch {}:".format(epoch))
if shift batch_size 1 >= len(data):
shift =0
## get the input and target for each batch by generate_batch
#function which shifts the input by shift value
## and form target
for i in range(total_batch):
inputs,targets=generate_batch(batch_size,shift)
shift = batch_size
# calculate loss
if(i0==0):
loss=sess.run(cross_entropy,feed_dict={X:inputs, Y:targets})
# We get index of next predicted character by
# the predict function
index =predict(inputs[0],200)
# pass the index to our ix_to_char dictionary and
#get the char
txt = ''.join(ix_to_char[ix] for ix in index)
print('Iteration %i: '%(i))
print ('n %s n' % (txt, ))
sess.run(optimiser,feed_dict={X:inputs,Y:targets})
我们可以看到输出在初始周期是一些随机字符,但是随着训练步骤的增加,我们会得到更好的结果:
代码语言:javascript复制Epoch 0:
Iteration 0:
wsadrpud,kpswkypeqawnlfyweudkgt,khdi nmgof' u vnvlmbis . snsblp,podwjqehb,e;g-'fyqjsyeg,byjgyotsrdf;;u,h.a;ik'sfc;dvtauofd.,q.;npsw'wjy-quw'quspfqw-
.
.
.
Epoch 113:
Iteration 0:
i wanna see you, yes, and she said yes!
卷积神经网络
CNN,也称为卷积网络(ConvNet),是一种特殊的神经网络,广泛用于计算机视觉。 CNN 的应用范围包括从自动驾驶汽车中的视觉功能到 Facebook 图片中朋友的自动标记。 CNN 利用空间信息来识别图像。 但是它们如何真正起作用? 神经网络如何识别这些图像? 让我们逐步进行此步骤。
CNN 通常包含三个主要层:
- 卷积层
- 池化层
- 全连接层
卷积层
当我们输入图像作为输入时,它实际上将转换为像素值矩阵。 这些像素值的范围为 0 到 255,此矩阵的大小为[图像高度 * 图像宽度 * 通道数]
。 如果输入图像的大小为64 x 64
,则像素矩阵大小将为64 x 64 x 3
,其中 3 表示通道号。 灰度图像具有 1 个通道,彩色图像具有 3 个通道(RGB)。 看下面的照片。 当将此图像作为输入输入时,它将转换为像素值矩阵,我们稍后将看到。 为了更好地理解,我们将考虑灰度图像,因为灰度图像具有 1 个通道,因此我们将获得 2D 矩阵。
输入图像如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0mVtWecK-1681653750849)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00224.jpeg)]
现在,让我们在下图中看到矩阵值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fUsKrxHB-1681653750849)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00225.gif)]
因此,这就是图像由矩阵表示的方式。 接下来发生什么? 网络如何从该像素的值识别图像? 现在,我们介绍一个称为卷积的操作。 它用于从图像中提取重要特征,以便我们可以了解图像的全部含义。 假设我们有一只狗的形象; 您认为这张图片的特征是什么,这将有助于我们了解这是狗的图片? 我们可以说身体结构,脸,腿,尾巴等等。 卷积运算将帮助网络学习狗的特征。 现在,我们将看到如何精确执行卷积运算以从图像中提取特征。
众所周知,每个图像都由一个矩阵表示。 假设我们有一个狗图像的像素矩阵,并将其称为输入矩阵。 我们还将考虑称为过滤器的另一个n x n
矩阵,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QnwyBWmF-1681653750849)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00226.gif)]
现在,该过滤器将在我们的输入矩阵上滑动一个像素,并执行逐元素乘法,从而生成一个数字。 困惑? 看下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GVQaYUYe-1681653750849)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00227.gif)]
也就是说,(13 * 0) (8 * 1) (18 * 0) (5 * 1) (3 * 1) (1 * 1) (1 * 0) (9 * 0) (0 * 1) = 17
。
同样,我们将过滤器矩阵在输入矩阵上移动一个像素,然后执行逐元素乘法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a4z1ocUU-1681653750849)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00228.gif)]
即(8 * 0) (18 * 1) (63 * 0) (3 * 1) (1 * 1) (2 * 1) (9 * 0) (0 * 0) (7 * 1) = 31
。
过滤器矩阵将在整个输入矩阵上滑动,执行逐元素乘法,并生成一个称为特征映射或激活图的新矩阵。 该操作称为卷积,如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GhkpvkuQ-1681653750850)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00229.gif)]
以下输出显示了实际的卷积图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CE8YnGGg-1681653750850)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00230.jpeg)]
您可以看到我们的过滤器已检测到实际图像中的边缘并产生了卷积图像。 类似地,使用不同的过滤器从图像中提取不同的特征。
例如,如果我们使用过滤器矩阵,比如说锐化过滤器:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fih4IF6L-1681653750850)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00231.jpeg)]
那么我们的卷积图像将如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4OtRA4Ad-1681653750850)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00232.jpeg)]
因此,过滤器负责通过执行卷积运算从实际图像中提取特征。 将有多个过滤器用于提取生成特征映射的图像的不同特征。 特征映射的深度是我们使用的过滤器的数量。 如果我们使用 5 个过滤器提取特征并生成 5 个特征映射,则特征映射的深度为5
,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e5ydqTxi-1681653750850)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00233.gif)]
当我们有许多过滤器时,我们的网络将通过提取许多特征来更好地理解图像。 在构建 CNN 时,我们不必为此过滤矩阵指定值。 在训练过程中将学习此过滤器的最佳值。 但是,我们必须指定过滤器的数量和要使用的过滤器的大小。
我们可以使用过滤器在输入矩阵上滑动一个像素,然后执行卷积运算。 我们不仅可以滑动一个像素。 我们还可以在输入矩阵上滑动任意数量的像素。 我们在输入矩阵中滑过输入矩阵的像素数称为步幅。
但是,当滑动窗口(过滤器矩阵)到达图像边界时会发生什么? 在这种情况下,我们用零填充输入矩阵,以便可以在图像边缘应用过滤器。 图像上带有零的填充称为相同填充,或宽卷积或零填充,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hPun3Mxe-1681653750851)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00234.gif)]
除了用零填充以外,我们还可以简单地丢弃该区域。 这称为有效填充或窄卷积,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jNpFMksx-1681653750851)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00235.gif)]
执行卷积运算后,我们应用 ReLU 激活函数引入非线性。
池化层
在卷积层之后,我们有了池化层。 池化层用于减少特征映射的维数,并且仅保留必要的细节,因此可以减少计算量。 例如,要确定图像中是否有一只狗,我们不想了解狗在图像中的哪个位置,我们只需要狗的特征。 因此,池化层通过仅保留重要特征来减小空间大小。 有多种类型的池化操作。 最大池化是最常用的池化操作之一,我们仅从窗口内的特征映射中获取最大值。
带有2 x 2
过滤器且步幅为 2 的最大池如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g4Sxfnvt-1681653750851)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00236.gif)]
在平均池中,我们只取窗口内特征映射中元素的平均值,而在汇总池中,我们取窗口中特征映射中元素的总和。
合并操作不会更改特征映射的深度,只会影响高度和宽度。
全连接层
我们可以有多个卷积层,然后是池化层。 但是,这些层只会从输入图像中提取特征并生成激活图。 我们如何仅凭激活图对图像中是否有一条狗进行分类? 我们必须引入一个称为全连接层的新层。 当激活图(现在基本上是图像的特征)应用激活函数时,它将接收输入,并产生输出。 全连接层实际上是正常的神经网络,其中我们具有输入层,隐藏层和输出层。 在这里,我们使用卷积和池化层代替输入层,它们一起产生激活图作为输入。
CNN 架构
现在,让我们看看如何在 CNN 架构中组织所有这些层,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sYGyygbQ-1681653750851)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00237.gif)]
首先,将图像传递到卷积层,在卷积层中我们应用卷积运算以提取特征,然后将特征映射传递到池化层,在其中减小大小。 我们可以根据用例添加任意数量的卷积和池化层。 此后,我们可以添加一个神经网络,该神经网络的末尾有一个隐藏层,称为全连接层,该层对图像进行分类。
使用 CNN 对时尚产品分类
现在,我们将看到如何使用 CNN 对时尚产品进行分类。
首先,我们将照常导入所需的库:
代码语言:javascript复制import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
现在,我们将读取数据。 该数据集位于tensorflow.examples
中,因此我们可以按以下方式直接提取数据:
from tensorflow.examples.tutorials.mnist import input_data
fashion_mnist = input_data.read_data_sets('data/fashion/', one_hot=True)
我们将检查数据中包含的内容:
代码语言:javascript复制print("No of images in training set {}".format(fashion_mnist.train.images.shape))
print("No of labels in training set {}".format(fashion_mnist.train.labels.shape))
print("No of images in test set {}".format(fashion_mnist.test.images.shape))
print("No of labels in test set {}".format(fashion_mnist.test.labels.shape))
代码语言:javascript复制No of images in training set (55000, 784)
No of labels in training set (55000, 10)
No of images in test set (10000, 784)
No of labels in test set (10000, 10)
因此,我们在training set
中具有55000
数据点,在test set
中具有10000
数据点。 我们还具有10
标签,这意味着我们具有10
类别。
我们有10
个产品类别,并将为所有这些产品加上标签:
labels = {
0: 'T-shirt/top',
1: 'Trouser',
2: 'Pullover',
3: 'Dress',
4: 'Coat',
5: 'Sandal',
6: 'Shirt',
7: 'Sneaker',
8: 'Bag',
9: 'Ankle boot'
}
现在,我们来看一些图像:
代码语言:javascript复制img1 = fashion_mnist.train.images[41].reshape(28,28)
# Get corresponding integer label from one-hot encoded data
label1 = np.where(fashion_mnist.train.labels[41] == 1)[0][0]
# Plot sample
print("y = {} ({})".format(label1, labels[label1]))
plt.imshow(img1, cmap='Greys')
输出和视觉效果如下:
代码语言:javascript复制y = 6 (Shirt)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-61iDLYT9-1681653750851)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00238.gif)]
那是一件很不错的衬衫,不是吗? 我们将再看一张图片:
代码语言:javascript复制img1 = fashion_mnist.train.images[19].reshape(28,28)
# Get corresponding integer label from one-hot encoded data
label1 = np.where(fashion_mnist.train.labels[19] == 1)[0][0]
# Plot sample
print("y = {} ({})".format(label1, labels[label1]))
plt.imshow(img1, cmap='Greys')
输出和视觉效果如下:
代码语言:javascript复制y = 8 (Bag)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZFobeBWk-1681653750852)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00239.gif)]
这是一个很好的提包!
因此,现在,我们必须建立一个卷积神经网络,该网络实际上将所有这些图像分类为各自的类别。 我们为输入图像和输出标签定义占位符。 由于我们的输入图像的大小为784
,因此我们为输入x
定义了一个占位符,如下所示:
x = tf.placeholder(tf.float32, [None, 784])
我们需要将输入调整为[p,q,r,s]
格式,其中q
和r
是输入图像的实际大小,即28 x 28
,s
是通道号。 由于我们只有灰度图像,因此s
的值为1
。 p
表示训练样本的数量,即批量大小。 由于我们不知道批量大小,因此可以将其设置为-1
,并且在训练过程中会动态更改它:
x_shaped = tf.reshape(x, [-1, 28, 28, 1])
由于我们有10
个不同的标签,因此我们为输出定义了占位符,如下所示:
y = tf.placeholder(tf.float32, [None, 10])
现在,我们需要定义一个称为conv2d
的函数,该函数实际上执行卷积运算,即输入矩阵(x
)与过滤器(w
)的元素级相乘,步长为1
。 和SAME
填充。
我们设置strides = [1, 1, 1, 1]
。 步幅的第一个和最后一个值设置为1
,这意味着我们不想在训练样本和不同通道之间移动。 步幅的第二个和第三个值也设置为1
,这意味着我们将过滤器在高度和宽度方向上移动1
像素:
def conv2d(x, w):
return tf.nn.conv2d(x, w, strides=[1, 1, 1, 1], padding='SAME')
我们定义了一个称为maxpool2d
的函数来执行池化操作。 我们以2
和SAME
填充跨度执行最大池化。 ksize
表示我们的合并窗口形状:
def maxpool2d(x):
return tf.nn.max_pool(x, ksize=[1, 2, 2, 1], strides=[1, 2, 2, 1], padding='SAME')
接下来,我们定义权重和偏差。 我们将构建一个具有两个卷积层,然后是一个全连接层和一个输出层的卷积网络,因此我们将定义所有这些层的权重。 权重实际上是卷积层中的过滤器。
因此,权重矩阵将初始化为[filter_shape[0],filter_shape[1], number_of_input_channel, filter_size]
。
我们将使用5 x 5
过滤器,并将过滤器大小设置为32
。 由于我们使用灰度图像,因此我们的输入通道号将为1
。 因此,我们的权重矩阵将为[5,5,1,32]
:
w_c1 = tf.Variable(tf.random_normal([5,5,1,32]))
当第二个卷积层将来自具有32
作为其通道输出的第一卷积层的输入时,到下一层的输入通道数变为32
:
w_c2 = tf.Variable(tf.random_normal([5,5,32,64]))
接下来,我们初始化偏差:
代码语言:javascript复制b_c1 = tf.Variable(tf.random_normal([32]))
b_c2 = tf.Variable(tf.random_normal([64]))
现在,我们在第一个卷积层上执行操作,即对输入x
进行卷积操作,并激活 ReLU,然后进行最大池化:
conv1 = tf.nn.relu(conv2d(x, w_c1) b_c1)
conv1 = maxpool2d(conv1)
现在,第一个卷积层的结果将传递到下一个卷积层,在此我们对具有 ReLU 激活的第一个卷积层的结果执行卷积运算,然后进行最大池化:
代码语言:javascript复制conv2 = tf.nn.relu(conv2d(conv1, w_c2) b_c2)
conv2 = maxpool2d(conv2)
经过两个带卷积和池化操作的卷积层后,我们的输入图像将从28 * 28 * 1
降采样为7 * 7 * 1
。 我们需要先平整该输出,然后再将其馈送到全连接层。 然后,第二个卷积层的结果将被馈送到全连接层中,我们将其与权重相乘,添加偏差并应用 ReLU 激活:
x_flattened = tf.reshape(conv2, [-1, 7`7`64])
w_fc = tf.Variable(tf.random_normal([7`7`64,1024]))
b_fc = tf.Variable(tf.random_normal([1024]))
fc = tf.nn.relu(tf.matmul(x_flattened,w_fc) b_fc)
现在,我们需要为输出层定义权重和偏差,即[number of neurons in the current layer, number of neurons layer in the next layer]
:
w_out = tf.Variable(tf.random_normal([1024, 10]))
b_out = tf.Variable(tf.random_normal([10]))
我们可以通过将全连接层的结果与权重矩阵相乘并加上偏差来获得输出。 我们将使用softmax
激活函数来获得输出的概率:
output = tf.matmul(fc, w_out) b_out
yhat = tf.nn.softmax(output)
我们可以将损失函数定义为交叉熵损失。 我们将使用一种称为 Adam 优化器的新型优化器,而不是使用梯度下降优化器,来使损失函数最小化。 :
代码语言:javascript复制cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=output, labels=y))optimiser = tf.train.AdamOptimizer(learning_rate=learning_rate).minimize(cross_entropy)
接下来,我们将如下计算accuracy
:
correct_prediction = tf.equal(tf.argmax(y, 1), tf.argmax(yhat, 1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
并定义超参数:
代码语言:javascript复制epochs = 10
batch_size = 100
现在,我们将开始 TensorFlow 会话并构建模型:
代码语言:javascript复制init_op = tf.global_variables_initializer()
with tf.Session() as sess:
sess.run(init_op)
total_batch = int(len(fashion_mnist.train.labels) / batch_size)
# For each epoch
for epoch in range(epochs):
avg_cost = 0
for i in range(total_batch):
batch_x, batch_y = fashion_mnist.train.next_batch(batch_size=batch_size)
_, c = sess.run([optimiser, cross_entropy],
feed_dict={x: batch_x, y: batch_y})
avg_cost = c / total_batch
print("Epoch:", (epoch 1), "cost =""{:.3f}".format(avg_cost))
print(sess.run(accuracy, feed_dict={x: mnist.test.images, y: mnist.test.labels}))
总结
在本章中,我们学习了神经网络的实际工作原理,然后使用 TensorFlow 构建了一个神经网络来对手写数字进行分类。 我们还看到了不同类型的神经网络,例如 RNN,可以记住内存中的信息。 然后,我们看到了 LSTM 网络,该网络用于通过保持多个门来将信息保留在内存中(只要需要)来克服消失的梯度问题。 我们还看到了另一个有趣的神经网络,用于识别图像,称为 CNN。 我们看到了 CNN 如何使用不同的层来理解图像。 之后,我们学习了如何使用 TensorFlow 构建 CNN 以识别时尚产品。
在下一章第 8 章,“使用深度 Q 网络玩 Atari 游戏”中,我们将看到神经网络实际上将如何帮助我们的 RL 智能体更有效地学习。
问题
问题列表如下:
- 线性回归和神经网络有什么区别?
- 激活函数有什么用?
- 为什么我们需要计算梯度下降中的梯度?
- RNN 的优势是什么?
- 什么是消失和爆炸的梯度问题?
- LSTM 中的门是什么?
- 池化层的用途是什么?
进一步阅读
深度学习是一个巨大的话题。 要探索有关深度学习和其他相关算法的更多信息,请查看以下非常有用的链接:
- 有关此 CNN 的更多信息,请访问这门很棒的斯坦福课程
- 通过此很棒的博客文章深入研究 RNN
八、深度 Q 网络和 Atari 游戏
深度 Q 网络(DQN)是非常流行且广泛使用的深度强化学习(DRL)算法之一。 实际上,它在发布之后,在强化学习(RL)社区周围引起了很多轰动。 该算法由 Google 的 DeepMind 的研究人员提出,在玩任何 Atari 游戏时,只要将游戏屏幕作为输入,就可以达到人类水平的结果。
在本章中,我们将探讨 DQN 的工作原理,并学习如何通过仅将游戏屏幕作为输入来构建可玩任何 Atari 游戏的 DQN。 我们将研究 DQN 架构的一些改进,例如双重 DQN 和决斗网络架构。
在本章中,您将学习:
- 深度 Q 网络(DQN)
- DQN 的架构
- 建立智能体来玩 Atari 游戏
- 双 DQN
- 优先经验回放
什么是深度 Q 网络?
在继续之前,首先让我们回顾一下 Q 函数。 什么是 Q 函数? Q 函数(也称为状态动作值函数)指定a
在s
状态下的状态。 因此,我们将每个状态下所有可能动作的值存储在一个称为 Q 表的表中,并选择一个状态下具有最大值的动作作为最佳动作。 还记得我们是如何学习这个 Q 函数的吗? 我们使用了 Q 学习,这是一种非策略性的时差学习算法,用于估计 Q 函数。 我们在第 5 章“时间差异学习”中对此进行了研究。
到目前为止,我们已经看到了状态数量有限且动作有限的环境,并且我们对所有可能的状态动作对进行了详尽搜索,以找到最佳 Q 值。 想想一个环境,我们有很多状态,并且在每个状态下,我们都有很多动作可以尝试。 仔细检查每个状态下的所有操作将非常耗时。 更好的方法是使用某些参数θ
将Q
函数近似为Q(s, a; θ) ≈ Q*(s, a)
。 我们可以使用权重为θ
的神经网络来估计每种状态下所有可能动作的Q
值。 当我们使用神经网络来近似Q
函数时,我们可以称其为 Q 网络。 好的,但是我们如何训练网络,我们的目标函数是什么? 回顾我们的 Q 学习更新规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M9M5yEht-1681653750852)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00243.jpeg)]。
r r maxQ(s', a)
是目标值,Q(s, a)
是预测值; 我们试图通过学习正确的策略来最大程度地降低这一值。
同样,在 DQN 中,我们可以将损失函数定义为目标值和预测值之间的平方差,并且我们还将尝试通过更新权重θ
来最大程度地减少损失:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yfwj2HW4-1681653750852)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00247.jpeg)]
其中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-noMR7lfJ-1681653750852)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00248.jpeg)]
我们更新权重,并通过梯度下降使损失最小化。 简而言之,在 DQN 中,我们使用神经网络作为函数近似器来近似Q
函数,并且通过梯度下降使误差最小化。
DQN 的架构
现在,我们对 DQN 有了基本的了解,我们将详细介绍 DQN 的工作原理以及用于玩 Atari 游戏的 DQN 的架构。 我们将研究每个组件,然后将整个算法视为一个整体。
卷积网络
DQN 的第一层是卷积网络,网络的输入将是游戏屏幕的原始帧。 因此,我们采用原始框架并将其传递给卷积层以了解游戏状态。 但是原始帧将具有210 x 160
像素和 128 个调色板,并且如果我们直接输入原始像素,显然将需要大量的计算和内存。 因此,我们将像素下采样为84 x 84
,并将 RGB 值转换为灰度值,然后将经过预处理的游戏屏幕作为卷积层的输入。 卷积层通过识别图像中不同对象之间的空间关系来理解游戏屏幕。 我们使用两个卷积层,然后使用具有 ReLU 作为激活函数的全连接层。 在这里,我们不使用池化层。
当执行诸如对象检测或分类之类的任务时,池层非常有用,其中我们不考虑对象在图像中的位置,而只想知道所需对象是否在图像中。 例如,如果我们要对图像中是否有狗进行分类,我们只查看图像中是否有狗,而不检查狗在哪里。 在那种情况下,使用池化层对图像进行分类,而与狗的位置无关。 但是对于我们来说,要了解游戏屏幕,位置很重要,因为它描述了游戏状态。 例如,在乒乓游戏中,我们不仅要分类游戏屏幕上是否有球。 我们想知道球的位置,以便我们下一步行动。 这就是为什么我们在架构中不使用池化层的原因。
好的,我们如何计算 Q 值? 如果我们传递一个游戏画面和一个动作作为 DQN 的输入,它将给我们 Q 值。 但这将需要一个完整的前向通过,因为一个状态中将有许多动作。 而且,游戏中将有许多状态,每个动作都有一个前移,这在计算上将是昂贵的。 因此,我们只需将游戏屏幕单独作为输入,并通过将输出层中的单元数设置为游戏状态下的动作数,即可获得该状态下所有可能动作的 Q 值。
下图显示了 DQN 的架构,我们在其中馈送了一个游戏屏幕,它提供了该游戏状态下所有动作的 Q 值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7EtSblfR-1681653750852)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00249.jpeg)]
为了预测游戏状态的 Q 值,我们不仅仅使用当前的游戏屏幕; 我们还考虑了过去的四个游戏屏幕。 这是为什么? 考虑“吃豆人”游戏,其中“吃豆人”的目标是移动并吞噬所有点。 仅查看当前的游戏屏幕,我们就无法知道吃豆人的前进方向。 但是,如果我们有过去的游戏画面,我们可以了解吃豆人的发展方向。 我们使用过去的四个游戏屏幕以及当前的游戏屏幕作为输入。
经验回放
我们知道,在 RL 环境中,我们通过执行某些操作a
,从一个状态s
转移到下一状态s'
,并获得奖励r
。 我们将此转移信息作为<s, a, r, s'>
保存在称为回放缓冲区或经验回放的缓冲区中。 这些转移称为智能体的经验。
经验回放的关键思想是,我们使用从回放缓冲区采样的转移来训练深度 Q 网络,而不是使用最后的转移进行训练。 智能体的经历一次相关,因此从回放缓冲区中随机选择一批训练样本将减少智能体的经历之间的相关性,并有助于智能体更好地从广泛的经验中学习。
而且,神经网络将过拟合相关经验,因此通过从答复缓冲区中选择随机的经验批量,我们将减少过拟合。 我们可以使用统一采样来采样经验。 我们可以将经验回放视为队列而不是列表。 回放缓冲区将仅存储固定数量的最新经验,因此,当出现新信息时,我们将删除旧信息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hgEKE1vj-1681653750853)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00251.gif)]
目标网络
在损失函数中,我们计算目标值和预测值之间的平方差:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-csM0j18x-1681653750853)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00252.jpeg)]
我们使用相同的 Q 函数来计算目标值和预测值。 在前面的公式中,您可以看到相同的权重θ
用于目标Q
和预测的Q
。 由于同一网络正在计算预测值和目标值,因此两者之间可能会有很大差异。
为避免此问题,我们使用称为目标网络的单独网络来计算目标值。 因此,我们的损失函数变为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s1Yez61D-1681653750853)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00254.jpeg)]
您可能会注意到目标Q
的参数是θ'
而不是θ
。 我们的实际 Q 网络用于预测Q
值,它通过使用梯度下降来学习θ
的正确权重。 将目标网络冻结几个时间步,然后通过从实际 Q 网络复制权重来更新目标网络权重。 冻结目标网络一段时间,然后使用实际的 Q 网络权重更新其权重,以稳定训练。
裁剪奖励
我们如何分配奖励? 奖励分配因每个游戏而异。 在某些游戏中,我们可以分配奖励,例如 1 表示获胜,-1 表示损失,而 0 则不计任何收益,但是在某些其他游戏中,我们必须分配诸如 100 表示执行某项操作和 50 表示进行另一项操作的奖励。 为避免出现此问题,我们将所有奖励分别裁剪为 -1 和 1。
了解算法
现在,我们将了解 DQN 的整体工作方式。 DQN 涉及的步骤如下:
- 首先,我们预处理游戏屏幕(状态
s
)并将其馈送到 DQN,DQN 将返回该状态下所有可能动作的Q
值。 - 现在,我们使用
ε
贪婪策略选择一个动作:对于概率epsilon
,我们选择一个随机动作a
;对于概率为 1epsilon
,我们选择一个具有最大Q
的动作值,例如a = argmax(Q(s, a; θ))
。 - 在选择动作
a
之后,我们在s
状态下执行此动作,然后移至新的s'
状态并获得奖励。 下一个状态s'
是下一个游戏屏幕的预处理图像。 - 我们将此转移存储在
<s,a,r,s'>
的回放缓冲区中。 - 接下来,我们从回放缓冲区中抽取一些随机的转移批量并计算损失。
- 我们知道损失就像目标
Q
与预测的Q
之间的平方差一样。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H8HWW6AO-1681653750853)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00259.jpeg)] - 我们针对实际网络参数
θ
执行梯度下降,以最大程度地减少这种损失。 - 在每个
k
个步骤之后,我们将实际网络权重θ
复制到目标网络权重θ'
。 - 对于
M
个剧集,我们重复这些步骤。
建立智能体来玩 Atari 游戏
现在,我们将看到如何建立一个智能体来玩任何 Atari 游戏。 您可以在此处获得 Jupyter 笔记本的完整代码及其说明。
首先,我们导入所有必需的库:
代码语言:javascript复制import numpy as np
import gym
import tensorflow as tf
from tensorflow.contrib.layers import flatten, conv2d, fully_connected
from collections import deque, Counter
import random
from datetime import datetime
我们可以使用此处提供的任何 Atari 游戏环境。
在此示例中,我们使用“吃豆人”游戏环境:
代码语言:javascript复制env = gym.make("MsPacman-v0")
n_outputs = env.action_space.n
此处显示了吃豆人环境:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1oyu3KnC-1681653750853)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00263.jpeg)]
现在,我们定义了preprocess_observation
函数,用于预处理输入游戏屏幕。 我们减小图像大小并将图像转换为灰度:
color = np.array([210, 164, 74]).mean()
def preprocess_observation(obs):
# Crop and resize the image
img = obs[1:176:2, ::2]
# Convert the image to greyscale
img = img.mean(axis=2)
# Improve image contrast
img[img==color] = 0
# Next we normalize the image from -1 to 1
img = (img - 128) / 128 - 1
return img.reshape(88,80,1)
好的,现在我们定义一个q_network
函数来构建我们的 Q 网络。 Q 网络的输入将是游戏状态X
。
我们构建一个 Q 网络,该网络包含三个具有相同填充的卷积层,然后是一个全连接层:
代码语言:javascript复制tf.reset_default_graph()
def q_network(X, name_scope):
# Initialize layers
initializer = tf.contrib.layers.variance_scaling_initializer()
with tf.variable_scope(name_scope) as scope:
# initialize the convolutional layers
layer_1 = conv2d(X, num_outputs=32, kernel_size=(8,8), stride=4, padding='SAME', weights_initializer=initializer)
tf.summary.histogram('layer_1',layer_1)
layer_2 = conv2d(layer_1, num_outputs=64, kernel_size=(4,4), stride=2, padding='SAME', weights_initializer=initializer)
tf.summary.histogram('layer_2',layer_2)
layer_3 = conv2d(layer_2, num_outputs=64, kernel_size=(3,3), stride=1, padding='SAME', weights_initializer=initializer)
tf.summary.histogram('layer_3',layer_3)
# Flatten the result of layer_3 before feeding to the
# fully connected layer
flat = flatten(layer_3)
fc = fully_connected(flat, num_outputs=128, weights_initializer=initializer)
tf.summary.histogram('fc',fc)
output = fully_connected(fc, num_outputs=n_outputs, activation_fn=None, weights_initializer=initializer)
tf.summary.histogram('output',output)
# Vars will store the parameters of the network such as weights
vars = {v.name[len(scope.name):]: v for v in tf.get_collection(key=tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope.name)}
return vars, output
接下来,我们定义一个epsilon_greedy
函数以执行ε
贪婪策略。 在ε
贪婪策略中,我们选择概率为 1epsilon
的最佳操作,或者选择概率为epsilon
的随机操作。
我们使用衰减的epsilon
贪婪策略,其中epsilon
的值会随着时间的流逝而衰减,因为我们不想永远探索。 因此,随着时间的流逝,我们的策略只会利用良好的行动:
epsilon = 0.5
eps_min = 0.05
eps_max = 1.0
eps_decay_steps = 500000
def epsilon_greedy(action, step):
p = np.random.random(1).squeeze()
epsilon = max(eps_min, eps_max - (eps_max-eps_min) * step/eps_decay_steps)
if np.random.rand() < epsilon:
return np.random.randint(n_outputs)
else:
return action
现在,我们初始化长度为 20000 的经验回放缓冲区,其中包含该经验。
我们将智能体的所有经验(状态,动作,奖励)存储在经验回放缓冲区中,并抽样此小批经验来训练网络:
代码语言:javascript复制def sample_memories(batch_size):
perm_batch = np.random.permutation(len(exp_buffer))[:batch_size]
mem = np.array(exp_buffer)[perm_batch]
return mem[:,0], mem[:,1], mem[:,2], mem[:,3], mem[:,4]
接下来,我们定义所有超参数:
代码语言:javascript复制num_episodes = 800
batch_size = 48
input_shape = (None, 88, 80, 1)
learning_rate = 0.001
X_shape = (None, 88, 80, 1)
discount_factor = 0.97
global_step = 0
copy_steps = 100
steps_train = 4
start_steps = 2000
logdir = 'logs'
现在,我们为输入定义placeholder
,例如游戏状态:
X = tf.placeholder(tf.float32, shape=X_shape)
我们定义一个布尔值in_training_mode
来切换训练:
in_training_mode = tf.placeholder(tf.bool)
我们构建我们的 Q 网络,该网络接受输入X
并为状态中的所有动作生成 Q 值:
mainQ, mainQ_outputs = q_network(X, 'mainQ')
同样,我们建立目标 Q 网络:
代码语言:javascript复制targetQ, targetQ_outputs = q_network(X, 'targetQ')
为我们的动作值定义placeholder
:
X_action = tf.placeholder(tf.int32, shape=(None,))
Q_action = tf.reduce_sum(targetQ_outputs * tf.one_hot(X_action, n_outputs), axis=-1, keep_dims=True)
将主要 Q 网络参数复制到目标 Q 网络:
代码语言:javascript复制copy_op = [tf.assign(main_name, targetQ[var_name]) for var_name, main_name in mainQ.items()]
copy_target_to_main = tf.group(*copy_op)
为我们的输出定义一个placeholder
,例如动作:
y = tf.placeholder(tf.float32, shape=(None,1))
现在我们计算损失,它是实际值和预测值之间的差:
代码语言:javascript复制loss = tf.reduce_mean(tf.square(y - Q_action))
我们使用AdamOptimizer
来最大程度地减少损失:
optimizer = tf.train.AdamOptimizer(learning_rate)
training_op = optimizer.minimize(loss)
在 TensorBoard 中设置日志文件以进行可视化:
代码语言:javascript复制loss_summary = tf.summary.scalar('LOSS', loss)
merge_summary = tf.summary.merge_all()
file_writer = tf.summary.FileWriter(logdir, tf.get_default_graph())
接下来,我们启动 TensorFlow 会话并运行模型:
代码语言:javascript复制init = tf.global_variables_initializer()
with tf.Session() as sess:
init.run()
# for each episode
for i in range(num_episodes):
done = False
obs = env.reset()
epoch = 0
episodic_reward = 0
actions_counter = Counter()
episodic_loss = []
# while the state is not the terminal state
while not done:
#env.render()
# get the preprocessed game screen
obs = preprocess_observation(obs)
# feed the game screen and get the Q values for each action
actions = mainQ_outputs.eval(feed_dict={X:[obs], in_training_mode:False})
# get the action
action = np.argmax(actions, axis=-1)
actions_counter[str(action)] = 1
# select the action using epsilon greedy policy
action = epsilon_greedy(action, global_step)
# now perform the action and move to the next state,
# next_obs, receive reward
next_obs, reward, done, _ = env.step(action)
# Store this transition as an experience in the replay buffer
exp_buffer.append([obs, action, preprocess_observation(next_obs), reward, done])
# After certain steps, we train our Q network with samples from the experience replay buffer
if global_step % steps_train == 0 and global_step > start_steps:
# sample experience
o_obs, o_act, o_next_obs, o_rew, o_done = sample_memories(batch_size)
# states
o_obs = [x for x in o_obs]
# next states
o_next_obs = [x for x in o_next_obs]
# next actions
next_act = mainQ_outputs.eval(feed_dict={X:o_next_obs, in_training_mode:False})
# reward
y_batch = o_rew discount_factor * np.max(next_act, axis=-1) * (1-o_done)
# merge all summaries and write to the file
mrg_summary = merge_summary.eval(feed_dict={X:o_obs, y:np.expand_dims(y_batch, axis=-1), X_action:o_act, in_training_mode:False})
file_writer.add_summary(mrg_summary, global_step)
代码语言:javascript复制 # now we train the network and calculate loss
train_loss, _ = sess.run([loss, training_op], feed_dict={X:o_obs, y:np.expand_dims(y_batch, axis=-1), X_action:o_act, in_training_mode:True})
episodic_loss.append(train_loss)
# after some interval we copy our main Q network weights to target Q network
if (global_step 1) % copy_steps == 0 and global_step > start_steps:
copy_target_to_main.run()
obs = next_obs
epoch = 1
global_step = 1
episodic_reward = reward
print('Epoch', epoch, 'Reward', episodic_reward,)
您可以看到如下输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6X6VlZxl-1681653750854)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00264.jpeg)]
我们可以在 TensorBoard 中看到 DQN 的计算图,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XZDhlawS-1681653750854)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00265.jpeg)]
我们可以在主网络和目标网络中可视化权重的分布:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7wPbK9UX-1681653750854)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00266.jpeg)]
我们还可以看到损失:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bPeRTz8s-1681653750854)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00267.jpeg)]
双 DQN
深度 Q 学习非常酷,对吗? 它已经普及了玩任何 Atari 游戏的学习。 但是 DQN 的问题在于,它倾向于高估Q
值。 这是因为Q
学习方程式中的最大值运算符。 最大运算符在选择和评估动作时使用相同的值。 那是什么意思?假设我们处于s
状态,并且我们有五个动作a[1]
至a[5]
。 假设a[3]
是最好的动作。 当我们估计状态为s
的所有这些动作的Q
值时,估计的Q
值会有些杂音并且与实际值有所不同。 由于这种噪声,动作a[2]
会比最佳动作a[3]
获得更高的值。 现在,如果我们选择最佳动作作为具有最大值的动作,则最终将选择次优动作a[2]
而不是最佳动作a[3]
。
我们可以通过具有两个单独的Q
函数来解决此问题,每个函数都是独立学习的。 一个Q
函数用于选择一个动作,另一个Q
函数用于评估一个动作。 我们可以通过调整 DQN 的目标函数来实现。 调用 DQN 的目标函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-scavmANy-1681653750855)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00268.jpeg)]
我们可以如下修改目标函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EsY83qCt-1681653750855)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00269.jpeg)]
在前面的公式中,我们有两个Q
函数,每个函数具有不同的权重。 因此,权重为θ'
的Q
函数用于选择操作,权重为θ-
的其他Q
函数用于评估操作。 我们还可以切换这两个Q
函数的角色。
优先经验回放
在 DQN 架构中,我们使用经验回放来消除训练样本之间的相关性。 但是,从记忆回放中均匀采样转移不是最佳方法。 相反,我们可以确定转换的优先级并根据优先级进行采样。 优先安排转移有助于网络快速有效地学习。 我们如何确定转移的优先级? 我们优先考虑具有较高 TD 误差的转换。 我们知道,TD 误差指定了估计的 Q 值和实际 Q 值之间的差。 因此,具有较高 TD 误差的转移是我们必须关注和学习的转移,因为这些转移与我们的估计背道而驰。 凭直觉,让我们说您尝试解决一系列问题,但是您无法解决其中两个问题。 然后,您仅将这两个问题放在首位,以专注于问题所在并尝试解决该问题:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JSEAC1Eg-1681653750855)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00272.gif)]
我们使用两种类型的优先级:比例优先级和基于等级的优先级。
在比例优先级中,我们将优先级定义为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ClXGOJal-1681653750855)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00273.jpeg)]
p[i]
是转换的优先级i
,δ[i]
是转换的 TD 误差i
,而ε
只是一些正常数,可确保每次转换具有非零优先级。 当δ
为零时,添加ε
将使转换具有优先级而不是零优先级。 但是,转换的优先级要比δ
不为零的转换低。 指数α
表示使用的优先级数量。 当α
为零时,则仅是均匀情况。
现在,我们可以使用以下公式将此优先级转换为概率:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yEMReLcO-1681653750855)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00282.jpeg)]
在基于等级的优先级划分中,我们将优先级定义为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u7g34MZo-1681653750855)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00283.jpeg)]
rank(i)
指定转移i
在回放缓冲区中的位置,在该位置中,转移从高 TD 误差到低 TD 误差被分类。 计算优先级后,我们可以使用相同的公式将优先级转换为概率:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IDa1H7CE-1681653750856)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00284.jpeg)]
决斗网络架构
我们知道Q
函数指定智能体在状态s
下执行动作a
有多好,而值函数则指定使智能体处于s
状态有多好。 现在,我们引入一个称为优势函数的新函数,该函数可以定义为值函数和优势函数之间的差。 优势函数指定与其他动作相比,智能体执行一个动作a
有多好。
因此,值函数指定状态的优劣,而优势函数指定动作的优劣。 如果我们将值函数和优势函数结合起来会发生什么? 这将告诉我们智能体在状态s
实际上是我们的Q
函数下执行动作a
有多好。 因此,我们可以像Q(s, a) = V(s) A(a)
中那样将Q
函数定义为值函数和优势函数的和。
现在,我们将看到决斗网络架构是如何工作的。 下图显示了决斗 DQN 的架构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YgfLyPjX-1681653750856)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00286.gif)]
决斗 DQN 的架构与 DQN 基本上相同,只是在末端的全连接层分为两个流。 一个流计算值函数,而另一个流计算优势函数。 最后,我们使用聚合层组合这两个流,并获得 Q 函数。
为什么我们必须将我们的 Q 函数计算分成两个流? 在许多状态,计算所有动作的值估计并不重要,尤其是当我们在一个状态中有较大的动作空间时; 那么大多数动作将不会对状态产生任何影响。 同样,可能会有许多具有冗余效果的动作。 在这些情况下,与现有 DQN 架构相比,决斗 DQN 可以更精确地估计 Q 值:
- 当我们在状态中有大量动作时,并且估计每个动作的值并不是很重要时,第一个流与值函数流中一样有用。
- 与优势函数流一样,第二个流在网络必须决定优先选择哪个操作的情况下很有用
聚合器层合并这两个流的值,并产生Q
函数。 因此,决斗网络比标准 DQN 架构更有效,更健壮。
总结
在本章中,我们学习了一种非常流行的深度强化学习算法,称为 DQN。 我们看到了如何使用深度神经网络来近似 Q 函数。 我们还学习了如何建立智能体来玩 Atari 游戏。 后来,我们研究了 DQN 的一些改进,例如双 DQN,它用于避免高估 Q 值。 然后,我们研究了优先级经验回放,优先级经验和决斗的网络架构,该架构将 Q 函数计算分为两个流,分别称为值流和优势流。
在下一章第 9 章,“实用深度循环 Q 网络玩末日之战”中,我们将介绍一种称为 DRQN 的非常酷的 DQN 变体,它利用 RNN 近似于一个 Q 函数。
问题
问题列表如下:
- 什么是 DQN?
- 经验回放有什么需要?
- 为什么我们要保留一个单独的目标网络?
- 为什么 DQN 高估了?
- 双重 DQN 如何避免高估 Q 值?
- 优先经验回放中的优先经验是怎么样的?
- 决斗架构有什么需求?
进一步阅读
- DQN 论文
- 双重 DQN 论文
- 决斗网络架构
九、用深度循环 Q 网络玩《毁灭战士》
在上一章中,我们介绍了如何使用深度 Q 网络(DQN)构建智能体以玩 Atari 游戏。 我们利用神经网络来近似 Q 函数,使用了卷积神经网络(CNN)来理解输入游戏画面,并利用过去的四个游戏画面来更好地理解当前的游戏状态。 在本章中,我们将学习如何利用循环神经网络(RNN)来提高 DQN 的表现。 我们还将研究马尔可夫决策过程(MDP)的部分可观察之处,以及如何使用深度循环 Q 网络(DRQN)。 接下来,我们将学习如何构建一个智能体来使用 DRQN 玩《毁灭战士》游戏。 最后,我们将看到 DRQN 的一种变体,称为深度注意力循环 Q 网络(DARQN),它增强了 DRQN 架构的注意力机制。
在本章中,您将学习以下主题:
- DRQN
- 部分可观察的 MDP
- DRQN 的架构
- 如何建立智能体以使用 DRQN 玩《毁灭战士》游戏
- DARQN
DRQN
那么,当我们在 Atari 游戏中以人为水平执行 DQN 时,为什么我们需要 DRQN? 为了回答这个问题,让我们理解部分可观察的马尔可夫决策过程(POMDP)的问题。 当我们可获得的关于环境的信息有限时,该环境称为部分可观察的 MDP。 到目前为止,在前面的章节中,我们已经看到了一个完全可观察的 MDP,在其中我们了解所有可能的动作和状态-尽管该智能体可能不知道转移和奖励的可能性,但它对环境有完整的了解,例如,冰冻的湖泊环境,我们清楚地知道了环境的所有状态和行为; 我们轻松地将该环境建模为一个完全可观察的 MDP。 但是大多数现实世界环境只能部分观察到。 我们看不到所有状态。 考虑智能体学习如何在现实环境中行走; 显然,智能体将不会完全了解环境,它将无法获得任何信息。 在 POMDP 中,状态仅提供部分信息,但是将有关过去状态的信息保留在内存中可能会帮助智能体更好地了解环境的性质并改善策略。 因此,在 POMDP 中,我们需要保留有关先前状态的信息,以便采取最佳措施。
为了回顾我们在前几章中学到的知识,请考虑以下所示的乒乓游戏。 仅通过查看当前的游戏屏幕,我们就可以知道球的位置,但是我们还需要知道球的运动方向和球的速度,以便采取最佳行动。 但是,仅查看当前的游戏屏幕并不能告诉我们球的方向和速度:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ih8WFYo2-1681653750856)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00287.jpeg)]
为了克服这个问题,我们将不仅仅考虑当前的游戏屏幕,而将使用过去的四个游戏屏幕来了解球的方向和速度。 这就是我们在 DQN 中看到的。 我们将过去的四个游戏屏幕以及当前的游戏屏幕作为输入输入到卷积层,并接收该状态下所有可能动作的 Q 值。 但是,您认为仅使用过去的四个屏幕将有助于我们了解不同的环境吗? 在某些环境下,我们甚至可能需要过去的 100 个游戏屏幕来更好地了解当前游戏状态。 但是,堆叠过去的n
游戏画面会减慢我们的训练过程,而且还会增加我们的经验回放缓冲区的大小。
因此,只要需要,我们就可以利用 RNN 的优势来理解和保留有关先前状态的信息。 在第 7 章“深度学习基础知识”中,我们了解了如何通过保留,忘记和更新所需的信息,将长短期记忆循环神经网络(LSTM RNN)用于生成文本以及了解文本的上下文。 我们将通过扩展 LSTM 层来修改 DQN 架构,以了解先前的信息。 在 DQN 架构中,我们用 LSTM RNN 替换了第一卷积后全连接层。 这样,我们也可以解决部分可观察性的问题,因为现在我们的智能体可以记住过去的状态并可以改进策略。
DRQN 的架构
接下来显示 DRQN 的架构。 它类似于 DQN,但是我们用 LSTM RNN 替换了第一卷积后全连接层,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C1YtkrS3-1681653750856)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00288.gif)]
因此,我们将游戏屏幕作为卷积层的输入。 卷积层对图像进行卷积并生成特征映射。 然后将生成的特征映射传递到 LSTM 层。 LSTM 层具有用于保存信息的内存。 LSTM 层保留有关重要的先前游戏状态的信息,并根据需要随时间步长更新其内存。 穿过全连接层后,它将输出Q
值。 因此,与 DQN 不同,我们不直接估计Q(s[t], a[t])
。 相反,我们估计Q(h[t], a[t])
,其中h[t]
是网络在上一个时间步长返回的输入。 即,h[t] = LSTM(h[t-1], o[t])
。 当我们使用 RNN 时,我们通过时间的反向传播来训练我们的网络。
等待。 经验回放缓冲区如何? 在 DQN 中,为避免相关的经验,我们使用了经验回放,该经验存储了游戏的转移,并使用了随机的一组经验来训练网络。 对于 DRQN,我们将整个剧集存储在经验缓冲区中,并从随机的剧集批量中随机采样n
个步骤。 因此,通过这种方式,我们既可以适应随机化,又可以适应另一种实际的经验。
训练智能体玩《毁灭战士》
毁灭战士是一款非常受欢迎的第一人称射击游戏。 游戏的目标是杀死怪物。 末日是部分可观察的 MDP 的另一个示例,因为智能体(玩家)的视角限制为 90 度。 该智能体对其余环境一无所知。 现在,我们将看到如何使用 DRQN 来训练我们的经纪人玩《毁灭战士》。
代替 OpenAI Gym,我们将使用 ViZDoom 包来模拟 Doom 环境以训练我们的智能体。 要了解有关 ViZDoom 包的更多信息,请访问其官方网站。 我们可以使用以下命令简单地安装 ViZDoom:
代码语言:javascript复制pip install vizdoom
ViZDoom 提供了许多 Doom 方案,可以在包文件夹vizdoom/scenarios
中找到这些方案。
基本《毁灭战士》游戏
在开始之前,让我们通过看一个基本示例来熟悉vizdoom
环境:
- 让我们加载必要的库:
from vizdoom import *
import random
import time
- 为
DoomGame
创建一个实例:
game = DoomGame()
- 众所周知,ViZDoom 提供了很多 Doom 方案,让我们加载基本方案:
game.load_config("basic.cfg")
init()
方法使用场景初始化游戏:
game.init()
- 现在,让我们定义一个带有热编码的
actions
的代码:
shoot = [0, 0, 1]
left = [1, 0, 0]
right = [0, 1, 0]
actions = [shoot, left, right]
- 现在,让我们开始玩游戏:
no_of_episodes = 10
for i in range(no_of_episodes):
# for each episode start the game
game.new_episode()
# loop until the episode is over
while not game.is_episode_finished():
# get the game state
state = game.get_state()
img = state.screen_buffer
# get the game variables
misc = state.game_variables
代码语言:javascript复制 # perform some action randomly and receive reward
reward = game.make_action(random.choice(actions))
print(reward)
# we will set some time before starting the next episode
time.sleep(2)
运行程序后,可以看到如下输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uQlJyiHK-1681653750856)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00289.jpeg)]
DRQN 的《毁灭战士》
现在,让我们看看如何利用 DRQN 算法来训练我们的智能体玩《毁灭战士》。 我们为成功杀死怪物分配正面奖励,为失去生命,自杀和失去弹药(子弹)分配负面奖励。 您可以在这里获得 Jupyter 笔记本的完整代码及其解释。 本节中使用的代码的权利归于 Luthanicus。
首先,让我们导入所有必需的库:
代码语言:javascript复制import tensorflow as tf
import numpy as np
import matplotlib.pyplot as plt
from vizdoom import *
import timeit
import math
import os
import sys
现在,让我们定义get_input_shape
函数,以在卷积层卷积后计算输入图像的最终形状:
def get_input_shape(Image,Filter,Stride):
layer1 = math.ceil(((Image - Filter 1) / Stride))
o1 = math.ceil((layer1 / Stride))
layer2 = math.ceil(((o1 - Filter 1) / Stride))
o2 = math.ceil((layer2 / Stride))
layer3 = math.ceil(((o2 - Filter 1) / Stride))
o3 = math.ceil((layer3 / Stride))
return int(o3)
现在,我们将定义DRQN
类,该类实现了 DRQN 算法。 检查每行代码之前的注释以了解它:
class DRQN():
def __init__(self, input_shape, num_actions, initial_learning_rate):
# first, we initialize all the hyperparameters
self.tfcast_type = tf.float32
# shape of our input, which would be (length, width, channels)
self.input_shape = input_shape
# number of actions in the environment
self.num_actions = num_actions
# learning rate for the neural network
self.learning_rate = initial_learning_rate
# now we will define the hyperparameters of the convolutional neural network
# filter size
self.filter_size = 5
# number of filters
self.num_filters = [16, 32, 64]
# stride size
self.stride = 2
# pool size
self.poolsize = 2
# shape of our convolutional layer
self.convolution_shape = get_input_shape(input_shape[0], self.filter_size, self.stride) * get_input_shape(input_shape[1], self.filter_size, self.stride) * self.num_filters[2]
# now, we define the hyperparameters of our recurrent neural network and the final feed forward layer
# number of neurons
self.cell_size = 100
# number of hidden layers
self.hidden_layer = 50
# drop out probability
self.dropout_probability = [0.3, 0.2]
# hyperparameters for optimization
self.loss_decay_rate = 0.96
self.loss_decay_steps = 180
# initialize all the variables for the CNN
# we initialize the placeholder for input whose shape would be (length, width, channel)
self.input = tf.placeholder(shape = (self.input_shape[0], self.input_shape[1], self.input_shape[2]), dtype = self.tfcast_type)
# we will also initialize the shape of the target vector whose shape is equal to the number of actions
self.target_vector = tf.placeholder(shape = (self.num_actions, 1), dtype = self.tfcast_type)
# initialize feature maps for our corresponding 3 filters
self.features1 = tf.Variable(initial_value = np.random.rand(self.filter_size, self.filter_size, input_shape[2], self.num_filters[0]),
dtype = self.tfcast_type)
self.features2 = tf.Variable(initial_value = np.random.rand(self.filter_size, self.filter_size, self.num_filters[0], self.num_filters[1]),
dtype = self.tfcast_type)
self.features3 = tf.Variable(initial_value = np.random.rand(self.filter_size, self.filter_size, self.num_filters[1], self.num_filters[2]),
dtype = self.tfcast_type)
# initialize variables for RNN
# recall how RNN works from chapter 7
self.h = tf.Variable(initial_value = np.zeros((1, self.cell_size)), dtype = self.tfcast_type)
# hidden to hidden weight matrix
self.rW = tf.Variable(initial_value = np.random.uniform(
low = -np.sqrt(6. / (self.convolution_shape self.cell_size)),
high = np.sqrt(6. / (self.convolution_shape self.cell_size)),
size = (self.convolution_shape, self.cell_size)),
dtype = self.tfcast_type)
# input to hidden weight matrix
self.rU = tf.Variable(initial_value = np.random.uniform(
low = -np.sqrt(6. / (2 * self.cell_size)),
high = np.sqrt(6. / (2 * self.cell_size)),
size = (self.cell_size, self.cell_size)),
dtype = self.tfcast_type)
# hidden to output weight matrix
self.rV = tf.Variable(initial_value = np.random.uniform(
low = -np.sqrt(6. / (2 * self.cell_size)),
high = np.sqrt(6. / (2 * self.cell_size)),
size = (self.cell_size, self.cell_size)),
dtype = self.tfcast_type)
# bias
self.rb = tf.Variable(initial_value = np.zeros(self.cell_size), dtype = self.tfcast_type)
self.rc = tf.Variable(initial_value = np.zeros(self.cell_size), dtype = self.tfcast_type)
# initialize weights and bias of feed forward network
# weights
self.fW = tf.Variable(initial_value = np.random.uniform(
low = -np.sqrt(6. / (self.cell_size self.num_actions)),
high = np.sqrt(6. / (self.cell_size self.num_actions)),
size = (self.cell_size, self.num_actions)),
dtype = self.tfcast_type)
# bias
self.fb = tf.Variable(initial_value = np.zeros(self.num_actions), dtype = self.tfcast_type)
# learning rate
self.step_count = tf.Variable(initial_value = 0, dtype = self.tfcast_type)
self.learning_rate = tf.train.exponential_decay(self.learning_rate,
self.step_count,
self.loss_decay_steps,
self.loss_decay_steps,
staircase = False)
# now let us build the network
# first convolutional layer
self.conv1 = tf.nn.conv2d(input = tf.reshape(self.input, shape = (1, self.input_shape[0], self.input_shape[1], self.input_shape[2])), filter = self.features1, strides = [1, self.stride, self.stride, 1], padding = "VALID")
self.relu1 = tf.nn.relu(self.conv1)
self.pool1 = tf.nn.max_pool(self.relu1, ksize = [1, self.poolsize, self.poolsize, 1], strides = [1, self.stride, self.stride, 1], padding = "SAME")
# second convolutional layer
self.conv2 = tf.nn.conv2d(input = self.pool1, filter = self.features2, strides = [1, self.stride, self.stride, 1], padding = "VALID")
self.relu2 = tf.nn.relu(self.conv2)
self.pool2 = tf.nn.max_pool(self.relu2, ksize = [1, self.poolsize, self.poolsize, 1], strides = [1, self.stride, self.stride, 1], padding = "SAME")
# third convolutional layer
self.conv3 = tf.nn.conv2d(input = self.pool2, filter = self.features3, strides = [1, self.stride, self.stride, 1], padding = "VALID")
self.relu3 = tf.nn.relu(self.conv3)
self.pool3 = tf.nn.max_pool(self.relu3, ksize = [1, self.poolsize, self.poolsize, 1], strides = [1, self.stride, self.stride, 1], padding = "SAME")
# add dropout and reshape the input
self.drop1 = tf.nn.dropout(self.pool3, self.dropout_probability[0])
self.reshaped_input = tf.reshape(self.drop1, shape = [1, -1])
# now we build the recurrent neural network, which takes the input from the last layer of the convolutional network
self.h = tf.tanh(tf.matmul(self.reshaped_input, self.rW) tf.matmul(self.h, self.rU) self.rb)
self.o = tf.nn.softmax(tf.matmul(self.h, self.rV) self.rc)
# add drop out to RNN
self.drop2 = tf.nn.dropout(self.o, self.dropout_probability[1])
# we feed the result of RNN to the feed forward layer
self.output = tf.reshape(tf.matmul(self.drop2, self.fW) self.fb, shape = [-1, 1])
self.prediction = tf.argmax(self.output)
# compute loss
self.loss = tf.reduce_mean(tf.square(self.target_vector - self.output))
# we use Adam optimizer for minimizing the error
self.optimizer = tf.train.AdamOptimizer(self.learning_rate)
# compute gradients of the loss and update the gradients
self.gradients = self.optimizer.compute_gradients(self.loss)
self.update = self.optimizer.apply_gradients(self.gradients)
self.parameters = (self.features1, self.features2, self.features3,
self.rW, self.rU, self.rV, self.rb, self.rc,
self.fW, self.fb)
现在,我们定义ExperienceReplay
类以实现经验回放缓冲区。 我们将智能体的所有经验(即状态,动作和奖励)存储在经验回放缓冲区中,并且我们抽取了这一小批量经验来训练网络:
class ExperienceReplay():
def __init__(self, buffer_size):
# buffer for holding the transition
self.buffer = []
# size of the buffer
self.buffer_size = buffer_size
# we remove the old transition if the buffer size has reached it's limit. Think off the buffer as a queue, when the new
# one comes, the old one goes off
def appendToBuffer(self, memory_tuplet):
if len(self.buffer) > self.buffer_size:
for i in range(len(self.buffer) - self.buffer_size):
self.buffer.remove(self.buffer[0])
self.buffer.append(memory_tuplet)
# define a function called sample for sampling some random n number of transitions
def sample(self, n):
memories = []
for i in range(n):
memory_index = np.random.randint(0, len(self.buffer))
memories.append(self.buffer[memory_index])
return memories
现在,我们定义train
函数来训练我们的网络:
def train(num_episodes, episode_length, learning_rate, scenario = "deathmatch.cfg", map_path = 'map02', render = False):
# discount parameter for Q-value computation
discount_factor = .99
# frequency for updating the experience in the buffer
update_frequency = 5
store_frequency = 50
# for printing the output
print_frequency = 1000
# initialize variables for storing total rewards and total loss
total_reward = 0
total_loss = 0
old_q_value = 0
# initialize lists for storing the episodic rewards and losses
rewards = []
losses = []
# okay, now let us get to the action!
# first, we initialize our doomgame environment
game = DoomGame()
# specify the path where our scenario file is located
game.set_doom_scenario_path(scenario)
# specify the path of map file
game.set_doom_map(map_path)
# then we set screen resolution and screen format
game.set_screen_resolution(ScreenResolution.RES_256X160)
game.set_screen_format(ScreenFormat.RGB24)
# we can add particles and effects we needed by simply setting them to true or false
game.set_render_hud(False)
game.set_render_minimal_hud(False)
game.set_render_crosshair(False)
game.set_render_weapon(True)
game.set_render_decals(False)
game.set_render_particles(False)
game.set_render_effects_sprites(False)
game.set_render_messages(False)
game.set_render_corpses(False)
game.set_render_screen_flashes(True)
# now we will specify buttons that should be available to the agent
game.add_available_button(Button.MOVE_LEFT)
game.add_available_button(Button.MOVE_RIGHT)
game.add_available_button(Button.TURN_LEFT)
game.add_available_button(Button.TURN_RIGHT)
game.add_available_button(Button.MOVE_FORWARD)
game.add_available_button(Button.MOVE_BACKWARD)
game.add_available_button(Button.ATTACK)
# okay, now we will add one more button called delta. The preceding button will only
# work like keyboard keys and will have only boolean values.
# so we use delta button, which emulates a mouse device which will have positive and negative values
# and it will be useful in environment for exploring
game.add_available_button(Button.TURN_LEFT_RIGHT_DELTA, 90)
game.add_available_button(Button.LOOK_UP_DOWN_DELTA, 90)
# initialize an array for actions
actions = np.zeros((game.get_available_buttons_size(), game.get_available_buttons_size()))
count = 0
for i in actions:
i[count] = 1
count = 1
actions = actions.astype(int).tolist()
# then we add the game variables, ammo, health, and killcount
game.add_available_game_variable(GameVariable.AMMO0)
game.add_available_game_variable(GameVariable.HEALTH)
game.add_available_game_variable(GameVariable.KILLCOUNT)
# we set episode_timeout to terminate the episode after some time step
# we also set episode_start_time which is useful for skipping initial events
game.set_episode_timeout(6 * episode_length)
game.set_episode_start_time(10)
game.set_window_visible(render)
# we can also enable sound by setting set_sound_enable to true
game.set_sound_enabled(False)
# we set living reward to 0, which rewards the agent for each move it does even though the move is not useful
game.set_living_reward(0)
# doom has different modes such as player, spectator, asynchronous player, and asynchronous spectator
# in spectator mode humans will play and agent will learn from it.
# in player mode, the agent actually plays the game, so we use player mode.
game.set_mode(Mode.PLAYER)
# okay, So now we, initialize the game environment
game.init()
# now, let us create instance to our DRQN class and create our both actor and target DRQN networks
actionDRQN = DRQN((160, 256, 3), game.get_available_buttons_size() - 2, learning_rate)
targetDRQN = DRQN((160, 256, 3), game.get_available_buttons_size() - 2, learning_rate)
# we will also create an instance to the ExperienceReplay class with the buffer size of 1000
experiences = ExperienceReplay(1000)
# for storing the models
saver = tf.train.Saver({v.name: v for v in actionDRQN.parameters}, max_to_keep = 1)
# now let us start the training process
# we initialize variables for sampling and storing transitions from the experience buffer
sample = 5
store = 50
# start the tensorflow session
with tf.Session() as sess:
# initialize all tensorflow variables
sess.run(tf.global_variables_initializer())
for episode in range(num_episodes):
# start the new episode
game.new_episode()
# play the episode till it reaches the episode length
for frame in range(episode_length):
# get the game state
state = game.get_state()
s = state.screen_buffer
# select the action
a = actionDRQN.prediction.eval(feed_dict = {actionDRQN.input: s})[0]
action = actions[a]
# perform the action and store the reward
reward = game.make_action(action)
# update total reward
total_reward = reward
# if the episode is over then break
if game.is_episode_finished():
break
# store the transition to our experience buffer
if (frame % store) == 0:
experiences.appendToBuffer((s, action, reward))
# sample experience from the experience buffer
if (frame % sample) == 0:
memory = experiences.sample(1)
mem_frame = memory[0][0]
mem_reward = memory[0][2]
# now, train the network
Q1 = actionDRQN.output.eval(feed_dict = {actionDRQN.input: mem_frame})
Q2 = targetDRQN.output.eval(feed_dict = {targetDRQN.input: mem_frame})
# set learning rate
learning_rate = actionDRQN.learning_rate.eval()
# calculate Q value
Qtarget = old_q_value learning_rate * (mem_reward discount_factor * Q2 - old_q_value)
# update old Q value
old_q_value = Qtarget
# compute Loss
loss = actionDRQN.loss.eval(feed_dict = {actionDRQN.target_vector: Qtarget, actionDRQN.input: mem_frame})
# update total loss
total_loss = loss
# update both networks
actionDRQN.update.run(feed_dict = {actionDRQN.target_vector: Qtarget, actionDRQN.input: mem_frame})
targetDRQN.update.run(feed_dict = {targetDRQN.target_vector: Qtarget, targetDRQN.input: mem_frame})
rewards.append((episode, total_reward))
losses.append((episode, total_loss))
print("Episode %d - Reward = %.3f, Loss = %.3f." % (episode, total_reward, total_loss))
total_reward = 0
total_loss = 0
让我们训练10000
剧集,其中每个剧集的长度为300
:
train(num_episodes = 10000, episode_length = 300, learning_rate = 0.01, render = True)
运行该程序时,可以看到如下所示的输出,还可以看到我们的智能体如何通过剧集学习:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Q2ImDz5H-1681653750857)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00290.jpeg)]
DARQN
我们通过添加可捕获时间依赖性的循环层来改进 DQN 架构,我们将其称为 DRQN。 您认为我们可以进一步改善我们的 DRQN 架构吗? 是。 通过在卷积层之上添加关注层,我们可以进一步改善我们的 DRQN 架构。 那么,关注层的功能是什么? 注意意味着单词的字面意思。 注意机制广泛用于图像字幕,对象检测等。 考虑神经网络为图像添加字幕的任务; 为了理解图像中的内容,网络必须注意图像中的特定对象以生成字幕。
类似地,当我们在 DRQN 中添加关注层时,我们可以选择并关注图像的小区域,最终这会减少网络中的参数数量,并减少训练和测试时间。 与 DRQN 不同,DARQN 中的 LSTM 层不仅存储了先前的状态信息以采取下一个最佳操作,而且还保留了先前的状态信息。 它还存储用于确定下一个图像焦点的信息。
DARQN 的架构
DARQN 的架构如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UiDdC3nH-1681653750857)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00291.gif)]
它由三层组成; 卷积层,注意力层和 LSTM 循环层。 游戏屏幕作为图像被馈送到卷积网络。 卷积网络处理图像并生成特征映射。 然后,特征贴图会进入关注层。 注意层将它们转换为向量,并产生它们的线性组合,称为上下文向量。 然后将上下文向量以及先前的隐藏状态传递到 LSTM 层。 LSTM 层提供两个输出; 一方面,它提供 Q 值来决定在某种状态下要执行的动作;另一方面,它可以帮助注意力网络确定在下一个时间步中要关注的图像区域,从而可以生成更好的上下文向量。 。
注意有两种类型:
- 软注意力:我们知道,卷积层产生的特征映射将作为输入提供给关注层,然后生成上下文向量。 轻描淡写地,这些上下文向量只是卷积层产生的所有输出(特征映射)的加权平均值。 根据功能的相对重要性选择权重。
- 硬注意力:硬注意力,根据某些位置选择策略
π
,我们仅关注图像在特定时间步长t
上的特定位置。 该策略由神经网络表示,其权重是策略参数,网络的输出是位置选择概率。 但是,硬注意力不比软注意力好多少。
总结
在本章中,我们学习了如何使用 DRQN 记住有关先前状态的信息,以及它如何克服部分可观察的 MDP 问题。 我们已经看到了如何训练我们的智能体使用 DRQN 算法玩《毁灭战士》游戏。 我们还了解了 DARQN 作为 DRQN 的改进,它在卷积层的顶部增加了一个关注层。 在此之后,我们看到了两种类型的注意力机制: 即软硬关注。
在下一章第 10 章,“异步优势演员评论家网络”中,我们将学习另一种有趣的深度强化学习算法,称为异步优势演员评论家网络。
问题
问题列表如下:
- DQN 和 DRQN 有什么区别?
- DQN 的缺点是什么?
- 我们如何在 DQN 中设置经验回放?
- DRQN 和 DARQN 有什么区别?
- 为什么我们需要 DARQN?
- 注意机制有哪些不同类型?
- 我们为什么要在《毁灭战士》中设定生存奖励?
进一步阅读
考虑以下内容,以进一步了解您的知识:
- DRQN 论文
- 使用 DRQN 玩 FPS 游戏
- DARQN 论文
十、异步优势演员评论家网络
在前面的章节中,我们已经看到了深度 Q 网络(DQN)多么酷,以及它如何成功地推广了学习玩具有人类水平表现的 Atari 系列游戏的方法 。 但是我们面临的问题是它需要大量的计算能力和训练时间。 因此,Google 的 DeepMind 引入了一种称为异步优势演员评论家(A3C)的新算法,该算法在其他深度强化学习算法中占主导地位,因为它需要较少的计算能力和训练时间。 A3C 的主要思想是,它使用多个智能体并行学习并汇总其整体经验。 在本章中,我们将了解 A3C 网络如何工作。 接下来,我们将学习如何使用 A3C 构建智能体以推动一座山。
在本章中,您将学习以下内容:
- 异步优势演员评论家算法
- A3C
- A3C 的架构
- A3C 如何运作
- 驾驶 A3C 上山
- TensorBoard 中的可视化
异步优势演员评论家
A3C 网络风起云涌,并接管了 DQN。 除了前面提到的优点之外,与其他算法相比,它还具有良好的准确率。 它在连续和离散动作空间中都可以很好地工作。 它使用多个智能体,每个智能体在实际环境的副本中与不同的探索策略并行学习。 然后,将从这些智能体获得的经验汇总到全局智能体。 全局智能体也称为主网络或全局网络,其他智能体也称为工作器。 现在,我们将详细了解 A3C 的工作原理以及与 DQN 算法的区别。
A3C
潜水之前,A3C 是什么意思? 这三个 As 代表什么?
在 A3C 中,第一个 A,异步表示其工作方式。 在这里,我们有多个与环境交互的智能体,而不是像 DQN 那样有单个智能体尝试学习最佳策略。 由于我们有多个智能体同时与环境交互,因此我们将环境的副本提供给每个智能体,以便每个智能体可以与自己的环境副本进行交互。 因此,所有这些多个智能体都称为辅助智能体,我们有一个单独的智能体,称为全局网络,所有智能体都向其报告。 全局网络聚合了学习内容。
第二个 A 是优势; 在讨论 DQN 的决斗网络架构时,我们已经看到了优势函数。 优势函数可以定义为 Q 函数和值函数之间的差。 我们知道 Q 函数指定状态下动作的状态,而值函数指定状态下状态的状态。 现在,凭直觉思考; 两者之间的区别意味着什么? 它告诉我们,与其他所有动作相比,智能体在状态s
下执行动作a
有多好。
第三个 A 是演员评论家; 该架构具有两种类型的网络,即演员和评论家。 演员的角色是学习策略,评论家的角色是评估演员学习的策略有多好。
A3C 的架构
现在,让我们看一下 A3C 的架构。 看下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D9yi7Iez-1681653750857)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00292.gif)]
仅通过查看上图就可以了解 A3C 的工作原理。 正如我们所讨论的,我们可以看到有多个工作程序智能体,每个工作智能体都与自己的环境副本进行交互。 然后,工作器将学习策略并计算策略损失的梯度,并将该梯度更新到全局网络。 每个智能体都会同时更新此全局网络。 A3C 的优点之一是,与 DQN 不同,我们在这里不使用经验回放内存。 实际上,这是 A3C 网络的最大优势之一。 由于我们有多个与环境交互并将信息聚合到全局网络的智能体,因此经验之间的相关性很低甚至没有。 经验回放需要占用所有经验的大量内存。 由于 A3C 不需要它,因此我们的存储空间和计算时间将减少。
A3C 如何运作
首先,辅助智能体重置全局网络,然后它们开始与环境进行交互。 每个工作器遵循不同的探索策略以学习最佳策略。 然后,他们计算值和策略损失,然后计算损失的梯度并将梯度更新到全局网络。 随着工作器智能体开始重置全局网络并重复相同的过程,该循环继续进行。 在查看值和策略损失函数之前,我们将了解优势函数的计算方式。 众所周知,优点是Q
函数和值函数之间的区别:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VebVxjh5-1681653750857)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00293.jpeg)]
由于我们实际上并未直接在 A3C 中计算Q
值,因此我们将折扣收益用作Q
值的估计值。 折扣收益R
可以写为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-95Z8E1zU-1681653750858)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00294.jpeg)]
我们将折价收益R
替换为Q
函数,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NDdbUYN0-1681653750858)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00295.jpeg)]
现在,我们可以将值损失写为折扣收益与状态值之间的平方差:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VlXeKTFo-1681653750858)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00296.jpeg)]
保单损失可以定义如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kn7qnGjQ-1681653750858)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00297.jpeg)]
好的,新项目H(π)
是什么? 它是熵项。 它用于确保充分探索策略。 熵告诉我们行动概率的扩散。 当熵值高时,每个动作的概率都将相同,因此智能体将不确定要执行哪个动作,而当熵值降低时,一个动作将比其他动作具有更高的概率,并且智能体可以选取这个可能性很高的动作。 因此,将熵添加到损失函数中会鼓励智能体进一步探索并避免陷入局部最优状态。
驾驶 A3C 上山
让我们通过山车示例了解 A3C。 我们的智能体是汽车,它被放置在两座山之间。 我们智能体的目标是向右上方爬。 但是,汽车不能一口气上山。 它必须来回驱动以建立动力。 如果我们的经纪人在开车上花费更少的精力,将获得高额奖励。 本节使用的代码归功于 Stefan Boschenriedter。 环境如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ICKLmZZu-1681653750858)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00298.jpeg)]
好的,让我们开始编码吧! 完整的代码可在 Jupyter 笔记本中获得,并在此处进行解释。
首先,让我们导入必要的库:
代码语言:javascript复制import gym
import multiprocessing
import threading
import numpy as np
import os
import shutil
import matplotlib.pyplot as plt
import tensorflow as tf
现在,我们将初始化所有参数:
代码语言:javascript复制# number of worker agents
no_of_workers = multiprocessing.cpu_count()
# maximum number of steps per episode
no_of_ep_steps = 200
# total number of episodes
no_of_episodes = 2000
global_net_scope = 'Global_Net'
# sets how often the global network should be updated
update_global = 10
# discount factor
gamma = 0.90
# entropy factor
entropy_beta = 0.01
# learning rate for actor
lr_a = 0.0001
# learning rate for critic
lr_c = 0.001
# boolean for rendering the environment
render=False
# directory for storing logs
log_dir = 'logs'
初始化我们的MountainCar
环境:
env = gym.make('MountainCarContinuous-v0')
env.reset()
获取states
和actions
的数量,以及action_bound
的数量:
no_of_states = env.observation_space.shape[0]
no_of_actions = env.action_space.shape[0]
action_bound = [env.action_space.low, env.action_space.high]
我们将在ActorCritic
类中定义演员评论家网络。 像往常一样,我们首先了解一个类中每个函数的代码,并在最后看到整个最终代码。 注释被添加到每行代码中,以更好地理解。 最后,我们将研究干净无注释的整个代码:
class ActorCritic(object):
def __init__(self, scope, sess, globalAC=None):
# first we initialize the session and RMS prop optimizer for both
# our actor and critic networks
self.sess=sess
self.actor_optimizer = tf.train.RMSPropOptimizer(lr_a, name='RMSPropA')
self.critic_optimizer = tf.train.RMSPropOptimizer(lr_c, name='RMSPropC')
# now, if our network is global then,
if scope == global_net_scope:
with tf.variable_scope(scope):
# initialize states and build actor and critic network
self.s = tf.placeholder(tf.float32, [None, no_of_states], 'S')
# get the parameters of actor and critic networks
self.a_params, self.c_params = self._build_net(scope)[-2:]
# if our network is local then,
else:
with tf.variable_scope(scope):
# initialize state, action, and also target value
# as v_target
self.s = tf.placeholder(tf.float32, [None, no_of_states], 'S')
self.a_his = tf.placeholder(tf.float32, [None, no_of_actions], 'A')
self.v_target = tf.placeholder(tf.float32, [None, 1], 'Vtarget')
# since we are in continuous actions space,
# we will calculate
# mean and variance for choosing action
mean, var, self.v, self.a_params, self.c_params = self._build_net(scope)
# then we calculate td error as the difference
# between v_target - v
td = tf.subtract(self.v_target, self.v, name='TD_error')
# minimize the TD error
with tf.name_scope('critic_loss'):
self.critic_loss = tf.reduce_mean(tf.square(td))
# update the mean and var value by multiplying mean
# with the action bound and adding var with 1e-4
with tf.name_scope('wrap_action'):
mean, var = mean * action_bound[1], var 1e-4
# we can generate distribution using this updated
# mean and var
normal_dist = tf.contrib.distributions.Normal(mean, var)
# now we shall calculate the actor loss.
# Recall the loss function.
with tf.name_scope('actor_loss'):
# calculate first term of loss which is log(pi(s))
log_prob = normal_dist.log_prob(self.a_his)
exp_v = log_prob * td
# calculate entropy from our action distribution
# for ensuring exploration
entropy = normal_dist.entropy()
# we can define our final loss as
self.exp_v = exp_v entropy_beta * entropy
# then, we try to minimize the loss
self.actor_loss = tf.reduce_mean(-self.exp_v)
# now, we choose an action by drawing from the
# distribution and clipping it between action bounds,
with tf.name_scope('choose_action'):
self.A = tf.clip_by_value(tf.squeeze(normal_dist.sample(1), axis=0), action_bound[0], action_bound[1])
# calculate gradients for both of our actor
# and critic networks,
with tf.name_scope('local_grad'):
self.a_grads = tf.gradients(self.actor_loss, self.a_params)
self.c_grads = tf.gradients(self.critic_loss, self.c_params)
# now, we update our global network weights,
with tf.name_scope('sync'):
# pull the global network weights to the local networks
with tf.name_scope('pull'):
self.pull_a_params_op = [l_p.assign(g_p) for l_p, g_p in zip(self.a_params, globalAC.a_params)]
self.pull_c_params_op = [l_p.assign(g_p) for l_p, g_p in zip(self.c_params, globalAC.c_params)]
# push the local gradients to the global network
with tf.name_scope('push'):
self.update_a_op = self.actor_optimizer.apply_gradients(zip(self.a_grads, globalAC.a_params))
self.update_c_op = self.critic_optimizer.apply_gradients(zip(self.c_grads, globalAC.c_params))
# next, we define a function called _build_net for building
# our actor and critic network
def _build_net(self, scope):
# initialize weights
w_init = tf.random_normal_initializer(0., .1)
with tf.variable_scope('actor'):
l_a = tf.layers.dense(self.s, 200, tf.nn.relu6, kernel_initializer=w_init, name='la')
mean = tf.layers.dense(l_a, no_of_actions, tf.nn.tanh,kernel_initializer=w_init, name='mean')
var = tf.layers.dense(l_a, no_of_actions, tf.nn.softplus, kernel_initializer=w_init, name='var')
with tf.variable_scope('critic'):
l_c = tf.layers.dense(self.s, 100, tf.nn.relu6, kernel_initializer=w_init, name='lc')
v = tf.layers.dense(l_c, 1, kernel_initializer=w_init, name='v')
a_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope '/actor')
c_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope '/critic')
return mean, var, v, a_params, c_params
# update the local gradients to the global network
def update_global(self, feed_dict):
self.sess.run([self.update_a_op, self.update_c_op], feed_dict)
# get the global parameters to the local networks
def pull_global(self):
self.sess.run([self.pull_a_params_op, self.pull_c_params_op])
# select action
def choose_action(self, s):
s = s[np.newaxis, :]
return self.sess.run(self.A, {self.s: s})[0]
现在,我们将初始化Worker
类:
class Worker(object):
def __init__(self, name, globalAC, sess):
# initialize environment for each worker
self.env = gym.make('MountainCarContinuous-v0').unwrapped
self.name = name
# create an ActorCritic agent for each worker
self.AC = ActorCritic(name, sess, globalAC)
self.sess=sess
def work(self):
global global_rewards, global_episodes
total_step = 1
# store state, action, reward
buffer_s, buffer_a, buffer_r = [], [], []
# loop if the coordinator is active and the global
# episode is less than the maximum episode
while not coord.should_stop() and global_episodes < no_of_episodes:
# initialize the environment by resetting
s = self.env.reset()
# store the episodic reward
ep_r = 0
for ep_t in range(no_of_ep_steps):
# Render the environment for only worker 1
if self.name == 'W_0' and render:
self.env.render()
# choose the action based on the policy
a = self.AC.choose_action(s)
# perform the action (a), receive reward (r),
# and move to the next state (s_)
s_, r, done, info = self.env.step(a)
# set done as true if we reached maximum step per episode
done = True if ep_t == no_of_ep_steps - 1 else False
ep_r = r
# store the state, action, and rewards in the buffer
buffer_s.append(s)
buffer_a.append(a)
# normalize the reward
buffer_r.append((r 8)/8)
# we update the global network after a particular time step
if total_step % update_global == 0 or done:
if done:
v_s_ = 0
else:
v_s_ = self.sess.run(self.AC.v, {self.AC.s: s_[np.newaxis, :]})[0, 0]
# buffer for target v
buffer_v_target = []
for r in buffer_r[::-1]:
v_s_ = r gamma * v_s_
buffer_v_target.append(v_s_)
buffer_v_target.reverse()
buffer_s, buffer_a, buffer_v_target = np.vstack(buffer_s), np.vstack(buffer_a), np.vstack(buffer_v_target)
feed_dict = {
self.AC.s: buffer_s,
self.AC.a_his: buffer_a,
self.AC.v_target: buffer_v_target,
}
# update global network
self.AC.update_global(feed_dict)
buffer_s, buffer_a, buffer_r = [], [], []
# get global parameters to local ActorCritic
self.AC.pull_global()
s = s_
total_step = 1
if done:
if len(global_rewards) < 5:
global_rewards.append(ep_r)
else:
global_rewards.append(ep_r)
global_rewards[-1] =(np.mean(global_rewards[-5:]))
global_episodes = 1
break
现在,让我们开始 TensorFlow 会话并运行我们的模型:
代码语言:javascript复制# create a list for string global rewards and episodes
global_rewards = []
global_episodes = 0
# start tensorflow session
sess = tf.Session()
with tf.device("/cpu:0"):
# create an instance to our ActorCritic Class
global_ac = ActorCritic(global_net_scope,sess)
workers = []
# loop for each worker
for i in range(no_of_workers):
i_name = 'W_%i' % i
workers.append(Worker(i_name, global_ac,sess))
coord = tf.train.Coordinator()
sess.run(tf.global_variables_initializer())
# log everything so that we can visualize the graph in tensorboard
if os.path.exists(log_dir):
shutil.rmtree(log_dir)
tf.summary.FileWriter(log_dir, sess.graph)
worker_threads = []
#start workers
for worker in workers:
job = lambda: worker.work()
t = threading.Thread(target=job)
t.start()
worker_threads.append(t)
coord.join(worker_threads)
输出如下所示。 如果您运行该程序,则可以看到我们的智能体如何在几个剧集中学会爬山:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nDoTcJGC-1681653750859)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00299.jpeg)]
TensorBoard 中的可视化
让我们在 TensorBoard 中可视化我们的网络。 要启动 TensorBoard,请打开您的终端并输入以下内容:
代码语言:javascript复制tensorboard --logdir=logs --port=6007 --host=127.0.0.1
这是我们的 A3C 网络。 我们拥有一个全局网络和四名员工:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y41W62OF-1681653750859)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00300.jpeg)]
让我们扩展我们的全局网络; 您可以看到我们有一位演员和一位评论家:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WHenUP3F-1681653750859)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00301.jpeg)]
好的,工作器的实际情况是什么? 让我们扩展我们的工作器网络。 您可以看到工作节点的表现如何:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qgsn9fzU-1681653750859)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00302.jpeg)]
同步节点呢? 那是在做什么?同步节点将本地梯度从本地网络推送到全局网络,并将梯度从全局网络推送到本地网络:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L5rCqdqv-1681653750859)(https://gitcode.net/apachecn/apachecn-dl-zh/-/raw/master/docs/handson-rl-py/img/00303.jpeg)]
总结
在本章中,我们学习了 A3C 网络的工作方式。 在 A3C 中,“异步”表示多个智能体通过与环境的多个副本进行交互而独立工作,“优势”表示“优势”函数,即 Q 函数和值函数之间的差异,“演员评论家”是指演员评论家网络,其中演员网络负责生成策略,评论家网络评估由演员网络生成的策略。 我们已经了解了 A3C 的工作原理,并了解了如何使用该算法解决山地车问题。
在下一章第 11 章,“策略梯度和优化”中,我们将看到无需 Q 函数即可直接优化策略的策略梯度方法。
问题
问题列表如下:
- 什么是 A3C?
- 这三个 A 代表什么?
- 列举 A3N 优于 DQN 的一项优势
- 全局和工作节点之间有什么区别?
- 为什么我们计算损失函数的熵?
- 解释 A3C 的工作原理。