将多个 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 个句子片段。这是预览
text author
I don't suppose the work Much matters. You may work for all of me. I've seen the time I've had to work myself. frost
Good. Beyond good and evil ? We are all that nowadays. huxley
The peculiar dumb expression on her face was lost on Eliphalet. The overseer had laughed coarsely. What skeered on 'em? said he. churchill
view raw sample-data.csv hosted with ❤ by GitHub

使用以下代码,我们将使用 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


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