Python 深度学习(三)(1)https://developer.aliyun.com/article/1511977
Atari Breakout 随机基准测试
在我们进一步探讨之前,让我们创建一个通过随机选择移动来玩 Breakout 的代理程序。这样,我们将有一个基准来评价我们的新代理程序:
from collections import deque import random import gym import numpy as np env = gym.make("Breakout-v0") observation = env.reset() reward_per_game = 0 scores = dequeu(maxlen=1000) while True: env.render() next_action = random.randint(1, 3) observation, reward, terminal, info = env.step(next_action) reward_per_game += reward
我们随机选择我们的移动方向;在 Breakout 中,移动的方式如下:
- 1:向左移动
- 2:保持静止
- 3:向右移动
if terminal: scores.append(reward_per_game) reward_per_game = 0 print(np.mean(scores)) env.reset()
如果我们已经玩了很多游戏,那么我们将存储并打印我们的分数,然后调用env.reset()
继续玩。通过让这个运行一段时间,我们可以看到随机 Breakout 倾向于每场比赛得分约为 1.4 分。让我们看看我们能用 Q-learning 做得更好多少。
我们必须处理的第一个问题是从我们的车杆任务中进行调整的状态空间要大得多。对于车杆输入,是 210 x 160 像素的完整屏幕,每个像素包含三个浮点数,分别是每种颜色的值。要理解游戏,这些像素必须与方块、球拍和球相关联,然后这些物体之间的交互必须在某种程度上被计算。更让事情变得更加困难的是,单个屏幕图像不足以理解游戏正在发生什么。球随时间以某种速度移动;要理解最佳移动,你不能仅仅依赖于当前屏幕图像。
处理这个问题有三种方法:一种是使用循环神经网络,它将根据先前的输出来判断当前状态。这种方法可以奏效,但训练难度较大。另一种方法是将屏幕输入作为当前帧和上一帧之间的增量。在图 9中,你会看到一个例子。由于在 Pong 中颜色并没有提供信息,因此两个帧都已经被转换为灰度。上一帧的图像已从当前帧的图像中减去。这可以让你看到球的路径和两个球拍移动的方向:
图 9:Pong 游戏的增量图像
这种方法对于只由移动元素组成的游戏(如 Pong)效果很好,但对于像 Breakout 这样的游戏,其中方块的位置是固定的,我们将丢失有关游戏状态的重要信息。事实上,我们只能在方块被击中时看到它的一瞬间,而我们尚未击中的方块将保持不可见。
对于 Breakout,我们将采取的第三种方法是将当前状态设置为游戏最近 n 个状态的图像,其中 n 为 2 或更多。这允许神经网络拥有做出对游戏状态的良好判断所需的所有信息。对于大多数游戏来说,默认值 n 为 4 是很好的选择;但对于 Breakout,已经发现 n 为 2 就足够了。尽可能使用较低的 n 值是很好的,因为这会减少我们的网络所需的参数数量。
屏幕预处理
全部代码在 Git 存储库中的deep_q_breakout.py
中。但我们将在此处逐个讨论与杆平衡示例的一些重要修改。首先是神经网络的类型。对于杆平衡,一个具有单个隐藏层的网络就足够了。但这涉及到将四个值映射到只有两个动作。现在,我们要处理screen_width * screen_height * color_channels * number_of_frames_of_state = 201600
被映射到三个动作,这是一个更高级别的复杂度。
我们可以做的第一件事是将屏幕调整大小,以便为自己省点力。经过实验,我们发现可以将屏幕缩小后仍可玩 Breakout。缩小两倍仍可看到球,球拍和所有方块。而且,图像空间中大部分都不是对代理有用的信息,顶部的得分、侧面和顶部的灰色区域,以及底部的黑色空间都可以从图像中裁剪掉。这使我们能够将 210 * 160 的屏幕缩减为更容易管理的 72 * 84,将参数数量减少了四分之三以上。
在 Breakout 游戏中,像素的颜色不包含任何有用的信息,所以我们可以用单一颜色代替三种颜色,这只有黑色或白色,将输入的数量再次减少到三分之一。现在我们只剩下 72 * 84 = 6048 个位,需要两帧的游戏才能学习。我们现在来写一个方法来处理 Breakout 的屏幕:
def pre_process(screen_image):
screen_image
参数将是我们从 OpenAIGym 的env.reset
或env.next_step
操作中获得的 Numpy 数组。它的形状为 210 * 160 * 3,每个项都是表示该颜色值的 0 到 255 之间的整数:
screen_image = screen_image[32:-10, 8:-8]
对 Numpy 数组的这个操作裁剪了图像,因此我们去掉了顶部的分数,底部的黑色空间和两侧的灰色区域:
screen_image = screen_image[::2, ::2, 0]
Python 数组的::2
参数意味着我们取每隔一个项目,幸运的是 Numpy 也支持这种操作。末尾的 0 表示我们只取红色通道,这很好,因为我们马上就要把它变成只有黑白两种颜色。screen_image
现在将被处理成 72 * 84 * 1 的大小:
screen_image[screen_image != 0] = 1
这将图像中不是完全黑色的一切设为 1。这在一些需要精确对比度的游戏中可能行不通,但对于 Breakout 游戏来说就有效了:
return screen_image.astype(np.float)
最后,这个方法返回的screen_image
确保类型转换成浮点数。这会在以后将值放入 TensorFlow 时节省时间。图 10展示了处理前后屏幕的样子。经过处理后,尽管不太美观,图像仍然包含你玩游戏所需的所有元素:
图 10:处理前后的 Breakout 样子
这使我们的状态为 72842 = 12800 位,意味着我们需要将我们的三个动作映射到 2^(12800) 种可能的状态。这听起来很多,但问题变得更简单了,因为尽管这是打砖块游戏中可能的所有状态的完整范围,但只有一组相当小且可预测的状态会发生。挡板在固定区域水平移动;一个像素将激活球,一些方块将存在于中央区域。可以很容易地想象从图像中提取出一些特征,这些特征可能更好地与我们想要采取的动作相关联 —— 例如,我们的挡板与球的相对位置、球的速度等 —— 这是深度神经网络可以捕捉到的特征。
创建一个深度卷积网络
接下来,让我们用一个深度卷积网络来替换小车摆动示例中的单隐藏层网络。卷积网络首次出现在 第四章,“无监督特征学习”。卷积网络是有意义的,因为我们处理的是图像数据。我们创建的网络将有三个卷积层,导致一个单一的平坦层,导致我们的输出。使用四个隐藏层有一定的直观意义,因为我们知道我们将需要从像素中检测非常抽象的不变表示,但也已经被证明对于一系列架构是成功的。因为这是一个深度网络,relu 激活函数是有意义的。图 11 显示了网络的样子:
图 11:我们的网络架构,将学习玩打砖块游戏。
这是创建我们的深度卷积网络的代码:
SCREEN_HEIGHT = 84 SCREEN_WIDTH = 74 STATE_FRAMES = 2 CONVOLUTIONS_LAYER_1 = 32 CONVOLUTIONS_LAYER_2 = 64 CONVOLUTIONS_LAYER_3 = 64 FLAT_HIDDEN_NODES = 512
这些常量将在我们的 create_network
方法中使用:
def create_network(): input_layer = tf.placeholder("float", [None, SCREEN_HEIGHT, SCREEN_WIDTH, STATE_FRAMES])
我们将我们的输入定义为高度、宽度和状态帧的乘积;none 维度将用于状态批次:
convolution_weights_1 = tf.Variable(tf.truncated_normal([8, 8, STATE_FRAMES, CONVOLUTIONS_LAYER_1], stddev=0.01)) convolution_bias_1 = tf.Variable(tf.constant(0.01, shape=[CONVOLUTIONS_LAYER_1]))
第一个卷积层将是一个 8x8 的窗口,跨越宽度和高度,同时接收状态帧。因此,它将获得关于当前图像的 8x8 部分和上一帧中该 8x8 补丁是什么样子的数据。每个补丁将映射到 32 个卷积,将成为下一层的输入。我们给偏置一个非常轻微的正值;这对于具有 relu 激活的层来说可能是有好处的,以减少 relu 函数造成的死神经元数量:
hidden_convolutional_layer_1 = tf.nn.relu( tf.nn.conv2d(input_layer, convolution_weights_1, strides=[1, 4, 4, 1], padding="SAME") + convolution_bias_1)
我们将权重和偏置变量放入卷积层中。这是通过 tf.nn.conv2d
方法创建的。设置 strides=[1, 4, 4, 1]
意味着 8x8 的卷积窗口将在图像的宽度和高度上每四个像素应用一次。所有的卷积层都将通过 relu 激活函数:
convolution_weights_2 = tf.Variable(tf.truncated_normal([4, 4, CONVOLUTIONS_LAYER_1, CONVOLUTIONS_LAYER_2], stddev=0.01)) convolution_bias_2 = tf.Variable(tf.constant(0.01, shape=[CONVOLUTIONS_LAYER_2])) hidden_convolutional_layer_2 = tf.nn.relu( tf.nn.conv2d(hidden_convolutional_layer_1, convolution_weights_2, strides=[1, 2, 2, 1], padding="SAME") + convolution_bias_2) convolution_weights_3 = tf.Variable(tf.truncated_normal([3, 3, CONVOLUTIONS_LAYER_2, CONVOLUTIONS_LAYER_3], stddev=0.01)) convolution_bias_3 = tf.Variable(tf.constant(0.01, shape=[CONVOLUTIONS_LAYER_2])) hidden_convolutional_layer_3 = tf.nn.relu( tf.nn.conv2d(hidden_convolutional_layer_2, convolution_weights_3, strides=[1, 1, 1, 1], padding="SAME") + convolution_bias_3)
创建接下来的两个卷积层的步骤与之前相同。我们的最后一个卷积层hidden_convolutional_layer_3
现在必须连接到一个扁平层:
hidden_convolutional_layer_3_flat = tf.reshape(hidden_convolutional_layer_3, [-1, 9*11*CONVOLUTIONAL_LAYER_3])
这将把我们的卷积层重新整形为单个扁平层,其维度为 none,9,11,64:
feed_forward_weights_1 = tf.Variable(tf.truncated_normal([FLAT_SIZE, FLAT_HIDDEN_NODES], stddev=0.01)) feed_forward_bias_1 = tf.Variable(tf.constant(0.01, shape=[FLAT_HIDDEN_NODES])) final_hidden_activations = tf.nn.relu( tf.matmul(hidden_convolutional_layer_3_flat, feed_forward_weights_1) + feed_forward_bias_1) feed_forward_weights_2 = tf.Variable(tf.truncated_normal([FLAT_HIDDEN_NODES, ACTIONS_COUNT], stddev=0.01)) feed_forward_bias_2 = tf.Variable(tf.constant(0.01, shape=[ACTIONS_COUNT])) output_layer = tf.matmul(final_hidden_activations, feed_forward_weights_2) + feed_forward_bias_2 return input_layer, output_layer
我们接着按照标准方式创建最后两个扁平层。请注意,最后一层没有激活函数,因为我们在这里学习的是给定状态下动作的价值,它具有无界范围。
现在我们的主循环需要添加以下代码,以便当前状态是多个帧的组合,在打砖块游戏中,STATE_FRAMES
设置为2
,但较高的数字也会起效:
screen_binary = preprocess(observation) if last_state is None: last_state = np.stack(tuple(screen_binary for _ in range(STATE_FRAMES)), axis=2)
如果我们没有last_state
,那么我们就构造一个新的 Numpy 数组,它只是当前的screen_binary
堆叠了我们想要的STATE_FRAMES
次:
else: screen_binary = np.reshape(screen_binary, (SCREEN_HEIGHT, SCREEN_WIDTH, 1)) current_state = np.append(last_state[:, :, 1:], screen_binary, axis=2)
否则,我们将新的screen_binary
添加到我们的last_state
的第一个位置以创建新的current_state
。然后我们只需要记住在主循环结束时将我们的last_state
重新分配为等于我们的当前状态:
last_state = current_state
现在可能遇到的一个问题是,我们的状态空间现在是一个大小为 84742 的数组,并且我们想要以 100 万个这样的数组的顺序作为过去观察的列表,用于训练。除非您的计算机非常强大,否则可能会遇到内存问题。幸运的是,这些数组中很多将是非常稀疏的,并且只包含两种状态,因此可以使用内存压缩来解决这个问题。这将牺牲一些 CPU 时间来节省内存;因此在使用之前,请考虑哪个对您更重要。在 Python 中实现它只需要几行代码:
import zlib import pickle observations.append(zlib.compress( pickle.dumps((last_state, last_action, reward, current_state, terminal), 2), 2))
在这里,我们压缩数据然后将其添加到我们的观察列表中:
mini_batch_compressed = random.sample(_observations, MINI_BATCH_SIZE) mini_batch = [pickle.loads(zlib.decompress(comp_item)) for comp_item in mini_batch_compressed]
当从列表中取样时,我们只在使用时解压我们的小批量样本。
我们可能遇到的另一个问题是,尽管推车杆可能只需要几分钟就能训练好,但打砖块的训练时间可能会以天计算。为了防止出现意外情况,比如断电关机,我们希望在训练过程中随时保存我们的网络权重。在 Tensorflow 中,这只需要几行代码:
CHECKPOINT_PATH = "breakout" saver = tf.train.Saver() if not os.path.exists(CHECKPOINT_PATH): os.mkdir(CHECKPOINT_PATH) checkpoint = tf.train.get_checkpoint_state(CHECKPOINT_PATH) if checkpoint: saver.restore(session, checkpoint.model_checkpoint_path)
这可以放在文件的开头,就在session.run(tf.initialize_all_variables())
这一行上面。然后我们只需执行以下命令:
saver.save(_session, CHECKPOINT_PATH + '/network')
这意味着每隔几千次训练迭代都要创建我们网络的定期备份。现在让我们看一下训练的效果如何:
我们可以看到,在 170 万次迭代后,我们玩的水平远远超出了随机水平。这种相同的 Q 学习算法已经尝试过多种 Atari 游戏,并且通过良好的超参数调整,在 Pong、Space Invaders 和 Q*bert 等游戏中能够达到人类水平或更高的表现。
Q 学习中的收敛问题
但是,并不是一帆风顺的。让我们看看在前面序列结束后代理的训练如何继续:
如您所见,在某个时刻,代理的能力出现了巨大且持续的下降,然后回到了类似的水平。这种情况的可能原因(尽管我们很难确切知道原因)之一是 Q 学习的问题之一。
Q 学习是针对其自身对状态动作对的表现期望进行训练的。这是一个移动的目标,因为每次运行训练步骤时,目标都会发生变化。我们希望它们朝着对奖励更准确的估计值的方向移动。但随着它们朝着那里前进,参数的小变化可能会导致相当极端的振荡。
一旦我们陷入了比先前的能力评估更差的状态,每个状态动作评估都必须调整到这个新现实。如果我们每场比赛平均得分为 30 分,而通过我们的新策略,我们只能得到 20 分,整个网络都必须调整到这个情况。
目标网络冻结(Minh 等人 2015 年,《通过深度强化学习实现人类水平控制》- 自然)可以帮助减少这种情况。第二个神经网络,称为目标网络,被创建为主训练网络的副本。在训练期间,我们使用目标网络生成用于训练主神经网络的目标值。通过这种方式,主网络正在学习针对更固定的点。目标网络的权重被冻结,但一旦过了一定数量的迭代次数或达到收敛标准,目标网络就会更新为来自主网络的值。已经证明,这个过程可以显著加快训练速度。
很多强化学习可能遇到的另一个问题与具有相当极端奖励的游戏相关。例如,吃下力量丸然后吃掉鬼魂给予了非常高的奖励。这些接收到的极端奖励可能会导致梯度问题,并导致次优学习。修复这个问题的非常简单但不够令人满意的方法叫做奖励剪切,它只是将从环境中接收到的奖励剪切在某个范围内(-1 和 +1 常用)。这种方法花费很少的精力,但它的问题在于代理已经丢失了关于这些更大奖励的信息。
另一种方法是所谓的归一化深度 Q 网络(Hasselt 等人——跨多个数量级学习价值,2016)。这涉及将神经网络设置为在 -1 到 1 范围内输出状态和动作的预期奖励。将输出放入此范围后,它通过以下方程进行处理:
这里,U(s, a) 是神经网络的输出。参数 σ 和 µ 可以通过确保目标网络和主网络之间的缩放输出保持恒定来计算出,如目标网络冻结中所述:
使用这种方法,神经网络梯度将更多地指向学习状态和动作的相对值,而不是简单地消耗精力学习 Q 值的规模。
策略梯度与 Q 学习
虽然我们举了一个例子,使用策略梯度来学习棋盘游戏,使用 Q 学习来学习计算机游戏,但这两种技术并不局限于此类型。最初,Q 学习被认为是更好的技术,但随着时间的推移和更好的超参数调整,策略梯度常常表现更好。1991 年利用神经网络和 Q 学习在博弈中取得了世界最佳表现,最新研究表明策略梯度对大多数雅达利游戏效果最佳。那么何时应该使用策略梯度而不是 Q 学习呢?
一个限制是,Q 学习只适用于离散动作任务,而策略梯度可以学习连续动作任务。此外,Q 学习是确定性算法,对于某些任务,最佳行为涉及一定程度的随机性。例如,石头、剪刀、布,任何偏离纯随机性的行为都可能被对手利用。
也存在在线学习与离线学习的方面。对于许多任务,特别是机器人控制任务,在线学习可能非常昂贵。需要从记忆中学习的能力,因此 Q 学习是最佳选择。不幸的是,无论是 Q 学习还是策略梯度的成功都会受到任务和超参数选择的影响很大;因此,在确定新任务的最佳学习方法时,实验似乎是最好的方法。
策略梯度也更容易陷入局部最小值。Q 学习更有可能找到全局最优解,但这样做的成本是未经证实的收敛,性能可能在达到全局最优解的过程中发生剧烈波动或完全失败。
但也有另一种方法,它兼具两者的优点,这就是演员-评论员方法。
演员-评论员方法
强化学习方法可以分为三大类:
- 基于价值的学习:这个方法试图学习处于某个状态的预期奖励/价值。然后可以根据其相对值来评估进入不同状态的可取性。Q 学习就是基于价值的学习的例子。
- 基于策略的学习:在这种方法中,不尝试评估状态,而是尝试不同的控制策略,并根据环境的实际奖励进行评估。策略梯度就是例子。
- 基于模型的学习:在这种方法中,代理试图对环境的行为进行建模,并选择基于模型模拟其可能采取的行动结果来评估其模型的行为。
演员评论方法都围绕着使用两个神经网络进行训练的想法。第一个,评论者,使用基于价值的学习来学习给定状态的值函数,即代理人实现的预期奖励。然后,演员网络使用基于策略的学习来最大化评论者的值函数。演员正在使用策略梯度进行学习,但现在其目标已经改变。不再是通过游戏获得的实际奖励,而是使用评论者对该奖励的估计。
Q 学习的一个重大问题是,在复杂情况下,算法很难收敛。由于 Q 函数的重新评估改变了选择的动作,实际的价值奖励可能会有很大的变化。例如,想象一个简单的走迷宫机器人。在迷宫中遇到的第一个 T 形交叉口,它最初向左移动。 Q 学习的连续迭代最终导致它确定右移是更可取的方式。但现在,因为其路径完全不同,现在必须重新计算每个其他状态评估; 先前学到的知识现在价值很低。 Q 学习由于策略的微小变化可能对奖励产生巨大影响而受到高方差的影响。
在演员评论中,评论者所做的事情与 Q 学习非常相似,但存在一个关键区别:不是学习给定状态的假设最佳动作,而是学习基于演员当前遵循的最可能的次优策略的预期奖励。
相反,策略梯度存在相反的高方差问题。由于策略梯度是随机地探索迷宫,某些移动可能被选择,实际上相当不错,但由于在同一次试验中选择了其他不良移动而被评估为不良。这是因为尽管策略更稳定,但与评估策略相关的方差很高。
这就是演员评论的目标,旨在共同解决这两个问题。基于价值的学习现在方差较低,因为策略现在更加稳定和可预测,而基于策略梯度的学习也更加稳定,因为现在它具有一个从中获取梯度的方差值函数。
方差减少的基线
演员评论方法有几种不同的变体:我们将首先看一下基线演员评论。在这里,评论者试图学习代理人从给定位置的平均表现,因此其损失函数将是这样的:
这里,是评论者网络在时间步t的状态的输出,r[t]是从时间步t开始的累积折现奖励。然后可以使用目标训练演员:
因为基线是从这个状态的平均表现中得出的,这样做的效果是大幅降低训练的方差。如果我们使用策略梯度一次运行小车杆任务,再使用基线一次,其中我们不使用批量规范化,我们可以看到基线表现更好。但如果我们加入批量规范化,结果并没有太大不同。对于比小车杆更复杂的任务,奖励可能随状态变化而变化很多,基线方法可能会更大程度地改善事物。这方面的一个例子可以在actor_critic_baseline_cart_pole.py
中找到。
广义优势估计器
基线方法在减少方差方面做得很好,但它不是真正的演员评论家方法,因为演员不是在学习评论者的梯度,而只是使用它来规范化奖励。广义优势估计器进一步前进,并将评论者的梯度纳入演员的目标中。
为了做到这一点,我们需要学习的不仅仅是代理处于的状态的价值,还有它采取的状态动作对的价值。如果V(s[t])是状态的价值,Q(s[t], a[t]*)*是状态动作对的价值,我们可以这样定义一个优势函数:
这将给我们带来动作a[t]在状态s[t]中的表现与代理在这个位置上平均动作之间的差异。向着这个函数的梯度移动应该会使我们最大化我们的奖励。而且,我们不需要另一个网络来估计Q(s[t], a[t]),因为我们可以利用我们在s[t+1]达到的状态的价值函数,而 Q 函数的定义如下:
在这里,r t 现在是该时间步的奖励,而不是基线方程中的累积奖励, 是未来奖励的折扣因子。我们现在可以将其代入,纯粹地给出我们的优势函数中的V项:
同样,这给我们提供了一个度量标准,用来判断评论者是否认为给定的动作改善了还是损害了位置的价值。我们将我们的演员损失函数中的累积奖励替换为优势函数的结果。这方面的完整代码在actor_critic_advantage_cart_pole.py
中。这种方法用于小车杆挑战可以完成,但可能比仅使用批量规范化的策略梯度花费更长的时间。但对于像学习电脑游戏这样更复杂的任务,优势演员-评论家可能表现最好。
异步方法
在本章中我们看到了许多有趣的方法,但它们都受到训练速度非常慢的限制。当我们在基本控制问题上运行时,例如推车和杆子任务,这并不是什么问题。但是对于学习 Atari 游戏或者未来可能想要学习的更复杂的人类任务来说,数天到数周的训练时间就太长了。
对于策略梯度和演员-评论家来说,时间限制的一个重要部分是,在在线学习时,我们只能同时评估一个策略。我们可以通过使用更强大的 GPU 和更大的处理器获得显著的速度提升;在线评估策略的速度将始终作为性能的硬性限制。
这就是异步方法旨在解决的问题。其想法是在多个线程上训练相同的神经网络的多个副本。每个神经网络在线针对其线程上运行的环境的一个单独实例进行训练。不同于对每个训练步骤更新每个神经网络,更新跨多个训练步骤存储。每x个训练步骤,来自每个线程的累积批量更新被汇总在一起,并应用于所有网络。这意味着网络权重将根据所有网络更新中参数值的平均变化进行更新。
这种方法已经被证明适用于策略梯度、演员-评论家和 Q 学习。它极大地改善了训练时间,甚至提高了性能。在异步方法的最佳版本中,被认为是最成功的广义游戏学习算法的异步优势演员-评论家方法,在撰写本文时,被认为是最成功的广义游戏学习算法。
基于模型的方法
到目前为止,我们已经展示的方法可以很好地学习各种任务,但是通过这些方法训练出来的智能体仍然可能遭受重大限制:
- 它训练速度非常慢;一个人可以通过几次游玩学会像乒乓球一样的游戏,而对于 Q 学习,可能需要数百万次游玩才能达到类似的水平。
- 针对需要长期规划的游戏,所有技术表现都非常糟糕。想象一个平台游戏,玩家必须从房间的一侧取回一把钥匙,以打开另一侧的门。游戏中很少会发生这种情况,即使发生了,学习到这个钥匙是导致门获得额外奖励的机会也微乎其微。
- 它无法制定策略或以任何方式适应新颖的对手。它可能可以在与训练对手对战时表现良好,但在面对游戏玩法上有新颖性的对手时,学会适应将需要很长时间。
- 如果在环境中给出一个新的目标,就需要重新训练。如果我们正在训练打乒乓球作为左挡板,然后我们改为右挡板,我们将很难重新利用先前学到的信息。一个人可以毫不费力地做到这一点。
所有这些观点都可以说与一个中心问题相关。Q 学习和策略梯度在游戏中为奖励优化参数非常成功,但它们并没有学习如何理解游戏。人类学习在许多方面与 Q 学习有所不同,但一个显著的不同是,当人类学习一个环境时,他们在某种程度上正在学习这个环境的模型。然后他们可以使用该模型进行预测或者想象在环境中采取不同行动会发生什么事情。
想象一个玩家学习下棋的情景:他可以思考如果他进行某个特定的移动会发生什么。他可以想象在这一步之后棋盘会呈现什么样子,在那个新的位置他将会有哪些选择。他甚至可以将对手考虑进他的模型中,这个玩家是什么性格,倾向于采取什么样的走法,他的心情如何。
这就是基于模型的强化学习方法的目标。基于模型的 Pong 方法旨在建立一个模拟,模拟出它可能采取的不同行动的结果,并努力使该模拟尽可能接近现实。一旦建立起一个良好的环境模型,学习最佳行动就变得简单得多,因为代理可以将当前状态视为马尔可夫链的根,并利用一些来自第七章, 棋盘游戏的深度学习的技术,比如 MCTS-UCT,从其模型中抽样以查看哪些行动有最佳结果。它甚至可以更进一步,使用在自身模型上训练的 Q 学习或策略梯度,而不是在环境上训练。
基于模型的方法还有一个优势,那就是它们可能使人工智能更容易适应变化。如果我们已经学会了一个环境模型,但想要在其中改变我们的目标,我们可以重复使用同一个模型,只需简单地调整模型内的策略。如果我们讨论的是机器人或者在物理世界中运作的其他人工智能,通过玩数百万次的情节来学习策略梯度是完全不切实际的,特别是考虑到现实世界中的每次实验都会耗费时间、能量,并且存在着由于意外事件而带来的风险。基于模型的方法可以缓解许多这些问题。
构建模型引发种种问题。如果你正在构建一个基于模型的代理来学习 Pong,你知道它发生在一个二维环境中,有两个球拍和一个球,并且基本的物理规则。你需要这些元素都在你的模型中才能成功。但如果你手工制作这些,那么学习就不会那么多,并且你的代理远离了泛化学习算法。对于模型来说,什么是正确的先验?我们如何构建一个足够灵活,可以学习世界中遇到的复杂事物,同时仍能成功学习特定内容的模型?
更正式地说,学习模型可以看作是学习一个函数,它给出下一个状态在给定当前状态和动作对的情况下:
如果环境是随机的,此函数甚至可能返回可能的下一状态的概率分布。一个深度神经网络自然是该函数的一个很好的选择,然后学习将采取以下步骤:
- 构建一个输入为当前状态,输出为下一个状态和奖励的动作网络。
- 从环境中遵循一种探索性策略,收集一系列状态动作转换。简单地随机行动可能是一个很好的初始选择。
- 使用状态动作转换的集合以监督的方式训练网络,以下一状态和状态奖励作为目标。
- 使用训练好的网络转换来确定使用 MCTS、策略梯度或 Q-learning 的最佳移动。
如果我们以倒立摆任务为例,并以 MSE 作为损失函数,我们可以发现训练深度神经网络准确预测该环境的所有状态转换很容易,包括新状态何时终止。这个示例代码在 Git 仓库中。
甚至可以使用卷积和循环层来学习更复杂的 Atari 游戏模型。这是网络架构的一个例子:
来源:http://cs231n.stanford.edu/reports2016/116_Report.pdf
一个这样的网络使用了两个卷积/反卷积层和 128 个节点的 RNN 来学习预测 Pong 游戏中的下一帧。它能够成功地预测模糊版本的下一帧,但发现该模型不够稳健,无法运行 MCTS 来预测未来一两帧的事件。
这种方法的修改版本效果好得多。在这种方法中,网络不再尝试进行反卷积来预测下一帧图像,而是仅仅尝试预测 RNN 输入在下一帧中将是什么,从而消除了反卷积的需要。该网络可以学会以足够高的水平玩乒乓球,以击败游戏内的人工智能,训练后平均每场比赛赢得 2.9 分。这离完全训练的深度 Q 网络可以达到的 20.0 分还有很长的路要走,但对于一种非常新的方法来说,这仍然是一个有希望的结果。类似的结果也在 Breakout 游戏中实现了。
摘要
在本章中,我们研究了使用强化学习构建计算机游戏代理的方法。我们介绍了三种主要方法:策略梯度、Q 学习和基于模型的学习,并展示了如何将深度学习与这些方法结合使用以实现人类或更高水平的表现。我们希望读者能够从本章中获得足够的知识,以便能够将这些技术应用到他们可能想要解决的其他游戏或问题中。强化学习是当前非常令人兴奋的研究领域。谷歌、Deepmind、OpenAI 和微软等公司都在大力投资以解锁这一未来。
在下一章中,我们将探讨异常检测以及如何应用深度学习方法来检测金融交易数据中的欺诈实例。
第九章:异常检测
在第四章中,我们看到了特征学习的机制,特别是自动编码器作为监督学习任务的无监督预训练步骤的使用。
在本章中,我们将应用类似的概念,但用于不同的用例,即异常检测。
出色的异常检测器之一是找到智能数据表示,可以轻易表现出与正态分布的偏差。深度自动编码器在学习基础数据的高级抽象和非线性关系方面表现非常好。我们将展示深度学习如何非常适合异常检测。
在本章中,我们将首先解释离群点检测和异常检测概念之间的差异和共同之处。读者将通过一个想象的欺诈案例研究,随后通过示例,展示在现实世界应用中存在异常的危险以及自动和快速检测系统的重要性。
在进入深度学习实现之前,我们将介绍一些广泛应用于传统机器学习的技术家族及其当前局限性。
我们将应用在第四章中看到的深度自动编码器的架构,但用于一种特定的半监督学习,也称为新颖性检测。我们将提出两种强大的方法:一种基于重建错误,另一种基于低维特征压缩。
我们将介绍 H2O,这是一个最受欢迎的用于构建简单但可扩展的前馈多层神经网络的开源框架之一。
最后,我们将使用 H2O 自动编码器模型的 Python API 编写一些异常检测示例。
第一个例子将重用你在第三章中看到的 MNIST 数字数据集,深度学习基础和第四章中看到的无监督特征学习,但用于检测书写不良的数字。第二个例子将展示如何检测心电图时间序列中的异常脉动。
总结一下,本章将涵盖以下主题:
- 什么是异常和离群点检测?
- 异常检测的实际应用
- 流行的浅层机器学习技术
- 使用深度自动编码器进行异常检测
- H2O 概述
- 代码示例:
- MNIST 数字异常识别
- 心电图脉动检测
什么是异常和离群点检测?
异常检测通常与离群值检测和新奇检测相关,它是识别在同质数据集中偏离预期模式的项目、事件或观察结果。
异常检测是关于预测未知的。
每当我们在数据中发现一个不一致的观察结果,我们可以称之为异常或离群值。尽管这两个词经常可以互换使用,但实际上它们指的是两个不同的概念,正如 Ravi Parikh 在他的一篇博客文章中描述的那样(https://blog.heapanalytics.com/garbage-in-garbage-out-how-anomalies-can-wreck-your-data/
):
“异常值是一个远离分布均值或中位数的合法数据点。它可能是不寻常的,比如 9.6 秒的 100 米赛跑,但仍在现实范围内。异常是由与其余数据生成过程不同的过程生成的非法数据点。”
让我们尝试用一个简单的欺诈检测示例来解释两者的区别。
在一份交易日志中,我们观察到一个特定客户每个工作日的午餐平均花费 10 美元。突然间,有一天他们花了 120 美元。这当然是一个离群值,但也许那天他们决定用信用卡支付整笔账单。如果这些交易中有几笔远高于预期金额的订单,那么我们可以识别出异常。异常是指当单一的罕见事件理由不再成立时,例如,连续三个订单的交易金额超过 120 美元。在这种情况下,我们谈论的是异常,因为已经从一个不同的过程生成了重复和相关的离群值模式,可能是信用卡欺诈,与通常的行为相比。
当阈值规则可以解决许多检测问题时,发现复杂的异常需要更高级的技术。
如果一个克隆的信用卡进行了大量金额为 10 美元的微支付,基于规则的检测器可能会失败。
通过简单地查看每个维度上的度量值,异常生成过程仍然可能隐藏在平均分布内。单一维度信号不会触发任何警报。让我们看看如果我们在信用卡欺诈示例中添加一些额外维度会发生什么:地理位置、当地时区的时间以及一周中的日期。
让我们更详细地分析同一个欺诈示例。我们的客户是一名全职员工,居住在罗马,但在米兰工作。每个周一早上,他乘火车去上班,然后在周六早上回罗马看朋友和家人。他喜欢在家做饭;他一周只出去吃几次晚餐。在罗马,他住在他的亲戚附近,所以他周末从不必准备午餐,但他经常喜欢和朋友出去过夜。预期行为的分布如下:
- 金额:介于 5 到 40 美元之间
- 位置:米兰 70%和罗马 30%
- 一天中的时间:70%在中午到下午 2 点之间,30%在晚上 9 点到 11 点之间。
- 一周中的日期:一周内均匀分布
有一天,他的信用卡被克隆了。欺诈者住在他的工作地附近,为了不被抓住,他们每天晚上约 10 点在一家同伙的小店里系统地进行 25 美元的小额支付。
如果我们只看单个维度,欺诈交易将只是略微偏离预期分布,但仍然可接受。金额和一周中的日期的分布效果将保持更多或更少相同,而位置和一天中的时间将稍微增加到米兰的晚上时间。
即使是系统地重复,他生活方式的微小变化也是一个合理的解释。欺诈行为很快就会变成新的预期行为,即正常状态。
让我们考虑联合分布:
- 约 70%的金额在米兰午餐时间约 10 美元左右,只在工作日
- 约 30%的金额在周末晚餐时间在罗马约 30 美元左右
在这种情况下,欺诈行为在第一次发生时会立即被标记为异常值,因为米兰夜间超过 20 美元的交易非常罕见。
给出前面的例子,我们可能会认为考虑更多维度可以使我们的异常检测更智能。就像任何其他机器学习算法一样,你需要在复杂性和泛化之间找到一个权衡。
如果维度过多,所有观察结果都会投射到一个空间中,其中所有观察结果彼此等距离。因此,一切都将成为“异常值”,按照我们定义异常值的方式,这本质上使整个数据集“正常”。换句话说,如果每个点看起来都一样,那么你就无法区分这两种情况。如果维度太少,模型将无法从草堆中发现异常值,可能会让它在大量分布中隐藏更长时间,甚至永远。
然而,仅识别异常值是不够的。异常值可能是由于罕见事件、数据收集中的错误或噪音引起的。数据总是肮脏的,充满了不一致性。第一条规则是“永远不要假设你的数据是干净和正确的”。找到异常值只是一个标准例程。更令人惊讶的是发现偶发且无法解释的重复行为:
“数据科学家意识到,他们最好的日子与发现数据中真正奇怪的特征的日子重合。”
《草堆与针》:异常检测,作者:Gerhard Pilcher & Kenny Darrell,数据挖掘分析师,Elder Research, Inc.
特定异常模式的持续存在是我们正在监控的系统中发生了变化的信号。真正的异常检测发生在观察到基础数据生成过程中的系统偏差时。
这也影响到数据预处理步骤。与许多机器学习问题相反,在异常检测中,你不能只过滤掉所有的异常值!尽管如此,你应该仔细区分它们的性质。你确实想要过滤掉错误的数据条目,删除噪声,并对剩余的数据进行归一化。最终,你想要在清理后的数据集中检测到新颖性。
异常检测的现实应用
异常情况可能发生在任何系统中。从技术上讲,你总是可以找到一个在系统历史数据中找不到的从未见过的事件。在某些情况下检测到这些观察结果的影响可能会产生巨大的影响(积极和消极)。
在执法领域,异常检测可以用于揭示犯罪活动(假设你在一个平均人足够诚实以便识别突出分布之外的罪犯的地区)。
在网络系统中,异常检测可以帮助发现外部入侵或用户的可疑活动,例如,一个意外或故意向公司内部网络以外泄露大量数据的员工。或者可能是黑客在非常用端口和/或协议上打开连接。在互联网安全的特定案例中,异常检测可以用于通过简单地观察非受信任域名上的访客激增来阻止新的恶意软件传播。即使网络安全不是你的核心业务,你也应该通过数据驱动的解决方案来保护你的网络,以便在出现未识别的活动时监控并提醒你。
另一个类似的例子是许多主要社交网络的身份验证系统。专门的安全团队已经开发出可以衡量每个单独活动或活动序列以及它们与其他用户的中位行为有多远的解决方案。每当算法标记一项活动为可疑时,系统将提示你进行额外的验证。这些技术可以大大减少身份盗窃,并提供更大的隐私保护。同样,相同的概念也可以应用于金融欺诈,正如我们在前面的例子中看到的那样。
由人类行为产生的异常是最受欢迎的应用之一,但也是最棘手的。这就像一场国际象棋比赛。一方面,你有专业领域的专家、数据科学家和工程师开发先进的检测系统。另一方面,你有黑客,他们了解这场比赛,研究对手的走法。这就是为什么这种系统需要大量的领域知识,并且应该设计成具有反应性和动态性的。
并非所有的异常都来自“坏人”。在营销中,异常可以代表孤立的,但高利润的客户,可以用定制的报价来定位他们。他们不同和特殊的兴趣和/或有利可图的个人资料可用于检测离群客户。例如,在经济衰退期间,找到一些潜在客户,尽管大趋势,他们的利润增长,这可能是适应你的产品和重新设计业务策略的一个想法。
其他应用包括医学诊断、硬件故障检测、预测性维护等。这些应用也需要灵活性。
商机,就像新的恶意软件一样,每天都可能出现,它们的生命周期可能非常短,从几小时到几周。如果你的系统反应慢,你可能会太晚,永远追不上你的竞争对手。
人工检测系统不能扩展,通常也遭受泛化的困扰。正常行为的偏差并不总是显而易见,分析师可能难以记住整个历史以进行比对,这是异常检测的核心要求。如果异常模式隐藏在数据中实体的抽象和非线性关系中,情况会变得复杂。需要智能和完全自动化的系统,能够学习复杂的互动关系,提供实时和准确的监控,是该领域创新的下一个前沿。
Python 深度学习(三)(3)https://developer.aliyun.com/article/1511980