Python 强化学习项目(三)
原文:
annas-archive.org/md5/8a22ccc4f94e0a5a98e16b22a2b1f959译者:飞龙
第八章:生成深度学习图像分类器
在过去的十年里,深度学习凭借在计算机视觉、自然语言处理、语音识别等多个应用领域取得的卓越成果,赢得了广泛的声誉。一些人类研究者设计并开发的模型也获得了广泛的关注,包括 AlexNet、Inception、VGGNet、ResNet 和 DenseNet;其中一些模型现在已成为各自任务的标准。然而,似乎模型越优秀,其架构就越复杂,特别是在卷积层之间引入残差连接后。设计一个高性能神经网络的任务因此变得异常艰巨。因此,问题随之而来:是否有可能让一个算法学会生成神经网络架构?
正如本章标题所示,确实有可能训练一个神经网络来生成在给定任务中表现良好的神经网络。在本章中,我们将介绍由 Google Brain 团队的 Barret Zoph 和 Quoc V. Le 开发的一个新型框架——神经架构搜索(以下简称NAS),它利用深度强化学习训练一个控制器,生成子网络来学习完成任务。我们将学习策略梯度方法(特别是 REINFORCE)如何训练这样的控制器。随后,我们将实现一个控制器,使用 NAS 生成在CIFAR-10数据集上进行训练的子网络。
在本章中,我们将涵盖以下内容:
-
理解 NAS 以及它如何学会生成其他神经网络
-
实现一个简单的 NAS 框架,用于生成神经网络并在
CIFAR-10数据集上进行训练
你可以从以下来源找到后续主题的原始资料:
-
Zoph, B., 和 Le, Q. V. (2016). 通过强化学习的神经架构搜索。arXiv 预印本 arXiv:1611.01578。
-
Pham, H., Guan, M. Y., Zoph, B., Le, Q. V., 和 Dean, J. (2018). 通过参数共享的高效神经架构搜索。arXiv 预印本 arXiv:1802.03268。
神经架构搜索
接下来的几个部分将描述 NAS 框架。你将了解该框架如何使用一种叫做REINFORCE的强化学习方案来学习生成其他神经网络,完成任务。REINFORCE是一种策略梯度算法。
生成并训练子网络
生成神经网络架构的算法研究自 1970 年代以来便存在。NAS 与之前的研究不同之处在于,它能够应对大规模深度学习算法,并将任务表述为强化学习问题。更具体地说,代理,我们称之为控制器,是一个递归神经网络,它生成一系列值。你可以把这些值看作是子网络的某种遗传码,定义了子网络的架构;它设置了每个卷积核的大小、每个核的长度、每层的滤波器数量等等。在更先进的框架中,这些值还决定了各层之间的连接,从而生成残差层:
图 1:NAS 框架概览
此外,控制器输出的每个遗传码值都算作一个动作,a,它是以概率p采样的。由于控制器是一个递归神经网络,我们可以将t^(th)动作表示为。一旦我们拥有了动作列表,
—其中T是一个预定义的参数,用来设置遗传码的最大大小—我们就可以根据指定的架构生成子网络A:
图 2:控制器架构
一旦控制器生成了一个子网络,我们就对其在给定任务上进行训练,直到满足某些终止标准(例如,在指定的轮次后)。然后我们在验证集上评估子网络,得到一个验证准确率R。验证准确率作为控制器的奖励信号。所以,控制器的目标是最大化期望奖励:
这里,J是奖励函数(也称为拟合函数),是控制器的参数,方程右侧是给定子网络架构A时,奖励的期望值。在实际操作中,这个期望值是通过对控制器在一批次中生成的m个子网络模型的奖励进行平均来计算的:
训练控制器
我们如何使用这个奖励信号来更新控制器?请记住,这个奖励信号不像监督学习中的损失函数那样可微;我们不能仅仅通过控制器进行反向传播。相反,我们使用一种叫做REINFORCE的策略梯度方法,迭代地更新控制器的参数,。在 REINFORCE 中,奖励函数的梯度,J,相对于控制器参数,
,定义如下:
你可能记得在第六章《学习下围棋》中看到过类似的表达式。确实,这是 AlphaGo 和 AlphaGo Zero 用于更新其强化学习策略网络权重的策略梯度方法。我们当时简单介绍了这种方法,今天我们将更深入地探讨它。
让我们分解一下前面的方程。在右侧,我们希望表示选择某个架构 A 的概率。具体来说,
代表控制器在给定所有之前的动作、
和控制器参数
后采取某个动作的概率。再次强调,动作
对应于基因序列中代表子网络架构的 t^(th) 值。选择所有动作的联合概率,
,可以按照以下方式表示:
通过将这个联合概率转换到对数空间,我们可以将乘积转化为概率的和:
一般来说,我们希望最大化采取某个动作的对数条件概率。换句话说,我们希望提高控制器生成特定基因代码序列的可能性。因此,我们对这个目标执行梯度上升,求取关于控制器参数的对数概率的导数:
但是,我们如何更新控制器的参数,以便生成更好的架构呢?这时我们利用奖励信号 R。通过将前面的结果与奖励信号相乘,我们可以控制策略梯度的大小。换句话说,如果某个架构达到了较高的验证准确率(最高为 1.0),那么该策略的梯度将相对较强,控制器将学习生成类似的架构。相反,较小的验证准确率将意味着较小的梯度,这有助于控制器忽略这些架构。
REINFORCE 算法的一个问题是奖励信号 R 的方差可能很大,这会导致训练曲线的不稳定。为了减少方差,通常会从奖励中减去一个值 b,我们称之为基准函数。在 Zoph 等人的研究中,基准函数定义为过去奖励的指数移动平均。因此,我们的 REINFORCE 策略梯度现在定义为:
一旦得到这个梯度,我们就应用常规的反向传播算法来更新控制器的参数, 。
训练算法
控制器的训练步骤如下:
-
对于每一轮,执行以下操作:
-
生成 m 个子网络架构
-
在给定任务上训练子网络并获得 m 个验证准确度
-
计算
-
更新
-
在 Zoph 等人的研究中,训练过程通过多个控制器副本完成。每个控制器通过 参数化,而该参数本身以分布式方式存储在多个服务器中,这些服务器我们称之为参数服务器。
在每一轮训练中,控制器创建若干个子架构并独立训练。计算出的策略梯度随后被发送到参数服务器,以更新控制器的参数:
图 3:训练架构
控制器的参数在多个参数服务器之间共享。此外,多个控制器副本并行训练,每个副本为其各自的子网络架构批次计算奖励和梯度。
这种架构使得控制器可以在资源充足的情况下快速训练。然而,对于我们的目的,我们将坚持使用一个控制器来生成 m 个子网络架构。一旦我们训练了控制器指定的轮数,我们通过选择验证精度最好的子网络架构来计算测试准确度,并在测试集上测量其性能。
实现 NAS
在这一部分,我们将实现 NAS。具体来说,我们的控制器负责生成子网络架构,用于学习从CIFAR-10数据集中分类图像。子网络的架构将由一个数字列表表示。这个列表中的每四个值代表子网络中的一个卷积层,每个值描述卷积核大小、步长、滤波器数量和随后的池化层的池化窗口大小。此外,我们将子网络中的层数作为超参数指定。例如,如果我们的子网络有三层,那么它的架构将表示为一个长度为 12 的向量。如果我们的架构表示为[3, 1, 12, 2, 5, 1, 24, 2],那么这个子网络是一个两层网络,其中第一层的卷积核大小为 3,步长为 1,12 个滤波器,最大池化窗口大小为 2;第二层的卷积核大小为 5,步长为 1,24 个滤波器,最大池化窗口大小为 2。我们将每一层之间的激活函数设置为 ReLU。最后一层将对最后一个卷积层的输出进行展平,并应用一个线性层,宽度为类别数,之后应用 Softmax 激活。以下部分将带你完成实现。
child_network.py
我们将首先实现我们的子网络模块。这个模块包含一个名为ChildCNN的类,它根据某些架构配置构建子网络,这些配置我们称之为cnn_dna。如前所述,cnn_dna只是一个数字列表,每个值代表其对应卷积层的参数。在我们的config.py中,我们指定了子网络最多可以有多少层。对于我们的实现,每个卷积层由四个参数表示,每个参数分别对应卷积核大小、步长、滤波器数量和随后的最大池化窗口大小。
我们的ChildCNN是一个类,它的构造函数接受以下参数:
-
cnn_dna:网络架构 -
child_id:一个字符串,用于标识子网络架构 -
beta:L2 正则化的权重参数 -
drop_rate:丢弃率
import logging
import tensorflow as tf
logger = logging.getLogger(__name__)
class ChildCNN(object):
def __init__(self, cnn_dna, child_id, beta=1e-4, drop_rate=0.2, **kwargs):
self.cnn_dna = self.process_raw_controller_output(cnn_dna)
self.child_id = child_id
self.beta = beta
self.drop_rate = drop_rate
self.is_training = tf.placeholder_with_default(True, shape=None, name="is_training_{}".format(self.child_id))
self.num_classes = 10
我们还实现了一个辅助函数proces_raw_controller_output(),它解析控制器输出的cnn_dna:
def process_raw_controller_output(self, output):
"""
A helper function for preprocessing the output of the NASCell
Args:
output (numpy.ndarray) The output of the NASCell
Returns:
(list) The child network's architecture
"""
output = output.ravel()
cnn_dna = [list(output[x:x+4]) for x in range(0, len(output), 4)]
return cnn_dna
最后,我们包含了build方法,它使用给定的cnn_dna构建我们的子网络。你会注意到,尽管我们让控制器决定我们子网络的架构,但我们仍然硬编码了几个部分,例如激活函数tf.nn.relu以及卷积核的初始化方式。我们在每个卷积层后添加最大池化层的做法也是硬编码的。一个更复杂的 NAS 框架还会让控制器决定这些架构组件,代价是更长的训练时间:
def build(self, input_tensor):
"""
Method for creating the child neural network
Args:
input_tensor: The tensor which represents the input
Returns:
The tensor which represents the output logit (pre-softmax activation)
"""
logger.info("DNA is: {}".format(self.cnn_dna))
output = input_tensor
for idx in range(len(self.cnn_dna)):
# Get the configuration for the layer
kernel_size, stride, num_filters, max_pool_size = self.cnn_dna[idx]
with tf.name_scope("child_{}_conv_layer_{}".format(self.child_id, idx)):
output = tf.layers.conv2d(output,
# Specify the number of filters the convolutional layer will output
filters=num_filters,
# This specifies the size (height, width) of the convolutional kernel
kernel_size=(kernel_size, kernel_size),
# The size of the stride of the kernel
strides=(stride, stride),
# We add padding to the image
padding="SAME",
# It is good practice to name your layers
name="conv_layer_{}".format(idx),
activation=tf.nn.relu,
kernel_initializer=tf.contrib.layers.xavier_initializer(),
bias_initializer=tf.zeros_initializer(),
kernel_regularizer=tf.contrib.layers.l2_regularizer(scale=self.beta))
每个卷积层后面都跟着一个最大池化层和一个丢弃层:
# We apply 2D max pooling on the output of the conv layer
output = tf.layers.max_pooling2d(
output, pool_size=(max_pool_size, max_pool_size), strides=1,
padding="SAME", name="pool_out_{}".format(idx)
)
# Dropout to regularize the network further
output = tf.layers.dropout(output, rate=self.drop_rate, training=self.is_training)
最后,在经过几个卷积层、池化层和丢弃层之后,我们将输出体积展平并连接到一个全连接层:
# Lastly, we flatten the outputs and add a fully-connected layer
with tf.name_scope("child_{}_fully_connected".format(self.child_id)):
output = tf.layers.flatten(output, name="flatten")
logits = tf.layers.dense(output, self.num_classes)
return logits
我们的build方法的参数是一个输入张量,默认形状为(32, 32, 3),这是CIFAR-10数据的形状。读者可以自由调整此网络的架构,包括添加更多的全连接层或在卷积之间插入批量归一化层。
cifar10_processor.py
该模块包含处理CIFAR-10数据的代码,我们使用这些数据来训练我们的子网络。特别地,我们使用 TensorFlow 的原生tf.data.Dataset API 构建输入数据管道。那些已经使用 TensorFlow 一段时间的人可能更熟悉创建tf.placeholder张量并通过sess.run(..., feed_dict={...})提供数据。然而,这已经不是将数据输入网络的首选方式;事实上,它是训练网络最慢的方式,因为从numpy格式的数据到原生 TensorFlow 格式的重复转换会导致显著的计算开销。tf.data.Dataset通过将输入管道转化为 TensorFlow 操作,这些操作是符号图的一部分,解决了这个问题。换句话说,数据从一开始就直接转换为张量。这使得输入管道更加流畅,并能加速训练。
有关tf.data.Dataset API 的更多信息,请参考这个官方教程(www.tensorflow.org/guide/datasets_for_estimators)。
cifar10_processor.py包含一个方法,用于将CIFAR-10数据转换为张量。我们首先实现一个辅助函数来创建tf.data.Dataset对象:
import logging
import numpy as np
import tensorflow as tf
from keras.datasets import cifar10
from keras.utils import np_utils
logger = logging.getLogger(__name__)
def _create_tf_dataset(x, y, batch_size):
return tf.data.Dataset.zip((tf.data.Dataset.from_tensor_slices(x),
tf.data.Dataset.from_tensor_slices(y))).shuffle(500).repeat().batch(batch_size)
在主数据处理函数中,我们首先加载CIFAR-10数据。我们使用keras.datasets API 来完成这项工作(如果没有 Keras,请在终端中运行pip install keras):
def get_tf_datasets_from_numpy(batch_size, validation_split=0.1):
"""
Main function getting tf.Data.datasets for training, validation, and testing
Args:
batch_size (int): Batch size
validation_split (float): Split for partitioning training and validation sets. Between 0.0 and 1.0.
"""
# Load data from keras datasets api
(X, y), (X_test, y_test) = cifar10.load_data()
logger.info("Dividing pixels by 255")
X = X / 255.
X_test = X_test / 255.
X = X.astype(np.float32)
X_test = X_test.astype(np.float32)
y = y.astype(np.float32)
y_test = y_test.astype(np.float32)
# Turn labels into onehot encodings
if y.shape[1] != 10:
y = np_utils.to_categorical(y, num_classes=10)
y_test = np_utils.to_categorical(y_test, num_classes=10)
logger.info("Loaded data from keras")
split_idx = int((1.0 - validation_split) * len(X))
X_train, y_train = X[:split_idx], y[:split_idx]
X_valid, y_valid = X[split_idx:], y[split_idx:]
然后我们将这些 NumPy 数组转换为 TensorFlow 张量,直接将其输入网络。实际上,我们的_create_tf_dataset辅助函数中发生了什么?我们使用tf.dataset.Dataset.from_tensor_slices()函数将数据和标签(它们都是 NumPy 数组)转换为 TensorFlow 张量。然后通过将这些张量打包创建原生数据集。打包后的shuffle、repeat和batch函数定义了我们希望输入管道如何工作。在我们的案例中,我们对输入数据进行随机洗牌,当达到数据集末尾时重复数据集,并以给定的批量大小进行分批。我们还计算每个数据集的批次数并返回它们:
train_dataset = _create_tf_dataset(X_train, y_train, batch_size)
valid_dataset = _create_tf_dataset(X_valid, y_valid, batch_size)
test_dataset = _create_tf_dataset(X_test, y_test, batch_size)
# Get the batch sizes for the train, valid, and test datasets
num_train_batches = int(X_train.shape[0] // batch_size)
num_valid_batches = int(X_valid.shape[0] // batch_size)
num_test_batches = int(X_test.shape[0] // batch_size)
return train_dataset, valid_dataset, test_dataset, num_train_batches, num_valid_batches, num_test_batches
就这样,我们得到了一个比使用feed_dict更快的优化输入数据管道。
controller.py
controller.py模块是所有内容汇聚的地方。我们将实现控制器,负责训练每个子网络以及它自己的参数更新。首先,我们实现一个辅助函数,计算一组数字的指数移动平均值。我们使用这个作为 REINFORCE 梯度计算的基准函数,如前所述,用来计算过去奖励的指数移动平均值:
import logging
import numpy as np
import tensorflow as tf
from child_network import ChildCNN
from cifar10_processor import get_tf_datasets_from_numpy
from config import child_network_params, controller_params
logger = logging.getLogger(__name__)
def ema(values):
"""
Helper function for keeping track of an exponential moving average of a list of values.
For this module, we use it to maintain an exponential moving average of rewards
Args:
values (list): A list of rewards
Returns:
(float) The last value of the exponential moving average
"""
weights = np.exp(np.linspace(-1., 0., len(values)))
weights /= weights.sum()
a = np.convolve(values, weights, mode="full")[:len(values)]
return a[-1]
接下来,我们定义我们的Controller类:
class Controller(object):
def __init__(self):
self.graph = tf.Graph()
self.sess = tf.Session(graph=self.graph)
self.num_cell_outputs = controller_params['components_per_layer'] * controller_params['max_layers']
self.reward_history = []
self.architecture_history = []
self.divison_rate = 100
with self.graph.as_default():
self.build_controller()
有几个属性需要注意:self.num_cell_outputs表示我们递归神经网络(RNN)应该输出的值的数量,并对应子网络架构配置的长度。self.reward_history和self.architecture_history只是缓冲区,允许我们跟踪 RNN 生成的奖励和子网络架构。
生成控制器的方法
接下来,我们实现一个用于生成控制器的方法,称为build_controller。构建控制器的第一步是定义输入占位符。我们创建了两个占位符——一个用于子网络 DNA,作为输入传递给 RNN,以生成新的子网络 DNA,另一个是用于存储折扣奖励的列表,以便在计算 REINFORCE 梯度时使用:
def build_controller(self):
logger.info('Building controller network')
# Build inputs and placeholders
with tf.name_scope('controller_inputs'):
# Input to the NASCell
self.child_network_architectures = tf.placeholder(tf.float32, [None, self.num_cell_outputs],
name='controller_input')
# Discounted rewards
self.discounted_rewards = tf.placeholder(tf.float32, (None, ), name='discounted_rewards')
然后,我们定义 RNN 的输出张量(将在此处实现)。注意,RNN 的输出值较小,范围在(-1, 1)之间。所以,我们将输出乘以 10,以便生成子网络的 DNA:
# Build controller
with tf.name_scope('network_generation'):
with tf.variable_scope('controller'):
self.controller_output = tf.identity(self.network_generator(self.child_network_architectures),
name='policy_scores')
self.cnn_dna_output = tf.cast(tf.scalar_mul(self.divison_rate, self.controller_output), tf.int32,
name='controller_prediction')
然后,我们定义损失函数和优化器。我们使用RMSPropOptimizer作为反向传播算法,其中学习率按指数衰减。与通常在其他神经网络模型中调用optimizer.minimize(loss)不同,我们调用compute_gradients方法来获得计算 REINFORCE 梯度所需的梯度:
# Set up optimizer
self.global_step = tf.Variable(0, trainable=False)
self.learning_rate = tf.train.exponential_decay(0.99, self.global_step, 500, 0.96, staircase=True)
self.optimizer = tf.train.RMSPropOptimizer(learning_rate=self.learning_rate)
# Gradient and loss computation
with tf.name_scope('gradient_and_loss'):
# Define policy gradient loss for the controller
self.policy_gradient_loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(
logits=self.controller_output[:, -1, :],
labels=self.child_network_architectures))
# L2 weight decay for Controller weights
self.l2_loss = tf.reduce_sum(tf.add_n([tf.nn.l2_loss(v) for v in
tf.trainable_variables(scope="controller")]))
# Add the above two losses to define total loss
self.total_loss = self.policy_gradient_loss + self.l2_loss * controller_params["beta"]
# Compute the gradients
self.gradients = self.optimizer.compute_gradients(self.total_loss)
# Gradients calculated using REINFORCE
for i, (grad, var) in enumerate(self.gradients):
if grad is not None:
self.gradients[i] = (grad * self.discounted_rewards, var)
最后,我们在控制器参数上应用 REINFORCE 梯度:
with tf.name_scope('train_controller'):
# The main training operation. This applies REINFORCE on the weights of the Controller
self.train_op = self.optimizer.apply_gradients(self.gradients, global_step=self.global_step)
logger.info('Successfully built controller')
实际的控制器网络是通过network_generator函数创建的。如前所述,控制器是一个具有特殊类型单元的递归神经网络。然而,我们不必从头实现这一点,因为 TensorFlow 的开发者已经实现了一个自定义的tf.contrib.rnn.NASCell。我们只需要使用这个来构建我们的递归神经网络并获得输出:
def network_generator(self, nas_cell_hidden_state):
# number of output units we expect from a NAS cell
with tf.name_scope('network_generator'):
nas = tf.contrib.rnn.NASCell(self.num_cell_outputs)
network_architecture, nas_cell_hidden_state = tf.nn.dynamic_rnn(nas, tf.expand_dims(
nas_cell_hidden_state, -1), dtype=tf.float32)
bias_variable = tf.Variable([0.01] * self.num_cell_outputs)
network_architecture = tf.nn.bias_add(network_architecture, bias_variable)
return network_architecture[:, -1:, :]
使用控制器生成子网络
现在,我们实现一个方法,通过控制器生成一个子网络:
def generate_child_network(self, child_network_architecture):
with self.graph.as_default():
return self.sess.run(self.cnn_dna_output, {self.child_network_architectures: child_network_architecture})
一旦我们生成了子网络,就调用train_child_network函数来训练它。该函数接受child_dna和child_id,并返回子网络达到的验证精度。首先,我们实例化一个新的tf.Graph()和一个新的tf.Session(),这样子网络就与控制器的图分开:
def train_child_network(self, cnn_dna, child_id):
"""
Trains a child network and returns reward, or the validation accuracy
Args:
cnn_dna (list): List of tuples representing the child network's DNA
child_id (str): Name of child network
Returns:
(float) validation accuracy
"""
logger.info("Training with dna: {}".format(cnn_dna))
child_graph = tf.Graph()
with child_graph.as_default():
sess = tf.Session()
child_network = ChildCNN(cnn_dna=cnn_dna, child_id=child_id, **child_network_params)
接着我们定义输入数据管道,使用我们在此实现的tf.data.Dataset创建器。具体来说,我们使用tf.data.Iterator创建一个生成器,每次调用iterator.get_next()时都会生成一批输入张量。我们分别为训练集和验证集初始化一个迭代器。这一批输入张量包含CIFAR-10图像及其对应的标签,我们会在最后解包它们:
# Create input pipeline
train_dataset, valid_dataset, test_dataset, num_train_batches, num_valid_batches, num_test_batches = \
get_tf_datasets_from_numpy(batch_size=child_network_params["batch_size"])
# Generic iterator
iterator = tf.data.Iterator.from_structure(train_dataset.output_types, train_dataset.output_shapes)
next_tensor_batch = iterator.get_next()
# Separate train and validation set init ops
train_init_ops = iterator.make_initializer(train_dataset)
valid_init_ops = iterator.make_initializer(valid_dataset)
# Build the graph
input_tensor, labels = next_tensor_batch
input_tensor成为子网络build方法的参数。接着我们定义了训练所需的所有 TensorFlow 操作,包括预测、损失、优化器和精度操作:
# Build the child network, which returns the pre-softmax logits of the child network
logits = child_network.build(input_tensor)
# Define the loss function for the child network
loss_ops = tf.nn.softmax_cross_entropy_with_logits_v2(labels=labels, logits=logits, name="loss")
# Define the training operation for the child network
train_ops = tf.train.AdamOptimizer(learning_rate=child_network_params["learning_rate"]).minimize(loss_ops)
# The following operations are for calculating the accuracy of the child network
pred_ops = tf.nn.softmax(logits, name="preds")
correct = tf.equal(tf.argmax(pred_ops, 1), tf.argmax(labels, 1), name="correct")
accuracy_ops = tf.reduce_mean(tf.cast(correct, tf.float32), name="accuracy")
initializer = tf.global_variables_initializer()
接着我们训练子网络。请注意,在调用sess.run(...)时,我们不再传递feed_dict参数。相反,我们只是调用想要运行的操作(loss_ops、train_ops和accuracy_ops)。这是因为输入已经在子网络的计算图中以张量的形式表示:
# Training
sess.run(initializer)
sess.run(train_init_ops)
logger.info("Training child CNN {} for {} epochs".format(child_id, child_network_params["max_epochs"]))
for epoch_idx in range(child_network_params["max_epochs"]):
avg_loss, avg_acc = [], []
for batch_idx in range(num_train_batches):
loss, _, accuracy = sess.run([loss_ops, train_ops, accuracy_ops])
avg_loss.append(loss)
avg_acc.append(accuracy)
logger.info("\tEpoch {}:\tloss - {:.6f}\taccuracy - {:.3f}".format(epoch_idx,
np.mean(avg_loss), np.mean(avg_acc)))
训练完成后,我们计算验证精度并返回:
# Validate and return reward
logger.info("Finished training, now calculating validation accuracy")
sess.run(valid_init_ops)
avg_val_loss, avg_val_acc = [], []
for batch_idx in range(num_valid_batches):
valid_loss, valid_accuracy = sess.run([loss_ops, accuracy_ops])
avg_val_loss.append(valid_loss)
avg_val_acc.append(valid_accuracy)
logger.info("Valid loss - {:.6f}\tValid accuracy - {:.3f}".format(np.mean(avg_val_loss),
np.mean(avg_val_acc)))
return np.mean(avg_val_acc)
最后,我们实现了一个用于训练 Controller 的方法。由于计算资源的限制,我们不会并行化训练过程(即每个 Controller 周期内并行训练m个子网络)。相反,我们会顺序生成这些子网络,并跟踪它们的均值验证精度。
train_controller 方法
train_controller方法在构建 Controller 之后被调用。因此,第一步是初始化所有变量和初始状态:
def train_controller(self):
with self.graph.as_default():
self.sess.run(tf.global_variables_initializer())
step = 0
total_rewards = 0
child_network_architecture = np.array([[10.0, 128.0, 1.0, 1.0] *
controller_params['max_layers']], dtype=np.float32)
第一个child_network_architecture是一个类似架构配置的列表,将作为参数传递给NASCell,从而输出第一个子网络 DNA。
训练过程由两个for循环组成:一个是 Controller 的周期数,另一个是每个 Controller 周期内生成的子网络数。在内层for循环中,我们使用NASCell生成新的child_network_architecture,并基于它训练一个子网络以获得验证精度:
for episode in range(controller_params['max_episodes']):
logger.info('=============> Episode {} for Controller'.format(episode))
step += 1
episode_reward_buffer = []
for sub_child in range(controller_params["num_children_per_episode"]):
# Generate a child network architecture
child_network_architecture = self.generate_child_network(child_network_architecture)[0]
if np.any(np.less_equal(child_network_architecture, 0.0)):
reward = -1.0
else:
reward = self.train_child_network(cnn_dna=child_network_architecture,
child_id='child/{}'.format("{}_{}".format(episode, sub_child)))
episode_reward_buffer.append(reward)
在获得m次验证精度后,我们使用均值奖励和相对于上一个子网络 DNA 计算出的梯度来更新 Controller。同时,我们还会记录过去的均值奖励。通过之前实现的ema方法,我们计算出基准值,并将其从最新的均值奖励中减去。然后我们调用self.sess.run([self.train_op, self.total_loss]...)来更新 Controller 并计算 Controller 的损失:
mean_reward = np.mean(episode_reward_buffer)
self.reward_history.append(mean_reward)
self.architecture_history.append(child_network_architecture)
total_rewards += mean_reward
child_network_architecture = np.array(self.architecture_history[-step:]).ravel() / self.divison_rate
child_network_architecture = child_network_architecture.reshape((-1, self.num_cell_outputs))
baseline = ema(self.reward_history)
last_reward = self.reward_history[-1]
rewards = [last_reward - baseline]
logger.info("Buffers before loss calculation")
logger.info("States: {}".format(child_network_architecture))
logger.info("Rewards: {}".format(rewards))
with self.graph.as_default():
_, loss = self.sess.run([self.train_op, self.total_loss],
{self.child_network_architectures: child_network_architecture,
self.discounted_rewards: rewards})
logger.info('Episode: {} | Loss: {} | DNA: {} | Reward : {}'.format(
episode, loss, child_network_architecture.ravel(), mean_reward))
就是这样!你可以在主 GitHub 仓库中找到controller.py的完整实现。
测试 ChildCNN
既然我们已经实现了child_network和controller,接下来就可以通过我们的Controller测试ChildCNN的训练,使用自定义的子网络配置。我们希望确保,在合理的架构下,ChildCNN能够充分学习。
要做到这一点,首先打开你最喜欢的终端,并启动一个 Jupyter 控制台:
$ ipython
Python 3.6.4 (default, Jan 6 2018, 11:49:38)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.
我们首先配置日志记录器,这样就能在终端上看到输出:
In [1]: import sys
In [2]: import logging
In [3]: logging.basicConfig(stream=sys.stdout,
...: level=logging.DEBUG,
...: format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
...:
In [4]:
接下来,我们从controller.py导入Controller类:
In [4]: import numpy as np
In [5]: from controller import Controller
In [6]:
然后,我们手工设计一些子网络架构,并将其传递给 Controller 的train_child_network函数:
In [7]: dna = np.array([[3, 1, 30, 2], [3, 1, 30, 2], [3, 1, 40, 2]])
最后,我们实例化我们的Controller并调用train_child_network方法:
In [8]: controller = Controller()
...
2018-09-16 01:58:54,978 controller INFO Successfully built controller
In [9]: controller.train_child_network(dna, "test")
2018-09-16 01:58:59,208 controller INFO Training with dna: [[ 3 1 30 2]
[ 3 1 30 2]
[ 3 1 40 2]]
2018-09-16 01:58:59,605 cifar10_processor INFO Dividing pixels by 255
2018-09-16 01:59:01,289 cifar10_processor INFO Loaded data from keras
2018-09-16 01:59:03,150 child_network INFO DNA is: [[3, 1, 30, 2], [3, 1, 30, 2], [3, 1, 40, 2]]
2018-09-16 01:59:14,270 controller INFO Training child CNN first for 1000 epochs
如果成功,经过若干轮训练后,你应该会看到不错的准确度:
2018-09-16 06:25:01,927 controller INFO Epoch 436: loss - 1.119608 accuracy - 0.663
2018-09-16 06:25:19,310 controller INFO Epoch 437: loss - 0.634937 accuracy - 0.724
2018-09-16 06:25:36,438 controller INFO Epoch 438: loss - 0.769766 accuracy - 0.702
2018-09-16 06:25:53,413 controller INFO Epoch 439: loss - 0.760520 accuracy - 0.711
2018-09-16 06:26:10,530 controller INFO Epoch 440: loss - 0.606741 accuracy - 0.812
config.py
config.py模块包含了 Controller 和子网络使用的配置。在这里,你可以调整多个训练参数,比如训练轮数、学习率以及 Controller 每个 epoch 生成的子网络数量。你还可以尝试调整子网络的大小,但请注意,子网络越大,训练所需的时间就越长,包括 Controller 和子网络的训练时间:
child_network_params = {
"learning_rate": 3e-5,
"max_epochs": 100,
"beta": 1e-3,
"batch_size": 20
}
controller_params = {
"max_layers": 3,
"components_per_layer": 4,
'beta': 1e-4,
'max_episodes': 2000,
"num_children_per_episode": 10
}
这些数字中的一些(例如max_episodes)是任意选择的。我们鼓励读者调整这些数字,以理解它们如何影响 Controller 和子网络的训练。
train.py
这个train.py模块充当我们训练 Controller 的顶层入口:
import logging
import sys
from .controller import Controller
if __name__ == '__main__':
# Configure the logger
logging.basicConfig(stream=sys.stdout,
level=logging.DEBUG,
format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
controller = Controller()
controller.train_controller()
就这样;一个生成其他神经网络的神经网络!确保你的实现有以下目录结构:
src
|-- __init__.py
|-- child_network.py
|-- cifar10_processor.py
|-- config.py
|-- constants.py
|-- controller.py
`-- train.py
要执行训练,只需运行以下命令:
$ python train.py
如果一切顺利,你应该会看到如下输出:
2018-09-16 04:13:45,484 src.controller INFO Successfully built controller
2018-09-16 04:13:45,542 src.controller INFO =============> Episode 0 for Controller
2018-09-16 04:13:45,952 src.controller INFO Training with dna: [[ 2 10 2 4 1 1 12 14 7 1 1 1]] 2018-09-16 04:13:45.953482: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1484] Adding visible gpu devices: 0
2018-09-16 04:13:45.953530: I tensorflow/core/common_runtime/gpu/gpu_device.cc:965] Device interconnect StreamExecutor with strength 1 edge matrix:
2018-09-16 04:13:45.953543: I tensorflow/core/common_runtime/gpu/gpu_device.cc:971] 0
2018-09-16 04:13:45.953558: I tensorflow/core/common_runtime/gpu/gpu_device.cc:984] 0: N
2018-09-16 04:13:45.953840: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1097] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 wi th 21618 MB memory) -> physical GPU (device: 0, name: Tesla M40 24GB, pci bus id: 0000:03:00.0, compute capability: 5.2)
2018-09-16 04:13:47,143 src.cifar10_processor INFO Dividing pixels by 255
2018-09-16 04:13:55,119 src.cifar10_processor INFO Loaded data from keras
2018-09-16 04:14:09,050 src.child_network INFO DNA is: [[2, 10, 2, 4], [1, 1, 12, 14], [7, 1, 1, 1]]
2018-09-16 04:14:21,326 src.controller INFO Training child CNN child/0_0 for 100 epochs
2018-09-16 04:14:32,830 src.controller INFO Epoch 0: loss - 2.351300 accuracy - 0.100
2018-09-16 04:14:43,976 src.controller INFO Epoch 1: loss - 2.202928 accuracy - 0.180
2018-09-16 04:14:53,412 src.controller INFO Epoch 2: loss - 2.102713 accuracy - 0.220
2018-09-16 04:15:03,704 src.controller INFO Epoch 3: loss - 2.092676 accuracy - 0.232
2018-09-16 04:15:14,349 src.controller INFO Epoch 4: loss - 2.092633 accuracy - 0.240
你应该会看到每个子网络架构的 CIFAR-10 训练日志中的日志语句。在 CIFAR-10 训练过程中,我们会打印每一轮的损失和准确度,以及返回给 Controller 的验证准确度。
额外的练习
在这一部分,我们实现了适用于CIFAR-10数据集的 NAS 框架。虽然这是一个很好的开始,但还有其他功能可以实现,我们将其留给读者作为练习:
-
我们如何让 Controller 创建能够解决其他领域问题的子网络,例如文本和语音识别?
-
我们如何让 Controller 并行训练多个子网络,以加快训练过程?
-
我们如何使用 TensorBoard 可视化训练过程?
-
我们如何让 Controller 设计包含残差连接的子网络?
其中一些练习可能需要对代码库做出显著修改,但对加深你对 NAS 的理解是有帮助的。我们强烈推荐尝试这些练习!
NAS 的优势
NAS 最大的优势在于无需花费大量时间为特定问题设计神经网络。这也意味着即使不是数据科学家的人,只要能够准备数据,也能创建机器学习代理。事实上,谷歌已经将这个框架产品化为 Cloud AutoML,允许任何人以最小的努力训练定制化的机器学习模型。根据谷歌的说法,Cloud AutoML 提供了以下优势:
-
用户只需与简单的图形界面交互即可创建机器学习模型。
-
如果用户的数据集尚未标注,他们可以让 Cloud AutoML 为其数据集添加标注。这与亚马逊的 Mechanical Turk 服务类似。
-
由 Cloud AutoML 生成的模型保证具有高准确性和快速性能。
-
一个简单的端到端管道,用于上传数据、训练和验证模型、部署模型以及创建用于获取预测的 REST 端点。
当前,Cloud AutoML 可用于图像分类/检测、自然语言处理(文本分类)和翻译。
欲了解更多关于 Cloud AutoML 的信息,请访问他们的官方网站:cloud.google.com/automl/
NAS 的另一个优势是能够生成比人工设计的模型更紧凑的模型。根据 Hieu Pham 等人所著的《通过参数共享实现高效的神经架构搜索》一文,最新的最先进的CIFAR-10分类神经网络有 2620 万个参数,而一个 NAS 生成的神经网络,其测试准确率与人工设计的网络相当(人工设计网络为 97.44%,NAS 生成网络为 97.35%),但只有 330 万个参数。值得注意的是,像 VGG16、ResNet50 和 InceptionV3 这样的旧模型,分别有 1.38 亿、2500 万和 2300 万个参数。参数规模的大幅减少使得推理时间和模型存储更加高效,这两者在将模型部署到生产环境时都非常重要。
总结
在本章中,我们实现了 NAS,这是一个框架,其中强化学习代理(控制器)生成子神经网络来完成特定任务。我们研究了控制器如何通过策略梯度方法学习生成更好的子网络架构的理论。接着,我们实现了一个简化版本的 NAS,该版本生成能够学习分类CIFAR-10图像的子网络。
欲了解更多相关话题,请参考以下链接列表:
-
通过强化学习实现的 NAS:
arxiv.org/abs/1611.01578 -
高效的 NAS 通过参数共享:
arxiv.org/pdf/1802.03268 -
Google Cloud AutoML:
cloud.google.com/automl/ -
极棒的架构搜索——一个关于生成神经网络的论文精选列表:
github.com/markdtw/awesome-architecture-search
NAS 框架标志着深度学习领域的一个令人兴奋的发展,因为我们已经弄清楚了如何自动设计神经网络架构,这一决定以前是由人类做出的。现在已经有了改进版的 NAS 和其他能够自动生成神经网络的算法,我们鼓励读者也去了解这些内容。
第九章:预测未来股价
金融市场是任何经济的重要组成部分。一个经济要繁荣,其金融市场必须稳固。自从机器学习的出现以来,许多公司开始在股票和其他金融资产的购买中采用算法交易。这个方法已被证明是成功的,并且随着时间的推移而变得更加重要。由于其迅速崛起,已经开发并采用了多种机器学习模型来进行算法交易。一种流行的用于交易的机器学习模型是时间序列分析。你已经学习过强化学习和 Keras,在本章中,它们将被用来开发一个可以预测股价的模型。
背景问题
自动化几乎渗透到了每个行业,金融市场也不例外。创建自动化的算法交易模型可以在购买前对股票进行更快速、更准确的分析。多个指标可以以人类无法达到的速度进行分析。而且,在交易中,情绪化的操作是危险的。机器学习模型可以解决这个问题。同时,交易成本也减少了,因为不再需要持续的监督。
在本教程中,你将学习如何将强化学习与时间序列建模结合起来,以便基于真实数据预测股票价格。
使用的数据
我们将使用的数据是标准普尔 500 指数。根据维基百科,它是 基于 500 家大型公司的市值的美国股票市场指数,这些公司在纽约证券交易所或纳斯达克上市并拥有普通股。 这里是数据的链接(ca.finance.yahoo.com/quote/%255EGSPC/history?p=%255EGSPC)。
数据包含以下列:
-
日期:这表示考虑的日期
-
开盘价:这表示当天市场开盘时的价格
-
最高价:这表示当天市场的最高价格
-
低价:这表示当天市场的最低价格
-
收盘价:这表示当天市场收盘时的价格,经过拆股调整
-
调整后收盘价:这表示经过拆股和分红调整后的收盘价
-
交易量:这表示可用的股票总量
用于训练数据的日期如下:
Start: 14 August 2006
End: 13th August 2015
在网站上,按以下方式筛选日期,并下载数据集:
对于测试,我们将使用以下日期范围:
Start: 14 August 2015
End: 14 August 2018
相应地更改网站上的日期,并下载用于测试的数据集,如下所示:
在接下来的部分,我们将定义一些代理可以执行的可能操作。
分步指南
我们的解决方案使用了一个基于演员-评论员的强化学习模型,并结合了时间序列,帮助我们基于股票价格预测最佳操作。可能的操作如下:
-
持有:这意味着根据价格和预期利润,交易者应该持有股票。
-
卖出:这意味着根据价格和预期利润,交易者应该卖出股票。
-
购买:这意味着根据价格和预期利润,交易者应该购买股票。
演员-评论员网络是一类基于两个交互网络模型的强化学习方法。这些模型有两个组成部分:演员和评论员。在我们的案例中,我们将使用神经网络作为网络模型。我们将使用你已经学过的 Keras 包来创建神经网络。我们希望改进的奖励函数是利润。
演员接受环境状态,然后返回最佳动作,或者返回一个指向动作的概率分布的策略。这似乎是执行强化学习的一种自然方式,因为策略是作为状态的函数直接返回的。
评论员评估由演员网络返回的动作。这类似于传统的深度 Q 网络;在环境状态和一个动作下,返回一个分数,表示在给定状态下采取该动作的价值。评论员的工作是计算一个近似值,然后用它来根据梯度更新演员。评论员本身是通过时序差分算法进行训练的。
这两个网络是同时训练的。随着时间的推移,评论员网络能够改善其Q_value预测,演员也学会了如何根据状态做出更好的决策。
这个解决方案由五个脚本组成,它们将在接下来的章节中描述。
演员脚本
演员脚本是在这里定义策略模型的。我们首先从 Keras 中导入某些模块:layers、optimizers、models 和 backend。这些模块将帮助我们构建神经网络:让我们从导入 Keras 的所需函数开始。
from keras import layers, models, optimizers
from keras import backend as K
- 我们创建了一个名为
Actor的类,它的对象接受state和action大小的参数:
class Actor:
# """Actor (policy) Model. """
def __init__(self, state_size, action_size):
self.state_size = state_size
self.action_size = action_size
- 上述代码展示了状态大小,表示每个状态的维度,以及动作大小,表示动作的维度。接下来,调用一个函数来构建模型,如下所示:
self.build_model()
- 构建一个策略模型,将状态映射到动作,并从定义输入层开始,如下所示:
def build_model(self):
states = layers.Input(shape=(self.state_size,), name='states')
- 向模型中添加隐藏层。共有两层密集层,每一层后面跟着批归一化和激活层。这些密集层是正则化的。两层分别有 16 个和 32 个隐藏单元:
net = layers.Dense(units=16,kernel_regularizer=layers.regularizers.l2(1e-6))(states)
net = layers.BatchNormalization()(net)
net = layers.Activation("relu")(net)
net = layers.Dense(units=32,kernel_regularizer=layers.regularizers.l2(1e-6))(net)
net = layers.BatchNormalization()(net)
net = layers.Activation("relu")(net)
- 最终的输出层将预测具有
softmax激活函数的动作概率:
actions = layers.Dense(units=self.action_size, activation='softmax', name = 'actions')(net)
self.model = models.Model(inputs=states, outputs=actions)
- 通过使用动作值(
Q_value)的梯度来定义损失函数,如下所示:
action_gradients = layers.Input(shape=(self.action_size,))
loss = K.mean(-action_gradients * actions)
- 定义
optimizer和训练函数,如下所示:
optimizer = optimizers.Adam(lr=.00001)
updates_op = optimizer.get_updates(params=self.model.trainable_weights, loss=loss)
self.train_fn = K.function(
inputs=[self.model.input, action_gradients, K.learning_phase()],
outputs=[],
updates=updates_op)
演员网络的自定义训练函数,利用与动作概率相关的 Q 梯度进行训练。通过这个自定义函数,训练的目标是最大化利润(换句话说,最小化Q_values的负值)。
评论家脚本
我们首先导入 Keras 的一些模块:layers、optimizers、models 和 backend。这些模块将帮助我们构建神经网络:
from keras import layers, models, optimizers
from keras import backend as K
- 我们创建了一个名为
Critic的类,其对象接收以下参数:
class Critic:
"""Critic (Value) Model."""
def __init__(self, state_size, action_size):
"""Initialize parameters and build model.
Params
======
state_size (int): Dimension of each state
action_size (int): Dimension of each action
"""
self.state_size = state_size
self.action_size = action_size
self.build_model()
- 构建一个评论家(价值)网络,它将
state和action对(Q_values)映射,并定义输入层,如下所示:
def build_model(self):
states = layers.Input(shape=(self.state_size,), name='states')
actions = layers.Input(shape=(self.action_size,), name='actions')
- 为状态路径添加隐藏层,如下所示:
net_states = layers.Dense(units=16,kernel_regularizer=layers.regularizers.l2(1e-6))(states)
net_states = layers.BatchNormalization()(net_states)
net_states = layers.Activation("relu")(net_states)
net_states = layers.Dense(units=32, kernel_regularizer=layers.regularizers.l2(1e-6))(net_states)
- 为动作路径添加隐藏层,如下所示:
net_actions = layers.Dense(units=32,kernel_regularizer=layers.regularizers.l2(1e-6))(actions)
- 合并状态路径和动作路径,如下所示:
net = layers.Add()([net_states, net_actions])
net = layers.Activation('relu')(net)
- 添加最终输出层,以产生动作值(
Q_values):
Q_values = layers.Dense(units=1, name='q_values',kernel_initializer=layers.initializers.RandomUniform(minval=-0.003, maxval=0.003))(net)
- 创建 Keras 模型,如下所示:
self.model = models.Model(inputs=[states, actions], outputs=Q_values)
- 定义
optimizer并编译一个模型以进行训练,使用内置的损失函数:
optimizer = optimizers.Adam(lr=0.001)
self.model.compile(optimizer=optimizer, loss='mse')
- 计算动作梯度(
Q_values相对于actions的导数):
action_gradients = K.gradients(Q_values, actions)
- 定义一个附加函数来获取动作梯度(供演员模型使用),如下所示:
self.get_action_gradients = K.function(
inputs=[*self.model.input, K.learning_phase()],
outputs=action_gradients)
到此为止,评论家脚本已完成。
代理脚本
在这一部分,我们将训练一个代理,该代理将基于演员和评论家网络执行强化学习。我们将执行以下步骤以实现此目标:
-
创建一个代理类,其初始化函数接收批次大小、状态大小和一个评估布尔函数,用于检查训练是否正在进行中。
-
在代理类中,创建以下方法:
-
导入
actor和critic脚本:
from actor import Actor
from critic import Critic
- 导入
numpy、random、namedtuple和deque,这些模块来自collections包:
import numpy as np
from numpy.random import choice
import random
from collections import namedtuple, deque
- 创建一个
ReplayBuffer类,该类负责添加、抽样和评估缓冲区:
class ReplayBuffer:
#Fixed sized buffer to stay experience tuples
def __init__(self, buffer_size, batch_size):
#Initialize a replay buffer object.
#parameters
#buffer_size: maximum size of buffer. Batch size: size of each batch
self.memory = deque(maxlen = buffer_size) #memory size of replay buffer
self.batch_size = batch_size #Training batch size for Neural nets
self.experience = namedtuple("Experience", field_names = ["state", "action", "reward", "next_state", "done"]) #Tuple containing experienced replay
- 向重放缓冲区记忆中添加一个新的经验:
def add(self, state, action, reward, next_state, done):
e = self.experience(state, action, reward, next_state, done)
self.memory.append(e)
- 随机从记忆中抽取一批经验元组。在以下函数中,我们从记忆缓冲区中随机抽取状态。这样做是为了确保我们输入模型的状态在时间上没有相关性,从而减少过拟合:
def sample(self, batch_size = 32):
return random.sample(self.memory, k=self.batch_size)
- 返回当前缓冲区内存的大小,如下所示:
def __len__(self):
return len(self.memory)
- 使用演员-评论家网络进行学习的强化学习代理如下所示:
class Agent:
def __init__(self, state_size, batch_size, is_eval = False):
self.state_size = state_size #
- 动作数量定义为 3:坐下、买入、卖出
self.action_size = 3
- 定义重放记忆的大小:
self.buffer_size = 1000000
self.batch_size = batch_size
self.memory = ReplayBuffer(self.buffer_size, self.batch_size)
self.inventory = []
- 定义训练是否正在进行中的变量。在训练和评估阶段,此变量将会变化:
self.is_eval = is_eval
- Bellman 方程中的折扣因子:
self.gamma = 0.99
- 可以通过以下方式对演员和评论家网络进行软更新:
self.tau = 0.001
- 演员策略模型将状态映射到动作,并实例化演员网络(本地和目标模型,用于软更新参数):
self.actor_local = Actor(self.state_size, self.action_size)
self.actor_target = Actor(self.state_size, self.action_size)
- 评论家(价值)模型,它将状态-动作对映射到
Q_values,如下所示:
self.critic_local = Critic(self.state_size, self.action_size)
- 实例化评论模型(使用本地和目标模型以允许软更新),如下所示:
self.critic_target = Critic(self.state_size, self.action_size)
self.critic_target.model.set_weights(self.critic_local.model.get_weights())
- 以下代码将目标模型参数设置为本地模型参数:
self.actor_target.model.set_weights(self.actor_local.model.get_weights()
- 给定一个状态,使用演员(策略网络)和演员网络的
softmax层输出返回一个动作,返回每个动作的概率。一个返回给定状态的动作的方法如下:
def act(self, state):
options = self.actor_local.model.predict(state)
self.last_state = state
if not self.is_eval:
return choice(range(3), p = options[0])
return np.argmax(options[0])
- 基于训练模型中的动作概率返回随机策略,并在测试期间返回与最大概率对应的确定性动作。智能体在每一步执行的动作集如下所示:
def step(self, action, reward, next_state, done):
- 以下代码向记忆中添加一个新经验:
self.memory.add(self.last_state, action, reward, next_state,
done)
- 以下代码断言记忆中有足够的经验以进行训练:
if len(self.memory) > self.batch_size:
- 以下代码从记忆中随机抽取一批样本进行训练:
experiences = self.memory.sample(self.batch_size)
- 从采样的经验中学习,如下所示:
self.learn(experiences)
- 以下代码将状态更新到下一个状态:
self.last_state = next_state
- 通过演员和评论者从采样的经验中学习。创建一个方法,通过演员和评论者从采样的经验中学习,如下所示:
def learn(self, experiences):
states = np.vstack([e.state for e in experiences if e is not None]).astype(np.float32).reshape(-1,self.state_size)
actions = np.vstack([e.action for e in experiences if e is not None]).astype(np.float32).reshape(-1,self.action_size)
rewards = np.array([e.reward for e in experiences if e is not None]).astype(np.float32).reshape(-1,1)
dones = np.array([e.done for e in experiences if e is not None]).astype(np.float32).reshape(-1,1)
next_states = np.vstack([e.next_state for e in experiences if e is not None]).astype(np.float32).reshape(-1,self.state_size)
- 返回回放组件中每个经验的独立数组,并基于下一个状态预测动作,如下所示:
actions_next = self.actor_target.model.predict_on_batch(next_states)
- 预测演员输出的
Q_value,用于下一个状态,如下所示:
Q_targets_next = self.critic_target.model.predict_on_batch([next_states, actions_next])
- 以基于时间差的
Q_value作为评论网络的标签,如下所示:
Q_targets = rewards + self.gamma * Q_targets_next * (1 - dones)
- 将评论模型拟合到目标的时间差,如下所示:
self.critic_local.model.train_on_batch(x = [states, actions], y = Q_targets)
- 使用评论网络输出关于动作概率的梯度训练演员模型(本地模型):
action_gradients = np.reshape(self.critic_local.get_action_gradients([states, actions, 0]),(-1, self.action_size))
- 接下来,定义一个自定义的训练函数,如下所示:
self.actor_local.train_fn([states, action_gradients, 1])
- 接下来,初始化两个网络的参数软更新,如下所示:
self.soft_update(self.actor_local.model, self.actor_target.model)
- 该方法根据参数
tau对模型参数进行软更新,以避免模型发生剧烈变化。通过执行基于参数tau的软更新来更新模型(以避免剧烈的模型变化)的方法如下:
def soft_update(self, local_model, target_model):
local_weights = np.array(local_model.get_weights())
target_weights = np.array(target_model.get_weights())
assert len(local_weights) == len(target_weights)
new_weights = self.tau * local_weights + (1 - self.tau) * target_weights
target_model.set_weights(new_weights)
这结束了智能体脚本。
辅助脚本
在这个脚本中,我们将通过以下步骤创建一些有助于训练的函数:
- 导入
numpy和math模块,如下所示:
import numpy as np
import math
- 接下来,定义一个将价格格式化为两位小数的函数,以减少数据的歧义性:
def formatPrice(n):
if n>=0:
curr = "$"
else:
curr = "-$"
return (curr +"{0:.2f}".format(abs(n)))
- 从 CSV 文件中返回一个股票数据向量。将数据中的收盘股价转换为向量,并返回所有股价的向量,如下所示:
def getStockData(key):
datavec = []
lines = open("data/" + key + ".csv", "r").read().splitlines()
for line in lines[1:]:
datavec.append(float(line.split(",")[4]))
return datavec
- 接下来,定义一个函数来根据输入向量生成状态。通过生成上一阶段创建的向量中的状态来创建时间序列。这个函数有三个参数:数据;时间 t(你想预测的日期);以及窗口(回溯多少天)。然后,将衡量这些向量之间的变化率,并基于 sigmoid 函数:
def getState(data, t, window):
if t - window >= -1:
vec = data[t - window+ 1:t+ 1]
else:
vec = -(t-window+1)*[data[0]]+data[0: t + 1]
scaled_state = []
for i in range(window - 1):
- 接下来,用 sigmoid 函数将状态向量从 0 到 1 进行缩放。sigmoid 函数可以将任何输入值映射到 0 到 1 范围内。这有助于将值标准化为概率:
scaled_state.append(1/(1 + math.exp(vec[i] - vec[i+1])))
return np.array([scaled_state])
所有必要的函数和类现在都已定义,因此我们可以开始训练过程。
训练数据
我们将基于代理和辅助方法来训练数据。这将为我们提供三种操作之一,基于当天股票价格的状态。这些状态可以是买入、卖出或保持。在训练过程中,将预测每一天的预定操作,并计算该操作的价格(利润、损失或不变)。在训练期结束时,将计算累计总和,我们将看到是否有盈利或亏损。目标是最大化总利润。
让我们从导入开始,如下所示:
from agent import Agent
from helper import getStockData, getState
import sys
- 接下来,定义要考虑的市场交易天数作为窗口大小,并定义神经网络训练的批量大小,如下所示:
window_size = 100
batch_size = 32
- 使用窗口大小和批量大小实例化股票代理,如下所示:
agent = Agent(window_size, batch_size)
- 接下来,从 CSV 文件中读取训练数据,使用辅助函数:
data = getStockData("^GSPC")
l = len(data) - 1
- 接下来,将回合数定义为
300。代理将在数据上查看这么多次。一个回合表示对数据的完整遍历:
episode_count = 300
- 接下来,我们可以开始遍历回合,如下所示:
for e in range(episode_count):
print("Episode " + str(e) + "/" + str(episode_count))
- 每个回合必须以基于数据和窗口大小的状态开始。库存初始化后,再遍历数据:
state = getState(data, 0, window_size + 1)
agent.inventory = []
total_profit = 0
done = False
- 接下来,开始遍历每一天的股票数据。基于
state,代理预测动作的概率:
for t in range(l):
action = agent.act(state)
action_prob = agent.actor_local.model.predict(state)
next_state = getState(data, t + 1, window_size + 1)
reward = 0
- 如果代理决定不对股票进行任何操作,则
action可以保持不变。另一个可能的操作是买入(因此,股票将被加入库存),如下所示:
if action == 1:
agent.inventory.append(data[t])
print("Buy:" + formatPrice(data[t]))
- 如果
action为2,代理卖出股票并将其从库存中移除。根据销售情况,计算利润(或损失):
elif action == 2 and len(agent.inventory) > 0: # sell
bought_price = agent.inventory.pop(0)
reward = max(data[t] - bought_price, 0)
total_profit += data[t] - bought_price
print("sell: " + formatPrice(data[t]) + "| profit: " +
formatPrice(data[t] - bought_price))
if t == l - 1:
done = True
agent.step(action_prob, reward, next_state, done)
state = next_state
if done:
print("------------------------------------------")
print("Total Profit: " + formatPrice(total_profit))
print("------------------------------------------")
- 在训练过程中,你可以看到类似下面的日志。股票在特定的价格下被买入和卖出:
sell: $2102.15| profit: $119.30
sell: $2079.65| profit: $107.36
Buy:$2067.64
sell: $2108.57| profit: $143.75
Buy:$2108.63
Buy:$2093.32
Buy:$2099.84
Buy:$2083.56
Buy:$2077.57
Buy:$2104.18
sell: $2084.07| profit: $115.18
sell: $2086.05| profit: $179.92
------------------------------------------
Total Profit: $57473.53
- 接下来,从 CSV 文件中读取测试数据。初始状态由数据推断得出。这些步骤与训练过程中的单一回合非常相似:
test_data = getStockData("^GSPC Test")
l_test = len(test_data) - 1
state = getState(test_data, 0, window_size + 1)
- 利润从
0开始。代理初始化时库存为零,并且处于测试模式:
total_profit = 0
agent.inventory = []
agent.is_eval = False
done = False
- 接下来,迭代交易的每一天,代理可以根据数据做出决策。每天,代理决定一个动作。根据动作,股票会被持有、卖出或买入:
for t in range(l_test):
action = agent.act(state)
- 如果操作是
0,则没有交易。在这期间,状态可以被称为持有。
next_state = getState(test_data, t + 1, window_size + 1)
reward = 0
- 如果操作是
1,则通过将股票加入库存来购买股票,操作如下:
if action == 1:
agent.inventory.append(test_data[t])
print("Buy: " + formatPrice(test_data[t]))
- 如果操作是
2,代理通过将股票从库存中移除来卖出股票。价格差异被记录为利润或亏损:
elif action == 2 and len(agent.inventory) > 0:
bought_price = agent.inventory.pop(0)
reward = max(test_data[t] - bought_price, 0)
total_profit += test_data[t] - bought_price
print("Sell: " + formatPrice(test_data[t]) + " | profit: " + formatPrice(test_data[t] - bought_price))
if t == l_test - 1:
done = True
agent.step(action_prob, reward, next_state, done)
state = next_state
if done:
print("------------------------------------------")
print("Total Profit: " + formatPrice(total_profit))
print("------------------------------------------")
- 一旦脚本开始运行,模型将随着训练逐步变得更好。你可以查看日志,内容如下:
Sell: $2818.82 | profit: $44.80
Sell: $2802.60 | profit: $4.31
Buy: $2816.29
Sell: $2827.22 | profit: $28.79
Buy: $2850.40
Sell: $2857.70 | profit: $53.21
Buy: $2853.58
Buy: $2833.28
------------------------------------------
Total Profit: $10427.24
模型已进行了交易,总利润为$10,427。请注意,这种交易方式不适合现实世界,因为交易涉及更多成本和不确定性;因此,这种交易风格可能会产生不利影响。
最终结果
在训练数据之后,我们用test数据集对模型进行了测试。我们的模型总共获得了$10427.24的利润。模型的最佳之处在于,利润随着时间的推移不断增加,表明它正在有效学习并采取更好的行动。
总结
总之,机器学习可以应用于多个行业,并且在金融市场中可以非常高效地应用,正如你在本章中所见。我们可以结合不同的模型,正如我们结合了强化学习和时间序列,以生成更强大的模型来满足我们的用例。我们讨论了使用强化学习和时间序列来预测股市。我们使用了一个演员-评论家模型,根据股票价格的状态来决定最佳行动,目的是最大化利润。最终,我们获得了一个结果,展示了总体利润,并且利润随时间增加,表明代理在每个状态中学得更多。
在下一章中,你将学习未来的工作领域。
第十章:展望未来
在过去的几百页中,我们面临了许多挑战,并应用了强化学习和深度学习算法。为了总结我们的强化学习(RL)之旅,本章将探讨我们尚未涵盖的该领域的几个方面。我们将从讨论强化学习的几个缺点开始,任何从业者或研究人员都应该对此有所了解。为了以积极的语气结束,我们将描述该领域近年来所看到的许多令人兴奋的学术进展和成就。
强化学习的缺点
到目前为止,我们只讨论了强化学习算法能做什么。对于读者而言,强化学习可能看起来是解决各种问题的灵丹妙药。但为什么我们在现实生活中并没有看到强化学习算法的广泛应用呢?现实情况是,该领域存在许多缺点,阻碍了其商业化应用。
为什么有必要谈论该领域的缺陷?我们认为这将帮助你建立一个更全面、更客观的强化学习观念。此外,理解强化学习和机器学习的弱点是一个优秀的机器学习研究员或从业者的重要素质。在接下来的小节中,我们将讨论强化学习目前面临的一些最重要的局限性。
资源效率
当前的深度强化学习算法需要大量的时间、训练数据和计算资源,才能达到理想的熟练程度。对于像 AlphaGo Zero 这样的算法,它的强化学习算法在没有任何先验知识和经验的情况下学习围棋,资源效率成为了将此类算法推广到商业规模的主要瓶颈。回想一下,当 DeepMind 实现 AlphaGo Zero 时,他们需要在数千万场游戏中使用数百个 GPU 和数千个 CPU 来训练代理。为了让 AlphaGo Zero 达到合理的熟练度,它需要进行数百万场游戏,相当于数十万人的一生所进行的游戏数量。
除非未来普通消费者能够轻松利用像谷歌和英伟达今天所提供的庞大计算能力,否则开发超人类的强化学习算法的能力仍将远远超出公众的掌控。这意味着,强大的、资源密集型的强化学习算法将被少数几家机构垄断,这可能并非一件好事。
因此,在有限资源下使强化学习算法可训练将继续是社区必须解决的重要问题。
可重复性
在众多科学研究领域,一个普遍存在的问题是无法重复学术论文和期刊中所声称的实验结果。在 2016 年《自然》杂志(世界上最著名的科学期刊)进行的一项调查中,70%的受访者表示他们未能重复自己或其他研究者的实验结果。此外,对于无法重复实验结果的态度十分严峻,90%的研究人员认为确实存在可重复性危机。
《自然》报道的原始工作可以在这里找到:www.nature.com/news/1-500-scientists-lift-the-lid-on-reproducibility-1.19970。
尽管这项调查面向多个学科的研究人员,包括生物学和化学,但强化学习也面临类似的问题。在论文《深度强化学习的重要性》(参考文献见本章末尾;你可以在arxiv.org/pdf/1709.06560.pdf查看在线版本)中,Peter Henderson 等人研究了深度强化学习算法的不同配置对实验结果的影响。这些配置包括超参数、随机数生成器的种子以及网络架构。
在极端情况下,他们发现,在对同一个模型进行训练时,使用两组五个不同的随机种子配置,最终得到的两个模型的平均回报存在显著差异。此外,改变其他设置,如 CNN 架构、激活函数和学习率,也对结果产生深远影响。
不一致和无法重复的结果意味着什么呢?随着强化学习和机器学习的应用和普及以接近指数的速度增长,互联网上可自由获取的强化学习算法实现数量也在增加。如果这些实现无法重现它们声称能够达到的结果,这将会在现实应用中引发重大问题和潜在危险。毫无疑问,没有人希望他们的自动驾驶汽车被实现得无法做出一致的决策!
可解释性/可追溯性
我们已经看到,代理的策略可以返回单一的动作或一组可能动作的概率分布,而它的价值函数可以返回某一状态的期望程度。那么,模型如何解释它是如何得出这些预测的呢?随着强化学习变得更加流行并有可能在现实应用中得到广泛应用,将会有越来越大的需求去解释强化学习算法的输出。
今天,大多数先进的强化学习算法都包含深度神经网络,而这些网络目前只能通过一组权重和一系列非线性函数来表示。此外,由于神经网络的高维特性,它们无法提供任何有意义的、直观的输入与相应输出之间的关系,普通人难以理解。因此,深度学习算法通常被称为“黑盒”,因为我们很难理解神经网络内部究竟发生了什么。
为什么强化学习算法需要具有可解释性?假设一辆自动驾驶汽车发生了车祸(假设这只是两辆车之间无害的小碰撞,驾驶员没有受伤)。人类驾驶员可以解释导致事故发生的原因;他们能够说明为什么采取某个特定的操作,以及事故发生时究竟发生了什么。这将帮助执法部门确定事故原因,并可能追究责任。然而,即使我们使用现有的算法创造出一个能够驾驶汽车的智能体,这依然是做不到的。
如果不能解释预测结果,用户和大众将难以信任任何使用机器学习的软件,尤其是在算法需要为做出重要决策负责的应用场景中。这对强化学习算法在实际应用中的普及构成了严重障碍。
易受攻击的风险
深度学习算法在多个任务中展现了惊人的成果,包括计算机视觉、自然语言处理和语音识别。在一些任务中,深度学习已经超越了人类的能力。然而,最近的研究表明,这些算法对攻击极为脆弱。所谓攻击,指的是对输入进行难以察觉的修改,从而导致模型表现出不同的行为。举个例子:
对抗攻击的示意图。通过对图像添加难以察觉的扰动,攻击者可以轻易欺骗深度学习图像分类器。
最右侧的图片是通过将左侧的原始图像和中间的扰动图像相加得到的结果。即便是最准确、表现最好的深度神经网络图像分类器,也无法将右侧的图像识别为山羊,反而将其误判为烤面包机。
这些例子让许多研究人员感到震惊,因为人们没想到深度学习算法如此脆弱,并容易受到此类攻击。这一领域现在被称为对抗性机器学习,随着越来越多的研究者关注深度学习算法的鲁棒性和漏洞,它的知名度和重要性也在迅速提升。
强化学习算法同样无法避免这些结果和攻击。根据 Anay Pattanaik 等人撰写的题为*《带有对抗攻击的鲁棒深度强化学习》*(arxiv.org/abs/1712.03632)的论文,对抗性攻击强化学习算法可以定义为任何可能的扰动,导致智能体在该状态下采取最差行动的概率增加。例如,我们可以在 Atari 游戏的屏幕上添加噪声,目的是欺骗玩游戏的 RL 智能体做出错误的决策,从而导致更低的分数。
更为严重的应用包括向街道标志添加噪声,以欺骗自动驾驶汽车将 STOP 标志误认为速度限制标志,或让 ATM 识别1,000,000 支票,甚至欺骗面部识别系统将攻击者的面孔识别为其他用户的面孔。
不用多说,这些漏洞进一步增加了在实际、关乎安全的使用场景中采用深度学习算法的风险。虽然目前已有大量努力在应对对抗性攻击,但深度学习算法要足够强大以适应这些使用场景,仍然有很长的路要走。
强化学习的未来发展
前几节可能为深度学习(DL)和强化学习(RL)描绘了一个严峻的前景。然而,不必感到完全沮丧;事实上,现在正是深度学习和强化学习的激动人心时刻,许多重大的研究进展正在持续塑造该领域,并促使其以飞快的速度发展。随着计算资源和数据的不断增加,扩展和改进深度学习和强化学习算法的可能性也在不断扩展。
解决局限性
首先,前述问题已被研究界认识和承认,正在有多个方向进行解决。在 Pattanaik 等人研究中,作者不仅展示了当前深度强化学习算法容易受到对抗性攻击的影响,还提出了可以使这些算法对这些攻击更具鲁棒性的方法。特别是,通过在经过对抗性扰动的示例上训练深度 RL 算法,模型能够提高其对类似攻击的鲁棒性。这一技术通常被称为对抗训练。
此外,研究界正在积极采取行动解决可复现性问题。ICLR 和 ICML 是机器学习领域两个最大的会议,它们举办了挑战赛,邀请参与者重新实现并重新运行已提交论文中的实验,以复制报告的结果。参与者随后需要通过撰写可复现性报告来批评原始工作,报告应描述问题陈述、实验方法、实施细节、分析以及原始论文的可复现性。该挑战由 Joelle Pineau 和麦吉尔大学组织,旨在促进实验和学术工作的透明度,确保结果的可复现性和完整性。
关于 ICLR 2018 可复现性挑战的更多信息可以在此找到:www.cs.mcgill.ca/~jpineau/ICLR2018-ReproducibilityChallenge.html。同样,关于 ICML 原始可复现性研讨会的信息可以在此找到:sites.google.com/view/icml-reproducibility-workshop/home。
迁移学习
另一个越来越受到关注的重要话题是迁移学习。迁移学习是机器学习中的一种范式,其中在一个任务上训练的模型经过微调后,用于完成另一个任务。
例如,我们可以训练一个模型来识别汽车图像,并使用该模型的权重来初始化一个相同的模型,该模型学习识别卡车。主要的直觉是,通过在一个任务上进行训练学到的某些抽象概念和特征,可以迁移到其他类似的任务。这一思想同样适用于许多强化学习问题。一个学会玩特定 Atari 游戏的智能体应该能够熟练地玩其他 Atari 游戏,而不需要从头开始训练,就像人类一样。
Demis Hassabis,DeepMind 的创始人和深度强化学习的先驱,在最近的一次演讲中提到,迁移学习是实现通用智能的关键。而我认为,成功实现迁移学习的关键在于获取概念性知识,这些知识是从你学习的地方的感知细节中抽象出来的。
Demis Hassabis 的引用和相关演讲可以在此找到:www.youtube.com/watch?v=YofMOh6_WKo
在计算机视觉和自然语言处理领域,已经有多个进展,其中利用从一个领域初始化的知识和先验知识来学习另一个领域的数据。
这在第二领域缺乏数据时尤其有用。被称为少样本或单样本学习,这些技术允许模型即使在数据集较小的情况下,也能很好地学习执行任务,如下图所示:
一个关于少样本学习分类器学习如何为数据量较少的类别划分良好决策边界的示例
强化学习中的少样本学习涉及让智能体在给定任务上达到高水平的熟练度,而不依赖于大量的时间、数据和计算资源。设想一个可以轻松微调以在任何其他视频游戏中表现良好的通用游戏玩家智能体,且使用现成的计算资源;这将使强化学习算法的训练更加高效,从而更易于让更广泛的受众访问。
多智能体强化学习
另一个取得显著进展的有前景领域是多智能体强化学习。与我们之前看到的只有一个智能体做出决策的问题不同,这一主题涉及多个智能体同时并协作地做出决策,以实现共同目标。与此相关的最重要的工作之一是 OpenAI 的 Dota2 对战系统,名为OpenAI Five。Dota2 是世界上最受欢迎的大型多人在线角色扮演游戏(MMORPGs)之一。与围棋和 Atari 等传统的强化学习游戏相比,Dota2 由于以下原因更为复杂:
-
多个智能体:Dota2 游戏包含两支五人队伍,每支队伍争夺摧毁对方的基地。因此,决策不仅仅由一个智能体做出,而是由多个智能体同时做出。
-
可观察性:屏幕仅显示智能体角色的周围环境,而不是整个地图。这意味着游戏的整体状态,包括对手的位置和他们的行动,是不可观察的。在强化学习中,我们称这种情况为部分可观察状态。
-
高维度性:Dota2 智能体的观察可以包括 20,000 个数据点,每个点展示了人类玩家可能在屏幕上看到的内容,包括健康状态、控制角色的位置、敌人的位置以及任何攻击。而围棋则需要更少的数据点来构建一个观察(19 x 19 棋盘,历史走法)。因此,观察具有高维度性和复杂性。这同样适用于决策,Dota2 AI 的动作空间包含 17 万个可能性,包括移动、施放技能和使用物品的决策。
要了解更多关于 OpenAI 的 Dota2 AI 的信息,请查看他们的项目博客:blog.openai.com/openai-five/。
此外,通过对传统强化学习算法进行创新升级,OpenAI Five 中的每个智能体都能够学会与其他智能体协作,共同实现摧毁敌方基地的目标。它们甚至能够学习到一些经验丰富的玩家使用的团队策略。以下是 Dota2 玩家队伍与 OpenAI Five 之间比赛的一张截图:
OpenAI 对抗人类玩家(来源:www.youtube.com/watch?v=eaBYhLttETw)
尽管这个项目需要极高的资源要求(240 个 GPU、120,000 个 CPU 核心、约 200 年人类游戏时间),它展示了当前的 AI 算法确实能够在一个极为复杂的环境中互相合作,达成共同目标。这项工作象征着 AI 和强化学习研究的另一个重要进展,并展示了当前技术的潜力。
摘要
这标志着我们在强化学习的入门之旅的结束。在本书的过程中,我们学习了如何实现能够玩 Atari 游戏、在 Minecraft 中导航、预测股市价格、玩复杂的围棋棋盘游戏,甚至生成其他神经网络来训练CIFAR-10数据的智能体。在此过程中,您已经掌握并习惯了许多基础的和最先进的深度学习与强化学习算法。简而言之,您已经取得了很多成就!
但这段旅程并不会也不应当就此结束。我们希望,凭借您新获得的技能和知识,您将继续利用深度学习和强化学习算法,解决本书之外的实际问题。更重要的是,我们希望本指南能激励您去探索机器学习的其他领域,进一步发展您的知识和经验。
强化学习社区面临许多障碍需要克服。然而,未来值得期待。随着该领域的日益流行和发展,我们迫不及待想要看到该领域将取得的新进展和里程碑。我们希望读者在完成本指南后,能够感到更加充实并准备好构建强化学习算法,并为该领域做出重要贡献。
参考文献
Open Science Collaboration. (2015)。估算心理学科学的可重复性。Science, 349(6251), aac4716。
Henderson, P., Islam, R., Bachman, P., Pineau, J., Precup, D., 和 Meger, D. (2017)。真正重要的深度强化学习。arXiv 预印本 arXiv:1709.06560。
Pattanaik, A., Tang, Z., Liu, S., Bommannan, G., 和 Chowdhary, G. (2018 年 7 月)。抗干扰的强大深度强化学习。载于第 17 届国际自主代理与多智能体系统会议论文集(第 2040-2042 页)。国际自主代理与多智能体系统基金会。