Google Vertex AI 权威指南(二)
原文:
annas-archive.org/md5/8868875aad4bcd1618fe77db5ab7a304译者:飞龙
第七章:使用 Vertex AI 训练完全自定义的 ML 模型
在前面的章节中,我们学习了如何使用最小技术专长训练无代码(Auto-ML)以及低代码(BQML)机器学习(ML)模型。当解决常见的机器学习问题时,这些解决方案非常实用。然而,有时问题或数据本身非常复杂,需要开发自定义的人工智能(AI)模型,在大多数情况下是大型基于深度学习的模型。在自定义模型上工作需要机器学习、深度学习和人工智能领域的显著技术专长。有时,即使拥有这种专长,由于资源、计算能力和适当的元数据跟踪机制不足,管理大规模自定义深度学习模型的训练和实验也变得非常困难。
为了使机器学习开发者的生活更轻松,Vertex AI 提供了一个托管环境,用于启动大规模的自定义训练作业。Vertex AI 管理的作业让我们能够跟踪有用的元数据,通过 Google Cloud 控制台 UI 监控作业,并且无需主动监控即可启动大规模批量推理作业。在本章中,我们将学习如何在 Google Vertex AI 上使用自定义深度学习模型。具体来说,我们将涵盖以下主题:
-
使用 TensorFlow 构建基本的深度学习模型
-
将模型打包以提交给 Vertex AI 进行训练作业
-
监控模型训练进度
-
评估训练后的模型
技术要求
本章需要具备深度学习框架 TensorFlow 和神经网络的入门级知识。代码工件可以在以下 GitHub 仓库中找到 - github.com/PacktPublishing/The-Definitive-Guide-to-Google-Vertex-AI/tree/main/Chapter07
使用 TensorFlow 构建基本的深度学习模型
TensorFlow,简称TF,是一个用于构建机器学习模型的端到端平台。TensorFlow 框架的主要重点是简化深度神经网络的开发、训练、评估和部署。当涉及到处理非结构化数据(例如图像、视频、音频等)时,基于神经网络的解决方案比主要依赖于手工特征的传统的机器学习方法取得了显著更好的结果。深度神经网络擅长从高维数据点中理解复杂模式(例如,包含数百万像素的图像)。在本节中,我们将使用 TensorFlow 开发一个基本的基于神经网络的模型。在接下来的几节中,我们将看到 Vertex AI 如何帮助设置可扩展和系统化的自定义模型训练/调优。
重要提示
需要注意的是,TensorFlow 并不是 Vertex AI 支持的唯一机器学习框架。Vertex AI 支持许多不同的机器学习框架和开源项目,包括 Pytorch、Spark 和 XGBoost。Pytorch 是增长最快的机器学习框架之一,借助 Vertex AI 的 Pytorch 集成,我们可以轻松地在生产环境中训练、部署和编排 PyTorch 模型。Vertex AI 提供了预构建的训练和托管容器,并支持 PyTorch 模型的优化分布式训练。同样,Vertex AI 为包括 XGBoost、TensorFlow、Pytorch 和 Scikit-learn 在内的多个机器学习框架提供了预构建的训练、托管和可解释性功能。
实验 – 将黑白图像转换为彩色图像
在这个实验中,我们将开发一个基于 TensorFlow 的深度学习模型,该模型以黑白图像为输入,并将它们转换为彩色图像。由于这个练习需要开发一个自定义模型,我们将从 Jupyter Notebook 中的初始开发工作开始。第一步是在 Vertex AI Workbench 中使用预配置的 TensorFlow 图像创建一个用户管理的 Jupyter Notebook。有关如何成功创建 Vertex AI Workbench 笔记本实例的更多详细信息,请参阅第四章,Vertex AI Workbench。接下来,让我们从 JupyterLab 应用程序启动一个 Jupyter Notebook。我们现在已经准备好开始我们的实验了。
我们将在笔记本的第一个单元格中开始导入有用的库(预构建的 Python 包)。在这个实验中,我们将使用以下 Python 库 – numpy用于多维数组操作,TensorFlow 用于开发深度学习模型,OpenCV(或cv2)用于图像处理,以及matplotlib用于绘制图像或图表:
import numpy as np
import tensorflow
import glob
import cv2
import matplotlib.pyplot as plt
%matplotlib inline
我们需要一个包含至少几千张图像的图像数据集来训练和测试我们的模型。在这个实验中,我们将使用Oxford-IIIT Pet数据集,这是一个公开且免费使用的数据集。该数据集包含来自 30 多个不同标注类别的约 7k 张宠物图像。数据集可以从以下网站下载:www.robots.ox.ac.uk/~vgg/data/pets/。
我们也可以使用以下终端命令下载这个数据集:
wget https://thor.robots.ox.ac.uk/~vgg/data/pets/images.tar.gz
wget https://thor.robots.ox.ac.uk/~vgg/data/pets/annotations.tar.gz
下载完成后,将压缩文件放在与我们的笔记本相同的目录中。现在,让我们在笔记本单元格中创建一个!符号,这样我们就可以在 Jupyter Notebook 中运行终端命令了):
!mkdir data
!tar -xf images.tar.gz -C data
!mkdir labels
!tar -xf annotations.tar.gz -C labels
由于我们当前的实验是关于将黑白图像转换为彩色版本,我们将不会使用标注。现在,让我们快速在一个新单元格中验证是否已成功将所有图像复制到data文件夹中:
all_image_files = glob.glob("data/images/*.jpg")
print("Total number of image files : ", \
len(all_image_files))
在这里,glob模块通过列出数据目录内所有的.jpg图像路径来帮助我们。这个列表的长度将与图像数量相等。前面的代码应该打印出以下输出:
Total number of image files : 7390
现在我们已经成功下载并提取了数据,让我们检查一些图像以确保一切正常。下面的代码块将绘制一些带有注释的随机图像:
for i in range(3):
plt.figure(figsize=(13, 13))
for j in range(6):
img_path = np.random.choice(all_image_files)
img = cv2.imread(img_path)
img_class = img_path.split("/")[-1].split("_")[0]
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
plt.subplot(660 + 1 + j)
plt.imshow(img)
plt.axis('off')
plt.title(img_class)
plt.show()
在这里,我们通过从图像路径名称本身提取图像类别(或注释),因为所有图像的文件名中都有宠物类别。前面代码的输出应该类似于图 7.1中所示。
图 7.1 – 宠物数据集的一些样本
现在我们已经验证了我们的数据集,让我们将这些图像分成三个集合——训练集、验证集和测试集——就像我们通常为训练/验证和测试机器学习模型所做的那样。我们将保留 60%的图像用于训练,20%用于验证,剩下的 20%用于测试。完成这些分割的一个简单方法如下:
train_files = all_image_files[:int( \
len(all_image_files)*0.6)]
validation_files = all_image_files[int( \
len(all_image_files)*0.6):int(len(all_image_files)*0.8)]
test_files = all_image_files[int( \
len(all_image_files)*0.8):]
print(len(train_files), len(validation_files), \
len(test_files))
本实验的主要重点是开发一个深度学习模型,该模型可以将黑白图像转换为彩色图像。为了学习这种映射,模型将需要黑白图像及其对应的彩色版本对来学习这种映射。我们的数据集已经包含了彩色图像。我们将利用 OpenCV 库将它们转换为灰度(黑白)图像,并将它们作为模型输入。我们将比较输出与它们的彩色版本。需要注意的是,我们的深度学习模型将接受固定大小的图像作为输入,因此我们还需要将所有输入图像调整到相同的分辨率。在我们的实验中,我们将所有图像的分辨率更改为 80x80。我们已经有图像路径的训练、验证和测试分割。我们现在可以读取这些文件,为训练、验证和测试准备数据。下面的代码块可以用来准备之前描述的数据集。
让我们先定义空列表来分别存储训练、验证和测试数据:
train_x = []
train_y = []
val_x = []
val_y = []
test_x = []
test_y = []
在这里,我们读取训练图像,将它们调整到所需的大小,为每个图像创建一个黑白版本,并将它们存储为目标图像:
for file in train_files:
try:
img = cv2.imread(file)
img = cv2.resize(img, (80,80))
color_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
black_n_white_img = cv2.cvtColor(color_img, \
cv2.COLOR_RGB2GRAY)
except:
continue
train_x.append((black_n_white_img-127.5)/127.5)
train_y.append((color_img-127.5)/127.5)
同样,我们对验证文件重复相同的流程:
for file in validation_files:
try:
img = cv2.imread(file)
img = cv2.resize(img, (80,80))
color_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
black_n_white_img = cv2.cvtColor(color_img, \
cv2.COLOR_RGB2GRAY)
except:
continue
val_x.append((black_n_white_img-127.5)/127.5)
val_y.append((color_img-127.5)/127.5)
现在,以类似的方式准备测试文件:
for file in test_files:
try:
img = cv2.imread(file)
img = cv2.resize(img, (80,80))
color_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
black_n_white_img = cv2.cvtColor(color_img, \
cv2.COLOR_RGB2GRAY)
except:
continue
test_x.append((black_n_white_img-127.5)/127.5)
test_y.append((color_img-127.5)/127.5)
注意,图像是用 0 到 255 之间的像素值表示的。我们通过减去并除以 127.5 来归一化像素值,并将它们带入[-1, 1]的范围内。数据归一化使得深度学习模型的优化更加平滑和稳定。
现在我们已经成功地将数据集准备好,包括训练集、验证集和测试集的分割,让我们检查一些样本以确认数据已经被正确准备。下面的代码块选择了一些随机的训练集图像并将它们绘制出来。
让我们绘制一些输入图像,以了解数据:
# input images
indexes = np.random.choice(range(0,4000), size=3)
print("Input Samples (black and white): ")
plt.figure(figsize=(7,7))
for i in range(3):
plt.subplot(330+1+i)
plt.imshow((train_x[indexes[i]]+1.0)/2.0, cmap='gray')
plt.axis('off')
plt.show()
我们还将绘制这些随机选择的图像的输出版本(彩色版本):
# corresponding output images
print("Output Samples (colored): ")
plt.figure(figsize=(7,7))
for i in range(3):
plt.subplot(330+1+i)
plt.imshow((train_y[indexes[i]]+1.0)/2.0)
plt.axis('off')
plt.show()
如果一切正确,我们应该看到输入-输出对图像与图 7**.2中的非常相似。
图 7.2 – 数据验证的样本输入-输出对
由于我们在这里处理图像数据,我们将使用基于卷积神经网络(CNN)的模型,以便我们可以从图像数据中提取有用的特征。当前的研究表明,CNN 在从图像数据中提取特征和其他有用信息方面非常有用。由于我们将在这里使用 CNN,我们需要将我们的图像数据集转换为 NumPy 数组,并且还需要为每个黑白输入图像添加一个通道维度(CNN 接受图像输入为一个三维数组,每个维度分别对应宽度、高度和通道)。彩色图像已经具有三个通道,每个通道对应一个颜色值 – R、G 和 B。以下代码块根据之前描述的步骤准备我们的最终数据集:
train_x = np.expand_dims(np.array(train_x),-1)
val_x = np.expand_dims(np.array(val_x),-1)
test_x = np.expand_dims(np.array(test_x),-1)
train_y = np.array(train_y)
val_y = np.array(val_y)
test_y = np.array(test_y)
现在,再次检查我们数据集分割的维度:
print(train_x.shape, train_y.shape, val_x.shape, \
val_y.shape, test_x.shape, test_y.shape)
应该打印出类似以下内容:
(4430, 80, 80, 1) (4430, 80, 80, 3) (1478, 80, 80, 1) (1478, 80, 80, 3) (1476, 80, 80, 1) (1476, 80, 80, 3)
从数据角度来看,一切看起来都很不错。让我们跳转到定义我们的神经网络架构。
在这个实验中,我们将定义一个基于 TensorFlow 的 CNN,它以黑白图像为输入,并预测它们的彩色变体作为输出。模型架构可以大致分为两部分 – 编码器和解码器。模型的编码器部分以黑白图像为输入,并通过四个下采样卷积层从中提取有用的特征。每个卷积层后面跟着 LeakyReLU 激活层和批归一化层,除了最后一层,它用dropout层代替了批归一化层。通过编码器模型后,一个尺寸为(80, 80, 1)的输入图像变成了一个尺寸为(5, 5, 256)的特征向量。
模型的第二部分被称为解码器。解码器部分从编码器的输出中获取特征向量,并将其转换回对应输入图像的彩色版本。解码器由四个转置卷积或上采样层组成。每个解码器层后面跟着 ReLU 激活层和批归一化层,除了最后一层,它有 tanh 激活而没有归一化层。tanh 激活将最终输出向量的值限制在[-1,1]的范围内,这是我们输出图像所期望的。
以下代码块定义了 TensorFlow 模型:
def tf_model():
black_n_white_input = tensorflow.keras.layers.Input( \
shape=(80, 80, 1))
enc = black_n_white_input
编码器部分从这里开始,在同一个函数内:
#Encoder part
enc = tensorflow.keras.layers.Conv2D(32, kernel_size=3, \
strides=2, padding='same')(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
enc = tensorflow.keras.layers.Conv2D(64, kernel_size=3, \
strides=2, padding='same')(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
enc = tensorflow.keras.layers.Conv2D(128, \
kernel_size=3, strides=2, padding='same')(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
enc = tensorflow.keras.layers.Conv2D(256, \
kernel_size=1, strides=2, padding='same')(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.Dropout(0.5)(enc)
解码器部分从这里开始,在同一个函数内:
#Decoder part
dec = enc
dec = tensorflow.keras.layers.Conv2DTranspose(256, \
kernel_size=3, strides=2, padding='same')(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2DTranspose(128, \
kernel_size=3, strides=2, padding='same')(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2DTranspose(64, \
kernel_size=3, strides=2, padding='same')(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2DTranspose(32, \
kernel_size=3, strides=2, padding='same')(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2D(3, kernel_size=3,\
padding='same')(dec)
最后,添加 tanh 激活函数以获得所需输出的彩色图像:
color_image = tensorflow.keras.layers.Activation('tanh')(dec)
return black_n_white_input, color_image
现在,让我们创建一个 TensorFlow 模型对象并打印我们模型的摘要:
black_n_white_input, color_image = tf_model()
model = tensorflow.keras.models.Model( \
inputs=black_n_white_input, outputs=color_image)
model.summary()
这应该会打印出如图图 7.3所示的模型摘要。
图 7.3 – TensorFlow 模型摘要(在 GitHub 上查看完整摘要)
从摘要中我们可以看出,我们的模型大约有 110 万个可训练参数。下一步是编译 TensorFlow 模型:
_optimizer = tensorflow.keras.optimizers.Adam(\
learning_rate=0.0002, beta_1=0.5)
model.compile(loss='mse', optimizer=_optimizer)
我们使用学习率为 0.0002 的 Adam 优化器以及值为 0.5 的beta_1参数。在这里,beta_1代表第一矩估计的指数衰减率,学习率告诉优化器在训练期间更新模型参数值的速率。其余的参数值保持默认。我们的想法是传递一个黑白图像并重建其彩色版本,因此我们将使用均方误差(MSE)作为像素级别的重建损失函数。
现在我们已经准备好开始训练了。我们将在这个实验中使用 128 个批次的尺寸,训练模型大约 100 个周期,并检查结果。以下代码片段开始训练:
history = model.fit(
train_x,
train_y,
batch_size=128,
epochs=100,
validation_data=(val_x, val_y),
)
输出日志应类似于以下内容:
Epoch 1/100
35/35 [==============================] - 25s 659ms/step - loss: 0.2940 - val_loss: 0.1192
Epoch 2/100
35/35 [==============================] - 20s 585ms/step - loss: 0.1117 - val_loss: 0.0917
Epoch 3/100
35/35 [==============================] - 20s 580ms/step - loss: 0.0929 - val_loss: 0.0784
Epoch 4/100
35/35 [==============================] - 20s 577ms/step - loss: 0.0832 - val_loss: 0.0739
Epoch 5/100
35/35 [==============================] - 20s 573ms/step - loss: 0.0778 - val_loss: 0.0698
. . . . .
. . . . .
. . . . .
. . . . .
Epoch 100/100
35/35 [==============================] - 20s 559ms/step - loss: 0.0494 - val_loss: 0.0453
为了检查我们的训练是否顺利,我们可以查看history变量中的损失图表:
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'val'], loc='upper right')
plt.show()
前面的代码片段将绘制所有训练周期的训练和验证损失作为折线图。输出图表应类似于图 7.4。正如我们所见,随着训练的进行,训练和验证损失持续下降。我们的训练朝着正确的方向进行,这让人感到放心。
图 7.4 – 训练和验证损失
最后一步是检查一个未见过的测试数据集上的结果。以下代码从test_set中选择一些随机样本并为它们生成模型输出。我们还绘制了输入图像、模型生成的彩色图像和实际彩色图像,以便理解。
samples = np.random.choice(range(0, len(test_files)), size=5)
绘制几个测试图像以验证模型输出:
# show input images
print("Black-n-White Input images!")
plt.figure(figsize=(8,8))
for i in range(5):
plt.subplot(550+1+i)
plt.imshow((test_x[samples[i]]+1.0)/2.0, cmap='gray')
plt.axis('off')
plt.show()
在这里,模型生成一个彩色版本:
# generate color images from model
print("Model Outputs!")
plt.figure(figsize=(8,8))
for i in range(5):
plt.subplot(550+1+i)
model_input = test_x[samples[i]]
output = model.predict(np.array([model_input]))
plt.imshow((output[0]+1.0)/2.0)
plt.axis('off')
plt.show()
此外,还可以绘制一个参考的彩色版本:
# show real color output images
print("Real Color Version!")
plt.figure(figsize=(8,8))
for i in range(5):
plt.subplot(550+1+i)
plt.imshow((test_y[samples[i]]+1.0)/2.0)
plt.axis('off')
plt.show()
当绘制模型输出或输入图像时,我们将 1.0 加到图像数组上,然后除以 2.0。我们这样做是因为在数据预处理过程中,我们将图像像素值归一化到[-1,1]的范围内。但理想情况下,图像像素值不能为负,因此我们需要对图像绘制目的进行逆变换。所以,加 1.0 并除以 2.0 将像素值带入[0,1]的范围内,这是 Matplotlib 绘图所支持的。参见图 7.5。
图 7.5 – 黑白到彩色模型输出
如前所述的输出所示,我们的模型正在学习某种着色,这当然不是理想的,但看起来仍然相当不错。一个有趣的现象是它不是随机填充颜色渐变;我们可以清楚地看到主要对象,因为它们与背景有不同的对比度。鉴于我们有一个非常小的模型和小的训练数据集,这种性能相当有希望。
然而,这并不是解决图像着色问题的最佳模型架构。如今,生成模型如 生成对抗网络(GANs)为这类问题提供了最佳结果。我们将在本书的后面部分研究 GANs,但现在,让我们继续这个简单的实验。接下来,我们将使用其他 Vertex AI 工具,这些工具将使我们的实验更容易进行。
将模型打包以提交给 Vertex AI 作为训练作业
上一节演示了在 Vertex AI Workbench 笔记本上进行的小型图像着色实验。笔记本非常适合小规模和快速实验,但当涉及到大规模实验(具有更多的计算和/或内存需求)时,建议将其作为 Vertex AI 作业启动,并指定所需的机器规格(如果需要,指定加速器如 GPU 或 TPU)以实现最佳实验。Vertex AI 作业还允许我们并行执行大量实验,而无需等待单个实验的结果。使用 Vertex AI 作业进行实验跟踪也非常简单,因此,借助保存的元数据和 Vertex AI UI,比较您最新的实验与过去的实验变得更加容易。现在,让我们使用上一节中的模型实验设置,并将其作为 Vertex AI 训练作业启动。
重要注意事项
Vertex AI 作业在容器化环境中运行,因此为了启动一个实验,我们必须将我们的整个代码(包括读取数据、预处理、模型构建、训练和评估)打包成一个在容器内运行的单一脚本。Google Cloud 提供了大量的预构建容器镜像用于训练和评估(预先安装了所需的框架依赖,如 TensorFlow、PyTorch 等)。此外,我们还可以灵活地定义我们自己的自定义容器,包含我们可能需要的任何类型的依赖。
对于上一节中的实验,由于我们将开源数据下载到我们的 Jupyter 环境中,这些数据尚未存在于 Google Cloud Storage(GCS)(即 GCS 存储桶或 BigQuery)。因此,首先,我们需要将这些数据存储在某个地方,以便我们的 Vertex AI 训练作业可以在训练容器内读取它。为了让我们更容易操作,我们将我们的预处理数据上传到存储桶中。这将节省我们在作业容器内再次准备数据的精力。我们可以使用以下脚本将我们的准备数据保存到 GCS 存储桶中:
from io import BytesIO
import numpy as np
from tensorflow.python.lib.io import file_io
dest = 'gs://data-bucket-417812395597/' # Destination to save in GCS
## saving training data
np.save(file_io.FileIO(dest+'train_x', 'w'), train_x)
np.save(file_io.FileIO(dest+'train_y', 'w'), train_y)
## saving validation data
np.save(file_io.FileIO(dest+'val_x', 'w'), val_x)
np.save(file_io.FileIO(dest+'val_y', 'w'), val_y)
## saving test data
np.save(file_io.FileIO(dest+'test_x', 'w'), test_x)
np.save(file_io.FileIO(dest+'test_y', 'w'), test_y)
注意,在执行此代码之前,我们必须创建一个存储桶,用于存储这些 NumPy 数组。在这种情况下,我们已创建了一个名为 data-bucket-417812395597 的存储桶。
我们可以使用以下脚本在任意数量的训练作业/实验中读取这些 NumPy 数组:
train_x = np.load(BytesIO(file_io.read_file_to_string( \
dest+'train_x', binary_mode=True)))
train_y = np.load(BytesIO(file_io.read_file_to_string( \
dest+'train_y', binary_mode=True)))
val_x = np.load(BytesIO(file_io.read_file_to_string( \
dest+'val_x', binary_mode=True)))
val_y = np.load(BytesIO(file_io.read_file_to_string( \
dest+'val_y', binary_mode=True)))
test_x = np.load(BytesIO(file_io.read_file_to_string( \
dest+'test_x', binary_mode=True)))
test_y = np.load(BytesIO(file_io.read_file_to_string( \
dest+'test_y', binary_mode=True)))
我们的数据需求现在已经全部设置好了。接下来,让我们着手设置我们的 Vertex AI 训练作业。
首先,我们将安装一些必要的包,这些包用于定义和启动 Vertex AI 作业:
# Install the packages
! pip3 install --upgrade google-cloud-aiplatform \
google-cloud-storage \
pillow
一旦完成包安装,我们将转到一个新的笔记本并导入有用的库:
import numpy as np
import glob
import matplotlib.pyplot as plt
import os
from google.cloud import aiplatform
%matplotlib inline
接下来,我们将定义我们的项目配置:
PROJECT_ID='41xxxxxxxxx7'
REGION='us-west2'
BUCKET_URI='gs://my-training-artifacts'
注意,我们已经创建了一个名为 my-training-artifacts 的存储桶,用于存储 Vertex AI 作业产生的所有中间元数据和工件。
接下来,让我们使用我们的项目配置初始化 Vertex AI SDK:
aiplatform.init(project=PROJECT_ID, location=REGION, \
staging_bucket=BUCKET_URI)
对于我们的实验,我们将使用预构建的 TensorFlow 图像作为我们的模型也是基于 TensorFlow。让我们定义要使用的图像:
TRAIN_VERSION = "tf-cpu.2-9"
DEPLOY_VERSION = "tf2-cpu.2-9"
TRAIN_IMAGE = "us-docker.pkg.dev/vertex-ai/training/{}:latest".format(TRAIN_VERSION)
DEPLOY_IMAGE = "us-docker.pkg.dev/vertex-ai/prediction/{}:latest".format(DEPLOY_VERSION)
在本节中,我们只是启动一个简单的训练作业。在下一节中,我们还将部署和测试我们的训练好的模型。
接下来,让我们为训练定义一些命令行参数(这些可以根据需要修改):
JOB_NAME = "vertex_custom_training"
MODEL_DIR = "{}/{}".format(BUCKET_URI, JOB_NAME)
TRAIN_STRATEGY = "single"
EPOCHS = 20
STEPS = 100
CMDARGS = [
"--epochs=" + str(EPOCHS),
"--steps=" + str(STEPS),
"--distribute=" + TRAIN_STRATEGY,
]
我们还应该提供一个有意义的作业名称;这将帮助我们区分我们的实验与其他并行运行的实验。
下一步是写下我们的整个训练脚本——从读取数据、定义模型、训练到将模型保存到一个文件中。我们将把上一节中的整个代码写入一个名为 task.py 的文件。以下是我们 task.py 文件的内容:
%%writefile task.py
# Single, Mirror and Multi-Machine Distributed Training
import tensorflow as tf
import tensorflow
from tensorflow.python.client import device_lib
import argparse
import os
import sys
from io import BytesIO
import numpy as np
from tensorflow.python.lib.io import file_io
文件的下述部分解析命令行参数:
# parse required arguments
parser = argparse.ArgumentParser()
parser.add_argument('--lr', dest='lr', \
default=0.001, type=float, \
help='Learning rate.')
parser.add_argument('--epochs', dest='epochs', \
default=10, type=int, \
help='Number of epochs.')
parser.add_argument('--steps', dest='steps', \
default=35, type=int, \
help='Number of steps per epoch.')
parser.add_argument('--distribute', dest='distribute', \
type=str, default='single', \
help='distributed training strategy')
args = parser.parse_args()
在这里,我们打印一些版本和环境配置以跟踪当前设置:
print('Python Version = {}'.format(sys.version))
print('TensorFlow Version = {}'.format(tf.__version__))
print('TF_CONFIG = {}'.format(os.environ.get('TF_CONFIG', \
'Not found')))
print('DEVICES', device_lib.list_local_devices())
在这里,我们定义了一个训练策略:
# Single Machine, single compute device
if args.distribute == 'single':
if tf.test.is_gpu_available():
strategy = tf.distribute.OneDeviceStrategy(device="/gpu:0")
else:
strategy = tf.distribute.OneDeviceStrategy(device="/cpu:0")
# Single Machine, multiple compute device
elif args.distribute == 'mirror':
strategy = tf.distribute.MirroredStrategy()
# Multiple Machine, multiple compute device
elif args.distribute == 'multi':
strategy = tf.distribute.experimental.MultiWorkerMirroredStrategy()
# Multi-worker configuration
print('num_replicas_in_sync = {}'.format(strategy.num_replicas_in_sync))
现在,我们准备用于训练、验证和测试的数据集:
# Preparing dataset
BUFFER_SIZE = 10000
BATCH_SIZE = 128
def make_datasets_unbatched():
# Load train, validation and test sets
dest = 'gs://data-bucket-417812395597/'
train_x = np.load(BytesIO(
file_io.read_file_to_string(dest+'train_x', \
binary_mode=True)
))
train_y = np.load(BytesIO(
file_io.read_file_to_string(dest+'train_y', \
binary_mode=True)
))
val_x = np.load(BytesIO(
file_io.read_file_to_string(dest+'val_x', \
binary_mode=True)
))
val_y = np.load(BytesIO(
file_io.read_file_to_string(dest+'val_y', \
binary_mode=True)
))
test_x = np.load(BytesIO(
file_io.read_file_to_string(dest+'test_x', \
binary_mode=True)
))
test_y = np.load(BytesIO(
file_io.read_file_to_string(dest+'test_y', \
binary_mode=True)
))
return train_x, train_y, val_x, val_y, test_x, test_y
现在,我们像之前讨论的那样定义我们的 TensorFlow 模型:
def tf_model():
black_n_white_input = tensorflow.keras.layers.Input(shape=(80, 80, 1))
enc = black_n_white_input
这里是 TF 模型编码器部分的定义:
#Encoder part
enc = tensorflow.keras.layers.Conv2D(
32, kernel_size=3, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
enc = tensorflow.keras.layers.Conv2D(
64, kernel_size=3, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
enc = tensorflow.keras.layers.Conv2D(
128, kernel_size=3, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
enc = tensorflow.keras.layers.Conv2D(
256, kernel_size=1, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.Dropout(0.5)(enc)
编码器部分现在已经完成。接下来,我们在同一个函数中定义模型的解码器部分:
#Decoder part
dec = enc
dec = tensorflow.keras.layers.Conv2DTranspose(
256, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2DTranspose(
128, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2DTranspose(
64, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2DTranspose(
32, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2D(
3, kernel_size=3, padding='same'
)(dec)
Here, we apply tanh activation function to get the colored output image -
color_image = tensorflow.keras.layers.Activation('tanh')(dec)
return black_n_white_input, color_image
现在,我们已经准备好构建和编译我们的 TensorFlow 模型:
# Build the and compile TF model
def build_and_compile_tf_model():
black_n_white_input, color_image = tf_model()
model = tensorflow.keras.models.Model(
inputs=black_n_white_input,
outputs=color_image
)
_optimizer = tensorflow.keras.optimizers.Adam(
learning_rate=0.0002,
beta_1=0.5
)
model.compile(
loss='mse',
optimizer=_optimizer
)
return model
下面的代码块使用定义的设置启动训练,并将训练好的模型保存下来:
# Train the model
NUM_WORKERS = strategy.num_replicas_in_sync
# Here the batch size scales up by number of workers since
# `tf.data.Dataset.batch` expects the global batch size.
GLOBAL_BATCH_SIZE = BATCH_SIZE * NUM_WORKERS
MODEL_DIR = os.getenv("AIP_MODEL_DIR")
train_x, train_y, _, _, _, _ = make_datasets_unbatched()
with strategy.scope():
# Creation of dataset, and model building/compiling need to be within
# `strategy.scope()`.
model = build_and_compile_tf_model()
model.fit(
train_x,
train_y,
epochs=args.epochs,
steps_per_epoch=args.steps
)
model.save(MODEL_DIR)
现在我们已经设置了所有配置,并且我们的训练脚本 task.py 已经准备好了,我们就可以定义并启动我们的自定义训练作业在 Vertex AI 上。
让我们定义我们的自定义 Vertex AI 训练作业:
job = aiplatform.CustomTrainingJob(
display_name=JOB_NAME,
script_path="task.py",
container_uri=TRAIN_IMAGE,
requirements=[],
model_serving_container_image_uri=DEPLOY_IMAGE,
)
最后一步是启动作业:
MODEL_DISPLAY_NAME = "tf_bnw_to_color"
# Start the training job
model = job.run(
model_display_name=MODEL_DISPLAY_NAME,
args=CMDARGS,
machine_type = "n1-standard-16",
replica_count=1,
)
此设置在 n1-standard-16 机器上启动一个 Vertex AI 自定义训练作业,正如前面 job.run 方法中定义的参数。当我们在一个笔记本单元中启动作业时,它给我们一个 Google Cloud 控制台 UI 的 URL。通过点击它,我们可以在 Vertex AI UI 中监控我们的作业日志。
Vertex AI 训练作业在 Google Cloud 控制台 UI 中看起来类似于 图 7*.6*。在这里,我们可以重新验证我们在启动时定义的作业配置和参数:
图 7.6 – Vertex AI 训练作业
Vertex AI UI 允许我们监控所有训练/定制作业的近乎实时日志。我们可以在 UI 中监控我们的训练,看起来类似于 图 7*.7*:
图 7.7 – Google Cloud 控制台上的 Vertex AI 训练作业实时日志
通过日志来监控训练进度可能不是最佳方式,因为我们可能想要跟踪一些参数,例如损失和准确度。在下一节中,我们将学习如何设置基于 TensorBoard 的实时监控训练进度。然而,这些日志对于调试目的来说确实非常有用;如果我们的管道在执行成功完成之前失败,我们总是可以检查这些日志来识别根本原因。
监控模型训练进度
在上一节中,我们看到了如何轻松地使用所需的配置和机器类型启动 Vertex AI 定制训练作业。这些 Vertex AI 训练作业对于运行需要高计算能力(多个 GPU 或 TPUs)的大型实验非常有用,这些实验可能需要运行几天。在基于 Jupyter Notebook 的环境中运行这种长时间运行的实验并不可行。启动 Vertex AI 作业的另一个优点是,所有元数据和血缘关系都以系统化的方式跟踪,这样我们就可以稍后回来查看过去的实验,并以简单准确的方式与最新的实验进行比较。
另一个重要方面是监控训练作业的实时进度(包括损失和准确度等指标)。为此,我们可以在 Vertex AI 作业中轻松设置 Vertex AI TensorBoard,并以近乎实时的方式跟踪进度。在本节中,我们将为之前的实验设置一个 TensorBoard 实例。
大多数代码/脚本将与上一节相似。在这里,我们只需检查设置 TensorBoard 监控所需的修改。
首先,我们需要在 task.py 文件中进行一些小的修改,以考虑 TensorFlow 回调,因为我们想监控训练损失。为了保持整洁,我们将修改一个重命名为 task2.py 的 task.py 文件副本。以下是在 model.fit 函数中的更改:
### Create a TensorBoard callback and write to the gcs path provided by AIP_TENSORBOARD_LOG_DIR
tensorboard_callback = tf.keras.callbacks.TensorBoard(
log_dir=os.environ['AIP_TENSORBOARD_LOG_DIR'],
histogram_freq=1)
model.fit(
train_x,
train_y,
epochs=args.epochs,
steps_per_epoch=args.steps,
callbacks=[tensorboard_callback],
)
在前面的脚本中,我们只定义了一个 TensorFlow 回调对象,并将其传递到 model.fit 函数中。
使用 TensorBoard 需要一个服务账户(而不是个人用户账户)。如果我们还没有设置服务账户,我们可以使用以下脚本快速设置一个服务账户。服务账户用于在 Google Cloud 上授予服务、虚拟机和其他工具的权限:
SERVICE_ACCOUNT="dummy-sa"
IS_COLAB=False
if (
SERVICE_ACCOUNT == ""
or SERVICE_ACCOUNT is None
or SERVICE_ACCOUNT == "dummy-sa"
):
# Get your service account from gcloud
if not IS_COLAB:
shell_output = ! gcloud auth list 2>/dev/null
SERVICE_ACCOUNT = shell_output[2].replace("*", \
"").strip()
如果我们在使用 colab,以下代码片段将相应地创建一个服务账户:
else: # IS_COLAB:
shell_output = ! gcloud projects describe $PROJECT_ID
project_number = shell_output[-1].split(":")[1].strip().replace("'", "")
SERVICE_ACCOUNT = f"{project_number}-compute@developer.gserviceaccount.com"
print("Service Account:", SERVICE_ACCOUNT)
下一步是创建一个我们将用于监控训练的 Vertex AI TensorBoard 实例。
设置 TensorBoard 实例:
TENSORBOARD_NAME = "training-monitoring" # @param {type:"string"}
if (
TENSORBOARD_NAME == ""
or TENSORBOARD_NAME is None
or TENSORBOARD_NAME == "training-monitoring"
):
TENSORBOARD_NAME = PROJECT_ID + "-tb-" + TIMESTAMP
tensorboard = aiplatform.Tensorboard.create(
display_name=TENSORBOARD_NAME, project=PROJECT_ID, \
location=REGION
)
Let's verify if the TensorBoard instance was successfully created or not - TENSORBOARD_RESOURCE_NAME = tensorboard.gca_resource.name
print("TensorBoard resource name:", TENSORBOARD_RESOURCE_NAME)
我们需要一个用于我们的 Vertex AI 作业的临时存储桶,以便它可以将事件日志写入该位置:
BUCKET_URI = "gs://tensorboard-staging" # @param {type:"string"}
if BUCKET_URI == "" or BUCKET_URI is None or BUCKET_URI == "gs://[your-bucket-name]":
BUCKET_URI = "gs://" + PROJECT_ID + "aip-" + TIMESTAMP
! gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}
GCS_BUCKET_OUTPUT = BUCKET_URI + "/output/"
我们现在已经准备好定义我们的自定义训练作业:
JOB_NAME = "tensorboard-example-job-{}".format(TIMESTAMP)
BASE_OUTPUT_DIR = "{}{}".format(GCS_BUCKET_OUTPUT, JOB_NAME)
job = aiplatform.CustomTrainingJob(
display_name=JOB_NAME,
script_path="task2.py",
container_uri=TRAIN_IMAGE,
requirements=[],
model_serving_container_image_uri=DEPLOY_IMAGE,
staging_bucket=BASE_OUTPUT_DIR,
)
我们现在可以使用以下脚本启动 Vertex AI 作业。在这里,我们可以选择机器类型,还可以指定replica_count参数,该参数控制当前作业要运行的副本数量:
MODEL_DISPLAY_NAME = "tf_bnw_to_color_tb"
# Start the training job
model = job.run(
model_display_name=MODEL_DISPLAY_NAME,
service_account=SERVICE_ACCOUNT,
tensorboard=TENSORBOARD_RESOURCE_NAME,
args=CMDARGS,
machine_type = "n1-standard-8",
replica_count=1,
)
一旦我们启动作业,它将给我们一个 URL,用于在 Google Cloud 控制台 UI 中定位 Vertex AI 作业,就像上一节中那样;但这次,它还会给我们一个指向 Vertex TensorBoard UI 的 URL。使用这个 URL,我们将能够以近乎实时的方式监控我们的训练。
这就是我们的小实验看起来像什么(见图 7.8):
图 7.8 – 用于实时监控实验的 Vertex TensorBoard
我们可以配置它以显示更多我们实验中所需的指标。现在我们能够启动 Vertex AI 训练、监控它,并且还能保存我们的 TensorFlow 训练模型,让我们继续到模型评估部分。
评估训练模型
在本节中,我们将从上一节中提取已经训练好的模型,并在测试数据上启动一个批量推理作业。这里的第一个步骤将是将我们的测试数据加载到 Jupyter Notebook 中:
from io import BytesIO
import numpy as np
from tensorflow.python.lib.io import file_io
dest = 'gs://data-bucket-417812395597/'
test_x = np.load(BytesIO(file_io.read_file_to_string(dest+'test_x',\
binary_mode=True)))
test_y = np.load(BytesIO(file_io.read_file_to_string(dest+'test_y',\
binary_mode=True)))
print(test_x.shape, test_y.shape)
下一步是从我们的测试数据中创建实例的 JSON 有效负载并将其保存到云存储位置。批推理模块将能够读取这些实例并执行推理:
import json
BATCH_PREDICTION_INSTANCES_FILE = "batch_prediction_instances.jsonl"
BATCH_PREDICTION_GCS_SOURCE = (
BUCKET_URI + "/batch_prediction_instances/" + BATCH_PREDICTION_INSTANCES_FILE
)
在这里,我们将输入图像转换为可序列化的格式,以便预测服务可以接受 JSON 文件作为输入:
# converting to serializable format
x_test = [(image).astype(np.float32).tolist() for image in test_x]
# Write instances at JSONL
with open(BATCH_PREDICTION_INSTANCES_FILE, "w") as f:
for x in x_test:
f.write(json.dumps(x) + "\n")
# Upload to Cloud Storage bucket
! gsutil cp batch_prediction_instances.jsonl BATCH_PREDICTION_GCS_SOURCE
print("Uploaded instances to: ", BATCH_PREDICTION_GCS_SOURCE)
现在我们的测试数据集实例已准备好在云存储桶中。我们可以对它们启动批量预测,批推理模块将输出结果保存到同一存储桶中的新文件夹中:
MIN_NODES = 1
MAX_NODES = 1
# The name of the job
BATCH_PREDICTION_JOB_NAME = "bnw_to_color_batch_prediction"
# Folder in the bucket to write results to
DESTINATION_FOLDER = "batch_prediction_results"
# The Cloud Storage bucket to upload results to
BATCH_PREDICTION_GCS_DEST_PREFIX = BUCKET_URI + "/" + DESTINATION_FOLDER
在这里,我们使用 SDK 调用批预测服务:
# Make SDK batch_predict method call
batch_prediction_job = model.batch_predict(
instances_format="jsonl",
predictions_format="jsonl",
job_display_name=BATCH_PREDICTION_JOB_NAME,
gcs_source=BATCH_PREDICTION_GCS_SOURCE,
gcs_destination_prefix = BATCH_PREDICTION_GCS_DEST_PREFIX,
model_parameters=None,
starting_replica_count=MIN_NODES,
max_replica_count=MAX_NODES,
machine_type="n1-standard-4",
sync=True,
)
如果需要,我们还可以在 Google Cloud 控制台 UI 中监控批量预测作业的进度。一旦这个作业完成,我们可以在定义的目标文件夹中检查输出。
摘要
在本章中,我们学习了如何使用基于 Vertex AI 的托管训练环境以及启动自定义训练任务。在 Vertex AI 上启动自定义训练任务具有许多优势,例如托管元数据跟踪、无需主动监控任务,以及能够并行启动任意数量的实验,选择你想要的机器规格来运行你的实验,使用云控制台 UI 以近乎实时的方式监控训练进度和结果,并在保存的模型上运行托管批量推理任务。它还与其他 GCP 产品紧密集成。
阅读本章后,你应该能够开发并在 Vertex AI Workbench 笔记本上运行自定义深度学习模型(使用如 TensorFlow 等框架)。其次,你应该能够启动长时间运行的 Vertex AI 自定义训练任务,并理解托管 Vertex AI 训练框架的优势。托管的 Google Cloud 控制台界面和 TensorBoard 使得监控和评估各种 Vertex AI 训练任务变得容易。
现在我们已经对在 GCP 上使用 Vertex AI 进行模型训练有了很好的理解,接下来我们将学习下一章中的模型可解释性。
第八章:机器学习模型的可解释性
在快速发展的机器学习(ML)和人工智能(AI)世界中,开发能够提供准确预测的模型不再是唯一目标。随着组织越来越依赖数据驱动的决策,理解模型预测背后的理由变得至关重要。机器学习模型中可解释性需求的增长源于道德、监管和实际方面的考虑,这就是可解释人工智能(XAI)概念发挥作用的地方。
本章深入探讨了可解释机器学习模型的复杂性,这是机器学习操作(MLOps)景观中的一个关键组成部分,重点关注它们在 Google Cloud 生态系统中的实现。尽管对 XAI 技术和工具的全面探索超出了本章的范围,但我们旨在为您提供构建透明、可解释和有责任感的机器学习模型所需的知识和技能,这些模型可以使用 GCP 上的可解释机器学习工具。
本章将涵盖以下主题:
-
可解释人工智能是什么,为什么它对机器学习操作(MLOps)从业者来说很重要?
-
可解释人工智能技术的概述
-
可解释人工智能功能在 Google Cloud Vertex AI 中可用
-
使用 Vertex AI 的可解释性功能的动手练习
随着我们进入本章,我们将确立可解释性的重要性及其在增强机器学习模型中的信任、责任和公平性方面的作用。接下来,我们将讨论实现机器学习可解释性的各种技术,从传统的可解释模型到用于更复杂模型(如深度学习)的解释技术。然后,我们将深入了解 Google Cloud 的可解释人工智能(XAI)产品,这些产品有助于开发和使用可解释机器学习模型。
除了理解可解释机器学习模型外,本章还将通过实际操作示例指导您,说明这些概念在实际场景中的应用。
到本章结束时,您将准备好设计、部署和评估 Google Cloud 上的可解释机器学习模型,确保您的组织在迈向道德和负责任的人工智能采用的竞赛中保持领先。
可解释人工智能是什么,为什么它对机器学习操作(MLOps)从业者来说很重要?
XAI 指的是在人工智能领域使用的方法和技术,旨在使人工智能模型的决策过程对人类透明、可解释和可理解。XAI 不是作为黑盒运行,其中输入数据进入,决策或预测出来,而决策过程不清晰,XAI 寻求揭示模型的内部运作。这种透明度使用户、开发人员和利益相关者能够信任并验证系统的决策,确保它们符合道德、法律和实际考虑。
随着机器学习的持续进步及其应用渗透到各个行业,对透明和可解释模型的需求已成为一个紧迫的问题。XAI 旨在通过开发理解、解释和解释 ML 模型的技术来解决这个问题。对于与 Google Cloud 合作的 MLOps 从业者来说,将 XAI 纳入他们的工作流程可以带来几个好处,包括提高信任度、合规性和增强模型性能。
首先,让我们看看 XAI 对 MLOps 从业者的重要性及其对 ML 模型开发和部署的影响的关键原因。
建立信任和信心
XAI 可以通过提供模型如何做出决策的清晰易懂的解释来帮助 MLOps 从业者建立对模型的信任。这对于处理可能没有技术背景的利益相关者尤为重要,因为解释模型行为的能力可以增加对其预测的信心。此外,对模型内部运作有更深入的理解,使从业者能够更好地传达他们解决方案的限制和优势,从而在合作者和最终用户之间培养信任。
监管合规
随着机器学习模型被更广泛地采用,世界各地的监管机构越来越要求人工智能系统具有更高的透明度和问责制。XAI 技术可以帮助 MLOps 从业者通过提供模型决策过程的见解来确保遵守这些规定。这在医疗保健、金融和人力资源等行业尤为重要,在这些行业中,有偏见或不公平决策的后果可能非常严重。通过将 XAI 纳入他们的工作流程,从业者可以证明他们的模型遵守相关法律和伦理指南。
模型调试和改进
XAI 在模型开发和调试过程中对 MLOps 从业者来说非常有价值。通过提供模型预测方式的见解,XAI 可以帮助识别模型可能表现不佳、过拟合或存在偏差的区域。有了这些信息,从业者可以对他们的模型进行有针对性的调整,从而提高性能并实现更稳健的解决方案。这个迭代过程可以节省时间和资源,使从业者能够专注于解决影响他们模型的最关键问题。
伦理考量
随着机器学习模型的力量和影响力增长,MLOps 从业者的责任也随之增加,以确保这些模型被道德地使用。XAI 可以帮助从业者识别和解决模型可能产生的任何意外后果或偏差。通过理解模型是如何做出决定的,从业者可以更好地确保他们的解决方案是公平的、无偏见的,并与伦理原则一致。
在 Google Cloud 生态系统中将 XAI 集成到 MLOps 工作流程中可以为从业者带来众多好处。从与利益相关者建立信任和确保合规性,到提高模型性能和解决伦理问题,XAI 的重要性不容小觑。随着机器学习领域的不断发展,将 XAI 集成到 MLOps 实践中将变得越来越重要,这对于开发和应用透明、可解释和负责任的 AI 解决方案至关重要。
可解释人工智能技术
不同的技术可供选择,以适应各种类型的数据,包括表格、图像和文本数据。每种数据类型都带来其自身的挑战和复杂性,需要定制的方法来为机器学习模型的决策过程提供有意义的见解。本小节将列出适用于表格、图像和文本数据的各种 XAI 技术。下一节将深入探讨在 Google Cloud 中作为开箱即用功能提供的那些技术。
全局与局部可解释性
可解释性可以分为两类:局部可解释性和全局可解释性。这些术语有时也被称为局部和全局特征重要性:
-
全局可解释性关注特征对模型的整体影响。这通常是通过在整个数据集上计算平均特征归因值来获得的。具有高绝对值的特征表明它对模型预测有显著影响。
-
局部可解释性提供了关于每个特征对特定实例预测贡献多少的见解。特征归因值提供了关于特定特征相对于基线预测对预测的影响的信息。
图像数据技术
XAI 技术在图像数据中通常关注可视化图像中对于模型预测贡献最大的区域。以下是一些关键技术:
-
集成梯度
集成梯度是一种专门为深度学习模型(如神经网络)设计的归因技术。它计算模型输出相对于输入特征(在图像数据的情况下为像素)的梯度,并将这些梯度沿从基线输入到感兴趣实例的直线路径进行积分。这个过程为图像中的每个像素分配一个重要性值,反映其对模型预测的贡献。集成梯度提供了对每个像素重要性的见解,并有助于识别模型预测中的潜在偏差或不足:
图 8.1 – 集成梯度解释
前面的图展示了集成梯度方法对图像的解释,它突出了模型在预测期间赋予高重要性的图像像素。
-
扩展相关性加权重要性归因(XRAI)
XRAI 是一种 XAI 方法,用于可视化给定模型预测中图像的最重要区域。它是集成梯度方法的扩展,该方法结合了像素级归因与分割技术,以生成更连贯和可解释的视觉表示。通过识别图像中最重要的部分,XRAI 提供了对模型决策过程的洞察,并有助于识别模型预测中的潜在偏差或问题:
图 8.2 – XRAI
前面的图展示了 XRAI 对图像解释的方法,它突出了模型在预测期间赋予高重要性的图像区域。
-
局部可解释模型无关 解释 (LIME)
LIME 是一种 XAI 技术,为任何分类器的单个预测提供局部解释。在图像数据的上下文中,LIME 在特定实例周围生成合成数据点(扰动图像),从模型中获得预测,并使用这些数据点与实例的邻近度进行加权,拟合一个可解释的模型(例如,线性回归)。生成的模型提供了对特定实例预测中最重要的区域的洞察。通过可视化这些区域,从业者可以更好地理解模型的决策过程,并识别模型预测中的潜在偏差或问题。
-
梯度加权类激活 映射 (Grad-CAM)
Grad-CAM 是一种针对深度学习模型(特别是卷积神经网络(CNNs))的可视化技术。它为给定模型预测中的图像生成最重区域的热图可视化。Grad-CAM 计算预测类别分数相对于最后一层卷积层的特征图的梯度,然后使用这些梯度计算特征图的加权总和。生成的热图突出了对模型预测贡献最大的图像区域。Grad-CAM 提供了对模型决策过程的洞察,并有助于识别模型预测中的潜在偏差或不足。
这些技术提供了对模型决策过程的洞察,并有助于识别模型预测中的潜在偏差或不足。
表格数据技术
由结构化行和列组成的表格数据是机器学习中遇到的最常见数据类型之一。可以采用各种 XAI 技术来解释在表格数据上训练的模型:
-
局部可解释模型无关 解释 (LIME)
如其名所示,LIME 是一种 XAI 技术,为任何分类器的单个预测提供局部解释。它是通过在特定实例的附近用一个更简单、可解释的模型(例如,线性回归)来近似复杂模型来实现的。LIME 在实例周围生成合成数据点,从复杂模型中获取预测,并使用这些数据点拟合一个可解释的模型,这些数据点根据它们与实例的接近程度进行加权。生成的模型提供了关于对特定实例的预测贡献最大的特征的见解。
在下面的图中,我们使用 LIME 报告来解释一个训练有素的机器学习模型的决策,该模型根据乘客的属性(如性别、支付的票价、他们所乘坐的乘客等级等)预测泰坦尼克号乘客的生存概率。泰坦尼克号生存数据集是一个常见的公开可用数据集,用作分类模型的示例。让我们看看 LIME 是否可以帮助我们了解模型的行为:
图 8.3 – 使用 LIME 解释分类模型
最左侧的图表显示了所选乘客的预测生存概率。在这个例子中,模型预测这位乘客基于其关键属性有 40%的生存概率。中间的图表显示了使用 LIME 生成的按重要性排序的特征列表。根据这个图表,似乎决定泰坦尼克号乘客是否生存的前三个最重要的特征是他们的性别/性别、支付的票价以及他们所乘坐的乘客等级。这很有道理,因为我们知道女性首先被疏散,给了她们更高的整体生存机会。我们也知道支付较低票价和持有低等级票的乘客住在船的下层甲板/楼层,这些地方首先被淹没,而支付较高票价的乘客住在上层甲板,给了他们更好的生存机会。所以,你可以看到 LIME 和类似技术如何帮助解析黑盒机器学习模型,并帮助我们更好地理解为什么做出了特定的预测。你也会很高兴地知道,我们之前用来举例的泰坦尼克号乘客是一位 26 岁的女士,名叫 Laina Heikkinen,她在三等舱乘客区支付了 7.925 美元的票价,尽管我们的模型给她低于 40%的生存机会,但她还是幸存了下来。
-
Shapley Additive exPlanations (SHAP)(在 Vertex AI 上原生支持)
SHAP 是一种基于合作博弈论的特征重要性统一度量,提供了一种一致且公平的方式来分配特征重要性。通过计算每个特征的平均边际贡献,SHAP 为每个特征分配一个重要性值,该值反映了其对特定实例预测的贡献。Shapley 值是通过平均所有可能的特征组合中一个特征的边际贡献来计算的。SHAP 值提供了关于驱动模型预测的最具影响力的特征的见解,并且可以与各种模型一起使用。推荐模型类型包括非可微分的模型,如树的集成。它们也可以用于神经网络,其中 SHAP 可以提供关于每个输入特征对网络最终预测贡献的见解。通过分析 SHAP 值,您可以确定哪些特征对网络的输出影响最大,并了解输入与输出之间的关系:
图 8.4 – 基于 Shapley 方法的 Vertex AI 中的特征重要性图
-
置换 特征重要性
置换特征重要性是一种模型无关的技术,通过测量当特征值随机打乱时模型性能的变化来估计每个特征的重要性。这个过程重复多次,平均性能下降被用作特征重要性的估计。通过破坏特征与目标变量之间的关系,置换重要性有助于识别对模型预测影响最大的特征。
-
部分依赖 图 (PDP)
PDP 是一种可视化技术,它描绘了特定特征与模型预测结果之间的关系,同时保持所有其他特征不变。通过说明单个特征如何影响预测,PDP 可以帮助从业者更好地理解其模型的行为,并识别潜在的偏差或不一致性。
-
特征重要性(例如,GINI 重要性和线性模型中的系数)
特征重要性是一组量化输入特征对模型预测影响的技巧。这些方法可以帮助从业者识别最相关的特征,使他们能够在模型开发和调试期间专注于最重要的变量。以下是一些常见的特征重要性方法:
-
GINI 重要性:在决策树和随机森林中应用,GINI 重要性衡量的是特定特征在整个森林中所有树上的平均纯度减少(GINI 指数)。
-
线性模型中的系数:在线性回归和逻辑回归中,模型的系数可以用作特征重要性的度量,表示每个特征与目标变量之间关系的幅度和方向。较大的绝对系数值表示特征与目标变量之间关系更强。
-
这些技术有助于从业者理解输入特征与模型预测之间的关系,识别最有影响力的特征,并评估特定特征对单个预测的影响。
文本数据的技术
对于使用文本数据的自然语言处理模型,目标是识别对模型预测有最大贡献的最重要单词或短语。以下是一些针对文本数据的 XAI 技术:
-
文本特定 LIME:这是针对文本数据特别设计的 LIME 的一个版本,通过突出显示最重要的单词或短语来为单个预测提供解释。
在以下示例中,我们使用 LIME 来解释我们构建的用于将电影评论分类为正面或负面的机器学习模型为何得出特定的结论:
图 8.5 – 基于 LIME 的文本分类解释
如我们所见,电影评论预测为正面的概率为 0.77(77%)。橙色高亮的单词有助于提高正面的概率,而蓝色高亮的单词对将最终预测推向“负面”标签有显著贡献。右上角的图表显示了每个高亮单词对最终决策的相应贡献。例如,如果我们从评论文本中删除“amazing”这个词,正面的概率将下降 0.03:
-
文本特定 SHAP:这是针对文本数据特别设计的 SHAP 的一个版本,将重要性值分配给给定文本中的单个单词或短语
-
注意力机制:在像 Transformers 这样的深度学习模型中,注意力机制可以通过可视化注意力权重来提供关于单词与模型预测之间关系的见解。
现在我们已经熟悉了各种流行的 XAI 技术,让我们来看看 Google Cloud Vertex AI 中可用的不同功能,这些功能可以帮助我们使用这些技术构建 XAI 解决方案。
Google Cloud Vertex AI 中可用的可解释人工智能功能
Google Cloud Vertex AI 提供了一套工具和选项,旨在使人工智能系统更加易于理解。本节深入探讨了 Vertex AI 中可用的各种可解释人工智能(XAI)选项,展示了该平台如何推进透明机器学习的前沿。
广义上,Vertex AI 中可用的 XAI 选项可以分为两种类型:
- 基于特征的:特征归因指的是模型中每个特征对特定实例预测的贡献程度。在做出预测请求时,您会收到由您的模型生成的预测值。然而,在请求解释时,您不仅会收到预测值,还会收到特征归因信息。
重要的是要注意,特征归因主要适用于表格数据,但也包括图像数据的内置可视化功能。这使得更直观地理解和解释归因变得更容易。
- 基于示例的:Vertex AI 利用最近邻搜索来提供基于示例的解释。这种方法涉及找到与输入最接近的示例(通常来自训练数据),并返回最相似示例的列表。这种方法利用了相似输入可能产生相似预测的原则,使我们能够深入了解模型的行为。通过检查这些相似示例,我们可以更好地理解和解释模型输出。
Vertex AI 上可用的基于特征的解释技术
下表展示了 GCP 中可用的基于特征的解释方法。
| 方法 | 兼容的 Vertex AI 模型资源 | 示例 用例 |
|---|---|---|
| 样本 Shapley(SHAP) |
-
任何自定义训练的模型(在任何预测容器中运行)
-
AutoML 表格模型
|
- 表格数据的分类和回归
|
| 集成梯度 |
|---|
-
使用 TensorFlow 预构建容器进行预测服务的自定义训练 TensorFlow 模型
-
AutoML 图像模型
|
-
表格数据的分类和回归
-
图像数据的分类
|
| XRAI(基于排序面积积分的解释) |
|---|
-
使用 TensorFlow 预构建容器进行预测服务的自定义训练 TensorFlow 模型
-
AutoML 图像模型
|
- 图像数据的分类
|
表 8.1 – GCP 中可用的特征归因方法。来源:cloud.google.com/vertex-ai/docs/explainable-ai/overview
接下来,我们将学习如何使用这些 Vertex AI 功能来生成模型输出的解释。
使用 AutoML 表格数据模型中的模型特征重要性(基于 SHAP)功能
在以下练习中,我们将学习如何使用 Vertex AI 中的 XAI 功能来评估结构化数据 ML 模型中的特征重要性。
练习 1
目标:使用 Vertex AutoML Tables 的特征重要性功能来解释全局(模型级别)和局部(样本级别)行为
要使用的数据集:银行营销数据集(可在本书 GitHub 仓库的第八章文件夹中找到)
模型目标:预测客户是否会开设新的定期存款(特征标签 – deposit(是/否))
按以下步骤操作:
-
按照第五章中所示步骤,创建一个 AutoML 分类模型来预测客户开设定期存款的概率。
-
模型训练完成后,导航到模型注册 | 您的模型 | 您的模型版本(1 为新模型) | 评估选项卡。
-
滚动到特征 重要性部分。
-
下面的特征重要性图显示了不同模型特征的相对重要性:
图 8.6 – Vertex AI AutoML Tables 中的特征重要性图
如前一个图所示,我们模型中预测客户是否会响应外展并开设新的定期存款的最重要特征如下:
-
持续时间:客户与银行的关系时长
-
月份:这可能是因为业务的季节性
-
联系方式(方法 – 手机/固定电话):这可能是因为不同类型客户的通信偏好不同
-
结果:对这位客户最后一次促销活动的结果
下面的是最不重要的特征,根据前一个图:
-
默认:客户之前是否违约过?
-
婚姻(状态)
-
教育:达到的教育水平
-
之前:在此活动之前联系次数的数量
前面的信息可以帮助数据科学团队更好地理解模型的行为,并可能揭示他们对数据和客户行为的更多见解,并为未来的实验提供重要指导。
虽然在训练数据集上执行探索性数据分析超出了本书的范围,但对于感兴趣的任何人,你可以查看笔记本(第八章 – 机器学习模型可解释性 – 练习 1 补充资料)以了解特征与预测标签之间的相关性分析。
从这个特征重要性信息中,我们可以得出以下见解:
-
(
Deposit_Signup),这与前一个图中特征重要性方面持续时间高的趋势相一致。 -
联系方式:同样,相关性分析还显示,拥有手机的人与对活动做出存款响应的人有很强的相关性。
-
结果:相关性分析还显示,“成功”与有人开设存款之间存在强烈的关联。这意味着如果有人对上次活动做出了积极回应,那么这个人受到当前活动影响的可能性更大。
-
月份:在相关性分析中,我们还可以看到一些月份(特别是三月和五月)与活动的积极结果有很强的相关性。这可能意味着 AutoML 将月份作为一个重要特征呈现。
练习 2
目标:使用 Vertex AI 的特征归因功能来解释图像分类预测
要使用的数据集:Fast_Food_Classification_Dataset
按以下步骤创建图像分类模型:
-
从 Kaggle 下载并解压数据集:
www.kaggle.com/datasets/utkarshsaxenadn/fast-food-classification-dataset -
按照第 5 章 中所示步骤,使用
Fast_Food_Classification_Dataset创建一个 AutoML 图像分类数据集。确保选择数据类型和目标为图像分类(单标签),如图所示。
图 8.7 – 模型目标 – 图像分类(单标签)
- 一旦创建了空数据集,转到 浏览 选项卡并为您计划包含在模型中的每种食物类型添加新标签。在此示例中,我们上传了我们最喜欢的快餐,包括汉堡、甜甜圈、热狗和披萨,但请随意使用您想要的任何食物类型。
图 8.8 – 创建标签名称
-
现在,让我们上传不同快餐类型的图像并将它们标注/标记。您不需要数据集中所有的图像。每种食物类型只需大约 50 张图像就足够了。
对每种食物类型重复以下步骤:
- 上传图像 – 逐个标记图像很困难。为了使标记图像变得更容易,一次上传一种食物类型的图像。
图 8.9 – 为图像添加标签
如所示,一旦上传了特定食物项目的图像,您可以点击 未标记 然后点击 全选 以选择所有需要标记的图像。如果您一次上传并标记一种食物类型,则确保您只选择该类型的图像。如果您一次性上传所有图像,那么点击 未标记 选项卡最终会选中所有未标记的图像,需要您手动选择一种类型的图像。
-
选择图像后,点击 分配标签 并选择正确的食物类型标签。然后点击 保存。
对所有的不同 食物类型 进行此过程。
-
一旦所有图像都已上传并标记,导航到 Vertex AI 中的数据集并转到 浏览 选项卡。
-
点击 训练 新模型:
图 8.10 – 启动模型训练
-
在下一屏幕上,选择您用于训练新模型的数据集和注释集。然后选择以下选项并点击 继续:
目标:图像 分类(单标签)
模型训练方法:AutoML
选择模型的使用位置:云
图 8.11 – 训练配置/选项
-
在下一屏,选择训练新模型并输入新模型的名称。您可以保留所有其他选项不变,然后点击继续。
-
在下一屏,选择默认作为目标并点击继续:
图 8.12– 训练配置/选项
-
在下一屏(可解释性选项卡)中,勾选生成可解释位图选项。
-
在正上强调具有最高正影响力的区域。本质上,它照亮了模型做出积极预测的像素,对积极预测有显著贡献。将极性更改为负将强调区域,使模型远离预测正类,并有助于定位导致假阴性的区域。还有一个选择两者都,它通过显示正负贡献来提供全面的视图。
-
pink_green默认,其中绿色表示正贡献,粉色表示负贡献。另一方面,XRAI 可视化使用渐变色方案,默认为Viridis。在这个设置中,最有影响力的区域被黄色笼罩,而影响力较小的区域则被蓝色阴影覆盖。有关可用调色板的完整列表,请参阅 API 文档中的可视化消息。 -
叠加类型:此设置定义了原始图像在可视化中的展示方式。调整叠加可以增强可见性,尤其是在初始图像的固有属性掩盖了可视化细节时。
-
步骤:可以在此处指定用于近似路径积分的步骤数。建议从 50 开始,逐渐增加,直到“求和到差”属性落在所需的误差范围内。此值的有效范围在 1 到 100 之间:
图 8.13 – 可解释性配置
- 在计算和定价选项卡中,将预算设置为 8 小时。这指定了训练将运行的最大时间。
-
点击开始训练。
(咖啡休息时间太短了,所以也许可以去准备一顿七道菜的晚餐,然后几个小时后回来检查训练状态!)
模型训练完成后,我们需要按照以下步骤将模型部署到 Vertex AI 端点。
-
导航到模型注册 | 您的模型 | 您的模型版本(1 用于 新模型)。
-
导航到部署与测试选项卡并点击部署到端点。
-
输入端点名称并点击继续:
图 8.14 – 模型部署选项
- 在模型设置选项卡中,勾选为此模型启用特征归因,然后点击下方的编辑按钮以打开可解释性****选项菜单:
图 8.15 – 可解释性部署配置
-
在可解释性选项菜单中,选择集成梯度选项,因为我们首先创建一个端点来测试集成梯度技术。点击完成。
-
现在,重复这些步骤以创建一个用于XRAI可解释性选项的端点。这次,在端点名称后缀加上 XRAI,并在可解释性选项屏幕上选择XRAI。
-
到此为止,应该已经为模型创建了两个端点。
现在,通过上传一个甜甜圈的样本图片来测试模型,并评估模型返回的预测和解释:
-
在模型的部署与测试标签页中,通过点击端点 ID选择集成梯度端点。不要点击端点的名称,因为这会带您进入端点的设置屏幕。
-
点击上传并解释并选择您想要测试的图片。
-
Vertex AI 将处理图像,并展示图像的最终分类结果,以及一个解释(图像叠加将显示图像中重要性高的区域):
图 8.16 – 上传的图片
以下截图显示了基于机器学习模型的类别预测,以及 Vertex AI 生成的基于集成梯度的解释。解释图像展示了图像中帮助模型做出最终决策认为这是一张甜甜圈的关键区域/像素:
图 8.17 – 结果集成梯度解释和预测类别
- 您可以使用 XRAI 端点重复此步骤以获取使用 XRAI 技术生成的解释:
图 8.18 – XRAI 解释
如您所见,通过集成梯度和 XRAI 技术生成的解释图像,图像中靠近甜甜圈位置的区域/像素被突出显示,模型似乎专注于正确的区域。
现在,让我们看看基于示例的解释,在这里,我们不是根据输入实例的特征来解释结果,而是尝试通过查看数据集中与输入实例相似的示例来解释结果。
基于示例的解释
Vertex AI 的基于示例的解释功能使用最近邻搜索算法来找到与样本最接近的匹配项。本质上,当给定一个输入时,Vertex AI 会识别并提供一组示例,这些示例通常来自训练数据,并且与给定输入非常相似。这一功能基于一个共同的预期,即具有相似属性的输入将导致相应的预测。因此,这些识别出的示例成为理解并阐明我们模型的工作方式和决策的一种直观方式。
此方法在以下场景中可能非常有帮助:
-
识别错误标记的示例:如果解决方案定位到在向量空间中彼此靠近但标签不同的数据样本或嵌入,那么数据样本可能被错误标记。
-
决策支持:如果对新数据点的预测标签与在向量空间中出现在新数据点附近的其他数据点的真实标签相似,那么这可以帮助确认预测的有效性。
-
主动学习:在向量空间中,您可以识别出现在有标签样本附近的未标记样本,并将它们添加到训练数据中,标签与附近样本的标签相同。
Vertex AI 中基于示例的解释功能可以被任何为其输入提供嵌入(潜在表示)的模型利用。这意味着该模型应该能够将输入数据转换为潜在空间中的一组相关特征或向量。这排除了某些类型的模型,例如决策树等基于树的模型,因为它们固有的性质不创建这些潜在空间。
使用基于示例的解释的关键步骤
这里是关键步骤:
-
在模型创建时启用解释:首先创建一个已启用解释的模型并将其上传到 Vertex AI。当您创建/导入模型时,您可以使用模型的
explanationSpec字段为所有解释设置默认配置。为了促进基于示例的解释生成,您的模型应满足某些标准。存在两种潜在的情景:
-
您可以实现一个深度神经网络(DNN)模型,在这种情况下,应提供特定层或签名的名称。然后,该层或签名的输出被用作潜在空间。
-
或者,模型可以被设计为直接输出嵌入,从而作为潜在空间的表示。
这个潜在空间对于过程至关重要,因为它包含了在生成解释中起关键作用的示例表示。
-
-
将模型部署到端点:接下来,创建一个端点资源并将您的模型部署到该资源,从而建立一个可交互的通道。
-
探索生成的解释:最后,向部署的模型发出解释请求,并仔细审查提供的解释,以了解您的模型决策过程。
练习 3
自定义训练一个图像分类模型以生成实时预测并提供基于示例的解释——参见笔记 8.3 – 使用 Vertex AI 实现基于示例的解释 (github.com/PacktPublishing/The-Definitive-Guide-to-Google-Vertex-AI/blob/main/Chapter08/Chapter8_Explainable_AI_example_based.ipynb)
摘要
在本章中,我们深入探讨了可解释人工智能(XAI)的世界及其在现代机器学习运维(MLOps)中的相关性。我们讨论了 XAI 如何帮助建立信任、确保合规性、调试和改进模型,以及解决伦理问题。
我们探讨了针对不同类型数据的多种解释技术,包括表格数据、图像数据和文本数据。对于表格数据,讨论了 LIME、SHAP、排列特征重要性等技巧。对于图像数据,解释了集成梯度和 XRAI 等方法,而对于文本数据,则展示了针对文本数据的特定 LIME。
本章还概述了 GCP 中可用的 XAI 功能,包括基于特征和基于示例的解释。
到目前为止,您应该已经对 XAI、其重要性、各种技术和在 Vertex AI 背景下的实际应用有了很好的理解。随着人工智能领域的不断发展,XAI 在创建透明、值得信赖和公平的机器学习模型中的作用将只会增长。作为 MLOps 从业者,掌握这些技能对于引领道德和负责任的 AI 采用至关重要。
在下一章中,我们将介绍各种 Vertex AI 工具,这些工具可以帮助您迭代模型超参数,以提升您的机器学习解决方案的性能。
参考文献
cloud.google.com/vertex-ai/docs/explainable-ai/overview
Munn, Michael; Pitman, David. 可解释人工智能实践者. O’Reilly Media.
第九章:模型优化 – 超参数调整和 NAS
我们现在已经非常熟悉 Vertex AI 提供的一些与数据管理、无代码和低代码模型训练以及启动大规模定制模型训练作业(具有元数据跟踪和监控功能)相关的服务。作为机器学习从业者,我们知道我们训练的第一个模型很可能不是针对特定用例和数据集的最佳模型。因此,为了找到最佳模型(即最准确且偏差最小的模型),我们通常使用不同的模型优化技术。超参数调整(HPT)和神经架构搜索(NAS)是两种这样的模型优化技术。在本章中,我们将学习如何使用 Vertex AI 在 Google Cloud 上配置和启动模型优化实验。
在本章中,我们将首先了解模型优化技术(如 HPT)的重要性,然后学习如何在 Google Vertex AI 中快速设置和启动 HPT 作业。我们还将了解 NAS 的工作原理以及它与 HPT 的不同之处。本章涵盖的主题如下:
-
什么是 HPT 以及为什么它很重要?
-
在 Vertex AI 上设置 HPT 作业
-
NAS 是什么,它与 HPT 有何不同?
-
Vertex AI 上的 NAS 概述
技术要求
本章中展示的代码示例可以在以下 GitHub 仓库中找到:github.com/PacktPublishing/The-Definitive-Guide-to-Google-Vertex-AI/tree/main/Chapter09
什么是 HPT 以及为什么它很重要?
超参数调整,或简称 HPT,是一种在机器学习项目中非常常用的模型优化技术。在本节中,我们将了解超参数、调整它们的重要性以及寻找机器学习算法最佳超参数的不同方法。
什么是超参数?
当我们训练一个机器学习系统时,我们基本上有三类数据 – 输入数据、模型参数和模型超参数。输入数据指的是与我们要解决的问题相关的训练或测试数据。模型参数是我们修改的变量,我们试图调整它们以适应训练数据。另一方面,模型超参数是控制训练过程本身的变量。这些超参数在我们开始训练模型之前是固定的。例如,学习率、优化器、批量大小、神经网络中的隐藏层数量以及基于树的算法中的最大深度都是模型超参数的例子。
为什么选择 HPT?
你的机器学习模型将表现如何很大程度上取决于你在训练之前选择的超参数。超参数的值可以对模型性能指标(如准确率)、训练时间、偏差、公平性等方面产生重大影响。超参数调整或 HPT 是一种模型优化技术,它为学习算法选择一组最优的超参数。相同的机器学习算法可能需要完全不同的超参数值来泛化不同的数据模式。每个 HPT 作业都与一个目标函数相关联,它试图优化(最小化或最大化)该目标函数,并返回实现该最优值的超参数值。这个目标函数可以类似于模型训练目标(例如,损失函数)或者可以是一个完全新的指标。
当我们的最终模型(即 XGBoost)固定,并且我们有固定的测试集,我们想要优化所选模型的超参数时,我们会运行模型优化操作,如 HPT 或 NAS。一个典型的 HPT 作业运行多个试验,使用不同的超参数集,并返回导致最佳试验的超参数。这里的最佳试验代表优化与 HPT 作业相关的目标函数的试验。
搜索算法
在运行超参数优化(HPT)时,我们必须决定在超参数空间中运行哪种搜索算法。根据我们的需求,我们可以从多种不同的搜索算法中进行选择。以下是一些常用的方法:
-
网格搜索
-
随机搜索
-
贝叶斯优化
让我们讨论这些方法!
网格搜索
传统的 HPT 执行方式是网格搜索,这基本上是在手动指定的搜索空间上进行穷举搜索。网格搜索必须提供一个性能指标,它试图计算所有可能的超参数组合集,这些组合在保留的验证集(或训练集上的交叉验证)上测量。由于它运行所有提供的超参数范围的组合,因此设置这些范围时必须小心,并使用离散值。由于网格搜索独立运行所有试验,因此它可以并行化以获得更快的输出。
随机搜索
与网格搜索尝试所有组合的顺序和穷举搜索不同,随机搜索在每次试验中从提供的搜索空间中随机选择超参数。由于它随机选择超参数值,它也可以泛化到连续空间,如上所述。随机搜索再次高度可并行化,因为所有试验都是独立的。尽管它很简单,但随机搜索是测试新优化或搜索技术的重要基线之一。
贝叶斯优化
与网格搜索和随机搜索不同,贝叶斯优化方法构建了一个将超参数值映射到 HPT 目标函数的概率模型。因此,在每次新的试验中,它都会更好地了解它应该采取的方向,以找到给定目标函数在固定验证集上的最优超参数。它试图平衡探索和利用,并且已经证明在更少的试验中可以获得比先前技术更好的结果。但是,由于它从正在进行的试验中学习,它通常迭代地运行试验(因此它不太并行化)。
现在我们已经对 HPT 有了很好的理解,让我们了解如何在 Vertex AI 上设置和启动 HPT 作业。
在 Vertex AI 上设置 HPT 作业
在本节中,我们将学习如何使用 Vertex AI 设置 HPT 作业。我们将使用第七章,使用 Vertex AI 训练完全自定义的机器学习模型中相同的神经网络模型实验,并优化其超参数以获得最佳模型设置。
第一步是在 Vertex AI Workbench 中创建一个新的 Jupyter Notebook 并导入有用的库:
import numpy as np
import glob
import matplotlib.pyplot as plt
import os
import google.cloud.aiplatform as aiplatform
from google.cloud.aiplatform import hyperparameter_tuning as hpt
from datetime import datetime
TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
%matplotlib inline
接下来,我们设置项目配置:
PROJECT_ID='************'
REGION='us-west2'
SERVICE_ACCOUNT='417xxxxxxxxx7-compute@developer.gserviceaccount.com'
BUCKET_URI='gs://my-training-artifacts'
然后,我们初始化 Vertex AI SDK:
aiplatform.init(project=PROJECT_ID, location=REGION, \
staging_bucket=BUCKET_URI)
下一步是将完整的训练应用程序代码容器化。我们将把完整的训练代码放入一个 Python 文件中,即task.py。task.py文件应该包含整个流程,包括以下内容:
-
加载和准备训练数据
-
定义模型架构
-
训练模型(运行具有给定超参数作为参数的试验)
-
保存模型(可选)
-
将训练试验输出传递给
hypertune()方法
训练脚本应该有一个它想要调整的超参数列表,定义为参数:
def get_args():
'''Parses args. Must include all hyperparameters you want to tune.'''
parser = argparse.ArgumentParser()
parser.add_argument(
'--epochs',
required=True,
type=int,
help='training epochs')
parser.add_argument(
'--steps_per_epoch',
required=True,
type=int,
help='steps_per_epoch')
同样,我们还有其他重要的超参数,如学习率、批量大小、损失函数等:
parser.add_argument(
'--learning_rate',
required=True,
type=float,
help='learning rate')
parser.add_argument(
'--batch_size',
required=True,
type=int,
help='training batch size')
parser.add_argument(
'--loss',
required=True,
type=str,
help='loss function')
args = parser.parse_args()
return args
脚本应该有一个函数用于加载和准备训练集和验证集:
def make_datasets_unbatched():
# Load train, validation and test sets
dest = 'gs://data-bucket-417812395597/'
train_x = np.load(BytesIO(
file_io.read_file_to_string(dest+'train_x', \
binary_mode=True)
))
train_y = np.load(BytesIO(
file_io.read_file_to_string(dest+'train_y', \
binary_mode=True)
))
同样,验证集和测试数据部分也被加载:
val_x = np.load(BytesIO(
file_io.read_file_to_string(dest+'val_x', \
binary_mode=True)
))
val_y = np.load(BytesIO(
file_io.read_file_to_string(dest+'val_y', \
binary_mode=True)
))
test_x = np.load(BytesIO(
file_io.read_file_to_string(dest+'test_x', \
binary_mode=True)
))
test_y = np.load(BytesIO(
file_io.read_file_to_string(dest+'test_y', \
binary_mode=True)
))
return train_x, train_y, val_x, val_y, test_x, test_y
前一个函数从 GCS 加载已准备好的数据集。我们可以参考第七章,使用 Vertex AI 训练完全自定义的机器学习模型,以全面了解数据准备部分。
接下来,我们定义TensorFlow(TF)模型架构:
def tf_model():
black_n_white_input = tensorflow.keras.layers.Input(shape=(80, 80, 1))
enc = black_n_white_input
我们接下来定义模型的编码器部分:
#Encoder part
enc = tensorflow.keras.layers.Conv2D(
32, kernel_size=3, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
enc = tensorflow.keras.layers.Conv2D(
64, kernel_size=3, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
同样,我们将定义另外两个编码器层,具有递增的过滤器数量、3 的内核大小和 2 的步长,以便我们可以将图像压缩到重要的特征:
enc = tensorflow.keras.layers.Conv2D(
128, kernel_size=3, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
enc = tensorflow.keras.layers.Conv2D(
256, kernel_size=1, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.Dropout(0.5)(enc)
在同一函数内定义 TF 模型的解码器部分:
#Decoder part
dec = enc
dec = tensorflow.keras.layers.Conv2DTranspose(
256, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2DTranspose(
128, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
如我们所见,解码器的设计几乎与编码器部分相反。在这里,我们通过使用多层转置卷积和逐步减少通道到 3 来从压缩特征中重新创建图像,以生成最终的彩色图像输出:
dec = tensorflow.keras.layers.Conv2DTranspose(
64, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2DTranspose(
32, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2D(
3, kernel_size=3, padding='same'
)(dec)
添加一个tanh激活函数以获得最终的彩色输出图像:
color_image = tensorflow.keras.layers.Activation('tanh')(dec)
return black_n_white_input, color_image
还添加了一个函数来构建和编译 TF 模型:
# Build the and compile TF model
def build_and_compile_tf_model(loss_fn, learning_rate):
black_n_white_input, color_image = tf_model()
model = tensorflow.keras.models.Model(
inputs=black_n_white_input,
outputs=color_image
)
_optimizer = tensorflow.keras.optimizers.Adam(
learning_rate=learning_rate,
beta_1=0.5
)
model.compile(
loss=loss_fn,
optimizer=_optimizer
)
return model
最后,添加一个main函数来训练模型,并将超参数调整指标值提供给hypertune()函数。在我们的案例中,我们将优化验证数据集上的损失。请参见以下代码片段:
def main():
args = get_args()
设置配置并加载数据:
NUM_WORKERS = strategy.num_replicas_in_sync
# Global batch size should be scaled as per the number # of workers used in training. GLOBAL_BATCH_SIZE = args.batch_size * NUM_WORKERS
MODEL_DIR = os.getenv("AIP_MODEL_DIR")
train_x, train_y, val_x, val_y, _, _ = \
make_datasets_unbatched()
现在,让我们构建 TF 模型并将其拟合到训练数据上:
with strategy.scope():
# Creation of dataset, and model building/compiling need to be within
# `strategy.scope()`.
model = build_and_compile_tf_model(args.loss, \
args.learning_rate)
history = model.fit(
train_x,
train_y,
batch_size=GLOBAL_BATCH_SIZE,
epochs=args.epochs,
steps_per_epoch=args.steps_per_epoch,
validation_data=(val_x, val_y),
)
model.save(MODEL_DIR)
使用hypertune定义并报告超参数调整指标给 HPT 算法:
# DEFINE HPT METRIC
hp_metric = history.history['val_loss'][-1]
hpt = hypertune.HyperTune()
hpt.report_hyperparameter_tuning_metric(
hyperparameter_metric_tag='val_loss',
metric_value=hp_metric,
global_step=args.epochs)
如果我们将所有这些放入一个单独的 Python 文件中,我们的task.py文件应该看起来像以下这样:
%%writefile task.py
# Single, Mirror and Multi-Machine Distributed Training
加载我们任务的所有依赖项:
import tensorflow as tf
import tensorflow
from tensorflow.python.client import device_lib
import argparse
import os
import sys
from io import BytesIO
import numpy as np
from tensorflow.python.lib.io import file_io
import hypertune
解析参数,其中我们定义了用于调整的超参数:
def get_args():
'''Parses args. Must include all hyperparameters you want to tune.'''
parser = argparse.ArgumentParser()
parser.add_argument(
'--epochs',
required=True,
type=int,
help='training epochs')
parser.add_argument(
'--steps_per_epoch',
required=True,
type=int,
help='steps_per_epoch')
定义一些与超参数相关的更多参数,用于调整学习率、批大小和损失函数:
parser.add_argument(
'--learning_rate',
required=True,
type=float,
help='learning rate')
parser.add_argument(
'--batch_size',
required=True,
type=int,
help='training batch size')
parser.add_argument(
'--loss',
required=True,
type=str,
help='loss function')
args = parser.parse_args()
return args
设置用于训练的配置:
print('Python Version = {}'.format(sys.version))
print('TensorFlow Version = {}'.format(tf.__version__))
print('TF_CONFIG = {}'.format(os.environ.get('TF_CONFIG', \
'Not found')))
print('DEVICES', device_lib.list_local_devices())
根据要求定义基于训练分布策略的配置设置——可以是单一、镜像或多工作者策略:
DISTRIBUTE='single'
if DISTRIBUTE == 'single':
if tf.test.is_gpu_available():
strategy = tf.distribute.OneDeviceStrategy(device="/gpu:0")
else:
strategy = tf.distribute.OneDeviceStrategy(device="/cpu:0")
# Single Machine, multiple compute device
elif DISTRIBUTE == 'mirror':
strategy = tf.distribute.MirroredStrategy()
# Multiple Machine, multiple compute device
elif DISTRIBUTE == 'multi':
strategy = tf.distribute.experimental.MultiWorkerMirroredStrategy()print('num_replicas_in_sync = {}'.format(strategy.num_replicas_in_sync))
从 GCS 存储桶加载数据并准备训练、验证和测试分区:
# Preparing dataset
BUFFER_SIZE = 10000
def make_datasets_unbatched():
# Load train, validation and test sets
dest = 'gs://data-bucket-417812395597/'
train_x = np.load(BytesIO(
file_io.read_file_to_string(dest+'train_x', \
binary_mode=True)
))
train_y = np.load(BytesIO(
file_io.read_file_to_string(dest+'train_y', \
binary_mode=True)
))
类似地,加载验证和测试分区:
val_x = np.load(BytesIO(
file_io.read_file_to_string(dest+'val_x', \
binary_mode=True)
))
val_y = np.load(BytesIO(
file_io.read_file_to_string(dest+'val_y', \
binary_mode=True)
))
test_x = np.load(BytesIO(
file_io.read_file_to_string(dest+'test_x', \
binary_mode=True)
))
test_y = np.load(BytesIO(
file_io.read_file_to_string(dest+'test_y', \
binary_mode=True)
))
return train_x, train_y, val_x, val_y, test_x, test_y
定义将黑白图像转换为彩色图像的 TF 模型架构:
def tf_model():
black_n_white_input = tensorflow.keras.layers.Input(shape=(80, 80, 1))
enc = black_n_white_input
定义模型编码器部分:
#Encoder part
enc = tensorflow.keras.layers.Conv2D(
32, kernel_size=3, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
enc = tensorflow.keras.layers.Conv2D(
64, kernel_size=3, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
类似地,我们将定义两个额外的编码器层,具有递增的过滤器数量、3 的核大小和 2 的步长,以便我们可以将图像压缩到重要的特征:
enc = tensorflow.keras.layers.Conv2D(
128, kernel_size=3, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(enc)
enc = tensorflow.keras.layers.Conv2D(
256, kernel_size=1, strides=2, padding='same'
)(enc)
enc = tensorflow.keras.layers.LeakyReLU(alpha=0.2)(enc)
enc = tensorflow.keras.layers.Dropout(0.5)(enc)
定义模型的解码器部分:
#Decoder part
dec = enc
dec = tensorflow.keras.layers.Conv2DTranspose(
256, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2DTranspose(
128, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
如我们所见,解码器设计几乎与编码器部分相反。在这里,我们通过使用多层转置卷积重新创建图像,并逐步减少通道到 3 以生成最终的彩色图像输出:
dec = tensorflow.keras.layers.Conv2DTranspose(
64, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2DTranspose(
32, kernel_size=3, strides=2, padding='same'
)(dec)
dec = tensorflow.keras.layers.Activation('relu')(dec)
dec = tensorflow.keras.layers.BatchNormalization(momentum=0.8)(dec)
dec = tensorflow.keras.layers.Conv2D(
3, kernel_size=3, padding='same'
)(dec)
最后,使用tanh激活函数生成彩色图像输出:
color_image = tensorflow.keras.layers.Activation('tanh')(dec)
return black_n_white_input, color_image
以下函数将为我们构建和编译 TF 模型:
`# Build the and compile TF model
def build_and_compile_tf_model(loss_fn, learning_rate):
black_n_white_input, color_image = tf_model()
model = tensorflow.keras.models.Model(
inputs=black_n_white_input,
outputs=color_image
)
_optimizer = tensorflow.keras.optimizers.Adam(
learning_rate=learning_rate,
beta_1=0.5
)
model.compile(
loss=loss_fn,
optimizer=_optimizer
)
return model
现在,让我们定义主函数以开始执行我们的训练和调整任务。在这里,num_replicas_in_sync参数定义了在多工作者训练策略中不同工作者上并行运行多少个训练任务:
def main():
args = get_args()
NUM_WORKERS = strategy.num_replicas_in_sync
# Here the batch size scales up by number of workers since
# `tf.data.Dataset.batch` expects the global batch size.
GLOBAL_BATCH_SIZE = args.batch_size * NUM_WORKERS
MODEL_DIR = os.getenv("AIP_MODEL_DIR")
加载训练和验证数据以开始训练我们的 TF 模型:
train_x, train_y, val_x, val_y, _, _ = \
make_datasets_unbatched()
with strategy.scope():
# Creation of dataset, and model building/compiling need to be within
# `strategy.scope()`.
model = build_and_compile_tf_model(args.loss, \
args.learning_rate)
history = model.fit(
train_x,
train_y,
batch_size=GLOBAL_BATCH_SIZE,
epochs=args.epochs,
steps_per_epoch=args.steps_per_epoch,
validation_data=(val_x, val_y),
)
model.save(MODEL_DIR)
最后,使用hypertune包定义 HPT 指标:
# DEFINE HPT METRIC
hp_metric = history.history['val_loss'][-1]
hpt = hypertune.HyperTune()
hpt.report_hyperparameter_tuning_metric(
hyperparameter_metric_tag='val_loss',
metric_value=hp_metric,
global_step=args.epochs)
if __name__ == "__main__":
main()
接下来,我们在 GCS 中创建一个用于存储 HPT 作业中试验结果等工件的中转存储桶:
BUCKET_URI = "gs://hpt-staging" # @param {type:"string"}
if BUCKET_URI == "" or BUCKET_URI is None or BUCKET_URI == "gs://[your-bucket-name]":
BUCKET_URI = "gs://" + PROJECT_ID + "aip-" + TIMESTAMP
! gsutil mb -l {REGION} -p {PROJECT_ID} {BUCKET_URI}
GCS_OUTPUT_BUCKET = BUCKET_URI + "/output/"
下一步是将task.py文件中定义的整个训练代码容器化。超参数调整作业将使用此容器以不同的超参数作为参数启动不同的试验:
%%writefile Dockerfile
FROM gcr.io/deeplearning-platform-release/tf2-gpu.2-8
WORKDIR /
# Installs hypertune library
RUN pip install cloudml-hypertune
# Copies the trainer code to the Docker image.
COPY task.py .
# Sets up the entry point to invoke the trainer.
ENTRYPOINT ["python", "-m", "task"]
我们的 Dockerfile 已经准备好了——让我们构建并推送 Docker 镜像到Google Container Registry(GCR):
PROJECT_NAME="*******-project"
IMAGE_URI = (
f"gcr.io/{PROJECT_NAME}/example-tf-hptune:latest"
)
! docker build ./ -t $IMAGE_URI
! docker push $IMAGE_URI
现在我们已经准备好了包含所有所需训练代码的容器镜像。让我们配置 HPT 作业。
首先,我们定义我们希望试验运行的机器类型。机器规格将取决于模型的大小和训练数据集的大小。由于这是一个小实验,我们将使用 n1-standard-8 机器来运行它:
# The spec of the worker pools including machine type and Docker image
# Be sure to replace PROJECT_ID in the `image_uri` with your project.
worker_pool_specs = [
{
"machine_spec": {
"machine_type": "n1-standard-8",
"accelerator_type": None,
"accelerator_count": 0,
},
"replica_count": 1,
"container_spec": {
"image_uri": f"gcr.io/{PROJECT_NAME}/example-tf-hptune:latest"
},
}
]
注意,在工作者池规格中,我们还传递了我们创建的训练镜像的路径。
接下来,我们将定义我们的作业将使用以找到最佳超参数的参数空间:
# Dictionary representing parameters to optimize.
# The dictionary key is the parameter_id, which is passed into your training
# job as a command line argument,
# And the dictionary value is the parameter specification of the metric.
parameter_spec = {
"learning_rate": hpt.DoubleParameterSpec(min=0.0001, \
max=0.001, scale="log"),
"epochs": hpt.DiscreteParameterSpec(values=[10, 20, \
30], scale=None),
"steps_per_epoch": hpt.IntegerParameterSpec(min=100, \
max=300, scale="linear"),
"batch_size": hpt.DiscreteParameterSpec(values=[16,32,\
64], scale=None),
"loss": hpt.CategoricalParameterSpec(["mse"]), # we can add other loss values
}
参数空间应根据最佳实践和先验知识仔细定义,以便 HPT 作业不必在无关紧要的超参数范围内执行不必要的试验。
接下来,我们需要定义指标规格。在我们的案例中,因为我们正在尝试优化验证损失值,我们希望最小化它。在准确度的情况下,我们应该最大化我们的指标:
metric_spec = {"val_loss": "minimize"}
Vertex AI HPT 作业默认使用贝叶斯优化方法来找到我们设置的最好超参数。我们还有使用其他优化方法的选择。由于贝叶斯优化在大多数情况下效果最好,我们将在实验中使用它。
接下来,我们定义将运行我们的超参数调整试验的自定义作业:
my_custom_job = aiplatform.CustomJob(
display_name="example-tf-hpt-job",
worker_pool_specs=worker_pool_specs,
staging_bucket=GCS_OUTPUT_BUCKET,
)
最后,我们定义将使用前面定义的自定义作业启动试验的 HPT 作业:
hp_job = aiplatform.HyperparameterTuningJob(
display_name="example-tf-hpt-job",
custom_job=my_custom_job,
metric_spec=metric_spec,
parameter_spec=parameter_spec,
max_trial_count=5,
parallel_trial_count=3,
)
注意,max_trial_count 和 parallel_trial_count 参数在这里非常重要:
-
max_trial_count:您需要为服务将运行的试验数量设置一个上限。更多的试验通常会导致更好的结果,但会有一个收益递减的点,在此之后,额外的试验对您试图优化的指标的影响很小或没有影响。最佳实践是从较小的试验数量开始,在扩展之前了解您选择的超参数的影响程度。 -
parallel_trial_count:如果您使用并行试验,服务将提供多个训练处理集群。增加并行试验的数量可以减少超参数调整作业运行所需的时间;然而,这可能会降低作业的整体效果。这是因为默认的调整策略使用先前试验的结果来告知后续试验值分配。如果我们保持并行试验计数等于最大试验数,那么所有试验都将并行启动,并且我们将最终运行一个“随机参数搜索”,因为没有从先前试验的性能中学习的空间。
现在我们已经准备好了,我们可以启动 HPT 任务:
hp_job.run()
一旦启动作业,它就会提供一个链接到云控制台 UI,在那里我们可以监控我们的 HPT 试验和作业的进度。云控制台 UI 的外观类似于 图 9*.1* 中所示。
图 9.1 – 在云控制台 UI 中监控 HPT 任务
现在我们已经成功理解和启动了在 Vertex AI 上的 HPT 作业,让我们跳到下一部分,了解 NAS 模型优化技术。
什么是 NAS 以及它与 HPT 有何不同?
人工神经网络或ANNs今天被广泛用于解决复杂的机器学习问题。大多数时候,这些网络架构是由机器学习专家手工设计的,这并不总是最优的。神经架构搜索或NAS是一种自动化设计神经网络架构的过程的技术,通常比手工设计的网络表现更好。
虽然 HPT 和 NAS 都被用作模型优化技术,但它们的工作方式存在某些差异。HPT 假设一个给定的架构,并专注于优化导致最佳模型的超参数。HPT 优化超参数,如学习率、优化器、批量大小、激活函数等。另一方面,NAS 专注于优化特定于架构的参数(在某种程度上,它自动化了设计神经网络架构的过程)。NAS 优化参数,如层数、单元数、层之间的连接类型等。使用 NAS,我们可以根据准确性、延迟、内存、这些特性的组合或自定义指标来搜索最优的神经网络架构。
NAS 通常与比 HPT 更大的搜索空间一起工作,并控制网络架构的不同方面。然而,解决的根本问题是与 HPT 优化相同的。有许多基于 NAS 的优化方法,但在高层次上,任何 NAS 方法都有三个主要组件,如下所示:
-
搜索空间
-
优化方法
-
评估方法
让我们更深入地了解这些组件的每一个。
搜索空间
此组件控制要考虑的可能神经网络架构的集合。搜索空间通常是特定于问题的,例如,与视觉相关的问题可能会有卷积神经网络(CNN)层的可能性。然而,识别最佳架构的过程是通过 NAS 自动化的。仔细设计这些搜索空间仍然依赖于人类的专业知识。
优化方法
此组件决定如何导航搜索空间以找到给定应用的最佳可能架构。许多不同的优化方法已经应用于 NAS,例如强化学习(RL)、贝叶斯优化、基于梯度的优化、进化搜索等。这些方法中的每一种都有其评估架构的独特方式,但高层次的目标是专注于提供更好性能的搜索空间区域。这一方面的 NAS 与 HPT 优化方法相当相似。
评估方法
评估方法是用于评估所选优化方法设计的架构质量的一个组件。评估神经架构的一个简单方法是对其进行完全训练,但这种方法在计算上相当昂贵。作为替代,为了使神经架构搜索(NAS)更高效,已经开发出了部分训练和评估方法。为了提供更便宜的神经网络质量启发式度量,一些评估方法已经被开发出来。这些评估方法非常特定于 NAS,并利用神经网络的基本结构来估计网络的质量。这些方法的一些例子包括权重共享、超网络、网络形态学等。这些针对 NAS 的特定评估方法在实际上比完全训练便宜得多。
我们现在对 NAS 优化方法及其工作原理有了很好的理解。接下来,让我们探索 Vertex AI 提供的产品及其在 Google Cloud 上启动 NAS 的功能。
Vertex AI NAS 概述
Vertex AI NAS 是一种优化技术,可以用来为给定的机器学习用例找到最佳的神经网络架构。基于 NAS 的优化在准确性方面寻找最佳网络,但也可以通过其他约束来增强,例如延迟、内存或根据需求定制的指标。一般来说,可能的神经网络搜索空间可以相当大,NAS 可能支持高达 10²⁰ 的搜索空间。在过去的几年里,NAS 已经能够成功生成一些最先进的计算机视觉网络架构,包括 NASNet、MNasNet、EfficientNet、SpineNet、NAS-FPN 等。
虽然看起来可能很复杂,但 NAS 功能非常灵活且易于使用。初学者可以利用预构建的模块来探索搜索空间、训练脚本和 Jupyter 笔记本,以在自定义数据集上开始探索 Vertex AI NAS。如果您是专家,您可能开发自定义训练脚本、自定义搜索空间、自定义评估方法,甚至为非视觉用例开发应用程序。
Vertex AI 可以用来探索针对我们定制架构和用例的完整 NAS 功能集。以下是 Vertex AI 为我们提供的一些帮助,以便更方便地实现 NAS:
-
Vertex AI 提供了一种特定的 NAS 语言,可以利用它来定义一个自定义搜索空间,以尝试所需的可能神经网络架构集合,并将此空间与我们的自定义训练脚本集成。
-
预构建的最先进的搜索空间和训练器,可以立即使用并在 GPU 上运行。
-
一个预定义的 NAS 控制器,它从我们自定义定义的搜索空间中采样,以找到最佳的神经网络架构。
-
一组预构建的库和 Docker 镜像,可以用来在自定义硬件设置上计算延迟、FLOPS(每秒浮点运算次数)或内存使用情况。
-
Google Cloud 提供教程来解释 NAS 的使用方法。它还提供了设置 NAS 以高效运行 PyTorch 应用程序的示例和指导。
-
预构建工具用于设计代理任务。
-
有库支持可以利用,以报告自定义定义的指标并对它们进行分析。
-
Google Cloud 控制台在监控和管理 NAS 作业方面非常有帮助。我们还获得了一些易于使用的示例笔记本,以启动搜索。
-
基于每个项目或每个作业的 CPU/GPU 资源使用管理,借助预构建库。
-
一个 NAS 客户端,用于构建 Docker 镜像、启动 NAS 作业以及恢复基于 Python 的旧 NAS 搜索作业。
-
客户支持是基于 Google Cloud 控制台用户界面的。
这些功能可以帮助我们在不花费太多努力的情况下设置定制的 NAS 作业。现在让我们讨论一些与 NAS 一起工作的最佳实践。
NAS 最佳实践
这里需要注意的重要事项是,NAS 不是一个我们应该应用于所有机器学习问题的优化方法。在决定为我们的用例运行 NAS 作业之前,有一些事情需要牢记。以下是一些最佳实践:
-
NAS 不适用于调整模型的超参数。它只执行架构搜索,不建议比较这两种方法的成果。在某些配置中,HPT 可以在 NAS 之后进行。
-
NAS 不建议用于较小的或高度不平衡的数据集。
-
NAS 成本高昂,除非我们可以在没有极高期望的情况下花费几千美元,否则它不适合我们。
-
你应该首先尝试其他传统和常规的机器学习方法和技术,如超参数调整。只有当你没有看到传统方法带来进一步收益时,才应使用神经架构搜索。
由于预构建资源和公开发布的代码示例,在 Vertex AI 上设置 NAS 作业并不复杂。有了这些预构建功能、示例和最佳实践,我们应该能够设置一个定制的 NAS 作业,帮助我们找到满足项目目标的最佳架构。
摘要
在本章中,我们讨论了将模型优化技术应用于我们的应用程序以获得最佳性能的重要性。我们学习了两种模型优化方法——HPT 和 NAS,以及它们的相似之处和不同之处。我们还通过代码示例学习了如何在 Vertex AI 上设置和启动大规模 HPT 作业。此外,我们还讨论了一些最佳实践,以充分利用 HPT 和 NAS。
在阅读本章之后,你应该对“模型优化”这个术语及其在开发机器学习应用程序中的重要性有一个公正的理解。此外,你现在应该对使用 Google Cloud 上的 Vertex AI 工具快速设置从小型到大型规模的超参数调整实验充满信心。你还应该对 NAS、它与 HPT 的区别以及设置 NAS 作业的最佳实践有一个公正的理解。
现在我们已经了解了模型优化技术的重要性以及常见的方法,我们具备了开发高质量模型的良好基础。接下来,让我们学习如何部署这些模型,以便它们可以被下游应用所使用。
第十章:Vertex AI 部署和自动化工具 – 通过托管 Kubeflow 管道进行编排
在典型的机器学习(ML)解决方案中,我们通常会有很多应用程序和服务作为端到端工作流程的一部分。如果我们尝试使用一些定制的脚本和 cron 作业将这些服务和应用程序拼接在一起,管理工作流程就会变得非常棘手。因此,利用一些编排服务来仔细管理、扩展和监控复杂的工作流程变得非常重要。编排是将多个应用程序或服务拼接在一起以构建端到端解决方案工作流程的过程。Google Cloud 提供了多个编排服务,例如 Cloud Scheduler、Workflows 和 Cloud Composer,以大规模管理复杂的工作流程。Cloud Scheduler 非常适合单一、重复的任务,Workflows 更适合复杂的多服务编排,而 Cloud Composer 非常适合数据驱动的负载。
ML 工作流程有很多步骤,从数据准备到模型训练、评估等。除此之外,监控和版本跟踪变得更加具有挑战性。在本章中,我们将学习关于 GCP 工具如何有效地编排 ML 工作流程。本章涵盖的主要主题如下:
-
使用 Vertex AI 管道(托管 Kubeflow 管道)编排 ML 工作流程
-
使用 Cloud Composer(托管 Airflow)编排 ML 工作流程
-
Vertex AI 管道与 Cloud Composer 的比较
-
在 Vertex AI 上获取预测
-
管理 Vertex AI 上部署的模型
技术要求
本章中展示的代码示例可以在以下 GitHub 仓库中找到:github.com/PacktPublishing/The-Definitive-Guide-to-Google-Vertex-AI/tree/main/Chapter10
使用 Vertex AI 管道(托管 Kubeflow 管道)编排 ML 工作流程
ML 解决方案复杂且涉及许多步骤,包括数据准备、特征工程、模型选择、模型训练、测试、评估和部署。除此之外,在生产过程中跟踪和版本控制与 ML 模型相关的许多方面也非常重要。GCP 上的 Vertex AI 管道让我们能够以易于组合、共享和重现的方式对 ML 工作流程进行编码。Vertex AI 管道可以以完全托管的方式运行 Kubeflow 以及基于TensorFlow Extended(TFX)的 ML 管道。在本节中,我们将学习如何开发用于 ML 开发的 Kubeflow 管道作为 Vertex AI 管道。
Kubeflow 是一个 Kubernetes 原生解决方案,它简化了 ML 管道的编排,使得实验变得简单且可重复。此外,管道是可共享的。它提供了对执行监控、工作流程调度、元数据记录和版本控制等框架的支持。Kubeflow 管道是 ML 工作流程的描述,它将工作流程的多个小组件组合成一个有向无环图(DAG)。在幕后,它将在容器上运行管道组件,这提供了可移植性、可重复性和封装。每个管道组件是 ML 工作流程中的一步,执行特定的任务。一个组件的输出可能成为另一个组件的输入,依此类推。每个管道组件由代码组成,打包成执行管道中一步的 Docker 镜像,并在一个或多个 Kubernetes Pod 上运行。Kubeflow 管道可以用于 ETL 和 CI/CD 任务,但它们更常用于运行 ML 工作流程。
Vertex AI SDK 允许我们从 Jupyter Notebook 内部以编程方式创建和上传 Kubeflow 管道,但我们也可以使用控制台 UI 来处理管道。Vertex AI UI 允许我们可视化管道执行图。它还允许我们跟踪、监控和比较不同的管道执行。
使用 Python 开发 Vertex AI 管道
在本节中,我们将使用 Vertex AI SDK 在 Jupyter Notebook 中开发和启动一个简单的基于 Kubeflow 的 Vertex 管道。在这个例子中,我们将使用开源的葡萄酒质量数据集。让我们开始吧!
打开一个 Jupyter Notebook 并安装一些有用的库:
!pip3 install google-cloud-aiplatform
!pip3 install kfp --upgrade
!pip install google_cloud_pipeline_components
在一个新的单元格中,导入用于 Vertex 管道开发的库:
from typing import NamedTuple
import typing
import pandas as pd
from kfp.v2 import dsl
from kfp.v2.dsl import (Artifact, Dataset, Input, Model, Output, Metrics, ClassificationMetrics, component, OutputPath, InputPath)
from kfp.v2 import compiler
from google.cloud import bigquery
from google.cloud import aiplatform
from google.cloud.aiplatform import pipeline_jobs
from google_cloud_pipeline_components import aiplatform as gcc_aip
创建一个时间戳变量。它将有助于为管道对象创建唯一的名称:
from datetime import datetime
TIMESTAMP =datetime.now().strftime("%Y%m%d%H%M%S")
现在,我们将设置一些与项目相关的配置,例如project_id、区域、暂存桶和服务账户:
PROJECT_ID='417xxxxxxx97'
REGION='us-west2'
SERVICE_ACCOUNT='417xxxxxxx97-compute@developer.gserviceaccount.com'
BUCKET_URI='gs://my-training-artifacts'
在本节中,我们将使用葡萄酒质量数据集。葡萄酒质量数据集由 Cortez, Paulo, Cerdeira, A., Almeida, F., Matos, T. 和 Reis, J. (2009) 创建。您可以在以下链接查看:doi.org/10.24432/C56S3T。 (UCI 机器学习仓库。)
接下来,我们在笔记本单元格中加载并检查葡萄酒质量数据集,以了解数据和列:
df_wine = pd.read_csv("http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv", delimiter=";")
df_wine.head()
此代码片段的输出显示在图 10.1中。
图 10.1 – 葡萄酒质量数据集概述
这里是特征列的快速概述:
-
挥发性酸度:挥发性酸度列表示气态酸的含量 -
固定酸度:葡萄酒中发现的固定酸量,可以是酒石酸、琥珀酸、柠檬酸、苹果酸等等 -
残糖:此列表示葡萄酒发酵后剩余的糖量 -
柠檬酸:柠檬酸的含量,它天然存在于水果中 -
氯化物:葡萄酒中的盐含量 -
游离二氧化硫:二氧化硫,或 SO2,可以防止葡萄酒氧化和变质 -
总二氧化硫:葡萄酒中 SO2 的总含量 -
pH 值:pH 值用于检查葡萄酒的酸度 -
密度:表示葡萄酒的密度 -
硫酸盐:硫酸盐有助于保持葡萄酒的新鲜度,并保护其免受氧化和细菌侵害 -
酒精:葡萄酒中酒精的百分比
策略是预测葡萄酒质量,给定所有前面的参数。我们将将其转换为分类问题,如果葡萄酒的质量指标值 >=7,则称其为最佳质量。
管道组件
在这个练习中,我们将为我们的任务定义四个管道组件:
-
数据加载组件
-
模型训练组件
-
模型评估组件
-
模型部署组件
在这里,第一个组件加载数据,第二个组件使用这些数据来训练模型。第三个组件在测试数据集上评估训练好的模型。第四个组件将训练好的模型自动部署为 Vertex AI 端点。我们将对自动模型部署设置条件,例如,如果模型 ROC >= 0.8,则部署模型,否则不部署。
现在,让我们逐一定义这些组件。以下是一个加载并拆分数据到训练和测试分区的第一个组件。
要创建一个 Kubeflow 组件,我们可以用@component装饰器包装我们的函数。在这里,我们可以定义基本镜像,以及需要安装的依赖项:
备注
在实际项目或生产管道中,建议在包名称旁边写出版本号,以避免任何版本相关的冲突。
@component(
packages_to_install=["pandas", "pyarrow", "scikit-learn==1.0.0"],
base_image="python:3.9",
output_component_file="load_data_component.yaml"
)
在这里,我们定义一个函数,用于加载数据并将其拆分为训练集和测试集:
def get_wine_data(
url: str,
dataset_train: Output[Dataset],
dataset_test: Output[Dataset]
):
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split as tts
df_wine = pd.read_csv(url, delimiter=";")
df_wine['best_quality'] = [1 if x>=7 else 0 for x in df_wine.quality]
df_wine['target'] = df_wine.best_quality
df_wine = df_wine.drop(
['quality', 'total sulfur dioxide', 'best_quality'],
axis=1,
)
我们将保留大约 30%的数据用于测试,其余的用于训练,并将它们保存为 CSV 文件:
train, test = tts(df_wine, test_size=0.3)
train.to_csv(
dataset_train.path + ".csv",
index=False,
encoding='utf-8-sig',
)
test.to_csv(
dataset_test.path + ".csv",
index=False,
encoding='utf-8-sig',
)
要定义一个组件,我们可以用@component装饰器包装我们的 Python 函数。它允许我们传递基本镜像路径、要安装的包,如果需要将组件写入文件,还可以指定 YAML 文件路径。组件的 YAML 文件定义使其具有可移植性和可重用性。我们可以简单地创建一个包含组件定义的 YAML 文件,并在项目的任何位置加载此组件。请注意,我们还可以使用包含所有自定义依赖项的自定义容器镜像。
第一个组件实际上加载了葡萄酒质量数据集表,创建了之前讨论的二进制分类输出,删除了不必要的列,并将其最终分为训练文件和测试文件。在这里,训练和测试数据集文件是该组件的输出工件,可以在随后运行的组件中重用。
现在,让我们定义第二个组件,该组件在第一个组件生成的训练数据集上训练一个随机森林分类器。
第一步是装饰器,包括依赖项:
@component(
packages_to_install = [
"pandas",
"scikit-learn"
],
base_image="python:3.9",
output_component_file="model_training_component.yml",
)
接下来,我们定义我们的训练函数,它将模型拟合到训练数据上,并将其保存为 Pickle 文件。在这里,我们的输出工件将是一个模型,我们还可以将其与一些元数据相关联,如下面的函数所示。在这个函数内部,我们可以通过将元数据键和值放入 model.metadata 字典中来将模型工件与元数据关联。
def train_winequality(
dataset: Input[Dataset],
model: Output[Model],
):
from sklearn.ensemble import RandomForestClassifier
import pandas as pd
import pickle
data = pd.read_csv(dataset.path+".csv")
model_rf = RandomForestClassifier(n_estimators=10)
model_rf.fit(
data.drop(columns=["target"]),
data.target,
)
model.metadata["framework"] = "RF"
file_name = model.path + f".pkl"
with open(file_name, 'wb') as file:
pickle.dump(model_rf, file)
此组件在训练数据集上训练随机森林分类器模型,并将模型保存为 Pickle 文件。
接下来,让我们定义模型评估的第三个组件。我们以 @component 装饰器开始:
@component(
packages_to_install = [
"pandas",
"scikit-learn"
],
base_image="python:3.9",
output_component_file="model_evaluation_component.yml",
)
现在,我们定义实际的 Python 函数用于模型评估:
def winequality_evaluation(
test_set: Input[Dataset],
rf_winequality_model: Input[Model],
thresholds_dict_str: str,
metrics: Output[ClassificationMetrics],
kpi: Output[Metrics]
) -> NamedTuple("output", [("deploy", str)]):
from sklearn.ensemble import RandomForestClassifier
import pandas as pd
import logging
import pickle
from sklearn.metrics import roc_curve, confusion_matrix, accuracy_score
import json
import typing
这里是一个小的函数,用于控制模型的部署。只有当新模型的准确度高于某个阈值时,我们才部署新模型:
def threshold_check(val1, val2):
cond = "false"
if val1 >= val2 :
cond = "true"
return cond
data = pd.read_csv(test_set.path+".csv")
model = RandomForestClassifier()
file_name = rf_winequality_model.path + ".pkl"
with open(file_name, 'rb') as file:
model = pickle.load(file)
y_test = data.drop(columns=["target"])
y_target=data.target
y_pred = model.predict(y_test)
现在我们有了模型输出,我们可以计算准确度分数和 roc_curve,并将它们作为元数据记录:
y_scores = model.predict_proba(
data.drop(columns=["target"])
)[:, 1]
fpr, tpr, thresholds = roc_curve(
y_true=data.target.to_numpy(),
y_score=y_scores, pos_label=True
)
metrics.log_roc_curve(
fpr.tolist(),
tpr.tolist(),
thresholds.tolist()
)
metrics.log_confusion_matrix(
["False", "True"],
confusion_matrix(
data.target, y_pred
).tolist(),
)
最后,我们检查模型准确度,看它是否满足部署条件。我们从这里返回部署条件标志:
accuracy = accuracy_score(data.target, y_pred.round())
thresholds_dict = json.loads(thresholds_dict_str)
rf_winequality_model.metadata["accuracy"] = float(accuracy)
kpi.log_metric("accuracy", float(accuracy))
deploy = threshold_check(float(accuracy), int(thresholds_dict['roc']))
return (deploy,)
此组件使用组件 1(测试数据集)和组件 2(训练模型)的输出作为输入,并执行模型评估。此组件执行以下操作:
-
加载测试数据集
-
从 Pickle 文件中加载训练好的模型
-
将 ROC 曲线和混淆矩阵作为输出工件记录
-
检查模型准确度是否大于阈值
最后,我们定义模型部署组件。如果部署条件为 true,则该组件会自动将训练好的模型作为 Vertex AI 端点部署:
@component(
packages_to_install=["google-cloud-aiplatform", "scikit-learn", "kfp"],
base_image="python:3.9",
output_component_file="model_winequality_component.yml"
)
接下来,我们定义一个函数,当部署条件为真时,将部署葡萄酒质量模型。这个函数将被之前定义的 @component 装饰器包装起来,这样我们就可以在最终的管道定义中使用它:
def deploy_winequality(
model: Input[Model],
project: str,
region: str,
serving_container_image_uri : str,
vertex_endpoint: Output[Artifact],
vertex_model: Output[Model]
):
from google.cloud import aiplatform
aiplatform.init(project=project, location=region)
DISPLAY_NAME = "winequality"
MODEL_NAME = "winequality-rf"
ENDPOINT_NAME = "winequality_endpoint"
在这里,我们定义一个函数来创建我们模型的端点,以便我们可以用它进行推理:
def create_endpoint():
endpoints = aiplatform.Endpoint.list(
filter='display_name="{}"'.format(ENDPOINT_NAME),
order_by='create_time desc',
project=project,
location=region,
)
if len(endpoints) > 0:
endpoint = endpoints[0] # most recently created
else:
endpoint = aiplatform.Endpoint.create(
display_name=ENDPOINT_NAME, project=project, location=region
)
endpoint = create_endpoint()
在这里,我们以编程方式导入我们保存的模型:
#Import a model programmatically
model_upload = aiplatform.Model.upload(
display_name = DISPLAY_NAME,
artifact_uri = model.uri.replace("model", ""),
serving_container_image_uri = serving_container_image_uri,
serving_container_health_route=f"/v1/models/{MODEL_NAME}",
serving_container_predict_route=f"/v1/models/{MODEL_NAME}:predict",
serving_container_environment_variables={
"MODEL_NAME": MODEL_NAME,
},
)
最后,我们将上传的模型部署到所需的机器类型上,并按照所需的流量分配:
model_deploy = model_upload.deploy(
machine_type="n1-standard-4",
endpoint=endpoint,
traffic_split={"0": 100},
deployed_model_display_name=DISPLAY_NAME,
)
# Save data to the output params
vertex_model.uri = model_deploy.resource_name
现在我们管道的核心组件已经准备好了,我们可以继续定义我们的 Vertex Pipeline。
首先,我们需要为我们的管道提供一个唯一的名称:
DISPLAY_NAME = 'pipeline-winequality job{}'.format(TIMESTAMP)
管道定义是我们将这些组件拼接在一起以定义我们的机器学习工作流程(或执行图)的部分。在这里,我们可以控制哪些组件先运行,以及哪个组件的输出应该被馈送到另一个组件。以下脚本定义了我们实验的一个简单管道。
我们可以使用 @dsl.pipeline 装饰器来定义一个 Kubeflow 管道。我们可以在装饰器中传递一个 pipeline_root 参数,如下面的代码所示:
@dsl.pipeline(
pipeline_root=BUCKET_URI,
name="pipeline-winequality",
)
def pipeline(
url: str = "http://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-white.csv",
project: str = PROJECT_ID,
region: str = REGION,
display_name: str = DISPLAY_NAME,
api_endpoint: str = REGION+"-aiplatform.googleapis.com",
thresholds_dict_str: str = '{"roc":0.8}',
serving_container_image_uri: str = "us-docker.pkg.dev/vertex-ai/prediction/sklearn-cpu.0-24:latest"
):
我们可以在这里创建执行 DAG 并定义预定义组件的执行顺序。某些组件可能是相互依赖的,其中一个组件的输出是另一个组件的输入。依赖组件按顺序执行,而独立的组件可以并行执行:
# adding first component
data_op = get_wine_data(url)
# second component uses output of first component as input
train_model_op = train_winequality(data_op.outputs["dataset_train"])
# add third component (uses outputs of comp1 and comp2 as input)
model_evaluation_op = winequality_evaluation(
test_set=data_op.outputs["dataset_test"],
rf_winequality_model=train_model_op.outputs["model"],
# We deploy the model only if the model performance is above the threshold
thresholds_dict_str = thresholds_dict_str,
)
这是决定是否部署此模型的条件:
# condition to deploy the model
with dsl.Condition(
model_evaluation_op.outputs["deploy"]=="true",
name="deploy-winequality",
):
deploy_model_op = deploy_winequality(
model=train_model_op.outputs['model'],
project=project,
region=region,
serving_container_image_uri = serving_container_image_uri,
)
在这里,我们使用 @dsl.pipeline 装饰器来定义我们的流水线。请注意,在先前的定义中,前三个组件很简单,但第四个组件是使用 dsl.Condition() 定义的。只有当这个条件满足时,我们才会运行模型部署组件。因此,这就是我们控制何时部署模型的方法。如果我们的模型符合业务标准,我们可以选择自动部署它。
接下来,我们可以编译我们的流水线:
compiler.Compiler().compile(
pipeline_func=pipeline,
package_path='ml_winequality.json',
)
最后,我们可以将我们的流水线作业提交到 Vertex AI:
pipeline_job = pipeline_jobs.PipelineJob(
display_name="winequality-pipeline",
template_path="ml_winequality.json",
enable_caching=False,
location=REGION,
)
pipeline_job.run()
此脚本将在 Vertex AI 中启动我们的流水线。它还将为我们提供一个控制台 URL,以便监控流水线作业。
我们也可以通过在控制台中转到 Vertex AI 选项卡并点击 流水线 选项卡来定位流水线运行。图 10.2 是 Vertex AI 中我们示例作业的执行图截图。
图 10.2 – 从 Google Cloud 控制台 UI 观看的示例 Vertex 流水线的执行图
如我们所见,这个执行图包含了我们定义的所有四个组件。它还包括由组件生成的一切工件。如果我们点击 指标 工件,我们可以在控制台 UI 的右侧面板中看到输出值。它看起来类似于 图 10.3。
图 10.3 – 与我们的流水线执行相关的元数据和工件
这就是我们可以使用 Google Cloud 控制台 UI 跟踪我们的 ML 相关工作流的执行和指标的方法。一旦我们的流水线准备就绪,我们还可以使用 Vertex AI Pipelines 的本地调度器、Cloud Scheduler(我们可以定义一个计划)、Cloud Functions(基于事件的触发器)等服务来安排其执行。
现在,我们对如何在 Google Cloud 上作为 Vertex AI Pipelines 开发 Kubeflow 流水线有了很好的理解。我们现在应该能够从头开始开发和启动我们的自定义流水线。在下一节中,我们将了解 Cloud Composer 作为工作流编排的另一种解决方案。
使用 Cloud Composer(托管 Airflow)编排 ML 工作流
Cloud Composer 是建立在 Apache Airflow 开源项目之上的 Google Cloud 上的工作流程编排服务。主要区别在于 Composer 是完全托管,并且可以非常容易地与其他 GCP 工具集成。使用 Cloud Composer,我们可以编写、执行、调度或监控我们的跨多云和混合环境也得到支持的工作流程。Composer 管道是 DAG,可以使用 Python 轻松定义和配置。它附带丰富的连接器库,让我们可以一键部署我们的工作流程。在 Google Cloud 控制台上的工作流程图形表示使得监控和故障排除非常方便。我们的 DAG 自动同步确保我们的作业始终按计划进行。
Cloud Composer 通常被数据科学家和数据工程师用于构建复杂的数据管道(ETL 或 ELT 管道)。它也可以用作 ML 工作流程的编排器。由于 Apache 项目附带数百个操作员和传感器,Cloud Composer 对于数据相关的工作流程来说非常方便,这使得我们可以用很少的代码轻松地在多个云环境中进行通信。它还允许我们定义故障处理机制,例如在管道失败时发送电子邮件或 Slack 通知。
现在,让我们了解如何开发基于 Cloud Composer 的管道。
创建 Cloud Composer 环境
我们可以按照以下步骤使用 Google Cloud 控制台 UI 创建 Cloud Composer 环境:
-
启用 Cloud Composer API。
-
从控制台左侧面板中选择 Composer,然后点击 创建 以开始创建 Composer 环境(见 图 10.4)。
图 10.4 – 在 Google Cloud 控制台上创建 Composer 环境
- 点击 创建。创建环境大约需要 15-20 分钟。一旦完成,环境页面将如下所示(见 图 10.5)。
图 10.5 – 即可使用的 Cloud Composer 环境
- 点击 Airflow 以查看 Airflow 网页 UI。Airflow 网页 UI 如 图 10.6 所示。
图 10.6 – 使用我们的工作流程的 Airflow 网页 UI
如前一个屏幕截图所示,已经有一个 DAG 在运行 – airflow_monitoring.py 文件。见 图 10.7。
图 10.7 – 我们可以放置基于 Python 的 DAG 以进行执行的 GCS 位置
现在我们 Composer 的设置已经准备好了,我们可以快速检查它是否按预期工作。为了快速测试,我们将使用 Airflow 教程中的一个演示 DAG,并将其放入这个桶的dags文件夹中。如果一切正常,我们放入这个桶中的任何 DAG 都应该会自动与 Airflow 同步。
下面的代码是从 Airflow 教程中的一个演示 DAG:
from datetime import timedelta
from textwrap import dedent
# The DAG object; we'll need this to instantiate a DAG
from airflow import DAG
# Operators; we need this to operate!
from airflow.operators.bash import BashOperator
from airflow.utils.dates import days_ago
# These args will get passed on to each operator
# You can override them on a per-task basis during operator initialization
在创建操作符时,将使用以下具有一些默认参数的字典。默认情况下,这些参数将传递给每个操作符,但根据需要,我们也可以在操作符中覆盖一些这些参数:
default_args = {
'owner': 'airflow',
'depends_on_past': False,
'email': ['airflow@example.com'],
'email_on_failure': False,
'email_on_retry': False,
'retries': 1,
'retry_delay': timedelta(minutes=5),
# 'queue': 'bash_queue',
# 'execution_timeout': timedelta(seconds=300),
# 'on_failure_callback': some_function,
# 'on_success_callback': some_other_function,
# 'on_retry_callback': another_function,
# 'sla_miss_callback': yet_another_function,
}
这是我们定义我们的 DAG 的地方,执行步骤按照所需或要求的顺序:
with DAG(
'composer-test-dag',
default_args=default_args,
description='A simple composer DAG',
schedule_interval=timedelta(days=1),
start_date=days_ago(2),
tags=['example'],
) as dag:
在这里,我们定义了我们的代码将要执行的不同任务:
# t1, t2 and t3 are examples of tasks created by instantiating operators
t1 = BashOperator(
task_id='print_date',
bash_command='date',
)
t2 = BashOperator(
task_id='sleep',
depends_on_past=False,
bash_command='sleep 5',
retries=3,
)
您可以使用以下属性来记录您的任务:doc_md(Markdown)、doc(纯文本)、doc_rst、doc_json和doc_yaml,这些属性将在 UI 的任务实例****详情页面上显示:
t1.doc_md = dedent(
)
dag.doc_md = __doc__
dag.doc_md = """a documentation placed anywhere"""
templated_command = dedent(
"""
{% for i in range(5) %}
echo "{{ ds }}"
echo "{{ macros.ds_add(ds, 7)}}"
echo "{{ params.my_param }}"
{% endfor %}
"""
)
现在让我们定义t3任务:
t3 = BashOperator(
task_id='templated',
depends_on_past=False,
bash_command=templated_command,
params={'my_param': 'Parameter I passed in'},
)
在这里,我们定义了任务的执行顺序。t1需要在t2和t3之前执行,但t2和t3可以并行执行:
t1 >> [t2, t3]
一旦我们将这个.py文件上传到 dags 文件夹中的 GCS 桶,Airflow 将自动同步它。如果您刷新 Airflow web UI,它应该显示另一个 DAG,如图图 10.8所示。
图 10.8 – Airflow web UI 中 GCS 位置中存在的所有 DAG
如果我们能在 Airflow UI 中看到我们的 DAG 正在运行,这验证了我们的安装运行正常。现在,让我们打开这个 DAG 来检查实际的执行图。它应该看起来与图 10.9中显示的类似。
图 10.9 – Airflow web UI 中我们工作流程的执行图
尽管这是一个非常简单的 DAG,但它让我们了解到使用 Cloud Composer 与 Airflow 一起工作是多么容易。我们通过 Cloud Composer 获得的日志记录和监控水平相当惊人。Cloud Composer 让数据工程师的生活变得非常容易,这样他们就可以专注于定义复杂的数据管道,而不用担心基础设施和 Airflow 管理。
我们现在对如何使用 Vertex AI Pipelines 和 Cloud Composer(托管 Airflow 服务)作为 ML 工作流程的编排器有了很好的了解。现在让我们总结一下这两者之间的一些相似之处和不同之处。
Vertex AI Pipelines 与 Cloud Composer
在本节中,我们将讨论 Vertex AI Pipelines 和 Cloud Composer 在编排 ML 工作流程方面的关键相似之处和不同之处。基于这种比较,我们可以为我们的下一个 ML 项目选择最佳解决方案。以下是一个总结两个编排器在 ML 相关任务重要方面的要点列表:
-
在任务(作曲家)或容器化组件(Vertex AI Pipelines)方面,它们都易于使用,并将整体 ML 工作流程划分为更小的执行单元。
-
在组件之间传递数据的方式相似,如果数据量很大,可能需要一个中间存储系统。
-
Vertex AI Pipelines 提供了一系列可用的预构建组件,这些组件是开源的,因此开发者可以避免编写大量的模板代码。另一方面,在基于 Composer 的管道的情况下,我们需要编写整个工作流程。
-
根据设置环境的简便性,Vertex AI Pipelines 稍微容易一些。
-
它们都运行在 Kubernetes 上,但在 Vertex AI Pipelines 的情况下,无需担心集群、Pods 等。
-
Cloud Composer 非常适合数据相关任务。我们还可以将 ML 管道作为数据任务实现,但我们会失去许多 ML 相关功能,例如血缘跟踪、指标、实验比较和分布式训练。这些功能是 Vertex AI Pipelines 的默认功能。
-
数据工程师可能会更习惯于 Composer 管道,而 ML 工程师可能会更习惯于 Vertex AI Pipelines。
-
在许多情况下,Vertex AI Pipelines 的使用成本可能更低,因为我们只为我们使用的部分付费。另一方面,在 Composer 的情况下,一些 Pods 总是处于运行状态。
-
如果需要,一些 Vertex AI Pipelines 的功能也可以与 Composer 一起使用。
-
使用 Vertex AI Pipelines 不需要了解 Kubernetes,但使用 Cloud Composer 时,了解 Kubernetes 的常见方面很重要。
在阅读了这些比较点之后,我们可能会觉得选择我们下一个机器学习(ML)用例的最佳编排器变得容易。然而,这两个编排器都易于使用,并且在各个组织中普遍用于管理它们复杂的数据/ML 相关工作流程。
现在我们已经对 Google Cloud 上的 ML 编排工具及其优缺点有了很好的理解,我们准备开始开发生产级的 ML 管道。接下来,让我们学习如何在 Vertex AI 上获取预测。
在 Vertex AI 上获取预测
在本节中,我们将学习如何在 Vertex AI 上从我们的机器学习模型中获取预测。根据用例,预测请求可以是两种类型之一——在线预测(实时)和批量预测。在线预测是对模型端点发出的同步请求。在线预测是那些需要通过 API 调用及时请求输出以更新最终用户信息的应用程序所需要的。例如,Google Maps API 提供了近实时的交通更新,并需要在线预测请求。另一方面,批量预测是异步请求。如果我们的用例只需要批量预测,我们可能不需要将模型部署到端点,因为 Vertex AI 的 batchprediction 服务也允许我们从 GCS 位置中的保存模型执行批量预测,甚至不需要创建端点。批量预测适用于那些对响应时间不敏感且可以接受延迟响应的用例(例如,电子商务公司可能希望预测未来六个月左右的销售额)。使用批量预测,我们可以通过单个请求对大量数据进行预测。
获取在线预测
在模型可以被用来处理在线预测请求之前,我们必须将其部署到端点。模型部署本质上意味着将模型及其所需的基础设施(内存和计算)保持在内存中,以便能够以低延迟提供预测。根据用例和扩展需求,我们也可以将多个模型部署到单个端点,或者将单个模型部署到多个端点。
当你使用 Vertex AI API 部署模型时,你需要完成以下步骤:
-
创建端点。
-
获取端点 ID。
-
将模型部署到端点。
我们可以使用以下 Python 示例函数来创建 Vertex AI 端点。这个函数取自官方文档(cloud.google.com/vertex-ai/docs/general/deployment#api):
def create_endpoint_sample(
project: str,
display_name: str,
location: str,
):
aiplatform.init(project=project, location=location)
endpoint = aiplatform.Endpoint.create(
display_name=display_name,
project=project,
location=location,
)
print(endpoint.display_name)
print(endpoint.resource_name)
return endpoint
第二步是获取端点 ID,这样我们就可以用它来部署我们的模型。以下 shell 命令将给出我们项目位置内所有端点的列表。如果我们有端点名称,我们可以通过端点名称进行过滤:
gcloud ai endpoints list \
--region=LOCATION \
--filter=display_name=ENDPOINT_NAME
现在我们已经获得了端点 ID,我们可以将我们的模型部署到这个端点。在部署模型的过程中,我们可以指定多个副本的参数、加速器数量、加速器类型等。以下是一个示例 Python 函数,可以用来将模型部署到指定的端点。这个示例取自 Google Cloud 文档(cloud.google.com/vertex-ai/docs/general/deployment#api):
def deploy_model_with_dedicated_resources_sample(
project,
location,
model_name: str,
machine_type: str,
endpoint: Optional[aiplatform.Endpoint] = None,
deployed_model_display_name: Optional[str] = None,
traffic_percentage: Optional[int] = 0,
traffic_split: Optional[Dict[str, int]] = None,
min_replica_count: int = 1,
max_replica_count: int = 1,
accelerator_type: Optional[str] = None,
accelerator_count: Optional[int] = None,
explanation_metadata: Optional[explain.ExplanationMetadata] = None,
explanation_parameters: Optional[explain.ExplanationParameters] = None,
metadata: Optional[Sequence[Tuple[str, str]]] = (),
sync: bool = True,
):
在这里,我们初始化 Vertex AI SDK 并将我们的模型部署到端点:
aiplatform.init(project=project, location=location)
model = aiplatform.Model(model_name=model_name)
model.deploy(
endpoint=endpoint, deployed_model_display_name=deployed_model_display_name,
traffic_percentage=traffic_percentage,
traffic_split=traffic_split,
machine_type=machine_type,
min_replica_count=min_replica_count,
max_replica_count=max_replica_count,
accelerator_type=accelerator_type,
accelerator_count=accelerator_count,
explanation_metadata=explanation_metadata,
explanation_parameters=explanation_parameters,
metadata=metadata,
sync=sync,
)
model.wait()
print(model.display_name)
print(model.resource_name)
return model
一旦我们的模型部署到端点,它就准备好提供在线预测。现在我们可以向此端点发出在线预测请求。请参见以下示例请求:
def endpoint_predict_sample(
project: str, location: str, instances: list, endpoint: str
):
aiplatform.init(project=project, location=location)
endpoint = aiplatform.Endpoint(endpoint)
prediction = endpoint.predict(instances=instances)
print(prediction)
return prediction
instances[]对象是必需的,并且必须包含要获取预测的实例列表。请参见以下示例:
{
"instances": [
[0.0, 1.1, 2.2],
[3.3, 4.4, 5.5],
...
]
}
响应体也类似。它可能看起来像以下示例。此示例与先前的模型无关;只是为了理解目的:
{
"predictions": [
{
"label": "tree",
"scores": [0.2, 0.8]
},
{
"label": "bike",
"scores": [0.85, 0.15]
}
],
"deployedModelId": 123456789012345678
}
当处理输入时出现错误时的响应如下:
{"error": "Divide by zero"}
我们现在对如何使用 Vertex AI 端点获取在线预测有了很好的了解。但并非每个用例都需要按需或在线预测。有时我们想要对大量数据进行预测,但结果并不需要立即得到。在这种情况下,我们可以利用批量预测。让我们进一步讨论如何使用 Vertex AI 获取批量预测。
获取批量预测
如前所述,批量预测请求是异步的,不需要将模型始终部署到端点。要发出批量预测请求,我们指定一个输入源和一个输出位置(可以是云存储或 BigQuery),其中 Vertex AI 存储预测结果。输入源位置必须包含我们的输入实例,格式之一为:TFRecord、JSON Lines、CSV、BigQuery 等。
TFRecord 输入实例可能看起来像以下示例:
{"instances": [
{ "b64": "b64EncodedASCIIString" },
{ "b64": "b64EncodedASCIIString" }
]}
可以通过 Vertex AI API 编程方式或使用 Google Cloud 控制台 UI 请求批量预测。由于我们可以向批量预测请求传递大量数据,因此它们可能需要很长时间才能完成,具体取决于数据量和模型大小。
使用 Python 的 Vertex AI API 的一个示例批量预测请求可能看起来像以下 Python 函数。此示例代码取自官方文档(cloud.google.com/vertex-ai/docs/predictions/get-batch-predictions):
def create_batch_prediction_job_dedicated_resources_sample(
project: str,
location: str,
model_resource_name: str,
job_display_name: str,
gcs_source: Union[str, Sequence[str]],
gcs_destination: str,
instances_format: str = "jsonl",
machine_type: str = "n1-standard-2",
accelerator_count: int = 1,
accelerator_type: Union[str, aiplatform_v1.AcceleratorType] = "NVIDIA_TESLA_K80",
starting_replica_count: int = 1,
max_replica_count: int = 1,
sync: bool = True,
):
在这里,我们初始化 Vertex AI SDK,并在我们的部署模型上调用批量预测:
aiplatform.init(project=project, location=location)
my_model = aiplatform.Model(model_resource_name)
batch_prediction_job = my_model.batch_predict(
job_display_name=job_display_name,
gcs_source=gcs_source,
gcs_destination_prefix=gcs_destination,
instances_format=instances_format,
machine_type=machine_type,
accelerator_count=accelerator_count,
accelerator_type=accelerator_type,
starting_replica_count=starting_replica_count,
max_replica_count=max_replica_count,
sync=sync,
)
batch_prediction_job.wait()
print(batch_prediction_job.display_name)
print(batch_prediction_job.resource_name)
print(batch_prediction_job.state)
return batch_prediction_job
一旦批量预测请求完成,输出将保存在指定的云存储或 BigQuery 位置。
一个jsonl输出文件可能看起来像以下示例输出:
{ "instance": [1, 2, 3, 4], "prediction": [0.1,0.9]}
{ "instance": [5, 6, 7, 8], "prediction": [0.7,0.3]}
我们现在对 Vertex AI 上的在线和批量预测工作原理有了相当的了解。将批量预测与在线预测(消除部署需求)分开的想法可以节省大量的资源和成本。接下来,让我们讨论一些与 Google Vertex AI 上部署的模型相关的重要考虑因素。
管理 Vertex AI 上的部署模型
当我们将机器学习模型部署到端点时,我们将它与物理资源(计算)关联起来,以便它可以在低延迟下提供在线预测。根据需求,我们可能希望将多个模型部署到单个端点,或者将单个模型部署到多个端点。让我们了解这两种场景。
多个模型 - 单个端点
假设我们已经在生产环境中将一个模型部署到端点,并且我们已经找到了一些改进该模型的好主意。现在,假设我们已经训练了一个改进的模型,我们想要部署,但我们也不希望对我们的应用程序进行任何突然的更改。在这种情况下,我们可以将我们的最新模型添加到现有端点,并开始用新模型服务非常小的流量百分比。如果一切看起来都很完美,我们可以逐渐增加流量,直到它服务全部 100% 的流量。
单个模型 - 多个端点
当我们想要为不同的应用环境(如测试和生产)部署具有不同资源的模型时,这很有用。其次,如果我们的某个应用程序有高性能需求,我们可以使用高性能机器的端点来提供服务,而我们可以使用低性能机器为其他应用程序提供服务,以优化运营成本。
计算资源和扩展
Vertex AI 会分配计算节点来处理在线和批量预测。当我们部署我们的机器学习模型到端点时,我们可以自定义用于服务模型的虚拟机类型。如果需要,我们可以选择使用 GPU 或 TPUs 等加速器。具有更多计算资源的机器配置可以以更低的延迟处理预测,因此可以同时处理更多的预测请求。但是,这样的机器将比计算资源较低的机器成本更高。因此,根据用例和需求选择最合适的机器非常重要。
当我们部署用于在线预测的模型时,我们还可以配置一个预测节点以自动扩展。但是,批量预测的预测节点不会自动扩展。默认情况下,如果我们部署带有或没有专用 GPU 资源的模型,Vertex AI 将自动调整副本的数量,以便 CPU 或 GPU 使用率(以较高的为准)匹配默认的 60% 目标值。在这些条件下,Vertex AI 将进行扩展,即使这可能不是达到每秒查询数(QPS)和延迟目标所必需的。我们可以监控端点以跟踪 CPU 和加速器使用率、请求数量、延迟以及当前和目标副本数量等指标。
为了从成本角度确定预测容器的理想机器类型,我们可以将其部署到虚拟机实例上,并通过发送预测请求来基准测试实例,直到虚拟机达到大约 90%的 CPU 使用率。通过在多个不同的机器上重复进行这个实验,我们可以根据 QPS 值确定预测服务的成本。
摘要
在本章中,我们学习了两种流行的机器学习工作流程编排工具——Vertex AI Pipelines(托管 Kubeflow)和 Cloud Composer(托管 Airflow)。我们还为示例用例实现了一个 Vertex Pipeline,并且类似地,我们也使用 Cloud Composer 开发并执行了一个示例 DAG。Vertex AI Pipelines 和 Cloud Composer 都是 GCP 上的托管服务,使得设置和启动复杂的机器学习和数据相关的工作流程变得非常容易。最后,我们学习了在 Vertex AI 上为我们的自定义模型进行在线和批量预测,包括一些与模型部署相关的最佳实践。
阅读本章后,你应该对在 GCP 上执行机器学习工作流程编排的不同方式及其相似之处和不同之处有一个很好的理解。现在,你应该能够编写自己的机器学习工作流程,并通过 Vertex AI Pipelines 或 Cloud Composer 在 GCP 上编排它们。最后,你应该对使用 Vertex AI 进行在线和批量预测也充满信心。
现在我们已经对在 GCP 上部署机器学习模型以及编排机器学习工作流程有了很好的理解,我们可以开始为不同的用例开发生产级别的管道。沿着相似的方向,我们将在下一章学习一些机器学习治理的最佳实践和工具。