Go-深度学习实用指南-三-

202 阅读21分钟

Go 深度学习实用指南(三)

原文:zh.annas-archive.org/md5/cea3750df3b2566d662a1ec564d1211d

译者:飞龙

协议:CC BY-NC-SA 4.0

第八章:使用变分自编码器生成模型

在前一章中,我们已经探讨了 DQN 是什么以及我们可以在奖励或行动周围做出什么类型的预测。在本章中,我们将讨论如何构建一个 VAE 及其相对于标准自编码器的优势。我们还将探讨改变潜在空间维度对网络的影响。

让我们再来看看另一个自编码器。我们在第三章中已经介绍过自编码器,超越基础神经网络 – 自编码器和限制玻尔兹曼机,通过一个简单的例子生成了 MNIST 数字。现在我们将看看如何将其用于一个非常不同的任务——生成新的数字。

本章将涵盖以下主题:

  • 变分自编码器 (VAEs) 介绍

  • 在 MNIST 上构建 VAE

  • 评估结果并更改潜在维度

变分自编码器介绍

VAE 在本质上与更基本的自编码器非常相似;它学习如何将其输入的数据编码为简化表示,并且能够基于该编码在另一侧重新创建它。然而,标准自编码器通常仅限于去噪等任务。对于生成任务,使用标准自编码器存在问题,因为标准自编码器中的潜在空间不适合这种目的。它们产生的编码可能不是连续的——它们可能聚集在非常具体的部分周围,并且可能难以进行插值。

然而,由于我们想构建一个更具生成性的模型,并且不想复制我们输入的相同图像,因此我们需要对输入进行变化。如果我们尝试使用标准自编码器来做这件事,那么最终结果很可能会相当荒谬,特别是如果输入与训练集有很大差异。

标准自编码器的结构看起来有点像这样:

我们已经构建了这个标准自编码器;然而,VAE 有一种稍微不同的编码方式,使其看起来更像以下的图表:

VAE 与标准自编码器不同;它通过设计具有连续的潜在空间,使我们能够进行随机采样和插值。它通过将数据编码为两个向量来实现:一个用于存储其均值估计,另一个用于存储其标准差估计。

使用这些均值和标准差,然后我们对编码进行采样,然后将其传递给解码器。解码器然后根据采样编码生成结果。因为我们在采样过程中插入了一定量的随机噪声,所以实际的编码每次都会稍微有所不同。

通过允许此变化发生,解码器不仅仅局限于特定的编码;相反,在训练过程中,它可以跨越潜在空间的更大区域进行操作,因为它不仅仅暴露于数据的变化,还暴露于编码的变化。

为了确保编码在潜在空间中彼此接近,我们在训练过程中引入了一种称为Kullback-LeiblerKL)散度的度量。KL 散度用于衡量两个概率函数之间的差异。在这种情况下,通过最小化这种散度,我们可以奖励模型使编码彼此靠近,反之亦然,当模型试图通过增加编码之间的距离来作弊时。

在 VAEs 中,我们使用标准正态分布(即均值为 0,标准差为 1 的高斯分布)来测量 KL 散度。我们可以使用以下公式计算:

klLoss = 0.5 * sum(mean² + exp(sd) - (sd + 1))

不幸的是,仅仅使用 KL 散度是不够的,因为我们所做的只是确保编码不会散布得太远;我们仍然需要确保编码是有意义的,而不仅仅是相互混合。因此,为了优化 VAE,我们还添加了另一个损失函数来比较输入和输出。这将导致相似对象的编码(或者在 MNIST 的情况下是手写数字)更接近聚类在一起。这将使解码器能够更好地重建输入,并且允许我们通过操纵输入,在连续的轴上产生不同的结果。

在 MNIST 上构建 VAE

熟悉 MNIST 数据集以及普通自编码器的结果,这是您未来工作的一个极好的起点。正如您可能记得的那样,MNIST 由许多手写数字图像组成,每个数字尺寸为 28 x 28 像素。

编码

由于这是一个自编码器,第一步是构建编码部分,看起来像这样:

首先,我们有我们的两个全连接层:

w0 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(784, 256), gorgonia.WithName("w0"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))

w1 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(256, 128), gorgonia.WithName("w1"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))

每一层都使用 ReLU 激活函数:

// Set first layer to be copy of input
l0 = x
log.Printf("l0 shape %v", l0.Shape())

// Encoding - Part 1
if c1, err = gorgonia.Mul(l0, m.w0); err != nil {
   return errors.Wrap(err, "Layer 1 Convolution failed")
}
if l1, err = gorgonia.Rectify(c1); err != nil {
    return errors.Wrap(err, "Layer 1 activation failed")
}
log.Printf("l1 shape %v", l1.Shape())

if c2, err = gorgonia.Mul(l1, m.w1); err != nil {
    return errors.Wrap(err, "Layer 1 Convolution failed")
}
if l2, err = gorgonia.Rectify(c2); err != nil {
    return errors.Wrap(err, "Layer 1 activation failed")
}
log.Printf("l2 shape %v", l2.Shape())

然后,我们将它们连接到我们的均值和标准差层:

estMean := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(128, 8), gorgonia.WithName("estMean"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))

estSd := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(128, 8), gorgonia.WithName("estSd"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))

这些层以它们的形式使用,因此不需要特定的激活函数:

if l3, err = gorgonia.Mul(l2, m.estMean); err != nil {
    return errors.Wrap(err, "Layer 3 Multiplication failed")
}
log.Printf("l3 shape %v", l3.Shape())

if l4, err = gorgonia.HadamardProd(m.floatHalf, gorgonia.Must(gorgonia.Mul(l2, m.estSd))); err != nil {
    return errors.Wrap(err, "Layer 4 Multiplication failed")
}
log.Printf("l4 shape %v", l4.Shape())

抽样

现在,我们来讲一下 VAE(变分自编码器)背后的一部分魔力:通过抽样创建我们将馈送到解码器中的编码。作为参考,我们正在构建类似以下的东西:

如果您还记得本章早些时候的内容,我们需要在抽样过程中添加一些噪声,我们将其称为epsilon。这些数据用于我们的抽样编码;在 Gorgonia 中,我们可以通过GaussianRandomNode,输入参数为均值为0,标准差为1来实现这一点:

epsilon := gorgonia.GaussianRandomNode(g, dt, 0, 1, 100, 8)

然后,我们将这些信息馈送到我们的公式中以创建我们的抽样编码:

if sz, err = gorgonia.Add(l3, gorgonia.Must(gorgonia.HadamardProd(gorgonia.Must(gorgonia.Exp(l4)), m.epsilon))); err != nil {
    return errors.Wrap(err, "Layer Sampling failed")
}
log.Printf("sz shape %v", sz.Shape())

上述代码可能难以阅读。更简单地说,我们正在做以下工作:

sampled = mean + exp(sd) * epsilon

这使我们使用均值和标准差向量加上噪声成分进行了采样编码。这确保了每次的结果并不完全相同。

解码

在我们获得了采样的编码之后,我们将其馈送给我们的解码器,这本质上与我们的编码器具有相同的结构,但是顺序相反。布局看起来有点像这样:

在 Gorgonia 中的实际实现看起来像下面这样:

// Decoding - Part 3
if c5, err = gorgonia.Mul(sz, m.w5); err != nil {
    return errors.Wrap(err, "Layer 5 Convolution failed")
}
if l5, err = gorgonia.Rectify(c5); err != nil {
    return errors.Wrap(err, "Layer 5 activation failed")
}
log.Printf("l6 shape %v", l1.Shape())

if c6, err = gorgonia.Mul(l5, m.w6); err != nil {
    return errors.Wrap(err, "Layer 6 Convolution failed")
}
if l6, err = gorgonia.Rectify(c6); err != nil {
    return errors.Wrap(err, "Layer 6 activation failed")
}
log.Printf("l6 shape %v", l6.Shape())

if c7, err = gorgonia.Mul(l6, m.w7); err != nil {
    return errors.Wrap(err, "Layer 7 Convolution failed")
}
if l7, err = gorgonia.Sigmoid(c7); err != nil {
    return errors.Wrap(err, "Layer 7 activation failed")
}
log.Printf("l7 shape %v", l7.Shape())

我们在最后一层上放置了Sigmoid激活,因为我们希望输出比 ReLU 通常提供的更连续。

损失或成本函数

正如本章第一部分讨论的那样,我们优化了两种不同的损失源。

我们优化的第一个损失是输入图像与输出图像之间的实际差异;如果差异很小,这对我们来说是理想的。为此,我们展示输出层,然后计算到输入的差异。对于本例,我们使用输入和输出之间的平方误差之和,没有什么花哨的东西。在伪代码中,这看起来像下面这样:

valueLoss = sum(squared(input - output))

在 Gorgonia 中,我们可以按照以下方式实现它:

m.out = l7
valueLoss, err := gorgonia.Sum(gorgonia.Must(gorgonia.Square(gorgonia.Must(gorgonia.Sub(y, m.out)))))
if err != nil {
    log.Fatal(err)
}

我们的另一个损失组件是 KL 散度度量,其伪代码如下所示:

klLoss = sum(mean² + exp(sd) - (sd + 1)) / 2

我们在 Gorgonia 中的实现更冗长,大量使用了Must

valueOne := gorgonia.NewScalar(g, dt, gorgonia.WithName("valueOne"))
valueTwo := gorgonia.NewScalar(g, dt, gorgonia.WithName("valueTwo"))
gorgonia.Let(valueOne, 1.0)
gorgonia.Let(valueTwo, 2.0)

ioutil.WriteFile("simple_graph_2.dot", []byte(g.ToDot()), 0644)
klLoss, err := gorgonia.Div(
    gorgonia.Must(gorgonia.Sum(
        gorgonia.Must(gorgonia.Sub(
            gorgonia.Must(gorgonia.Add(
                gorgonia.Must(gorgonia.Square(m.outMean)),
                gorgonia.Must(gorgonia.Exp(m.outVar)))),
            gorgonia.Must(gorgonia.Add(m.outVar, valueOne)))))),
    valueTwo)
if err != nil {
    log.Fatal(err)
}

现在,剩下的就是一些日常管理和将所有内容联系在一起。我们将使用 Adam 的solver作为示例:

func (m *nn) learnables() gorgonia.Nodes {
    return gorgonia.Nodes{m.w0, m.w1, m.w5, m.w6, m.w7, m.estMean, m.estSd}
}

vm := gorgonia.NewTapeMachine(g, gorgonia.BindDualValues(m.learnables()...))
solver := gorgonia.NewAdamSolver(gorgonia.WithBatchSize(float64(bs)), gorgonia.WithLearnRate(0.01))

现在让我们评估一下结果。

评估结果

您会注意到,我们的 VAE 模型的结果比我们的标准自编码器要模糊得多:

在某些情况下,它还可能在几个不同数字之间犹豫不决,例如在以下示例中,它似乎接近解码为 7 而不是 9:

这是因为我们明确要求分布彼此接近。如果我们试图在二维图上可视化这一点,它看起来会有点像下面的样子:

您可以从上一个示例中看到,它可以生成每个手写数字的多个不同变体,还可以在不同数字之间的某些区域中看到它似乎在几个不同数字之间变形。

更改潜在维度

在足够的 epoch 之后,MNIST 上的 VAE 通常表现相当良好,但确保这一点的最佳方法是测试这一假设并尝试几种其他尺寸。

对于本书描述的实现,这是一个相当快速的更改:

w0 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(784, 256), gorgonia.WithName("w0"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))
w1 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(256, 128), gorgonia.WithName("w1"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))

w5 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(8, 128), gorgonia.WithName("w5"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))
w6 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(128, 256), gorgonia.WithName("w6"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))
w7 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(256, 784), gorgonia.WithName("w7"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))

estMean := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(128, 8), gorgonia.WithName("estMean"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))
estSd := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(128, 8), gorgonia.WithName("estSd"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))

floatHalf := gorgonia.NewScalar(g, dt, gorgonia.WithName("floatHalf"))
gorgonia.Let(floatHalf, 0.5)

epsilon := gorgonia.GaussianRandomNode(g, dt, 0, 1, 100, 8)

这里的基本实现是使用八个维度;要使其在两个维度上工作,我们只需将所有8的实例更改为2,结果如下:

w0 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(784, 256), gorgonia.WithName("w0"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))
w1 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(256, 128), gorgonia.WithName("w1"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))

w5 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(2, 128), gorgonia.WithName("w5"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))
w6 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(128, 256), gorgonia.WithName("w6"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))
w7 := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(256, 784), gorgonia.WithName("w7"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))

estMean := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(128, 2), gorgonia.WithName("estMean"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))
estSd := gorgonia.NewMatrix(g, dt, gorgonia.WithShape(128, 2), gorgonia.WithName("estSd"), gorgonia.WithInit(gorgonia.GlorotU(1.0)))

floatHalf := gorgonia.NewScalar(g, dt, gorgonia.WithName("floatHalf"))
gorgonia.Let(floatHalf, 0.5)

epsilon := gorgonia.GaussianRandomNode(g, dt, 0, 1, 100, 2)

现在我们只需重新编译代码然后运行它,这使我们能够看到当我们尝试具有更多维度的潜在空间时会发生什么。

正如我们所见,很明显,2 个维度处于劣势,但随着我们逐步升级,情况并不那么明显。您可以看到,平均而言,20 个维度产生了明显更锐利的结果,但实际上,模型的 5 维版本可能已经足够满足大多数需求:

总结

您现在已经学会了如何构建 VAE 以及使用 VAE 比标准自编码器的优势。您还了解了变动潜在空间维度对网络的影响。

作为练习,您应该尝试在 CIFAR-10 数据集上训练该模型,并使用卷积层而不是简单的全连接层。

在下一章中,我们将看看数据流水线是什么,以及为什么我们使用 Pachyderm 来构建或管理它们。

进一步阅读

  • 自编码变分贝叶斯, 迪德里克·P·金格玛,和 马克斯·威林

  • 变分自编码器教程, 卡尔·多尔舍

  • ELBO 手术:切割变分证据下界的又一种方法,马修·D·霍夫曼马修·J·约翰逊

  • 潜在对齐与变分注意力,邓云天Yoon Kim贾斯汀·邱郭小芬亚历山大·M·拉什

第三部分:流水线、部署与未来!

本节主要讲述构建深度学习流水线、部署以及未来深度学习发展的所有内容!

本节包括以下章节:

  • 第九章,构建深度学习流水线

  • 第十章,扩展部署

第九章:构建深度学习管道

到目前为止,对于我们讨论过的各种深度学习架构,我们假设我们的输入数据是静态的。我们处理的是固定的电影评论集、图像或文本。

在现实世界中,无论您的组织或项目是否包括来自自动驾驶汽车、物联网传感器、安全摄像头或客户产品使用的数据,您的数据通常会随时间变化。因此,您需要一种方式来集成这些新数据,以便更新您的模型。数据的结构可能也会发生变化,在客户或观众数据的情况下,可能需要应用新的转换操作。此外,为了测试它们对预测质量的影响,可能会添加或删除维度,这些维度可能不再相关或违反隐私法规。在这些情况下,我们该怎么办?

Pachyderm 就是这样一个有用的工具。我们想知道我们拥有什么数据,我们在哪里拥有它,以及如何确保数据被输入到我们的模型中。

现在,我们将研究如何使用 Pachyderm 工具处理网络中的动态输入值。这将帮助我们准备好在现实世界中使用和部署我们的系统。

通过本章结束时,您将学到以下内容:

  • 探索 Pachyderm

  • 集成我们的 CNN

探索 Pachyderm

本书的重点是在 Go 中开发深度学习系统。因此,自然而然地,现在我们正在讨论如何管理输入到我们网络中的数据,让我们看看一个同样用 Go 编写的工具。

Pachyderm 是一个成熟且可扩展的工具,提供容器化数据管道。在这些管道中,你可以从数据到工具等一切需求都集中在一个地方,可以维护和管理部署,并对数据本身进行版本控制。Pachyderm 团队将他们的工具称为数据的 Git,这是一个有用的类比。理想情况下,我们希望对整个数据管道进行版本控制,以便知道用于训练的数据,以及由此给出的特定预测X

Pachyderm 大大简化了管理这些管道的复杂性。Docker 和 Kubernetes 都在幕后运行。我们将在下一章节更详细地探讨这些工具,但现在我们只需知道它们对于实现可复制的构建以及可扩展的模型分布式训练至关重要。

安装和配置 Pachyderm

Pachyderm 有大量出色的文档可供参考,我们不会在这里重新讨论所有内容。相反,我们将带您了解基础知识,并构建一个简单数据管道的教程,以向我们在第六章中构建的 CNN 提供版本化图像数据,使用卷积神经网络进行对象识别

首先,您需要安装 Docker Desktop 并为您的操作系统启用 Kubernetes。在本示例中,我们使用 macOS。

完整的安装说明请参阅docs.docker.com/docker-for-mac/install/,以下是简要说明:

  1. 下载 Docker 的 .dmg 文件

  2. 安装或启动文件

  3. 启用 Kubernetes

要安装并运行 Pachyderm,请按照以下步骤操作:

  1. 要启用 Kubernetes,在启动 Docker 设置后选择适当的复选框,如下所示:

  1. 确保有几个绿色的圆形图标显示您的 Docker 和 Kubernetes 安装正在运行。如果是这样,我们可以通过进入终端并运行以下命令确认底层情况是否正常:
# kubectl get all
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 7m
  1. 在安装 Pachyderm 之前,请确保集群正在运行。我们使用 Homebrew 安装 Pachyderm,通过以下命令(请注意,您需要安装最新版本的 Xcode):
brew tap pachyderm/tap && brew install pachyderm/tap/pachctl@1.9
Updating Homebrew...
...
==> Tapping pachyderm/tap
Cloning into '/usr/local/Homebrew/Library/Taps/pachyderm/homebrew-tap'...
remote: Enumerating objects: 13, done.
remote: Counting objects: 100% (13/13), done.
remote: Compressing objects: 100% (12/12), done.
remote: Total 13 (delta 7), reused 2 (delta 0), pack-reused 0
Unpacking objects: 100% (13/13), done.
Tapped 7 formulae (47 files, 34.6KB).
==> Installing pachctl@1.9 from pachyderm/tap
...
==> Downloading https://github.com/pachyderm/pachyderm/releases/download/v1.9.0rc2/pachctl_1.9.0rc2_d
==> Downloading from https://github-production-release-asset-2e65be.s3.amazonaws.com/23653453/0d686a0
######################################################################## 100.0%
/usr/local/Cellar/pachctl@1.9/v1.9.0rc2: 3 files, 62.0MB, built in 26 seconds
  1. 现在您应该能够启动 Pachyderm 命令行工具了。首先,通过运行以下命令确认工具已成功安装并观察输出:
 pachctl help
Access the Pachyderm API.
..
Usage:
 pachctl [command]

Administration Commands:
..
  1. 我们几乎完成了集群设置,现在可以专注于获取和存储数据。最后一件事是使用以下命令在 Kubernetes 上部署 Pachyderm:
pachctl deploy local
no config detected at %q. Generating new config... 
/Users/xxx/.pachyderm/config.json
No UserID present in config. Generating new UserID and updating config at /Users/xxx/.pachyderm/config.json
serviceaccount "pachyderm" created
clusterrole.rbac.authorization.k8s.io "pachyderm" created
clusterrolebinding.rbac.authorization.k8s.io "pachyderm" created
deployment.apps "etcd" created
service "etcd" created
service "pachd" created
deployment.apps "pachd" created
service "dash" created
deployment.apps "dash" created
secret "pachyderm-storage-secret" created

Pachyderm is launching. Check its status with "kubectl get all"
Once launched, access the dashboard by running "pachctl port-forward"
  1. 执行以下命令检查集群状态。如果您在部署后立即运行该命令,应该会看到容器正在创建中:
kubectl get all
NAME READY STATUS RESTARTS AGE
pod/dash-8786f7984-tb5k9 0/2 ContainerCreating 0 8s
pod/etcd-b4d789754-x675p 0/1 ContainerCreating 0 9s
pod/pachd-fbbd6855b-jcf6c 0/1 ContainerCreating 0 9s
  1. 然后它们会过渡到 Running 状态:
kubectl get all
NAME READY STATUS RESTARTS AGE
pod/dash-8786f7984-tb5k9 2/2 Running 0 2m
pod/etcd-b4d789754-x675p 1/1 Running 0 2m
pod/pachd-fbbd6855b-jcf6c 1/1 Running 0 2m

接下来的部分将介绍数据的准备工作。

将数据导入 Pachyderm

让我们准备我们的数据。在这种情况下,我们使用来自第六章《使用卷积神经网络进行对象识别》的 CIFAR-10 数据集。如果您需要恢复,请从多伦多大学的源头拉取数据,如下所示:

wget https://www.cs.toronto.edu/~kriz/cifar-10-binary.tar.gz
...
cifar-10-binary.tar.gz 100%[==================================>] 162.17M 833KB/s in 2m 26s

将数据提取到临时目录,并在 Pachyderm 中创建 repo

# pachctl create repo data
# pachctl list repo
NAME CREATED SIZE (MASTER)
data 8 seconds ago 0B
bash-3.2$

现在我们有了一个存储库,让我们用 CIFAR-10 图像数据填充它。首先,让我们创建各个目录并分解各种 CIFAR-10 文件,以便我们可以将整个文件夹(从我们的数据或训练集)直接倒入。

现在我们可以执行以下命令,然后确认数据已成功传输到 repo

#pachctl put file -r data@master -f data/
#pachctl list repo
NAME CREATED SIZE (MASTER)
data 2 minutes ago 202.8MiB

我们可以深入了解 repo 包含的文件的详细信息:

pachctl list file data@master
COMMIT NAME TYPE COMMITTED SIZE
b22db05d23324ede839718bec5ff219c /data dir 6 minutes ago 202.8MiB

集成我们的 CNN

现在我们将从前面章节的 CNN 示例中获取示例,并进行一些必要的更新,以使用 Pachyderm 提供的数据打包和部署网络。

创建我们的 CNN 的 Docker 镜像

Pachyderm 数据流水线依赖于预先配置的 Docker 镜像。互联网上有很多 Docker 教程,因此我们在这里保持简单,讨论利用简单部署步骤为任何 Go 应用程序带来优势的所需操作。

让我们来看看我们的 Dockerfile:

FROM golang:1.12

ADD main.go /main.go

ADD cifar/ /cifar/

RUN export GOPATH=$HOME/go && cd / && go get -d -v .

就是这样!我们只需从 Docker Hub 获取 Go 1.12 镜像并将我们的 CIFAR CNN 放入我们的构建中。我们 Dockerfile 的最后一部分是设置 GOPATH 并满足我们的依赖项(例如,安装 Gorgonia)的命令。

执行以下命令来构建 Docker 镜像并观察输出:docker build -t cifarcnn

Sending build context to Docker daemon 212.6MB
Step 1/4 : FROM golang:1.12
 ---> 9fe4cdc1f173
Step 2/4 : ADD main.go /main.go
 ---> Using cache
 ---> 5edf0df312f4
Step 3/4 : ADD cifar/ /cifar/
 ---> Using cache
 ---> 6928f37167a8
Step 4/4 : RUN export GOPATH=$HOME/go && cd / && go get -d -v .
 ---> Running in 7ff14ada5e7c
Fetching https://gorgonia.org/tensor?go-get=1
Parsing meta tags from https://gorgonia.org/tensor?go-get=1 (status code 200)
get "gorgonia.org/tensor": found meta tag get.metaImport{Prefix:"gorgonia.org/tensor", VCS:"git", RepoRoot:"https://github.com/gorgonia/tensor"} at https://gorgonia.org/tensor?go-get=1

...

Fetching https://gorgonia.org/dawson?go-get=1
Parsing meta tags from https://gorgonia.org/dawson?go-get=1 (status code 200)
get "gorgonia.org/dawson": found meta tag get.metaImport{Prefix:"gorgonia.org/dawson", VCS:"git", RepoRoot:"https://github.com/gorgonia/dawson"} at https://gorgonia.org/dawson?go-get=1
gorgonia.org/dawson (download)
Removing intermediate container 7ff14ada5e7c
 ---> 3def2cada165
Successfully built 3def2cada165
Successfully tagged cifar_cnn:latest

我们的容器现在已准备好被引用在 Pachyderm 数据管道规范中。

更新我们的 CNN 以保存模型

我们需要向我们的 CNN 示例中添加一个简单的函数,以确保生成的模型被保存,这样它就可以被 Pachyderm 作为对象管理。让我们将以下内容添加到 main.go 中:

func (m *convnet) savemodel() (err error) {
  learnables := m.learnables()
  var f io.WriteCloser
  if f, err = os.OpenFile("model.bin", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644); err != nil {
    return
  }
  defer f.Close()
  enc := json.NewEncoder(f)
  for _, l := range learnables {
    t := l.Value().(*tensor.Dense).Data() // []float32
    if err = enc.Encode(t); err != nil {
      return
    }
  }

  return nil
}

创建数据管道

现在我们需要在标准 JSON 中指定一个数据管道。在这里,我们将一个存储库映射到一个目录,并在训练或推断模式下执行我们的网络。

让我们来看看我们的 cifar_cnn.json 文件:

{
 "pipeline": {
    "name": "cifarcnn"
  },
  "transform": {
    "image": "cifarcnn:latest",
    "cmd": [
  "go run main.go"
    ]
  },
  "enable_stats": true,
  "parallelism_spec": {
    "constant": "1"
  },
  "input": {
    "pfs": {
      "repo": "data",
      "glob": "/"
    }
  }
}

我们在这里选择的选项很简单,您可以看到对 Docker 镜像、命令和开关的引用,以及 repo 和我们指定的挂载点。请注意 parallelism_spec 选项。将其设置为默认值 1 以上,允许我们根据需要扩展特定管道阶段;例如,在推断阶段。

现在我们可以从上述模板创建管道:

pachctl create pipeline -f cifar_cnn.json

如果没有错误,这将返回您到命令提示符。然后,您可以检查管道的状态:

pachctl list pipeline 
NAME INPUT CREATED STATE / LAST JOB
cifarcnn data:/ 8 seconds ago running / running

我们可以动态调整并行度的级别,并通过更新我们的模板将配置推送到我们的集群中:

 "parallelism_spec": {
 "constant": "5"
 },

然后,我们可以更新我们的集群并检查我们的作业和 k8s 集群的 pod 状态:

#pachctl update pipeline -f cifar_cnn.json
#pachctl list job 
ID PIPELINE STARTED DURATION RESTART PROGRESS DL UL STATE
9339d8d712d945d58322a5ac649d9239 cifarcnn 7 seconds ago - 0 0 + 0 / 1 0B 0B running

#kubectl get pods
NAME READY STATUS RESTARTS AGE
dash-5c54745d97-gs4j2 2/2 Running 2 29d
etcd-b4d789754-x675p 1/1 Running 1 35d
pachd-fbbd6855b-jcf6c 1/1 Running 1 35d
pipeline-cifarcnn-v1-bwfrq 2/2 Running 0 2m

等待一段时间运行(并使用 pachctl logs 检查进度),我们可以看到我们成功的作业:

#pachctl list job
ID OUTPUT COMMIT STARTED DURATION RESTART PROGRESS DL UL STATE
9339d8d712d945d58322a5ac649d9239 cifarcnn 2 minutes ago About a minute 0 1 + 0 / 1 4.444KiB 49.86KiB success

可互换的模型

Pachyderm 管道的灵活性使您可以通过简单的更新或推送我们先前使用的 JSON 管道来轻松地将一个模型替换为另一个模型。

指定在 JSON 中指定管道的意义是什么?它是为了使其可重复!管道每次更新其数据(在我们的案例中,是为了对标签类别进行新预测)时都会重新处理数据。

在这里,我们更新 cifa_cnn.json 中的 image 标志,以引用我们容器化的 CNN 的一个版本,这个版本由于某些原因不包含 dropout:

"image": "pachyderm/cifar_cnn_train:nodropout"

然后我们可以像这样在集群上更新管道:

pachctl update pipeline -f cifar_cnn.json --reprocesses

将预测映射到模型

Pachyderm 的一个重要特性——特别是对于企业用例——是能够对模型和预测进行版本控制。比如说,你在预测客户偿还贷款的可能性时,看到了一批奇怪的预测结果。在排查模型为何做出这些决策的问题时,如果你正在对大团队进行多模型训练,那么翻阅电子邮件和提交历史记录将是一个糟糕的主意!

因此,从推断开始到模型,只需运行以下命令:

#pachctl list job

然后您可以获取相关的提交哈希并将其提供给以下命令,观察输出的详细信息:

#pachctl inspect job 9339d8d712d945d58322a5ac649d9239
...
Input:
{
 "pfs": {
 "name": "data",
 "repo": "data",
 "branch": "master",
 "commit": "b22db05d23324ede839718bec5ff219c",
 "glob": "/"
 }
}
...

#pachctl inspect commit data@b22db05d23324ede839718bec5ff219c
Commit: data@b22db05d23324ede839718bec5ff219c
Original Branch: master
Started: 11 minutes ago
Finished: 11 minutes ago
Size: 202.8MiB

您可以看到用于生成此预测的模型的确切提交,预测的来源,以及用于训练模型的数据:

#pachctl list file data@adb293f8a4604ed7b081c1ff030c0480
COMMIT NAME TYPE COMMITTED SIZE
b22db05d23324ede839718bec5ff219c /data dir 11 minutes ago 202.8MiB

使用 Pachyderm 仪表板

从技术上讲,这是 Pachyderm 企业版的一个功能,但由于我们希望尽可能包容您的使用情况选择,无论您的用例如何,我们将简要介绍 仪表板 工具。即使您不需要一个简单的视觉概览您的管道和数据,也可以通过 14 天的试用来探索其功能集。

启动 http://localhost:30800。您将看到一个基本的屏幕,其中包括以下内容:

  • 仓库(保存我们的 CIFAR-10 数据)

  • 管道

  • 作业或日志

  • 设置

让我们来看下面的截图:

正如你可能记得的那样,Pachyderm 希望你将你的数据仓库视为 Git 仓库。当你深入到下一个屏幕时,这一点显而易见:

仪表板为我们到目前为止一直在使用的 pachctl 工具提供了一个熟悉的 GUI 界面。

总结

在本章中,我们进行了实际操作,并了解了如何以可维护和可追踪的方式开始增强模型输入或输出组件的过程,以及可以使用哪些工具完成这些操作。从高层次来看,我们了解了数据管道的概念及其重要性,如何在 Pachyderm 中构建/部署/维护管道,以及用于可视化我们的仓库和管道的工具。

在下一章中,我们将深入探讨 Pachyderm 下面的一些技术,包括 Docker 和 Kubernetes,以及如何使用这些工具部署堆栈到云基础设施。

第十章:扩展部署

现在我们已经介绍了一个管理数据流水线的工具,现在是完全深入了解的时候了。我们的模型最终在软件的多层抽象下运行在我们在第五章中讨论过的硬件上,直到我们可以使用像go build --tags=cuda这样的代码为止。

我们在 Pachyderm 上构建的图像识别流水线部署是本地的。我们以一种与部署到云资源相同的方式进行了部署,而不深入探讨其具体外观。现在我们将专注于这一细节。

通过本章末尾,您应能够做到以下几点:

  • 识别和理解云资源,包括我们平台示例中的特定资源(AWS)。

  • 知道如何将您的本地部署迁移到云上

  • 理解 Docker 和 Kubernetes 以及它们的工作原理。

  • 理解计算与成本之间的权衡。

在云中迷失(和找到)

拥有一台配备 GPU 和 Ubuntu 系统的强大台式机非常适合原型设计和研究,但是当你需要将模型投入生产并实际进行每日预测时,你需要高可用性和可扩展性的计算资源。这究竟意味着什么?

想象一下,你已经拿我们的卷积神经网络CNN)例子,调整了模型并用自己的数据进行了训练,并创建了一个简单的 REST API 前端来调用模型。你想要围绕提供客户服务的一个小业务,客户支付一些费用,获得一个 API 密钥,可以提交图像到一个端点并获得一个回复,说明图像包含什么内容。作为服务的图像识别!听起来不错吧?

我们如何确保我们的服务始终可用和快速?毕竟,人们付费给你,即使是小的停机或可靠性下降也可能导致您失去客户。传统上的解决方案是购买一堆昂贵的服务器级硬件,通常是带有多个电源和网络接口的机架式服务器,以确保在硬件故障的情况下服务的连续性。您需要检查每个级别的冗余选项,从磁盘或存储到网络,甚至到互联网连接。

据说你需要两个一模一样的东西,这一切都需要巨大甚至是禁止性的成本。如果你是一家大型、有资金支持的初创公司,你有很多选择,但当然,随着资金曲线的下降,你的选择也会减少。自助托管变成了托管托管(并非总是如此,但对于大多数小型或初创用例而言是如此),这反过来又变成了存储在别人数据中心中的计算的标准化层,以至于你根本不需要关心底层硬件或基础设施。

当然,在现实中,并非总是如此。像 AWS 这样的云提供商将大部分乏味、痛苦(但必要)的工作,如硬件更换和常规维护,从中排除了。你不会丢失硬盘或陷入故障的网络电缆,如果你决定(嘿,一切都运作良好),为每天 10 万客户提供服务,那么你只需推送一个简单的基础架构规格变更。不需要给托管提供商打电话,协商停机时间,或去计算机硬件店。

这是一个非常强大的想法;你解决方案的实际执行——硅和装置的混合物,用于做出预测——几乎可以被视为一种事后的考虑,至少与几年前相比是如此。一般来说,维护云基础设施所需的技能集或方法被称为DevOps。这意味着一个人同时参与两个(或更多!)阵营。他们了解这些 AWS 资源代表什么(服务器、交换机和负载均衡器),以及如何编写必要的代码来指定和管理它们。

一个不断发展的角色是机器学习工程师。这是传统的 DevOps 技能集,但随着更多的Ops方面被自动化或抽象化,个人也可以专注于模型训练或部署,甚至扩展。让工程师参与整个堆栈是有益的。理解一个模型可并行化的程度,一个特定模型可能具有的内存需求,以及如何构建必要的分布式基础设施以进行大规模推理,所有这些都导致了一个模型服务基础设施,在这里各种设计元素不是领域专业化的产物,而是一个整体的集成。

构建部署模板

现在我们将组合所需的各种模板,以便在规模上部署和训练我们的模型。这些模板包括:

  • AWS 云形成模板:虚拟实例及相关资源

  • Kubernetes 或 KOPS 配置:K8s 集群管理

  • Docker 模板或 Makefile:创建图像以部署在我们的 K8s 集群上

我们在这里选择了一条特定的路径。AWS 有诸如弹性容器服务ECS)和弹性 Kubernetes 服务EKS)等服务,通过简单的 API 调用即可访问。我们在这里的目的是深入了解细节,以便您可以明智地选择如何扩展部署您自己的用例。目前,您可以更精细地控制容器选项,以及在将容器部署到纯净的 EC2 实例时如何调用您的模型。这些服务在成本和性能方面的权衡将在稍后的成本和性能折衷部分中进行讨论。

高级步骤

我们的迷你 CI/CD 流水线包括以下任务:

  1. 创建或推送训练或推理 Docker 镜像到 AWS ECS。

  2. 在 EC2 实例上创建或部署一个带有 Kubernetes 集群的 AWS 堆栈,以便我们可以执行下一步操作。

  3. 训练一个模型或进行一些预测!

现在我们将依次详细介绍每个步骤的细节。

创建或推送 Docker 镜像

Docker 肯定是一个引起了很多炒作的工具。除了人类时尚之外,其主要原因在于 Docker 简化了诸如依赖管理和模型集成等问题,使得可重复使用、广泛部署的构建成为可能。我们可以预先定义从操作系统获取的所需内容,并在我们知道依赖项是最新的时候将它们全部打包起来,这样我们所有的调整和故障排除工作都不会徒劳无功。

要创建我们的镜像并将其送到我们想要去的地方,我们需要两样东西:

  • Dockerfile:这定义了我们的镜像、Linux 的版本、要运行的命令以及在启动容器时运行的默认命令。

  • Makefile:这将创建镜像并将其推送到 AWS ECS。

让我们先看一下 Dockerfile:

FROM ubuntu:16.04

ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y --no-install-recommends \
 curl \
 git \
 pkg-config \
 rsync \
 awscli \
 wget \
 && \
 apt-get clean && \
 rm -rf /var/lib/apt/lists/*

RUN wget -nv https://storage.googleapis.com/golang/go1.12.1.linux-amd64.tar.gz && \
 tar -C /usr/local -xzf go1.12.1.linux-amd64.tar.gz

ENV GOPATH /home/ubuntu/go

ENV GOROOT /usr/local/go

ENV PATH $PATH:$GOROOT/bin

RUN /usr/local/go/bin/go version && \
 echo $GOPATH && \
 echo $GOROOT

RUN git clone https://github.com/PacktPublishing/Hands-On-Deep-Learning-with-Go

RUN go get -v gorgonia.org/gorgonia && \
 go get -v gorgonia.org/tensor && \
 go get -v gorgonia.org/dawson && \
 go get -v github.com/gogo/protobuf/gogoproto && \
 go get -v github.com/golang/protobuf/proto && \
 go get -v github.com/google/flatbuffers/go && \
 go get -v .

WORKDIR /

ADD staging/ /app

WORKDIR /app

CMD ["/bin/sh", "model_wrapper.sh"]

我们可以通过查看每行开头的大写声明来推断一般方法:

  1. 选择基础 OS 镜像使用FROM

  2. 使用ARG设置启动。

  3. 使用RUN运行一系列命令,使我们的 Docker 镜像达到期望的状态。然后将一个staging数据目录添加到/app

  4. 切换到新的WORKDIR

  5. 执行CMD命令,我们的容器将运行。

现在我们需要一个 Makefile。这个文件包含了将构建我们刚刚在 Dockerfile 中定义的镜像并将其推送到亚马逊容器托管服务 ECS 的命令。

这是我们的 Makefile:

cpu-image:
 mkdir -p staging/
 cp model_wrapper.sh staging/
 docker build --no-cache -t "ACCOUNTID.dkr.ecr.ap-southeast-2.amazonaws.com/$(MODEL_CONTAINER):$(VERSION_TAG)" .
 rm -rf staging/

cpu-push: cpu-image
 docker push "ACCOUNTID.dkr.ecr.ap-southeast-2.amazonaws.com/$(MODEL_CONTAINER):$(VERSION_TAG)"

与我们已经涵盖过的其他示例一样,我们正在使用sp-southeast-2地区;但是,您可以自由指定您自己的地区。您还需要包含您自己的 12 位 AWS 帐户 ID。

从这个目录(时间未到时,请耐心等待!)我们现在可以创建和推送 Docker 镜像。

准备您的 AWS 账户

您将看到有关 API 访问 AWS 的通知,以便 KOPS 管理您的 EC2 和相关计算资源。与此 API 密钥相关联的帐户还需要以下 IAM 权限:

  • AmazonEC2FullAccess

  • AmazonRoute53FullAccess

  • AmazonS3FullAccess

  • AmazonVPCFullAccess

您可以通过进入 AWS 控制台并按照以下步骤操作来启用程序化或 API 访问:

  1. 点击 IAM

  2. 从左侧菜单中选择“用户”,然后选择您的用户

  3. 选择“安全凭证”。然后,您将看到“访问密钥”部分

  4. 点击“创建访问密钥”,然后按照说明操作

结果产生的密钥和密钥 ID 将被用于您的 ~/.aws/credentials 文件或作为 shell 变量导出,以供 KOPS 和相关部署和集群管理工具使用。

创建或部署 Kubernetes 集群

我们的 Docker 镜像必须运行在某个地方,为什么不是一组 Kubernetes pod 呢?这正是分布式云计算的魔力所在。使用中央数据源,例如 AWS S3,在我们的情况下,会为训练或推理启动许多微实例,最大化 AWS 资源利用率,为您节省资金,并为企业级机器学习应用提供所需的稳定性和性能。

首先,进入存放这些章节的仓库中的 /k8s/ 目录。

我们将开始创建部署集群所需的模板。在我们的案例中,我们将使用 kubectl 的前端,默认的 Kubernetes 命令,它与主 API 进行交互。

Kubernetes

让我们来看看我们的 k8s_cluster.yaml 文件:

apiVersion: kops/v1alpha2
kind: Cluster
metadata:
  creationTimestamp: 2018-05-01T12:11:24Z
  name: $NAME
spec:
  api:
    loadBalancer:
      type: Public
  authorization:
    rbac: {}
  channel: stable
  cloudProvider: aws
  configBase: $KOPS_STATE_STORE/$NAME
  etcdClusters:
  - etcdMembers:
    - instanceGroup: master-$ZONE
      name: b
    name: main
  - etcdMembers:
    - instanceGroup: master-$ZONE
      name: b
    name: events
  iam:
    allowContainerRegistry: true
    legacy: false
  kubernetesApiAccess:
  - 0.0.0.0/0
  kubernetesVersion: 1.9.3
  masterInternalName: api.internal.$NAME
  masterPublicName: api.hodlgo.$NAME
  networkCIDR: 172.20.0.0/16
  networking:
    kubenet: {}
  nonMasqueradeCIDR: 100.64.0.0/10
  sshAccess:
  - 0.0.0.0/0
  subnets:
  - cidr: 172.20.32.0/19
    name: $ZONE
    type: Public
    zone: $ZONE
  topology:
    dns:
      type: Public
    masters: public
    nodes: public

让我们来看看我们的 k8s_master.yaml 文件:

apiVersion: kops/v1alpha2
kind: InstanceGroup
metadata:
  creationTimestamp: 2018-05-01T12:11:25Z
  labels:
    kops.k8s.io/cluster: $NAME
  name: master-$ZONE
spec:
  image: kope.io/k8s-1.8-debian-jessie-amd64-hvm-ebs-2018-02-08
  machineType: $MASTERTYPE
  maxSize: 1
  minSize: 1
  nodeLabels:
    kops.k8s.io/instancegroup: master-$ZONE
  role: Master
  subnets:
  - $ZONE

让我们来看看我们的 k8s_nodes.yaml 文件:

apiVersion: kops/v1alpha2
kind: InstanceGroup
metadata:
  creationTimestamp: 2018-05-01T12:11:25Z
  labels:
    kops.k8s.io/cluster: $NAME
  name: nodes-$ZONE
spec:
  image: kope.io/k8s-1.8-debian-jessie-amd64-hvm-ebs-2018-02-08
  machineType: $SLAVETYPE
  maxSize: $SLAVES
  minSize: $SLAVES
  nodeLabels:
    kops.k8s.io/instancegroup: nodes-$ZONE
  role: Node
  subnets:
  - $ZONE

这些模板将被输入到 Kubernetes 中,以便启动我们的集群。我们将用来部署集群和相关 AWS 资源的工具是 KOPS。在撰写本文时,该工具的当前版本为 1.12.1,并且所有部署均已使用此版本进行测试;较早版本可能存在兼容性问题。

首先,我们需要安装 KOPS。与我们之前的所有示例一样,这些步骤也适用于 macOS。我们使用 Homebrew 工具来管理依赖项,并保持安装的局部化和合理性:

#brew install kops
==> Installing dependencies for kops: kubernetes-cli
==> Installing kops dependency: kubernetes-cli
==> Downloading https://homebrew.bintray.com/bottles/kubernetes-cli-1.14.2.mojave.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/85/858eadf77396e1acd13ddcd2dd0309a5eb0b51d15da275b491
######################################################################## 100.0%
==> Pouring kubernetes-cli-1.14.2.mojave.bottle.tar.gz
==> Installing kops
==> Downloading https://homebrew.bintray.com/bottles/kops-1.12.1.mojave.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/86/862c5f6648646840c75172e2f9f701cb590b04df03c38716b5
######################################################################## 100.0%
==> Pouring kops-1.12.1.mojave.bottle.tar.gz
==> Caveats
Bash completion has been installed to:
 /usr/local/etc/bash_completion.d

zsh completions have been installed to:
 /usr/local/share/zsh/site-functions
==> Summary
 /usr/local/Cellar/kops/1.12.1: 5 files, 139.2MB
==> Caveats
==> kubernetes-cli
Bash completion has been installed to:
 /usr/local/etc/bash_completion.d

zsh completions have been installed to:
 /usr/local/share/zsh/site-functions
==> kops
Bash completion has been installed to:
 /usr/local/etc/bash_completion.d

zsh completions have been installed to:
 /usr/local/share/zsh/site-functions

我们可以看到 KOPS 已经安装,连同 kubectl 一起,这是与 API 直接交互的默认 K8s 集群管理工具。请注意,Homebrew 经常会输出关于命令完成的警告类型消息,可以安全地忽略这些消息;但是,如果出现关于符号链接配置的错误,请按照说明解决与任何现有的 kubectl 本地安装冲突的问题。

集群管理脚本

我们还需要编写一些脚本,以允许我们设置环境变量并根据需要启动或关闭 Kubernetes 集群。在这里,我们将整合我们编写的模板,KOPS 或 kubectl,以及我们在前几节中完成的 AWS 配置。

让我们来看看我们的 vars.sh 文件:

#!/bin/bash

# AWS vars
export BUCKET_NAME="hodlgo-models"
export MASTERTYPE="m3.medium"
export SLAVETYPE="t2.medium"
export SLAVES="2"
export ZONE="ap-southeast-2b"

# K8s vars
export NAME="hodlgo.k8s.local"
export KOPS_STATE_STORE="s3://hodlgo-cluster"
export PROJECT="hodlgo"
export CLUSTER_NAME=$PROJECT

# Docker vars
export VERSION_TAG="0.1"
export MODEL_CONTAINER="hodlgo-model"

我们可以看到这里的主要变量是容器名称、K8s 集群详细信息以及我们想要启动的 AWS 资源种类的一堆规格(及其所在的区域)。您需要用您自己的值替换这些值。

现在,我们可以编写相应的脚本,在完成部署或管理 K8s 集群后,清理我们的 shell 中的变量是一个重要部分。

让我们看看我们的 unsetvars.sh 文件:

#!/bin/bash

# Unset them vars

unset BUCKET_NAME
unset MASTERTYPE
unset SLAVETYPE
unset SLAVES
unset ZONE

unset NAME
unset KOPS_STATE_STORE

unset PROJECT
unset CLUSTER_NAME

unset VERSION_TAG
unset MODEL_CONTAINER

现在,启动我们的集群脚本将使用这些变量来确定集群的命名方式、节点数量以及部署位置。您将看到我们在一行中使用了一个小技巧来将环境变量传递到我们的 Kubernetes 模板或 KOPS 中;在未来的版本中,这可能不再需要,但目前这是一个可行的解决方法。

让我们看看我们的 cluster-up.sh 文件:

#!/bin/bash

## Bring up the cluster with kops

set -e

echo "Bringing up Kubernetes cluster"
echo "Using Cluster Name: ${CLUSTER_NAME}"
echo "Number of Nodes: ${SLAVES}"
echo "Using Zone: ${ZONE}"
echo "Bucket name: ${BUCKET_NAME}"

export PARALLELISM="$((4 * ${SLAVES}))"

# Includes ugly workaround because kops is unable to take stdin as input to create -f, unlike kubectl
cat k8s_cluster.yaml | envsubst > k8s_cluster-edit.yaml && kops create -f k8s_cluster-edit.yaml
cat k8s_master.yaml | envsubst > k8s_master-edit.yaml && kops create -f k8s_master-edit.yaml
cat k8s_nodes.yaml | envsubst > k8s_nodes-edit.yaml && kops create -f k8s_nodes-edit.yaml

kops create secret --name $NAME sshpublickey admin -i ~/.ssh/id_rsa.pub
kops update cluster $NAME --yes

echo ""
echo "Cluster $NAME created!"
echo ""

# Cleanup from workaround
rm k8s_cluster-edit.yaml
rm k8s_master-edit.yaml
rm k8s_nodes-edit.yaml

相应的 down 脚本将关闭我们的集群,并确保相应清理 AWS 资源。

让我们看看我们的 cluster-down.sh 文件:

#!/bin/bash

## Kill the cluster with kops

set -e

echo "Deleting cluster $NAME"
kops delete cluster $NAME --yes

构建和推送 Docker 容器

现在我们已经做好了准备,准备好了所有模板和脚本,我们可以继续实际制作 Docker 镜像,并将其推送到 ECR,以便进行完整集群部署之前使用。

首先,我们导出了本章前面生成的 AWS 凭证:

export AWS_DEFAULT_REGION=ap-southeast-2
export AWS_ACCESS_KEY_ID="<your key here>"
export AWS_SECRET_ACCESS_KEY="<your secret here>"

然后,我们获取容器存储库登录信息。这是必要的,以便我们可以推送创建的 Docker 镜像到 ECR,然后由我们的 Kubernetes 节点在模型训练或推理时拉取。请注意,此步骤假定您已安装 AWS CLI:

aws ecr get-login --no-include-email

此命令的输出应类似于以下内容:

docker login -u AWS -p xxxxx https://ACCOUNTID.dkr.ecr.ap-southeast-2.amazonaws.com

然后,我们可以执行 make cifarcnn-imagemake cifarcnn-push。这将构建 Dockerfile 中指定的 Docker 镜像,并将其推送到 AWS 的容器存储服务中。

在 K8s 集群上运行模型

您现在可以编辑我们之前创建的 vars.sh 文件,并使用您喜爱的命令行文本编辑器设置适当的值。您还需要创建一个存储 k8s 集群信息的存储桶。

完成这些步骤后,您可以启动您的 Kubernetes 集群:

source vars.sh
./cluster-up.sh

现在,KOPS 正通过 kubectl 与 Kubernetes 进行交互,以启动将运行您的集群的 AWS 资源,并在这些资源上配置 K8s 本身。在继续之前,您需要验证您的集群是否已成功启动:

kops validate cluster
Validating cluster hodlgo.k8s.local

INSTANCE GROUPS
NAME ROLE MACHINETYPE MIN MAX SUBNETS
master-ap-southeast-2a Master c4.large 1 1 ap-southeast-2
nodes Node t2.medium 2 2 ap-southeast-2

NODE STATUS
NAME ROLE READY
ip-172-20-35-114.ec2.internal node True
ip-172-20-49-22.ec2.internal master True
ip-172-20-64-133.ec2.internal node True

一旦所有 K8s 主节点返回 Ready,您就可以开始在整个集群节点上部署您的模型!

执行此操作的脚本很简单,并调用 kubectl 以与我们的 cluster_up.sh 脚本相同的方式应用模板。

让我们看看我们的 deploy-model.sh 文件:

#!/bin/bash

# envsubst doesn't exist for OSX. needs to be brew-installed
# via gettext. Should probably warn the user about that.
command -v envsubst >/dev/null 2>&1 || {
  echo >&2 "envsubst is required and not found. Aborting"
  if [[ "$OSTYPE" == "darwin"* ]]; then
    echo >&2 "------------------------------------------------"
    echo >&2 "If you're on OSX, you can install with brew via:"
    echo >&2 " brew install gettext"
    echo >&2 " brew link --force gettext"
  fi
  exit 1;
}

cat ${SCRIPT_DIR}/model.yaml | envsubst | kubectl apply -f -

概要

现在,我们来详细介绍 Kubernetes、Docker 和 AWS 的底层细节,以及如何根据您的钱包能力将尽可能多的资源投入到模型中。接下来,您可以采取一些步骤,定制这些示例以适应您的用例,或者进一步提升您的知识水平:

  • 将这种方法集成到您的 CI 或 CD 工具中(如 Bamboo、CircleCI、Puppet 等)

  • 将 Pachyderm 集成到您的 Docker、Kubernetes 或 AWS 解决方案中

  • 使用参数服务器进行实验,例如分布式梯度下降,进一步优化您的模型流水线