将多个 TensorFlow Hub 模块组合成一个 AdaNet 集成网络
2019 年 1 月 28 日
Sara Robinson 发表

您是否曾经开始构建一个 ML 模型,却发现您不确定哪个模型架构将产生最佳结果?输入基于 TensorFlow 的 AdaNet 框架。使用 AdaNet,您可以将多个模型馈送到 AdaNet 的算法中,它将在训练过程中找到所有模型的最佳组合。我最近一直在玩它,并且对集成与单个模型相比的准确性印象深刻。

等等。在我们继续之前:AdaNet 如何融入不断发展的 ML 空间?它是 AdaNet 论文 的开源实现。这篇论文概述了一个称为神经架构搜索的概念,它涉及自动设计针对特定任务的最佳 ML 模型架构的过程。它具有理论上支持的性能保证,并且运行速度很快。

这与 AutoML 有何关系?AutoML 涉及但不限于数据预处理、特征工程、模型族搜索和超参数调整。您可以将 AutoML 视为一个伞形概念,AdaNet 属于 AutoML 的模型架构搜索方面。另请注意,AutoML *研究* 不同于 Google Cloud AutoML,后者在后台使用 AutoML 概念,并针对希望构建自定义 ML 模型而无需编写模型代码的开发人员(我已经写了很多 博客文章 关于 Cloud AutoML)。

在这篇文章中,我将引导您使用 AdaNet 的 AutoEnsembleEstimator 构建一个集成网络。您可以使用 AdaNet 构建任何类型的网络(图像、文本、结构化数据等)。对于这个例子,我将构建一个文本分类模型,根据他们写的一些句子来预测作者。除了 AdaNet 之外,我们还将使用以下工具来构建此模型
ML model
我还将向您展示如何在使用 Cloud ML Engine 的情况下大规模地训练模型。为了纪念 20 年来首次进入公共领域的最新作品,我选择了一些在 1923 年出版书籍的作者作为训练数据。要跳转到此模型代码,请查看 GitHub 上的代码。

本示例使用 AdaNet 0.5.0、TensorFlow 1.12.0 和 TF Hub 0.2.0。

以下是我们将为我们的模型导入的包
import adanet
import numpy as np
import pandas as pd
import tensorflow as tf
import tensorflow_hub as hub
import urllib

from sklearn.preprocessing import LabelEncoder

下载数据

我已经从 古腾堡计划 下载了文学数据,并进行了一些预处理,将每个文本分成许多带有其特定作者标签的 1-2 个句子片段。这是预览

使用以下代码,我们将使用 urllib 下载 CSV,将其转换为 Pandas DataFrame,对数据进行混洗并进行预览
urllib.request.urlretrieve('https://storage.googleapis.com/authors-training-data/data.csv', 'data.csv')

data = pd.read_csv('data.csv')
data = data.sample(frac=1) # Shuffles the data
data.head()
然后我们将将其分成训练集和测试集,使用 80% 的数据进行训练
train_size = int(len(data) * .8)

train_text = data['text'][:train_size]
train_authors = data['author'][:train_size]

test_text = data['text'][train_size:]
test_authors = data['author'][train_size:]
标签是每个作者的字符串,我已经使用 Scikit Learn LabelEncoder 实用程序将它们编码为独热向量。我们可以在几行代码中完成此操作
encoder = LabelEncoder()
encoder.fit_transform(np.array(train_authors))
train_encoded = encoder.transform(train_authors)
test_encoded = encoder.transform(test_authors)
num_classes = len(encoder.classes_)

使用 TF Hub 嵌入列

我最喜欢的关于 TF Hub 文本模块的一点是,您可以使用一行代码实例化一个嵌入特征列,并且您不需要对文本输入进行任何预处理以将其转换为嵌入。TF Hub 将处理繁重的工作,因此您可以将原始文本直接馈送到模型中,而无需任何预处理。我在 这篇文章 中写了更多关于 TF Hub 的内容,因此在这里我不会详细介绍。总而言之,从头开始构建词嵌入需要 *大量* 时间和训练数据,而 TF Hub 为我们提供了许多可供选择的模型,从而简化了这一过程。

我们可以从许多不同的 TF Hub 文本嵌入模块 开始。(TF Hub 还提供图像和视频模块。)我们应该选择哪一个?很难说哪一个会对我们的文本产生最高的准确率,而这就是 AdaNet 派上用场的地方。我们可以使用不同的 TF Hub 模块构建多个 TF Estimator - 然后我们将它们都馈送到同一个 AdaNet 模型中,让 AdaNet 对它们进行集成以找到最佳模型。

首先,让我们定义我们的 TF Hub 嵌入列。当我们设置模型以告知 TensorFlow 它应该为我们的特征预期的数据格式时,我们将使用这些列
ndim_embeddings = hub.text_embedding_column(
  "ndim",
  module_spec="https://tfhub.dev/google/nnlm-en-dim128/1", trainable=False 
)
encoder_embeddings = hub.text_embedding_column(
  "encoder", 
  module_spec="https://tfhub.dev/google/universal-sentence-encoder/2", trainable=False)
现在我们可以定义我们将馈送到 AdaNet 模型的两个 Estimator。由于这是一个分类问题,因此我们将对两者都使用 DNNEstimator
estimator_ndim = tf.contrib.estimator.DNNEstimator(
  head=multi_class_head,
  hidden_units=[64,10],
  feature_columns=[ndim_embeddings]
)

estimator_encoder = tf.contrib.estimator.DNNEstimator(
  head=multi_class_head,
  hidden_units=[64,10],
  feature_columns=[encoder_embeddings]
)
这里发生了什么?hidden_units 告诉 TensorFlow 我们的网络在每一层中将有多少个神经元。对于这些中的每一个,它将在第一层中有 64 个,在第二层中有 10 个。feature_columns 是模型特征的列表。在这个例子中,我们只有一个(书的句子)。

构建我们的 AdaNet 模型

现在我们已经获得了两个 Estimator,我们准备将它们馈送到 AdaNet 模型中。在这个例子中,我使用 AdaNet 的 AutoEnsembleEstimator,这使得它非常简单。它将接收我创建的两个估算器,并通过对每个模型的预测进行平均来增量地创建集成。有关更多自定义选项,请查看 adanet.subnetwork Builder 和 Generator 类。使用 AutoEnsembleEstimator,我们可以将我们上面定义的两个模型都馈送到 candidate_pool 参数中的集成中
model_dir=os.path.join('/path/to/model/dir')

multi_class_head = tf.contrib.estimator.multi_class_head(
  len(encoder.classes_),
  loss_reduction=tf.losses.Reduction.SUM_OVER_BATCH_SIZE
)

estimator = adanet.AutoEnsembleEstimator(
    head=multi_class_head,
    candidate_pool=[
        estimator_ndim,
        estimator_encoder
    ],
    config=tf.estimator.RunConfig(
      save_summary_steps=1000,
      save_checkpoints_steps=1000,
      model_dir=model_dir
    ),
    max_iteration_steps=5000
)
那里有很多事情,让我们分解一下
  • headtf.contrib.estimator.Head 的一个实例,它告诉我们的模型如何为每个可能的集成计算损失和评估指标。AdaNet 将这些潜在的集成网络称为“候选”。有许多不同类型的头部(用于回归、多类分类等)。这里我们使用 multi_class_head,因为我们的模型中存在超过 2 个可能的标签类别。对于将多个标签分配给一个特定输入的模型,我们将使用 multi_label_head
  • config 设置了一些用于运行训练作业的参数:我们想要保存模型摘要和检查点的频率,以及 TF 应该将它们保存到的目录。请记住,如果您在 Colab 中训练模型,过于频繁地保存检查点可能会占用您的可用磁盘空间。
  • max_iteration_steps 告诉 AdaNet 在单个 *迭代* 中执行多少个训练步骤。迭代是指对一组候选进行训练,因此此数字(以及我们将稍后定义的总训练步骤)告诉 AdaNet 生成新的集成候选的频率。


我们将为此使用 TensorFlow 方便的 train_and_evaluate 函数,它将同时运行训练和评估。为了设置它,我们需要编写训练和评估输入函数。输入函数负责将数据馈送到我们的模型中。我们将在输入函数中使用 tf.data API。即使我们有两个使用不同特征列的独立模型,我们也可以将两个特征放在同一个字典中,这样我们只需要编写一个输入函数
train_features = {
  "ndim": train_text,
  "encoder": train_text
}

def input_fn_train():
  dataset = tf.data.Dataset.from_tensor_slices((train_features, train_authors))
  dataset = dataset.repeat().shuffle(100).batch(64)
  iterator = dataset.make_one_shot_iterator()
  data, labels = iterator.get_next()
  return data, labels
我们的评估特征和输入函数看起来非常相似
eval_features = {
  "ndim": test_text,
  "encoder": test_reviews
}

def input_fn_eval():
  dataset = tf.data.Dataset.from_tensor_slices((eval_features, test_authors))
  dataset = dataset.batch(64)
  iterator = dataset.make_one_shot_iterator()
  data, labels = iterator.get_next()
  return data, labels
我们快完成了!在训练之前,我们需要做的最后一件事是创建或训练和评估规范。您可以将其视为将所有内容连接在一起 - 由于我们正在一次运行训练和评估,因此这些规范将告诉我们的估算器为每个作业运行哪个输入函数
train_spec = tf.estimator.TrainSpec(
  input_fn=input_fn_train,
  max_steps=40000
)

eval_spec=tf.estimator.EvalSpec(
  input_fn=input_fn_eval,
  steps=None,
  start_delay_secs=10,
  throttle_secs=10
)
还记得我们在上面定义的 max_iteration_steps 吗?TrainSpec 中的 max_steps 参数是指要训练的总步数。这意味着我们将总共有 8 次迭代(8 组集成候选)。现在该运行训练和评估了
tf.estimator.train_and_evaluate(estimator, train_spec, eval_spec)

在 Cloud ML Engine 上训练我们的 AdaNet 模型

如果您尝试在 Colab 中运行上面的单元格,您可能会遇到内存限制。这就是云派上用场的地方。我们将使用 Cloud ML Engine 来训练我们的模型。为此,您需要 创建一个 GCP 项目 并启用计费。要使我们上面定义的模型准备好用于云,我们所需要做的就是以以下格式在本地打包我们的应用程序
setup.py
config.yaml

trainer/
  model.py
  __init__.py
您可以随意命名 trainer 目录 - 这是我们将使用我们的模型上传到 ML Engine 的 Python 包的名称。__init__.py 是一个空文件,model.py 包含上面所有代码。setup.py 包含我们包的名称和版本,以及我们用于创建模型的任何 Python 包依赖项。

config.yaml 是您指定任何与云相关的训练参数的地方。这些包括您是否将使用 GPU 或 TPU 进行训练,以及您需要多少个工作人员才能完成训练作业。所有配置选项都列在 这里

导出您的模型以供服务

在开始训练任务之前,我想提一下,你可以添加一些代码到上面提到的 `model.py` 文件中,以便在模型训练完成后将它导出到云存储。如果你现在不关心这一点,你可以跳到下一部分。

我们将使用 `LatestExporter` 类导出我们的模型。为了创建一个导出器,我们需要定义一个 服务输入函数。这最初让我感到困惑,但它与我们定义的其他输入函数并没有太大区别。它应该返回两件事:当模型被服务时,它应该期望的输入格式,以及服务器应该期望的输入格式。在我们的模型中,它们是相同的,但在某些情况下,你可能希望在输入被送入模型之前对它们进行一些预处理。因为我们的输入格式相同,所以服务输入函数非常简单。

def serving_input_fn():
    feature_placeholders = {
      'ndim' : tf.placeholder(tf.string, [None]),
      'encoder' : tf.placeholder(tf.string, [None])
    }

    return tf.estimator.export.ServingInputReceiver(feature_placeholders, feature_placeholders)
如果我们使用的 TF Hub 模块不允许我们直接传递原始文本,而是要求将文本转换为整数,我们的输入函数将在此处返回两个不同的对象。有了这个函数,我们可以定义我们的导出器。
exporter = tf.estimator.LatestExporter('exporter', serving_input_fn, exports_to_keep=None)
为了调用 `export()`,我们还需要模型的最后一个检查点和该检查点的评估结果。我们可以通过以下代码获取它们。
latest_ckpt = tf.train.latest_checkpoint(model_dir)

last_eval = estimator.evaluate(
    input_fn_eval,
    checkpoint_path=latest_ckpt
)

exporter.export(estimator, model_dir, latest_ckpt, last_eval, is_the_final_export=True)
哇!当它在 ML Engine 中运行时,它将保存我们的最终模型。

使用 gcloud 启动训练任务

为了在云端训练我们的模型,我们将创建一个云存储桶。这是模型检查点将存储的位置。我们还将指向 Tensorboard 到这个存储桶,以便我们可以在模型训练时查看模型的指标。我最喜欢启动 ML Engine 训练任务的方法是通过 gcloud CLI。首先,为任务定义一些环境变量。
export JOB_ID=unique_job_name
export JOB_DIR=gs://your/gcs/bucket/path
export PACKAGE_PATH=trainer/
export MODULE=trainer.model
export REGION=your_cloud_project_region
用你项目中特定的变量替换上面的字符串。然后你就可以使用以下 gcloud 命令进行训练。
gcloud ml-engine jobs submit training $JOB_ID --package-path $PACKAGE_PATH --module-name $MODULE --job-dir $JOB_DIR --region $REGION --runtime-version 1.12 --python-version 3.5 --config config.yaml
如果这个命令执行正确,你应该在控制台中看到一条消息,表示你的任务已排队。你可以从命令行流式传输日志,或者在云控制台的 **任务** 标签页中进行导航。
jobs tab in ML Engine on Cloud console

在 TensorBoard 中可视化 AdaNet 训练

你的训练任务正在运行,现在怎么办?幸运的是,你不需要等到它完成才能评估结果。你可以使用 TensorBoard,它使用你的训练任务创建的检查点文件来可视化准确性、损失和其他指标,并在训练运行时进行可视化。如果你在本地安装了 TensorFlow,好消息是——你已经可以通过命令行访问 TensorBoard。

运行以下命令将 TensorBoard 指向云存储上的日志目录。

tensorboard --logdir gs://your/gcs/checkpoint/path
然后将你的浏览器指向 `localhost:6006` 来查看训练进度,并导航到标量标签页:标量标签页 我承认:我一直避免使用 TensorBoard 直到现在(这么多图表可能让人望而生畏!)。但正如你很快就会看到的那样,TensorBoard 使我们更容易理解模型的性能,对于 AdaNet 来说它尤其有用。我们只关注准确性和 `adanet_loss` 图表。让我们从准确性开始,看看 `adanet_weighted_ensemble` 图表: 请记住,我们的模型每迭代 5000 步,这意味着每 5000 步 AdaNet 就会生成新的候选集成(除了第一次迭代,它只包含单个网络)。如果你将鼠标悬停在图表上,你可以看到每条线指的是哪次迭代和哪一个集成: 我们可以看到,在训练的这个阶段,第 7 次迭代的第二个集成(`t6_DNNEstimator1/eval`)具有最佳的准确性。TensorBoard 真正向我们展示了 AdaNet 组合模型的力量——随着训练的继续,集成准确性提高,并且远远高于单个网络自身的准确性(上面图表中左边的粉红色和浅蓝色线)。

损失(或误差)图表显示了类似的趋势:随着 AdaNet 生成和训练新的集成,误差稳步下降。

使用你导出的模型

如果你按照上面的步骤创建了一个输入服务函数并导出了你的模型,你应该在训练完成后在你的指定 GCS 存储桶中看到它。在幕后,AdaNet 将导出在给定迭代中具有最低损失(误差)的候选者。在导出文件夹中,你应该看到以下文件: 如果你想在 ML Engine 上服务你的模型(我将在后续文章中介绍),你可以按照 此处提供的部署步骤 指向 ML Engine 到这个存储桶。你也可以在本地下载这些文件,并根据你的需要服务模型。

因为如果你在没有对训练好的模型进行任何预测的情况下就离开了,那就太可惜了,所以让我们使用 ML Engine 的 `local predict` 从命令行对训练好的模型进行本地预测。我们只需要创建一个换行符分隔的 JSON 文件,其中包含我们想要预测的输入,并遵循与服务输入函数相同的格式。

以下是一个例子

{"encoder": "A strange land indeed! Could it be one with his native New England? Did Congress assemble from the Antipodes?", "ndim": "A strange land indeed! Could it be one with his native New England? Did Congress assemble from the Antipodes?"}
然后我们可以运行以下命令
gcloud ml-engine local predict --model-dir=gs://path/to/saved_model.pb --json-instances=path/to/test.json
这是响应
CLASS_IDS  CLASSES                                                                                               PROBABILITIES
[1]        [u'1']    [0.0043347785249352455, 0.8382837176322937, 0.12185576558113098, 0.025106186047196388, 0.010419543832540512]
这意味着我们的模型预测,这篇文章有 83% 的概率是由与我们标签数组中第一个索引相对应的作者撰写的(我们可以通过在上面记录 `encoder.classes_` 来获取这个索引),也就是丘吉尔。这是正确的!

下一步是什么?

现在你知道了如何使用 AdaNet 的 `AutoEnsembleEstimator` 构建模型,并在云端 ML Engine 上训练它。想要了解更多关于我在这里介绍的内容吗?查看以下资源还有一件事:你是 Keras 的粉丝吗?AdaNet 团队目前正在努力添加 Keras 支持!你可以在 这里 跟踪进度。
下一篇
Combining multiple TensorFlow Hub modules into one ensemble network with AdaNet

发布者 Sara Robinson

你是否曾经开始构建一个 ML 模型,但后来意识到你不确定哪种模型架构将产生最佳结果?介绍基于 TensorFlow 的 AdaNet 框架。使用 AdaNet,你可以将多个模型馈送到 AdaNet 的算法中,它将在训练过程中找到所有模型的最佳组合。我最近一直在玩它,并且已经 bee…