亚马逊 Sagemaker 学习指南第二版(三)

原文:annas-archive.org/md5/9ea32aa91e5ec8d38bb57552873cedb5

译者:飞龙

协议:CC BY-NC-SA 4.0

第八章:使用你的算法和代码

在上一章中,你学习了如何使用内置框架(如 scikit-learnTensorFlow)进行模型的训练和部署。通过 脚本模式,这些框架使你能够轻松使用自己的代码,而无需管理任何训练或推理容器。

在某些情况下,你的业务或技术环境可能使得使用这些容器变得困难,甚至不可能使用。也许你需要完全控制容器的构建方式,也许你希望实现自己的预测逻辑,或者你正在使用 SageMaker 本身不原生支持的框架或语言。

在本章中,你将学习如何根据自己的需求定制训练和推理容器。你还将学习如何使用 SageMaker SDK 或命令行开源工具来训练和部署你自己的自定义代码。

本章将涵盖以下主题:

  • 理解 SageMaker 如何调用你的代码

  • 定制内置框架容器

  • 使用 SageMaker 训练工具包构建自定义训练容器

  • 使用 Python 和 R 构建完全自定义的训练和推理容器

  • 使用自定义 Python 代码在 MLflow 上进行训练和部署

  • 为 SageMaker 处理构建完全自定义容器

技术要求

你将需要一个 AWS 账户才能运行本章中的示例。如果你还没有 AWS 账户,请访问aws.amazon.com/getting-started/创建一个。你还应该熟悉 AWS 免费套餐(aws.amazon.com/free/),它允许你在一定的使用限制内免费使用许多 AWS 服务。

你需要为你的账户安装并配置 AWS 命令行界面CLI)(aws.amazon.com/cli/)。

你需要一个正常工作的 Python 3.x 环境。安装 Anaconda 发行版(www.anaconda.com/)不是强制性的,但强烈推荐,因为它包含了我们需要的许多项目(Jupyter、pandasnumpy 等)。

你将需要一个正常工作的 Docker 安装环境。你可以在docs.docker.com找到安装说明和文档。

本书中包含的代码示例可以在 GitHub 上找到:github.com/PacktPublishing/Learn-Amazon-SageMaker-second-edition。你需要安装 Git 客户端来访问这些示例(git-scm.com/)。

理解 SageMaker 如何调用你的代码

当我们使用内置算法和框架时,并没有太多关注 SageMaker 实际上是如何调用训练和部署代码的。毕竟,“内置”意味着:直接拿来需要的工具并开始工作。

当然,如果我们想使用自定义代码和容器,情况就不一样了。我们需要了解它们如何与 SageMaker 接口,以便我们能够准确地实现它们。

在本节中,我们将详细讨论这一接口。让我们从文件布局开始。

理解 SageMaker 容器内的文件布局

为了简化我们的工作,SageMaker 估算器会自动将超参数和输入数据复制到训练容器中。同样,它们会自动将训练好的模型(以及任何检查点)从容器复制到 S3。在部署时,它们会执行反向操作,将模型从 S3 复制到容器中。

如你所想,这要求遵循文件布局约定:

  • 超参数以 JSON 字典形式存储在/opt/ml/input/config/hyperparameters.json中。

  • 输入通道存储在/opt/ml/input/data/CHANNEL_NAME中。我们在前一章中看到,通道名称与传递给fit() API 的名称匹配。

  • 模型应保存在/opt/ml/model中,并从该位置加载。

因此,我们需要在自定义代码中使用这些路径。现在,让我们看看如何调用训练和部署代码。

理解自定义训练选项

第七章使用内置框架扩展机器学习服务中,我们研究了脚本模式以及 SageMaker 如何使用它来调用我们的训练脚本。此功能由框架容器中额外的 Python 代码启用,即 SageMaker 训练工具包(github.com/aws/sagemaker-training-toolkit)。

简而言之,训练工具包将入口脚本、其超参数和依赖项复制到容器内。它还将从输入通道中复制数据到容器中。然后,它会调用入口脚本。有好奇心的朋友可以阅读src/sagemaker_training/entry_point.py中的代码。

在自定义训练代码时,你有以下选项:

  • 自定义现有的框架容器,只添加你额外的依赖和代码。脚本模式和框架估算器将可用。

  • 基于 SageMaker 训练工具包构建自定义容器。脚本模式和通用的Estimator模块将可用,但你需要安装其他所有依赖。

  • 构建一个完全自定义的容器。如果你想从头开始,或者不想在容器中添加任何额外的代码,这是最合适的选择。你将使用通用的Estimator模块进行训练,并且脚本模式将不可用。你的训练代码将直接调用(稍后会详细说明)。

理解自定义部署选项

框架容器包括用于部署的额外 Python 代码。以下是最流行框架的仓库:

就像训练一样,你有三个选项:

  • 自定义现有的框架容器。模型将使用现有的推理逻辑提供服务。

  • 基于 SageMaker 推理工具包构建自定义容器。模型将由多模型服务器提供服务。

  • 构建一个完全自定义的容器,去掉任何推理逻辑,改为实现自己的推理逻辑。

无论是使用单一容器进行训练和部署,还是使用两个不同的容器,都取决于你。许多不同的因素会影响决策:谁构建容器、谁运行容器等等。只有你能决定对你的特定设置来说,哪个选项是最佳的。

现在,让我们运行一些示例吧!

自定义现有框架容器

当然,我们也可以简单地写一个 Dockerfile,引用其中一个深度学习容器镜像 (github.com/aws/deep-learning-containers/blob/master/available_images.md),并添加我们自己的命令。请参见以下示例:

FROM 763104351884.dkr.ecr.us-east-1.amazonaws.com/tensorflow-training:2.4.1-cpu-py37-ubuntu18.04
. . .

相反,让我们在本地机器上自定义并重新构建PyTorch训练和推理容器。这个过程与其他框架类似。

构建环境

需要安装并运行 Docker。为了避免在拉取基础镜像时受到限制,建议你创建一个docker loginDocker Desktop

为了避免奇怪的依赖问题(我在看你,macOS),我还建议你在m5.large实例上构建镜像(应该足够),但请确保分配的存储空间超过默认的 8GB。我推荐 64GB。你还需要确保该 EC2 实例的IAM角色允许你推送和拉取 EC2 镜像。如果你不确定如何创建并连接到 EC2 实例,可以参考这个教程:docs.aws.amazon.com/AWSEC2/latest/UserGuide/EC2_GetStarted.html

在 EC2 上设置构建环境

我们将通过以下步骤开始:

  1. 一旦你的 EC2 实例启动,我们通过ssh连接到它。首先,我们安装 Docker,并将ec2-user添加到docker组。这将允许我们以非 root 用户身份运行 Docker 命令:

    $ sudo yum -y install docker
    $ sudo usermod -a -G docker ec2-user
    
  2. 为了应用此权限更改,我们登出并重新登录。

  3. 我们确保docker正在运行,并登录到 Docker Hub:

    $ service docker start
    $ docker login
    
  4. 我们安装git、Python 3 和pip

    $ sudo yum -y install git python3-devel python3-pip
    

我们的 EC2 实例现在已经准备好,我们可以继续构建容器。

构建训练和推理容器

这可以通过以下步骤完成:

  1. 我们克隆deep-learning-containers仓库,该仓库集中管理 TensorFlow、PyTorch、Apache MXNet 和 Hugging Face 的所有训练和推理代码,并添加了便捷的脚本来构建这些容器:

    $ git clone https://github.com/aws/deep-learning-containers.git
    $ cd deep-learning-containers
    
  2. 我们为我们的账户 ID、运行所在的区域以及我们将在 Amazon ECR 中创建的新仓库的名称设置环境变量:

    $ export ACCOUNT_ID=123456789012
    $ export REGION=eu-west-1
    $ export REPOSITORY_NAME=my-pt-dlc
    
  3. 我们在 Amazon ECR 中创建仓库并登录。有关详细信息,请参阅文档(docs.aws.amazon.com/ecr/index.html):

    $ aws ecr create-repository 
    --repository-name $REPOSITORY_NAME --region $REGION
    $ aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
    
  4. 我们创建一个虚拟环境,并安装 Python 依赖:

    $ python3 -m venv dlc
    $ source dlc/bin/activate
    $ pip install -r src/requirements.txt
    
  5. 在这里,我们想要为 PyTorch 1.8 构建训练和推理容器,支持 CPU 和 GPU。我们可以在pytorch/training/docker/1.8/py3/找到相应的 Docker 文件,并根据需要进行定制。例如,我们可以将 Deep Graph Library 固定为版本 0.6.1:

    && conda install -c dglteam -y dgl==0.6.1 \
    
  6. 编辑完 Docker 文件后,我们查看最新 PyTorch 版本的构建配置文件(pytorch/buildspec.yml)。我们决定自定义镜像标签,以确保每个镜像都能清楚地识别:

    BuildCPUPTTrainPy3DockerImage:
        tag: !join [ *VERSION, "-", *DEVICE_TYPE, "-", *TAG_PYTHON_VERSION, "-", *OS_VERSION, "-training" ]
    BuildGPUPTTrainPy3DockerImage:
        tag: !join [ *VERSION, "-", *DEVICE_TYPE, "-", *TAG_PYTHON_VERSION, "-", *CUDA_VERSION, "-", *OS_VERSION, "-training" ]
    BuildCPUPTInferencePy3DockerImage:
        tag: !join [ *VERSION, "-", *DEVICE_TYPE, "-", *TAG_PYTHON_VERSION, "-", *OS_VERSION, "-inference" ]
    BuildGPUPTInferencePy3DockerImage:
        tag: !join [ *VERSION, "-", *DEVICE_TYPE, "-", *TAG_PYTHON_VERSION, "-", *CUDA_VERSION, "-", *OS_VERSION, "-inference"]
    
  7. 最后,我们运行设置脚本并启动构建过程:

    $ bash src/setup.sh pytorch
    $ python src/main.py --buildspec pytorch/buildspec.yml --framework pytorch --device_types cpu,gpu --image_types training,inference
    
  8. 稍等片刻,所有四个镜像(加上一个示例镜像)都已构建完成,我们可以在本地 Docker 中看到它们:

    $ docker images
    123456789012.dkr.ecr.eu-west-1.amazonaws.com/my-pt-dlc   1.8.1-gpu-py36-cu111-ubuntu18.04-example-2021-05-28-10-14-15     
    123456789012.dkr.ecr.eu-west-1.amazonaws.com/my-pt-dlc   1.8.1-gpu-py36-cu111-ubuntu18.04-training-2021-05-28-10-14-15    
    123456789012.dkr.ecr.eu-west-1.amazonaws.com/my-pt-dlc   1.8.1-gpu-py36-cu111-ubuntu18.04-inference-2021-05-28-10-14-15
    123456789012.dkr.ecr.eu-west-1.amazonaws.com/my-pt-dlc   1.8.1-cpu-py36-ubuntu18.04-inference-2021-05-28-10-14-15         
    123456789012.dkr.ecr.eu-west-1.amazonaws.com/my-pt-dlc   1.8.1-cpu-py36-ubuntu18.04-training-2021-05-28-10-14-15          
    
  9. 我们也可以在 ECR 仓库中看到它们,如下图所示:https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_08_1.jpg

    图 8.1 – 在 ECR 中查看镜像

  10. 现在这些镜像可以通过 SageMaker SDK 使用。让我们用新的 CPU 镜像进行训练。我们只需要将其名称传递给PyTorch估算器的image_uri参数即可。请注意,我们可以去掉py_versionframework_version

    Estimator = PyTorch(
        image_uri='123456789012.dkr.ecr.eu-west-1.amazonaws.com/my-pt-dlc:1.8.1-cpu-py36-ubuntu18.04-training-2021-05-28-10-14-15',
        role=sagemaker.get_execution_role(),
        entry_point='karate_club_sagemaker.py',
        hyperparameters={'node_count': 34, 'epochs': 30},
        instance_count=1,
        instance_type='ml.m5.large')
    

如你所见,定制深度学习容器非常简单。现在,让我们深入一步,仅使用训练工具包进行操作。

使用带有 scikit-learn 的 SageMaker 训练工具包

在这个示例中,我们将使用 SageMaker 训练工具包构建一个自定义 Python 容器。我们将使用它在波士顿房价数据集上训练一个 scikit-learn 模型,使用脚本模式和 SKLearn 估算器。

我们需要三个构建模块:

  • 训练脚本。由于脚本模式将可用,我们可以使用与 第七章 中的 scikit-learn 示例完全相同的代码,使用内置框架扩展机器学习服务

  • 我们需要一个 Dockerfile 和 Docker 命令来构建自定义容器。

  • 我们还需要一个配置为使用我们自定义容器的 SKLearn 估算器。

让我们来处理容器:

  1. 一个 Dockerfile 可能会变得相当复杂,但这里我们不需要这么做!我们从 Docker Hub 上提供的官方 Python 3.7 镜像开始 (hub.docker.com/_/python)。我们安装 scikit-learn、numpypandasjoblib 和 SageMaker 训练工具包:

    FROM python:3.7
    RUN pip3 install --no-cache scikit-learn numpy pandas joblib sagemaker-training
    
  2. 我们使用 docker build 命令构建镜像,并将其标记为 sklearn-customer:sklearn

    $ docker build -t sklearn-custom:sklearn -f Dockerfile .
    

    镜像构建完成后,我们可以找到其标识符:

    $ docker images
    REPOSITORY          TAG         IMAGE ID   
    sklearn-custom      sklearn     bf412a511471         
    
  3. 使用 AWS CLI,我们在 Amazon ECR 中创建一个仓库来托管这个镜像,并登录到该仓库:

    $ aws ecr create-repository --repository-name sklearn-custom --region eu-west-1
    $ aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.eu-west-1.amazonaws.com/sklearn-custom:latest
    
  4. 使用镜像标识符,我们用仓库标识符标记镜像:

    $ docker tag bf412a511471 123456789012.dkr.ecr.eu-west-1.amazonaws.com/sklearn-custom:sklearn
    
  5. 我们将镜像推送到仓库:

    $ docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/sklearn-custom:sklearn
    

    现在,镜像已经准备好用于使用 SageMaker 估算器进行训练。

  6. 我们定义一个 SKLearn 估算器,将 image_uri 参数设置为我们刚创建的容器的名称:

    sk = SKLearn(
        role=sagemaker.get_execution_role(),
        entry_point='sklearn-boston-housing.py',
        image_name='123456789012.dkr.ecr.eu-west-1.amazonaws.com/sklearn-custom:sklearn',
        instance_count=1,
        instance_type='ml.m5.large',
        output_path=output,
        hyperparameters={
             'normalize': True,
             'test-size': 0.1
        }
    )
    
  7. 我们设置训练通道的位置,并像往常一样启动训练。在训练日志中,我们看到我们的代码确实以脚本模式被调用:

    /usr/local/bin/python -m sklearn-boston-housing 
    --normalize True --test-size 0.1
    

如你所见,定制训练容器非常简单。得益于 SageMaker 训练工具包,你可以像使用内置框架容器一样工作。我们这里使用了 scikit-learn,你也可以对其他所有框架做同样的操作。

然而,我们不能将这个容器用于部署,因为它不包含任何模型服务代码。我们应该添加定制代码来启动一个 Web 应用程序,这正是我们在下一个示例中要做的。

为 scikit-learn 构建完全自定义的容器

在这个示例中,我们将构建一个完全自定义的容器,里面没有任何 AWS 代码。我们将使用它在波士顿房价数据集上训练一个 scikit-learn 模型,使用通用的 Estimator 模块。使用相同的容器,我们将通过 Flask Web 应用程序部署该模型。

我们将按逻辑步骤进行操作,首先处理训练,然后更新代码以处理部署。

使用完全自定义容器进行训练

由于我们不能再依赖脚本模式,因此需要修改训练代码。这就是修改后的代码,你很容易就能理解它是怎么回事:

#!/usr/bin/env python
import pandas as pd
import joblib, os, json
if __name__ == '__main__':
    config_dir = '/opt/ml/input/config'
    training_dir = '/opt/ml/input/data/training'
    model_dir = '/opt/ml/model'
    with open(os.path.join(config_dir, 
    'hyperparameters.json')) as f:
        hp = json.load(f)
        normalize = hp['normalize']
        test_size = float(hp['test-size'])
        random_state = int(hp['random-state'])
    filename = os.path.join(training_dir, 'housing.csv')
    data = pd.read_csv(filename)
    # Train model
    . . . 
    joblib.dump(regr, 
                os.path.join(model_dir, 'model.joblib'))

使用 SageMaker 容器的标准文件布局,我们从 JSON 文件中读取超参数。然后,我们加载数据集,训练模型,并将其保存在正确的位置。

还有一个非常重要的区别,我们需要深入了解 Docker 来解释它。SageMaker 将以 docker run <IMAGE_ID> train 运行训练容器,并将 train 参数传递给容器的入口点。

如果你的容器有预定义的入口点,train 参数将会传递给它,比如 /usr/bin/python train。如果容器没有预定义的入口点,train 就是将要执行的实际命令。

为了避免烦人的问题,我建议你的训练代码满足以下要求:

  • 命名为 train——没有扩展名,只是 train

  • 使其可执行。

  • 确保它在 PATH 值中。

  • 脚本的第一行应该定义解释器的路径,例如 #!/usr/bin/env python

这应该能保证无论你的容器是否有预定义的入口点,都能正确调用你的训练代码。

我们将在 Dockerfile 中处理这个问题,从官方的 Python 镜像开始。注意,我们不再安装 SageMaker 训练工具包:

FROM python:3.7
RUN pip3 install --no-cache scikit-learn numpy pandas joblib
COPY sklearn-boston-housing-generic.py /usr/bin/train
RUN chmod 755 /usr/bin/train

脚本名称正确。它是可执行的,且 /usr/binPATH 中。

我们应该准备好了——让我们创建自定义容器并用它启动训练任务:

  1. 我们构建并推送镜像,使用不同的标签:

    $ docker build -t sklearn-custom:estimator -f Dockerfile-generic .
    $ docker tag <IMAGE_ID> 123456789012.dkr.ecr.eu-west-1.amazonaws.com/sklearn-custom:estimator
    $ docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/sklearn-custom:estimator
    
  2. 我们更新笔记本代码,使用通用的 Estimator 模块:

    from sagemaker.estimator import Estimator
    sk = Estimator(
        role=sagemaker.get_execution_role(),
        image_name='123456789012.dkr.ecr.eu-west-1.amazonaws.com/sklearn-custom:estimator',
        instance_count=1,
        instance_type='ml.m5.large',
        output_path=output,
        hyperparameters={
             'normalize': True,
             'test-size': 0.1,
             'random-state': 123
        }
    )
    
  3. 我们照常进行训练。

现在让我们添加代码来部署这个模型。

部署完全自定义容器

Flask 是一个非常流行的 Python Web 框架(palletsprojects.com/p/flask)。它简单且文档齐全。我们将用它来构建一个托管在容器中的简单预测 API。

就像我们的训练代码一样,SageMaker 要求将部署脚本复制到容器内。镜像将以 docker run <IMAGE_ID> serve 运行。

HTTP 请求将发送到端口 8080。容器必须提供 /ping URL 进行健康检查,并提供 /invocations URL 处理预测请求。我们将使用 CSV 格式作为输入。

因此,你的部署代码需要满足以下要求:

  • 命名为 serve——没有扩展名,只是 serve

  • 使其可执行。

  • 确保它在 PATH 中。

  • 确保容器暴露了端口8080

  • 提供代码处理 /ping/invocations URL。

这是更新后的 Dockerfile。我们安装 Flask,复制部署代码,并开放端口 8080

FROM python:3.7
RUN pip3 install --no-cache scikit-learn numpy pandas joblib
RUN pip3 install --no-cache flask
COPY sklearn-boston-housing-generic.py /usr/bin/train
COPY sklearn-boston-housing-serve.py /usr/bin/serve
RUN chmod 755 /usr/bin/train /usr/bin/serve
EXPOSE 8080

这是我们如何用 Flask 实现一个简单的预测服务:

  1. 我们导入所需的模块。我们从 /opt/ml/model 加载模型并初始化 Flask 应用程序:

    #!/usr/bin/env python
    import joblib, os
    import pandas as pd
    from io import StringIO
    import flask
    from flask import Flask, Response
    model_dir = '/opt/ml/model'
    model = joblib.load(os.path.join(model_dir, 
                        'model.joblib'))
    app = Flask(__name__)
    
  2. 我们实现 /ping URL 来进行健康检查,方法是简单地返回 HTTP 代码 200(OK):

    @app.route("/ping", methods=["GET"])
    def ping():
        return Response(response="\n", status=200)
    
  3. 我们实现了/invocations URL。如果内容类型不是text/csv,我们返回 HTTP 代码 415(不支持的媒体类型)。如果是,我们解码请求体并将其存储在文件样式的内存缓冲区中。然后,我们读取 CSV 样本,进行预测,并发送结果:

    @app.route("/invocations", methods=["POST"])
    def predict():
        if flask.request.content_type == 'text/csv':
            data = flask.request.data.decode('utf-8')
            s = StringIO(data)
            data = pd.read_csv(s, header=None)
            response = model.predict(data)
            response = str(response)
        else:
            return flask.Response(
                response='CSV data only', 
                status=415, mimetype='text/plain')
        return Response(response=response, status=200)
    
  4. 在启动时,脚本在 8080 端口上启动 Flask 应用程序:

    if __name__ == "__main__":
        app.run(host="0.0.0.0", port=8080)
    

    即使您还不熟悉 Flask,这也不算太难。

  5. 我们重新构建并推送镜像,然后使用相同的评估器再次进行训练。这里不需要进行任何更改。

  6. 我们部署模型:

    sk_predictor = sk.deploy(instance_type='ml.t2.medium',
                             initial_instance_count=1)
    

    提醒

    如果您在这里看到一些奇怪的行为(端点未部署、密秘的错误消息等),Docker 可能出了问题。sudo service docker restart应该能解决大多数问题。在/tmp中清理tmp*可能也会有所帮助。

  7. 我们准备了一些测试样本,将内容类型设置为text/csv,并调用预测 API:

    test_samples = ['0.00632, 18.00, 2.310, 0, 0.5380, 6.5750, 65.20, 4.0900, 1,296.0, 15.30, 396.90, 4.98',             
    '0.02731, 0.00, 7.070, 0, 0.4690, 6.4210, 78.90, 4.9671, 2,242.0, 17.80, 396.90, 9.14']
    sk_predictor.serializer =
        sagemaker.serializers.CSVSerializer()
    response = sk_predictor.predict(test_samples)
    print(response)
    

    您应该会看到类似于此的内容。API 已成功调用:

    b'[[29.801388899699845], [24.990809475886078]]'
    
  8. 完成后,我们删除端点:

    sk_predictor.delete_endpoint()
    

在下一个例子中,我们将使用 R 环境来训练和部署模型。这将让我们有机会暂时离开 Python 世界。正如您将看到的那样,事情并没有真正不同。

构建一个完全自定义的 R 容器

R 是一种用于数据探索和分析的流行语言。在本例中,我们将构建一个自定义容器,以在波士顿房屋数据集上训练和部署线性回归模型。

整个过程与为 Python 构建自定义容器类似。我们将使用plumber而不是 Flask 来构建我们的预测 API:

使用 R 和 plumber 进行编码

如果你对 R 不太熟悉,不要担心。这是一个非常简单的例子,我相信你能跟上:

  1. 我们编写一个函数来训练我们的模型。它从常规路径加载超参数和数据集。如果我们请求的话,它会对数据集进行标准化:

    # train_function.R
    library("rjson")
    train <- function() {
        hp <- fromJSON(file = 
              '/opt/ml/input/config/hyperparameters.json')
        normalize <- hp$normalize
        data <- read.csv(file = 
                '/opt/ml/input/data/training/housing.csv', 
                header=T)
        if (normalize) {
            data <- as.data.frame(scale(data))
        }
    

    它训练一个线性回归模型,考虑所有特征来预测房屋的中位数价格(medv列)。最后,它将模型保存在正确的位置:

        model = lm(medv~., data)
        saveRDS(model, '/opt/ml/model/model.rds')
    }
    
  2. 我们编写一个函数来提供预测服务。使用plumber注解,我们为健康检查定义了/ping URL,为预测定义了/invocations URL:

    # serve_function.R
    #' @get /ping
    function() {
      return('')
    }
    #' @post /invocations
    function(req) {
        model <- readRDS('/opt/ml/model/model.rds')
        conn <- textConnection(gsub('\\\\n', '\n', 
                               req$postBody))
        data <- read.csv(conn)
        close(conn)
        medv <- predict(model, data)
        return(medv)
    }
    
  3. 将这两个部分结合在一起,我们编写一个主函数,它将作为我们脚本的入口点。SageMaker 将传递trainserve命令行参数,并在我们的代码中调用相应的函数:

    library('plumber')
    source('train_function.R')
    serve <- function() {
        app <- plumb('serve_function.R')
        app$run(host='0.0.0.0', port=8080)}
    args <- commandArgs()
    if (any(grepl('train', args))) {
        train()
    }
    if (any(grepl('serve', args))) {
        serve()
    }
    

这就是我们需要的所有 R 代码。现在,让我们来处理容器。

构建自定义容器

我们需要构建一个自定义容器,存储 R 运行时和我们的脚本。Dockerfile 如下所示:

  1. 我们从Docker Hub的官方 R 镜像开始,并添加我们需要的依赖项(这些是我在我的机器上需要的;您的情况可能有所不同):

    FROM r-base:latest
    WORKDIR /opt/ml/
    RUN apt-get update
    RUN apt-get install -y libcurl4-openssl-dev libsodium-dev
    RUN R -e "install.packages(c('rjson', 'plumber')) "
    
  2. 然后,我们将我们的代码复制到容器中,并将主函数定义为其显式入口点:

    COPY main.R train_function.R serve_function.R /opt/ml/
    ENTRYPOINT ["/usr/bin/Rscript", "/opt/ml/main.R", "--no-save"]
    
  3. 我们在 ECR 中创建一个新仓库。然后,我们构建镜像(这可能需要一段时间,并涉及编译步骤),并推送它:

    $ aws ecr create-repository --repository-name r-custom --region eu-west-1
    $ aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.eu-west-1.amazonaws.com/r-custom:latest
    $ docker build -t r-custom:latest -f Dockerfile .
    $ docker tag <IMAGE_ID> 123456789012.dkr.ecr.eu-west-1.amazonaws.com/r-custom:latest
    $ docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/r-custom:latest
    

一切准备就绪,让我们开始训练并部署。

在 SageMaker 上训练和部署自定义容器

跳转到 Jupyter 笔记本,我们使用 SageMaker SDK 训练并部署我们的容器:

  1. 我们配置一个带有自定义容器的Estimator模块:

    r_estimator = Estimator(
        role = sagemaker.get_execution_role(),
        image_uri='123456789012.dkr.ecr.eu-west-1.amazonaws.com/r-custom:latest',
        instance_count=1,
        instance_type='ml.m5.large',
        output_path=output,
        hyperparameters={'normalize': False}
    )
    r_estimator.fit({'training':training})
    
  2. 一旦训练任务完成,我们像往常一样部署模型:

    r_predictor = r_estimator.deploy(
        initial_instance_count=1, 
        instance_type='ml.t2.medium')
    
  3. 最后,我们读取完整的数据集(为什么不呢?)并将其发送到端点:

    import pandas as pd
    data = pd.read_csv('housing.csv')
    data.drop(['medv'], axis=1, inplace=True)
    data = data.to_csv(index=False)
    r_predictor.serializer = 
        sagemaker.serializers.CSVSerializer()
    response = r_predictor.predict(data)
    print(response)
    

    输出应该像这样:

    b'[30.0337,25.0568,30.6082,28.6772,27.9288\. . .
    
  4. 完成后,我们删除端点:

    r_predictor.delete_endpoint()
    

无论你使用 Python、R,还是其他语言,构建和部署你自己的自定义容器都相对容易。然而,你仍然需要构建自己的网站应用程序,这可能是你既不知道如何做,也不喜欢做的事。如果我们有一个工具来处理所有这些麻烦的容器和网站问题,那该多好?

事实上,确实有一个平台:MLflow

使用你自己的代码在 MLflow 上进行训练和部署

MLflow 是一个开源机器学习平台(mlflow.org)。它由 Databricks(databricks.com)发起,Databricks 还为我们带来了Spark。MLflow 有许多功能,包括能够将 Python 训练的模型部署到 SageMaker。

本节并非旨在作为 MLflow 教程。你可以在www.mlflow.org/docs/latest/index.html找到文档和示例。

安装 MLflow

在本地机器上,让我们为 MLflow 设置一个虚拟环境并安装所需的库。以下示例是在 MLflow 1.17 上测试的:

  1. 我们首先初始化一个名为mlflow-example的新虚拟环境。然后,我们激活它:

    $ virtualenv mlflow-example
    $ source mlflow-example/bin/activate
    
  2. 我们安装了 MLflow 和训练脚本所需的库:

    $ pip install mlflow gunicorn pandas sklearn xgboost boto3
    
  3. 最后,我们下载已经在第七章中使用过的直接营销数据集,使用内置框架扩展机器学习服务

    $ wget -N https://sagemaker-sample-data-us-west-2.s3-us-west-2.amazonaws.com/autopilot/direct_marketing/bank-additional.zip
    $ unzip -o bank-additional.zip
    

设置完成。让我们开始训练模型。

使用 MLflow 训练模型

训练脚本为此次运行设置了 MLflow 实验,以便我们可以记录元数据(超参数、指标等)。然后,它加载数据集,训练一个 XGBoost 分类器,并记录模型:

# train-xgboost.py
import mlflow.xgboost
import xgboost as xgb
from load_dataset import load_dataset
if __name__ == '__main__':
    mlflow.set_experiment('dm-xgboost')
    with mlflow.start_run(run_name='dm-xgboost-basic') 
    as run:
        x_train, x_test, y_train, y_test = load_dataset(
            'bank-additional/bank-additional-full.csv')
        cls = xgb.XGBClassifier(
                  objective='binary:logistic', 
                  eval_metric='auc')
        cls.fit(x_train, y_train)
        auc = cls.score(x_test, y_test)
        mlflow.log_metric('auc', auc)
        mlflow.xgboost.log_model(cls, 'dm-xgboost-model')
        mlflow.end_run()

load_dataset()函数按其名称所示执行,并记录多个参数:

# load_dataset.py
import mlflow
import pandas as pd
from sklearn.model_selection import train_test_split
def load_dataset(path, test_size=0.2, random_state=123):
    data = pd.read_csv(path)
    data = pd.get_dummies(data)
    data = data.drop(['y_no'], axis=1)
    x = data.drop(['y_yes'], axis=1)
    y = data['y_yes']
    mlflow.log_param("dataset_path", path)
    mlflow.log_param("dataset_shape", data.shape)
    mlflow.log_param("test_size", test_size)
    mlflow.log_param("random_state", random_state)
    mlflow.log_param("one_hot_encoding", True)
    return train_test_split(x, y, test_size=test_size, 
                            random_state=random_state)

让我们训练模型并在 MLflow Web 应用程序中可视化其结果:

  1. 在我们刚刚在本地机器上创建的虚拟环境中,我们像运行任何 Python 程序一样运行训练脚本:

    $ python train-xgboost.py
    INFO: 'dm-xgboost' does not exist. Creating a new experiment
    AUC  0.91442097596504
    
  2. 我们启动 MLflow Web 应用程序:

    $ mlflow ui &
    
  3. 当我们将浏览器指向localhost:5000时,我们可以看到运行的信息,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_08_2.jpg

图 8.2 – 在 MLflow 中查看我们的任务

训练成功了。在我们将模型部署到 SageMaker 之前,我们必须构建一个 SageMaker 容器。事实证明,这是一件非常简单的事。

使用 MLflow 构建 SageMaker 容器

只需在本地机器上执行一个命令:

$ mlflow sagemaker build-and-push-container

MLflow 会自动构建一个与 SageMaker 兼容的 Docker 容器,并包含所有所需的依赖项。然后,它会在 Amazon ECR 中创建一个名为 mlflow-pyfunc 的仓库,并将镜像推送到该仓库。显然,这需要你正确设置 AWS 凭证。MLflow 将使用 AWS CLI 配置的默认区域。

一旦这个命令完成,你应该会在 ECR 中看到镜像,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_08_3.jpg

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_08_3.jpg)

图 8.3 – 查看我们的容器在 ECR 中

我们的容器现在已经准备好部署了。

在本地部署模型使用 MLflow

我们将使用以下步骤部署我们的模型:

  1. 我们可以通过一个命令在本地部署我们的模型,传递其运行标识符(可以在 MLflow 运行的 URL 中看到)和要使用的 HTTP 端口。这将启动一个基于 gunicorn 的本地 Web 应用程序:

    $ mlflow sagemaker run-local -p 8888 -m runs:/d08ab8383ee84f72a92164d3ca548693/dm-xgboost-model
    

    你应该看到类似这样的内容:

    [2021-05-26 20:21:23 +0000] [370] [INFO] Starting gunicorn 20.1.0
    [2021-05-26 20:21:23 +0000] [370] [INFO] Listening at: http://127.0.0.1:8000 (370)
    [2021-05-26 20:21:23 +0000] [370] [INFO] Using worker: gevent
    [2021-05-26 20:21:23 +0000] [381] [INFO] Booting worker with pid: 381 
    
  2. 我们的预测代码非常简单。我们从数据集加载 CSV 样本,将它们转换为 JSON 格式,并使用 requests 库发送到端点。requests 是一个流行的 Python HTTP 库(requests.readthedocs.io):

    # predict-xgboost-local.py 
    import json
    import requests
    from load_dataset import load_dataset
    port = 8888
    if __name__ == '__main__':
        x_train, x_test, y_train, y_test = load_dataset(
            'bank-additional/bank-additional-full.csv')
        input_data = x_test[:10].to_json(orient='split')
        endpoint = 'http://localhost:{}/invocations'
                   .format(port)
        headers = {'Content-type': 'application/json; 
                    format=pandas-split'}
        prediction = requests.post(
            endpoint, 
            json=json.loads(input_data),
            headers=headers)
        print(prediction.text)
    
  3. 在另一个 shell 中运行此代码,调用本地模型并输出预测结果:

    $ source mlflow-example/bin/activate
    $ python predict-xgboost-local.py
    [0.00046298891538754106, 0.10499032586812973, . . . 
    
  4. 完成后,我们使用 Ctrl + C 终止本地服务器。

现在我们确信我们的模型在本地可以正常工作,我们可以将它部署到 SageMaker。

使用 MLflow 在 SageMaker 上部署模型

这又是一个一行命令:

  1. 我们需要传递一个应用程序名称、模型路径和 SageMaker 角色的名称。你可以使用你在前几章中使用过的相同角色:

    $ mlflow sagemaker deploy \
    --region-name eu-west-1 \
    -t ml.t2.medium \
    -a mlflow-xgb-demo \
    -m runs:/d08ab8383ee84f72a92164d3ca548693/dm-xgboost-model \
    -e arn:aws:iam::123456789012:role/Sagemaker-fullaccess
    
  2. 几分钟后,端点进入服务状态。我们使用以下代码调用它。它加载测试数据集,并将前 10 个样本以 JSON 格式发送到以我们的应用程序命名的端点:

    # predict-xgboost.py 
    import boto3
    from load_dataset import load_dataset
    app_name = 'mlflow-xgb-demo'
    region = 'eu-west-1'
    if __name__ == '__main__':
        sm = boto3.client('sagemaker', region_name=region)
        smrt = boto3.client('runtime.sagemaker', 
                            region_name=region)
        endpoint = sm.describe_endpoint(
                  EndpointName=app_name)
        print("Status: ", endpoint['EndpointStatus'])
        x_train, x_test, y_train, y_test = load_dataset(
            'bank-additional/bank-additional-full.csv')
        input_data = x_test[:10].to_json(orient="split")
        prediction = smrt.invoke_endpoint(
            EndpointName=app_name,
            Body=input_data,
            ContentType='application/json;
                         format=pandas-split')
        prediction = prediction['Body']
                     .read().decode("ascii")
        print(prediction)
    

    等一下!我们没有使用 SageMaker SDK。这是怎么回事?

    在这个例子中,我们处理的是一个现有的端点,而不是通过拟合估算器并部署预测器创建的端点。

    我们仍然可以使用 SageMaker SDK 重新构建一个预测器,正如我们将在 第十一章 中看到的那样,部署机器学习模型。不过,我们使用了我们亲爱的老朋友 boto3,AWS 的 Python SDK。我们首先调用 describe_endpoint() API 来检查端点是否在服务中。然后,我们使用 invoke_endpoint() API 来……调用端点!现在,我们暂时不需要了解更多。

    我们在本地机器上运行预测代码,输出结果如下:

    $ python3 predict-xgboost.py
    Status:  InService
    0.00046298891538754106, 0.10499032586812973, 0.016391035169363022, . . .
    
  3. 完成后,我们使用 MLflow CLI 删除端点。这会清理为部署创建的所有资源:

    $ mlflow sagemaker delete -a mlflow-xgb-demo –region-name eu-west-1
    

使用 MLflow 的开发经验非常简单。它还有许多其他功能,您可能想要探索。

到目前为止,我们已经运行了训练和预测的示例。SageMaker 还有一个领域可以让我们使用自定义容器,SageMaker Processing,我们在[第二章中研究了它,处理数据准备技术。为了结束这一章,让我们为 SageMaker Processing 构建一个自定义的 Python 容器。

为 SageMaker 处理构建一个完全自定义的容器

我们将重用来自第六章的新闻头条示例,训练自然语言处理模型

  1. 我们从一个基于最小 Python 镜像的 Dockerfile 开始。我们安装依赖项,添加处理脚本,并将其定义为我们的入口点:

    FROM python:3.7-slim
    RUN pip3 install --no-cache gensim nltk sagemaker
    RUN python3 -m nltk.downloader stopwords wordnet
    ADD preprocessing-lda-ntm.py /
    ENTRYPOINT ["python3", "/preprocessing-lda-ntm.py"]
    
  2. 我们构建镜像并将其标记为sm-processing-custom:latest

    python:3.7 instead of python:3.7-slim. This makes it faster to push and download.
    
  3. 使用 AWS CLI,我们在 Amazon ECR 中创建一个存储库来托管这个镜像,并登录到该存储库:

    $ aws ecr create-repository --repository-name sm-processing-custom --region eu-west-1
    $ aws ecr get-login-password | docker login --username AWS --password-stdin 123456789012.dkr.ecr.eu-west-1.amazonaws.com/sm-processing-custom:latest
    
  4. 使用镜像标识符,我们用存储库标识符为镜像打标签:

    $ docker tag <IMAGE_ID> 123456789012.dkr.ecr.eu-west-1.amazonaws.com/sm-processing-custom:latest
    
  5. 我们将镜像推送到存储库:

    $ docker push 123456789012.dkr.ecr.eu-west-1.amazonaws.com/sm-processing-custom:latest
    
  6. 切换到 Jupyter 笔记本,我们使用新的容器配置一个通用的Processor对象,它相当于我们用于训练的通用Estimator模块。因此,不需要framework_version参数:

    from sagemaker.processing import Processor
    sklearn_processor = Processor( 
        image_uri='123456789012.dkr.ecr.eu-west-1.amazonaws.com/sm-processing-custom:latest',
        role=sagemaker.get_execution_role(),
        instance_type='ml.c5.2xlarge',
        instance_count=1)
    
  7. 使用相同的ProcessingInputProcessingOutput对象,我们运行处理作业。由于我们的处理代码现在存储在容器内,我们不需要像使用SKLearnProcessor时那样传递code参数:

    from sagemaker.processing import ProcessingInput, ProcessingOutput
    sklearn_processor.run(
        inputs=[
            ProcessingInput(
                source=input_data,
                destination='/opt/ml/processing/input')
        ],
        outputs=[
            ProcessingOutput(
                output_name='train_data',
                source='/opt/ml/processing/train/')
        ],
        arguments=[
            '--filename', 'abcnews-date-text.csv.gz'
        ]
    )
    
  8. 一旦训练作业完成,我们可以在 S3 中获取其输出。

这就结束了我们对 SageMaker 中自定义容器的探索。如您所见,只要它适合 Docker 容器,几乎可以运行任何东西。

概述

内置框架非常有用,但有时您需要一些稍微——或完全——不同的东西。无论是从内置容器开始还是从头开始,SageMaker 让您可以完全按照自己的需求构建训练和部署容器。自由为所有人!

在本章中,您学习了如何为数据处理、训练和部署定制 Python 和 R 容器。您还看到了如何使用 SageMaker SDK 及其常规工作流程来使用这些容器。您还了解了 MLflow,这是一个非常好的开源工具,允许您使用 CLI 训练和部署模型。

这就结束了我们对 SageMaker 建模选项的广泛介绍:内置算法、内置框架和自定义代码。在下一章中,您将学习 SageMaker 的功能,帮助您扩展训练作业。

第三部分:深入探索训练

在本节中,你将学习与扩展、模型优化、模型调试和成本优化相关的高级训练技巧。

本节包括以下章节:

  • 第九章扩展你的训练任务

  • 第十章高级训练技巧

第九章:扩展你的训练任务

在前四章中,你学习了如何使用内置算法、框架或你自己的代码来训练模型。

在本章中,你将学习如何扩展训练任务,使其能够在更大的数据集上训练,同时控制训练时间和成本。我们将首先讨论如何根据监控信息和简单的指南来做出扩展决策。你还将看到如何使用Amazon SageMaker Debugger收集分析信息,以了解训练任务的效率。接着,我们将探讨几种扩展的关键技术:管道模式分布式训练数据并行性模型并行性。之后,我们将在大型ImageNet数据集上启动一个大型训练任务,并观察如何进行扩展。最后,我们将讨论用于大规模训练的存储替代方案,即Amazon EFSAmazon FSx for Lustre

我们将讨论以下主题:

  • 理解何时以及如何进行扩展

  • 使用 Amazon SageMaker Debugger 监控和分析训练任务

  • 使用管道模式流式传输数据集

  • 分发训练任务

  • 在 ImageNet 上扩展图像分类模型

  • 使用数据并行性和模型并行性进行训练

  • 使用其他存储服务

技术要求

你需要一个 AWS 账户才能运行本章中包含的示例。如果你还没有账户,请访问aws.amazon.com/getting-started/创建一个。你还应熟悉 AWS 免费套餐(aws.amazon.com/free/),它允许你在一定的使用限制内免费使用许多 AWS 服务。

你需要为你的账户安装并配置 AWS 命令行界面 (CLI) (aws.amazon.com/cli/)。

你需要安装并配置pandasnumpy等工具。

本书中的代码示例可以在 GitHub 上找到,链接为github.com/PacktPublishing/Learn-Amazon-SageMaker-second-edition。你需要安装 Git 客户端来访问这些示例(git-scm.com/)。

理解何时以及如何进行扩展

在我们深入了解扩展技术之前,让我们首先讨论在决定是否需要扩展以及如何扩展时应考虑的监控信息。

理解扩展意味着什么

训练日志告诉我们任务持续了多长时间。但单凭这一点,其实并不太有用。多长时间是太长了呢?这个问题似乎非常主观,不是吗?此外,即使在相同的数据集和基础设施上,改变一个超参数也可能显著影响训练时间。批次大小就是一个例子,且这种情况还有很多。

当我们关注训练时间时,我认为我们实际上是在尝试回答三个问题:

  • 训练时间是否与我们的业务需求兼容?

  • 我们是否在充分利用我们所支付的基础设施?我们是否进行了资源不足或过度配置?

  • 我们能在不花更多钱的情况下训练得更快吗?

使训练时间适应业务需求

问问自己这个问题——如果您的训练任务运行速度加倍,直接对您的业务产生什么影响?在很多情况下,诚实的答案应该是没有。没有任何明确的业务指标会因此得到改善。

当然,一些公司运行的训练任务可能需要几天甚至几周——比如自动驾驶或生命科学领域。对他们来说,训练时间的任何显著减少意味着他们能够更快获得结果,进行分析,并启动下一个迭代。

其他一些公司希望拥有最新的模型,并且每小时都会重新训练。当然,训练时间需要控制在一定范围内,以确保能按时完成。

在这两种类型的公司中,扩展能力至关重要。对于其他公司而言,情况就不那么明确。如果您的公司每周或每月训练一次生产模型,训练时间提前 30 分钟能否带来明显的效果?可能不会。

有些人肯定会反对,说他们需要一直训练很多模型。我恐怕这是一种误解。由于 SageMaker 允许您根据需要随时创建按需基础设施,因此训练活动不会受到容量的限制。这种情况适用于物理基础设施,但不适用于云基础设施。即使您每天需要训练 1,000 个 XGBoost 任务,是否真的在乎每个任务需要 5 分钟而不是 6 分钟?可能不。

有人可能会反驳,“训练越快,成本越低。”再次提醒,这是一种误解。SageMaker 训练任务的成本是训练时间(以秒为单位)乘以实例类型的成本,再乘以实例数量。如果您选择更大的实例类型,训练时间很可能会缩短。它是否会缩短足够多,以抵消增加的实例成本?也许会,也许不会。某些训练工作负载能充分利用额外的基础设施,而某些则不能。唯一的办法就是进行测试并做出数据驱动的决策。

正确配置训练基础设施

SageMaker 支持一长串实例类型,看起来像一个非常诱人的糖果店 (aws.amazon.com/sagemaker/pricing/instance-types)。您所需要做的,就是调用一个 API 启动一个 8 GPU 的 EC2 实例——它比您公司允许您购买的任何服务器都要强大。提醒注意——不要忘记 URL 中的“定价”部分!

注意

如果“EC2 实例”这几个词对您来说没有太大意义,我强烈建议您阅读一下关于 Amazon EC2 的资料,docs.aws.amazon.com/AWSEC2/latest/UserGuide/concepts.html

当然,云基础设施不要求你提前支付大量资金购买和托管服务器。然而,AWS 的账单还是会在月底到来。因此,即使使用了诸如 Managed Spot Training(我们将在下一章讨论)等成本优化技术,正确配置你的训练基础设施仍然至关重要。

我的建议始终是一样的:

  • 确定依赖于训练时间的业务需求。

  • 从最小合理的基础设施开始。

  • 测量技术指标和成本。

  • 如果业务需求已满足,你是否过度配置了?有两种可能的答案:

    a) :缩小规模并重复。

    b) :你完成了。

  • 如果业务需求未能满足,识别瓶颈。

  • 进行一些扩展测试(更大的实例类型)和横向扩展(更多实例)。

  • 测量技术指标和成本。

  • 实施最适合你业务环境的解决方案。

  • 重复。

当然,这个过程的效果取决于参与者的水平。要保持批判性思维!“太慢”不是一个数据点——它只是一个意见。

决定何时扩展

在监控信息方面,你可以依赖三个来源:训练日志、Amazon CloudWatch 指标,以及 Amazon SageMaker Debugger 中的性能分析功能。

注意

如果“CloudWatch”对你来说意义不大,我强烈推荐你阅读一下 docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/

训练日志展示了总训练时间和每秒样本数。正如前一部分讨论的,总训练时间不是一个很有用的指标。除非你有非常严格的截止日期,否则最好忽略它。每秒样本数更为有趣。你可以用它来将你的训练任务与研究论文或博客文章中提供的基准进行比较。如果某人在相同的 GPU 上将同一个模型训练速度提高了一倍,你也应该能做到。当你接近那个数字时,你也能知道改进的空间不大,需要考虑其他扩展技术。

CloudWatch 提供了 1 分钟分辨率的粗粒度基础设施指标。对于简单的训练任务,这些指标足以检查你的训练是否高效利用了底层基础设施,并帮助你识别潜在的瓶颈。

对于更复杂的任务(分布式训练、自定义代码等),SageMaker Debugger 提供了细粒度、近实时的基础设施和 Python 指标,分辨率低至 100 毫秒。这些信息能帮助你深入分析并识别复杂的性能和扩展问题。

决定如何扩展

如前所述,你可以选择扩展(迁移到更大的实例)或横向扩展(使用多个实例进行分布式训练)。我们来看看各自的优缺点。

扩展

向上扩展很简单。你只需要更改实例类型。监控保持不变,而且只需要读取一个训练日志。最后但同样重要的是,单实例训练具有可预测性,并且通常能提供最佳的准确性,因为只有一组模型参数需要学习和更新。

另一方面,如果你的算法计算密集度不高或并行性不足,那么额外的计算能力可能不会带来好处。额外的 vCPU 和 GPU 只有在被充分利用时才有用。你的网络和存储层也必须足够快速,以确保它们始终保持繁忙,这可能需要使用 S3 以外的替代方案,从而增加一些额外的工程工作。即使你没有遇到这些问题,也有一个时刻,你会发现再没有更大的实例可以使用了!

通过多 GPU 实例进行扩展

尽管多 GPU 实例非常诱人,但它们也带来了特定的挑战。ml.g4dn.16xlargeml.p3dn.24xlarge 支持 100 Gbit 的网络和超快速的 SSD NVMe 本地存储。但这种性能水平是有代价的,你需要确保它真的是值得的。

你应该记住,更大并不总是更好。无论 GPU 之间的通信多么快速,它都会引入一些开销,这可能会影响较小训练任务的性能。这里,你也需要进行实验,找到最合适的平衡点。

根据我的经验,想要在多 GPU 实例上获得出色的性能需要一些努力。除非模型太大无法放入单个 GPU,或者算法不支持分布式训练,否则我建议首先尝试在单 GPU 实例上进行扩展。

扩展

扩展使你能够将大数据集分发到训练实例的集群中。即使你的训练任务无法线性扩展,与你的单实例训练相比,你仍然会看到明显的加速效果。你可以使用大量较小的实例,这些实例只处理数据集的一个子集,有助于控制成本。

另一方面,数据集需要以能够高效分发到训练集群的格式进行准备。由于分布式训练需要频繁的通信,网络 I/O 也可能成为瓶颈。不过,通常最大的问题是准确性,通常低于单实例训练的准确性,因为每个实例都使用自己的一组模型参数。这可以通过定期同步训练实例的工作来缓解,但这是一项成本高昂的操作,会影响训练时间。

如果你觉得扩展比看起来的要困难,你是对的。让我们尝试用第一个简单的例子将这些概念付诸实践。

扩展 BlazingText 训练任务

第六章训练自然语言处理模型 中,我们使用 BlazingText 和 Amazon 评论数据集训练了情感分析模型。当时,我们只训练了 100,000 条评论。这一次,我们将使用完整的数据集进行训练:180 万条评论(1.51 亿个单词)。

重新利用我们的 SageMaker Processing 笔记本,我们在 ml.c5.9xlarge 实例上处理完整的数据集,将结果存储在 S3 中,并将其馈送到训练作业中。训练集的大小已增长到 720 MB。

为了给 BlazingText 增加额外工作量,我们应用以下超参数来提高任务将学习的词向量的复杂度:

bt.set_hyperparameters(mode='supervised', vector_dim=300, word_ngrams=3, epochs=50)

我们在单个 ml.c5.2xlarge 实例上进行训练。它具有 8 个 vCPU 和 16 GB 的 RAM,并使用 gp2 类型(基于 SSD)。

该任务运行了 2,109 秒(略超过 35 分钟),最大速度为每秒 484 万个词。我们来看一下 CloudWatch 指标:

  1. SageMaker Studio 中的 实验与试验 面板开始,我们定位到训练作业,并右键点击 在试验详情中打开

  2. 然后,我们选择 AWS 设置 标签。向下滚动,我们看到一个名为 查看实例指标 的链接。点击它会直接带我们到训练作业的 CloudWatch 指标页面。

  3. 让我们在 所有指标 中选择 CPUUtilizationMemoryUtilization,并按下图所示进行可视化:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_1.jpg

图 9.1 – 查看 CloudWatch 指标

在右侧的 Y 轴上,内存利用率稳定在 20%,所以我们肯定不需要更多的 RAM。

仍然在右侧的 Y 轴上,磁盘利用率在训练过程中约为 3%,当模型被保存时,磁盘利用率会上升到 12%。我们为这个实例分配了太多的存储。默认情况下,SageMaker 实例会获得 30 GB 的 Amazon EBS 存储,那么我们浪费了多少钱呢?在 eu-west-1 区域,SageMaker 的 EBS 成本为每 GB 每月 0.154 美元,因此 30 GB 在 2,117 秒内的费用为 0.15430(2109/(24303600)) = 0.00376 美元。虽然这个金额非常低,但如果每月训练数千个任务,这些费用会累积起来。即使每年节省 10 美元,我们也应该节省!这可以通过在所有估算器中设置 volume_size 参数来轻松实现。

在左侧的 Y 轴上,我们看到 CPU 利用率稳定在 790% 左右,非常接近最大值 800%(8 个 vCPU 在 100% 使用率下)。这个任务显然是计算密集型的。

那么,我们有哪些选择呢?如果 BlazingText 支持监督模式下的分布式训练(但实际上不支持),我们可以考虑使用更小的 ml.c5.xlarge 实例(4 个 vCPU 和 8 GB 的 RAM)进行扩展。那样的 RAM 已经足够了,按小步增长容量是一种良好的实践。这就是右尺寸的核心:不多不少,正好合适。

无论如何,我们此时唯一的选择是扩展实例。查看可用实例列表,我们可以尝试 ml.c5.4xlarge。由于 BlazingText 支持单个 GPU 加速,ml.p3.2xlarge(1 个 NVIDIA V100 GPU)也是一个选项。

注意

写作时,成本效益较高的 ml.g4dn.xlarge 不幸未被 BlazingText 支持。

让我们尝试两者并比较训练时间和成本。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/011.jpg

ml.c5.4xlarge 实例在适度价格增加的情况下提供了不错的加速效果。有趣的是,工作负载仍然是计算密集型的,因此我决定尝试更大的 ml.c5.9xlarge 实例(36 个 vCPU),以进一步测试,但加速效果足以抵消成本增加。

GPU 实例速度几乎是原来的 3 倍,因为 BlazingText 已经过优化,能够利用数千个核心。它的费用大约是原来的 3 倍,如果最小化训练时间非常重要,这可能是可以接受的。

这个简单的示例告诉我们,调整训练基础设施并非神秘难懂。通过遵循简单的规则,查看一些指标并运用常识,你可以为你的项目找到合适的实例大小。

现在,让我们介绍 Amazon SageMaker Debugger 中的监控和分析功能,它将为我们提供更多关于训练任务性能的信息。

使用 Amazon SageMaker Debugger 监控和分析训练任务

SageMaker Debugger 包含监控和分析功能,使我们能够以比 CloudWatch 更低的时间分辨率收集基础设施和代码性能信息(通常每 100 毫秒一次)。它还允许我们配置和触发内置或自定义规则,监控训练任务中的不良条件。

分析功能非常容易使用,事实上,它默认是开启的!你可能在训练日志中注意到类似下面这样的行:

2021-06-14 08:45:30 Starting - Launching requested ML instancesProfilerReport-1623660327: InProgress

这告诉我们,SageMaker 正在自动运行一个分析任务,并与我们的训练任务并行进行。分析任务的作用是收集数据点,我们可以在 SageMaker Studio 中显示这些数据点,以便可视化指标并了解潜在的性能问题。

在 SageMaker Studio 中查看监控和分析信息

让我们回到 ml.p3.2xlarge 实例。我们右键点击它,然后选择 打开调试器以获取洞察。这会打开一个新标签,见下图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_2.jpg

图 9.2 – 查看监控和分析信息

在顶部,我们可以看到监控默认是开启的,而分析功能没有开启。在 概览 标签页中展开 资源使用情况总结 项目,我们可以看到基础设施指标的总结,如下一个截图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_3.jpg

图 9.3 – 查看使用情况总结

注意

P50、p95 和 p99 是百分位数。如果你不熟悉这个概念,可以访问 docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch_concepts.html#Percentiles 了解更多信息。

继续讨论 algo-1。例如,您可以在下一个截图中看到其 GPU 使用情况:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_4.jpg

图 9.4 – 查看 GPU 使用情况随时间变化

我们还可以看到一个非常好的系统使用情况视图,按 vCPU 和 GPU 每个一条线,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_5.jpg

图 9.5 – 查看系统使用情况随时间变化

所有这些信息会在训练任务运行时近乎实时更新。只需启动一个训练任务,打开此视图,几分钟后,图表就会显示并更新。

现在,让我们看看如何在训练任务中启用详细的分析信息。

在 SageMaker Debugger 中启用分析

分析会收集框架指标(TensorFlowPyTorchApache MXNet 和 XGBoost)、数据加载器指标和 Python 指标。对于后者,我们可以使用 CProfilePyinstrument

分析可以在估算器中配置(这是我们将使用的选项)。你也可以在 SageMaker Studio 中的运行任务上手动启用它(见 图 9.2 中的滑块)。

让我们重用我们在 第六章 中的 TensorFlow/Keras 示例,训练计算机视觉模型,并每 100 毫秒收集一次所有分析信息:

  1. 首先,我们创建一个 FrameworkProfile 对象,包含分析、数据加载和 Python 配置的默认设置。对于每一项配置,我们都可以指定精确的时间范围或步骤范围来进行数据收集:

    from sagemaker.debugger import FrameworkProfile, DetailedProfilingConfig, DataloaderProfilingConfig, PythonProfilingConfig, PythonProfiler
    framework_profile_params = FrameworkProfile(
     detailed_profiling_config=DetailedProfilingConfig(), 
     dataloader_profiling_config=DataloaderProfilingConfig(),
     python_profiling_config=PythonProfilingConfig(
       python_profiler=PythonProfiler.PYINSTRUMENT)
    )
    
  2. 然后,我们创建一个 ProfilerConfig 对象,设置框架参数和数据收集的时间间隔:

    from sagemaker.debugger import ProfilerConfig 
    profiler_config = ProfilerConfig(
        system_monitor_interval_millis=100,
        framework_profile_params=framework_profile_params)
    
  3. 最后,我们将这个配置传递给估算器,然后像往常一样进行训练:

    tf_estimator = TensorFlow(
        entry_point='fmnist.py',
        . . .                        
        profiler_config=profiler_config)
    
  4. 当训练任务运行时,分析数据会自动收集并保存在 S3 的默认位置(你可以通过 ProfilingConfig 中的 s3_output_path 参数定义自定义路径)。我们还可以使用 smdebug SDKgithub.com/awslabs/sagemaker-debugger)来加载并检查分析数据。

  5. 训练任务完成后不久,我们会在 概览 标签中看到汇总信息,如下图所示:https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_6.jpg

    图 9.6 – 查看分析信息

  6. 我们还可以下载详细的 HTML 格式报告(见 图 9.2 中的按钮)。例如,它会告诉我们哪些是最昂贵的 GPU 操作。毫无意外地,我们看到我们的 fmnist_model 函数和用于二维卷积的 TensorFlow 操作,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_7.jpg

图 9.7 – 查看分析报告

报告还包含训练过程中触发的内置规则信息,提醒我们如 GPU 使用率低、CPU 瓶颈等情况。这些规则有默认设置,如果需要,可以进行自定义。我们将在下一章详细讨论规则,当时我们将讨论如何使用 SageMaker Debugger 调试训练任务。

现在,让我们来看看训练任务中的一些常见扩展问题,以及我们如何解决它们。在此过程中,我们将提到本章后面将介绍的几个 SageMaker 特性。

解决训练中的挑战

我们将深入探讨挑战及其解决方案,如下所示:

我在训练实例上需要大量存储。

如前面的例子所述,大多数 SageMaker 训练实例使用 EBS 卷,你可以在估算器中设置它们的大小。EBS 卷的最大大小为 16 TB,所以应该绰绰有余。如果你的算法需要大量临时存储来存放中间结果,这就是解决方案。

我的数据集非常大,复制到训练实例上需要很长时间。

定义一下“长时间”!如果你在寻找快速解决方案,你可以使用具有高网络性能的实例类型。例如,ml.g4dnml.p3dn 实例支持 弹性结构适配器aws.amazon.com/hpc/efa),可以达到最高 100 Gbit/s 的速度。

如果这还不够,而且如果你正在对单个实例进行训练,你应该使用管道模式,它从 S3 流式传输数据,而不是复制数据。

如果训练是分布式的,你可以将 FullyReplicated 更改为 ShardedbyS3Key,这将仅将数据集的一部分分发到每个实例。这可以与管道模式结合使用,以提高性能。

我的数据集非常大,无法完全装入内存。

如果你想坚持使用单个实例,解决这个问题的快速方法是进行扩展。ml.r5d.24xlargeml.p3dn.24xlarge 实例具有 768 GB 的内存!如果分布式训练是一个选项,那么你应该配置它并应用数据并行处理。

CPU 使用率很低。

假设你没有过度配置,最可能的原因是 I/O 延迟(网络或存储)。CPU 被阻塞,因为它在等待从存储位置获取数据。

首先你应该检查数据格式。如前几章所述,RecordIOTFRecord 文件是不可避免的。如果你使用的是其他格式(如 CSV、单独的图片等),你应该在调整基础设施之前从这些地方入手。

如果数据是从 S3 复制到 EBS 卷的,你可以尝试使用具有更多 EBS 带宽的实例。有关详细信息,请访问以下位置:

docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-optimized.html

你还可以切换到具有本地 NVMe 存储的实例类型(g4dn 和 p3dn)。如果问题仍然存在,你应该检查读取数据并将其传递给训练算法的代码。它可能需要更多的并行处理。

如果数据是通过管道模式从 S3 流式传输的,那么你不太可能已经达到了 25 GB/s 的最大传输速度,但值得检查 CloudWatch 中的实例指标。如果你确定没有其他原因导致这种情况,你应该转向其他文件存储服务,如 Amazon EFSAmazon FSx for Lustre

GPU 内存利用率低。

GPU 没有从 CPU 获取足够的数据。你需要增加批次大小,直到内存利用率接近 100%。如果增加过多,你将会收到一个令人烦恼的 out of memory 错误信息,如下所示:

/opt/brazil-pkg-cache/packages/MXNetECL/MXNetECL-v1.4.1.1457.0/AL2012/generic-flavor/src/src/storage/./pooled_storage_manager.h:151: cudaMalloc failed: out of memory

在使用数据并行配置的多 GPU 实例时,你应该将传递给估算器的批次大小乘以实例中 GPU 的数量。

增加批次大小时,你需要考虑可用的训练样本数量。例如,我们在 第五章《训练计算机视觉模型》中使用的 Pascal VOC 数据集只有 1,464 个样本,因此可能没有必要将批次大小增加到 64 或 128 以上。

最后,批次大小对作业收敛性有重要影响。非常大的批次可能会使收敛变慢,因此你可能需要相应地增加学习率。

有时,你只能接受 GPU 内存利用率低的事实!

GPU 利用率低。

也许你的模型根本没有足够大,无法充分利用 GPU。你应该尝试在更小的 GPU 上进行缩放。

如果你正在处理一个大型模型,GPU 可能会因为 CPU 无法提供足够的数据而停滞。如果你能够控制数据加载代码,应该尝试增加更多的并行性,例如为数据加载和预处理增加更多的线程。如果无法控制代码,你应该尝试使用更多 vCPU 的更大实例类型。希望数据加载代码能够充分利用它们。

如果数据加载代码中有足够的并行性,那么慢 I/O 很可能是罪魁祸首。你应该寻找更快的替代方案(如 NVMe、EFS 或 FSx for Lustre)。

GPU 利用率高。

这是一个好情况!你在高效地使用你为其支付的基础设施。正如前面示例中讨论的,你可以尝试扩展(更多 vCPU 或更多 GPU),或者横向扩展(更多实例)。将两者结合使用,适用于诸如深度学习之类的高度并行工作负载。

现在我们对作业扩展有了更多的了解,接下来让我们学习更多 SageMaker 的功能,从管道模式开始。

使用管道模式流式传输数据集

估算器的默认设置是将数据集复制到训练实例中,这被称为 文件模式。而 管道模式 则直接从 S3 流式传输数据。该功能的名称来自于它使用 Unix 命名管道(也称为 FIFOs):在每个 epoch 开始时,每个输入通道会创建一个管道。

管道模式消除了将数据复制到训练实例的需求。显然,训练任务开始得更快。它们通常也运行得更快,因为管道模式经过高度优化。另一个好处是,您不必为训练实例上的数据集预置存储。

减少训练时间和存储意味着您可以节省成本。数据集越大,节省的成本就越多。您可以在以下链接找到基准测试:

aws.amazon.com/blogs/machine-learning/accelerate-model-training-using-faster-pipe-mode-on-amazon-sagemaker/

在实践中,您可以开始使用管道模式处理数百兆字节及以上的数据集。事实上,这项功能使您能够处理无限大的数据集。由于存储和内存需求不再与数据集大小挂钩,因此您的算法可以处理的数据量没有实际限制。可以在 PB 级别的数据集上进行训练。

使用管道模式与内置算法

管道模式的主要候选者是内置算法,因为大多数内置算法本身就支持此模式:

  • 线性学习器k-均值k-最近邻主成分分析随机切割森林神经主题建模:RecordIO 封装的 protobuf 或 CSV 数据

  • 因式分解机潜在狄利克雷分配:RecordIO 封装的 protobuf 数据

  • BlazingText(监督模式):增强的清单

  • 图像分类目标检测:使用 RecordIO 封装的 protobuf 数据或增强的清单

  • 语义分割:增强的清单。

您应该已经熟悉 im2rec 工具,该工具具有生成多个列表文件(--chunks)的选项。如果您已有现成的列表文件,当然也可以自己分割它们。

当我们讨论由 SageMaker Ground Truth 标注的数据集时,我们曾经查看过 增强清单 格式,详见 第五章,《训练计算机视觉模型》。对于计算机视觉算法,JSON Lines 文件包含 S3 中图像的位置及其标注信息。您可以在以下链接了解更多内容:

docs.aws.amazon.com/sagemaker/latest/dg/augmented-manifest.html

使用管道模式与其他算法和框架

TensorFlow 支持管道模式,感谢 AWS 实现的 PipeModeDataset 类。以下是一些有用的资源:

对于其他框架以及你自己的自定义代码,仍然可以在训练容器中实现管道模式。一个 Python 示例可以在以下链接找到:

github.com/awslabs/amazon-sagemaker-examples/tree/master/advanced_functionality/pipe_bring_your_own

使用 MLIO 简化数据加载

MLIO (https://github.com/awslabs/ml-io) 是一个 AWS 开源项目,允许你使用管道模式加载存储在内存、本地存储或 S3 中的数据。然后,可以将数据转换为不同的流行格式。

这里是一些高级特性:

  • 输入格式CSVParquet、RecordIO-protobuf、JPEGPNG

  • 转换格式:NumPy 数组、SciPy 矩阵、Pandas DataFrame、TensorFlow 张量、PyTorch 张量、Apache MXNet 数组以及 Apache Arrow

  • API 可用于 Python 和 C++

现在,让我们用管道模式运行一些示例。

使用管道模式训练分解机器

我们将重新访问在 第四章 中使用的示例,训练机器学习模型,当时我们在 MovieLens 数据集上训练了一个推荐模型。当时,我们使用了一个小版本的数据集,限制为 100,000 条评论。这一次,我们将使用最大版本:

  1. 我们下载并解压数据集:

    %%sh
    wget http://files.grouplens.org/datasets/movielens/ml-25m.zip
    unzip ml-25m.zip
    
  2. 该数据集包括来自 162,541 个用户的 25,000,095 条评论,涉及 62,423 部电影。与 100k 版本不同,电影编号不是顺序的。最后一部电影的 ID 为 209,171,这无谓地增加了特征的数量。另一种选择是重新编号电影,但我们这里不这样做:

    num_users=162541
    num_movies=62423
    num_ratings=25000095
    max_movieid=209171
    num_features=num_users+max_movieid
    
  3. 就像在 第四章*,训练机器学习模型* 中一样,我们将数据集加载到一个稀疏矩阵(来自 SciPy 的 lil_matrix),并将其拆分为训练集和测试集,然后将这两个数据集转换为 RecordIO 封装的 protobuf。考虑到数据集的大小,这个过程在一个小型 Studio 实例上可能需要 45 分钟。然后,我们将数据集上传到 S3。

  4. 接下来,我们配置两个输入通道,并将它们的输入模式设置为管道模式,而不是文件模式:

    From sagemaker import TrainingInput
    s3_train_data = TrainingInput (
        train_data,                                
        content_type='application/x-recordio-protobuf',
        input_mode='Pipe')
    s3_test_data = TrainingInput (
       test_data,                                        
       content_type='application/x-recordio-protobuf',                                           
       input_mode='Pipe')
    
  5. 然后,我们配置估算器,并像往常一样在 ml.c5.xlarge 实例(4 个虚拟 CPU、8 GB 内存,$0.23 每小时,位于 eu-west-1 区域)上进行训练。

查看训练日志,我们看到如下信息:

2021-06-14 15:02:08 Downloading - Downloading input data
2021-06-14 15:02:08 Training - Downloading the training image...

如预期的那样,复制数据集并未花费任何时间。文件模式下的相同步骤需要 66 秒。即使数据集只有 1.5 GB,管道模式依然非常有效。随着数据集的增大,这一优势将只会增加!

现在,让我们继续进行分布式训练。

分布式训练任务

分布式训练让你通过在 CPU 或 GPU 实例的集群上运行训练作业来扩展训练规模。它可以用来解决两个不同的问题:非常大的数据集和非常大的模型。

理解数据并行和模型并行

一些数据集太大,单个 CPU 或 GPU 在合理时间内无法完成训练。通过使用一种叫做数据并行的技术,我们可以将数据分配到训练集群中。完整的模型仍然加载到每个 CPU/GPU 上,但每个 CPU/GPU 只接收数据集的一部分,而不是整个数据集。理论上,这应该根据参与的 CPU/GPU 数量线性加速训练,但如你所料,现实往往不同。

信不信由你,一些最先进的深度学习模型太大,无法装载到单个 GPU 上。通过使用一种叫做模型并行的技术,我们可以将其拆分,并将各层分布到 GPU 集群中。因此,训练批次将跨多个 GPU 流动,由所有层共同处理。

现在,让我们看看在哪里可以在 SageMaker 中使用分布式训练。

为内置算法分发训练

数据并行几乎适用于所有内置算法(语义分割和 LDA 是显著的例外)。由于它们是用 Apache MXNet 实现的,因此会自动使用其原生的分布式训练机制。

为内置框架分发训练

TensorFlow、PyTorch、Apache MXNet 和Hugging Face都有原生的数据并行机制,并且都支持 SageMaker。Horovod (github.com/horovod/horovod) 也可用。

对于 TensorFlow、PyTorch 和 Hugging Face,你还可以使用更新的SageMaker 分布式数据并行库SageMaker 模型并行库。这两者将在本章后面讨论。

分布式训练通常需要在训练代码中进行框架特定的修改。你可以在框架文档中找到更多信息(例如 www.tensorflow.org/guide/distributed_training),以及在github.com/awslabs/amazon-sagemaker-examples上托管的示例笔记本:

  • sagemaker-python-sdk/tensorflow_script_mode_horovod

    b) advanced_functionality/distributed_tensorflow_mask_rcnn

  • sagemaker-python-sdk/keras_script_mode_pipe_mode_horovod

  • sagemaker-python-sdk/pytorch_horovod_mnist

每个框架都有其独特之处,但我们在前面章节中讨论的内容依然适用。如果你想最大限度地利用你的基础设施,需要关注批处理大小、同步等方面。进行实验、监控、分析并迭代!

为自定义容器分发训练

如果你使用的是自己的定制容器进行训练,你必须实现自己的分布式训练机制。说实话,这会是非常繁琐的工作。SageMaker 仅帮助提供集群实例的名称和容器网络接口的名称。这些信息可以在容器内的/opt/ml/input/config/resourceconfig.json文件中找到。

你可以在以下链接找到更多信息:

docs.aws.amazon.com/sagemaker/latest/dg/your-algorithms-training-algo-running-container.html

现在是时候进行分布式训练示例了!

在 ImageNet 上扩展图像分类模型

第五章,《训练计算机视觉模型》中,我们使用包含狗和猫图像的小型数据集(25,000 张训练图像)来训练图像分类算法。这一次,我们来挑战一个稍大的数据集。

我们将从头开始在ImageNet数据集上训练一个 ResNet-50 网络——这是许多计算机视觉应用的参考数据集(www.image-net.org)。2012 版包含 1,281,167 张训练图像(140 GB)和 50,000 张验证图像(6.4 GB),涵盖 1,000 个类别。

如果你想在较小的规模上进行实验,可以使用数据集的 5-10%。最终的准确度可能不会那么好,但对我们的目的来说无关紧要。

准备 ImageNet 数据集

这需要大量的存储空间——数据集大小为 150 GB,因此请确保你有至少 500 GB 的可用空间,以存储 ZIP 格式和处理后的格式。你还需要大量的带宽和耐心来下载它。我使用的是运行在us-east-1区域的 EC2 实例,我的下载花费了五天

  1. 访问 ImageNet 网站,注册以下载数据集并接受条款。你将获得一个用户名和访问密钥,允许你下载数据集。

  2. 其中一个 TensorFlow 的库包含一个很好的脚本,可以下载并解压数据集。使用nohup非常关键,这样即使会话终止,过程仍然会继续运行:

    $ git clone https://github.com/tensorflow/models.git
    $ export IMAGENET_USERNAME=YOUR_USERNAME
    $ export IMAGENET_ACCESS_KEY=YOUR_ACCESS_KEY
    $ cd models/research/inception/inception/data
    $ mv imagenet_2012_validation_synset_labels.txt synsets.txt
    $ nohup bash download_imagenet.sh . synsets.txt >& download.log &
    
  3. 一旦完成(再次强调,下载需要几天时间),imagenet/train目录包含训练数据集(每个类别一个文件夹)。imagenet/validation目录包含 50,000 张图片,存储在同一个文件夹中。我们可以使用一个简单的脚本按类别组织这些文件夹:

    $ wget https://raw.githubusercontent.com/juliensimon/aws/master/mxnet/imagenet/build_validation_tree.sh
    $ chmod 755 build_validation_tree.sh
    $ cd imagenet/validation
    $ ../../build_validation_tree.sh
    $ cd ../..
    
  4. 我们将使用 Apache MXNet 库中的im2rec工具来构建 RecordIO 文件。让我们先安装依赖项并获取im2rec

    $ sudo yum -y install python-devel python-pip opencv opencv-devel opencv-python
    $ pip3 install mxnet opencv-python –user
    $ wget https://raw.githubusercontent.com/apache/incubator-mxnet/master/tools/im2rec.py
    
  5. imagenet目录中,我们运行im2rec两次——第一次构建列表文件,第二次构建 RecordIO 文件。我们创建了大约 1 GB 大小的 RecordIO 文件(稍后我们会解释为什么这很重要)。我们还将图像的较小尺寸调整为224,这样算法就不需要再做此操作:

    $ cd imagenet
    $ python3 ../im2rec.py --list --chunks 6 --recursive val validation
    $ python3 ../im2rec.py --num-thread 16 --resize 224 val_ validation
    $ python3 ../im2rec.py --list --chunks 140 --recursive train train
    $ python3 ../im2rec.py --num-thread 16 --resize 224 train_ train
    
  6. 最后,我们将数据集同步到 S3:

    $ mkdir -p input/train input/validation
    $ mv train_*.rec input/train
    $ mv val_*.rec input/validation
    $ aws s3 sync input s3://sagemaker-us-east-1-123456789012/imagenet-split/input/
    

数据集现在已经准备好进行训练。

定义我们的训练作业

现在数据集已经准备好,我们需要思考一下训练作业的配置。具体来说,我们需要考虑以下内容:

  • 输入配置,定义数据集的位置和属性

  • 运行训练作业的基础设施要求

  • 配置算法的超参数

让我们详细看看这些项目。

定义输入配置

考虑到数据集的大小,管道模式似乎是一个好主意。出于好奇,我尝试过使用文件模式进行训练。即使拥有 100 Gbit/s 的网络接口,将数据集从 S3 复制到本地存储也花费了近 25 分钟。所以,还是使用管道模式吧!

你可能会想,为什么我们要把数据集拆分成多个文件。原因如下:

  • 一般来说,多个文件创造了更多的并行机会,使得编写快速数据加载和处理代码变得更加容易。

  • 我们可以在每个训练周期开始时对文件进行洗牌,消除由于样本顺序导致的任何潜在偏差。

  • 它使得处理数据集的一部分变得非常容易。

既然我们已经定义了输入配置,那基础设施要求呢?

定义基础设施要求

ImageNet 是一个庞大而复杂的数据集,需要大量训练才能达到良好的准确度。

经过快速测试,单个ml.p3.2xlarge实例在批量大小设置为 128 时,可以以每秒约 335 张图像的速度处理数据集。由于我们有大约 1,281,167 张图像,可以预计一个周期大约需要 3,824 秒(约 1 小时 4 分钟)。

假设我们需要训练 150 个周期以获得合理的准确率,我们预计作业将持续(3,824/3,600)*150 = 158 小时(约 6.5 天)。从商业角度来看,这可能不可接受。为了记录,在us-east-1区域,每小时$3.825 的实例费用下,这个作业大约需要花费$573。

我们来尝试使用ml.p3dn.24xlarge实例来加速我们的作业。每个实例配备八个 NVIDIA V100,每个有 32 GB 的 GPU 内存(是其他p3实例的两倍)。它们还配备 96 个Intel Skylake核心,768 GB 的 RAM,以及 1.8 TB 的本地 NVMe 存储。虽然我们在这里不会使用它,但后者对于长期运行的大规模作业来说是一个极好的存储选项。最后但同样重要的是,这种实例类型具有 100 Gbit/s 的网络连接,适合从 S3 进行数据流传输和实例间通信。

注意

每小时$35.894 的费用,你可能不想在家或工作时尝试,除非获得许可。你的服务配额可能根本不允许你运行如此多的基础设施,而且你必须首先联系 AWS 支持。

在下一章中,我们将讨论托管抢占训练—一种大幅降低训练成本的好方法。我们将在介绍完这一主题后再次回到 ImageNet 示例,因此你现在一定不要进行训练!

在 ImageNet 上训练

让我们配置训练任务:

  1. 我们在两个输入通道上配置了管道模式。训练通道的文件会被打乱,以增加额外的随机性:

    prefix = 'imagenet-split'
    s3_train_path = 
    's3://{}/{}/input/training/'.format(bucket, prefix)
    s3_val_path = 
    's3://{}/{}/input/validation/'.format(bucket, prefix)
    s3_output = 
    's3://{}/{}/output/'.format(bucket, prefix)
    from sagemaker import TrainingInput
    from sagemaker.session import ShuffleConfig
    train_data = TrainingInput(
       s3_train_path
       shuffle_config=ShuffleConfig(59),
       content_type='application/x-recordio',
       input_mode='Pipe')
    validation_data = TrainingInput(
       s3_val_path,
       content_type='application/x-recordio', 
       input_mode='Pipe')
    s3_channels = {'train': train_data, 
                   'validation': validation_data}
    
  2. 首先,我们配置 Estimator 模块,使用一个单独的 ml.p3dn.24xlarge 实例:

    from sagemaker import image_uris
    region_name = boto3.Session().region_name
    container = image_uris.retrieve(
        'image-classification', region)
    ic = sagemaker.estimator.Estimator(
         container,
         role= sagemaker.get_execution_role(),
         instance_count=1,                                 
         instance_type='ml.p3dn.24xlarge',
         output_path=s3_output)
    
  3. 我们设置了超参数,首先使用合理的批量大小 1,024,然后启动训练:

    ic.set_hyperparameters(
        num_layers=50,                 
        use_pretrained_model=0,        
        num_classes=1000,              
        num_training_samples=1281167,
        mini_batch_size=1024,
        epochs=2,
        kv_store='dist_sync',
        top_k=3)         
    

更新批量大小

每个 epoch 的时间是 727 秒。对于 150 个 epoch,这相当于 30.3 小时的训练(1.25 天),成本为 $1,087。好消息是我们加速了 5 倍。坏消息是成本增加了 2 倍。让我们开始扩展。

查看 CloudWatch 中的总 GPU 利用率,我们看到它没有超过 300%。也就是说,每个 GPU 的利用率为 37.5%。这可能意味着我们的批量大小太小,不能让 GPU 完全忙碌。让我们将批量大小提升到 (1,024/0.375)=2730,四舍五入到 2,736,以便能被 8 整除:

注意

根据算法版本,可能会出现 out of memory 错误。

ic.set_hyperparameters(
    num_layers=50,                 
    use_pretrained_model=0,        
    num_classes=1000,              
    num_training_samples=1281167,
    mini_batch_size=2736,         # <--------
    epochs=2,
    kv_store='dist_sync',
    top_k=3)         

再次训练,现在每个 epoch 持续 758 秒。看起来这次将 GPU 内存使用率最大化并没有带来太大的差异。也许它被同步梯度的成本抵消了?无论如何,尽可能保持 GPU 核心的高负载是一个好习惯。

增加更多实例

现在,让我们添加第二个实例以扩展训练任务:

ic = sagemaker.estimator.Estimator(
    container,
    role,
    instance_count=2,                 # <--------
    instance_type='ml.p3dn.24xlarge',
    output_path=s3_output)

现在每个 epoch 的时间是 378 秒!对于 150 个 epoch,这相当于 15.75 小时的训练,成本为 $1,221。与我们最初的任务相比,这速度提高了 2 倍,成本降低了 3 倍!

那四个实例怎么样?看看我们能否继续扩展:

ic = sagemaker.estimator.Estimator(
    container,
    role,
    instance_count=4,                 # <--------
    instance_type='ml.p3dn.24xlarge',
    output_path=s3_output)

现在每个 epoch 的时间是 198 秒!对于 150 个 epoch,这相当于 8.25 小时的训练,成本为 $1,279。我们再次加速了 2 倍,且成本略有增加。

现在,我们是否要训练八个实例?当然!谁不想在 64 个 GPU、327K CUDA 核心和 2 TB(!)GPU 内存上进行训练呢?

ic = sagemaker.estimator.Estimator(
    container,
    role,
    instance_count=8,                 # <--------
    instance_type='ml.p3dn.24xlarge',
    output_path=s3_output)

现在每个 epoch 的时间是 99 秒。对于 150 个 epoch,这相当于 4.12 小时的训练,成本为 $1,277。我们再次加速了 2 倍,而且没有增加任何额外成本。

总结一下

以最初成本的 2 倍,我们通过管道模式、分布式训练和最先进的 GPU 实例将训练任务加速了 38 倍。

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/02.jpg

图 9.8 训练任务的结果

不错!节省训练时间有助于加快迭代速度,更快地得到高质量模型,并更早地进入生产阶段。我很确定这很容易抵消额外的成本。不过,在下一章中,我们将看到如何通过托管的抢占式训练大幅削减训练成本。

既然我们已经熟悉了分布式训练,接下来让我们看看两个新的 SageMaker 库,用于数据并行和模型并行。

使用 SageMaker 数据和模型并行库进行训练

这两个库是在 2020 年底推出的,显著提升了大规模训练任务的性能。

SageMaker **分布式数据并行(DDP)**库实现了 GPU 集群上计算的高效分布。它通过消除 GPU 间通信来优化网络通信,最大化它们在训练中所花费的时间和资源。你可以通过以下链接了解更多内容:

aws.amazon.com/blogs/aws/managed-data-parallelism-in-amazon-sagemaker-simplifies-training-on-large-datasets/

DDP 支持 TensorFlow、PyTorch 和 Hugging Face。前两个需要对训练代码进行小的修改,但最后一个则不需要。由于 DDP 仅在大规模、长时间运行的训练任务中才有意义,因此可用的实例类型包括ml.p3.16xlargeml.p3dn24dnxlargeml.p4d.24xlarge

SageMaker **分布式模型并行(DMP)**库解决了另一个问题。某些大型深度学习模型过于庞大,无法适应单个 GPU 的内存。另一些模型勉强能够适应,但会迫使你使用非常小的批量大小,从而减慢训练速度。DMP 通过自动将模型分割到 GPU 集群中,并协调数据在这些分区之间流动,解决了这个问题。你可以通过以下链接了解更多内容:

aws.amazon.com/blogs/aws/amazon-sagemaker-simplifies-training-deep-learning-models-with-billions-of-parameters/

DMP 支持 TensorFlow、PyTorch 和 Hugging Face。再次说明,前两个需要对训练代码进行小的修改,而 Hugging Face 则不需要,因为其Trainer API 完全支持 DMP。

让我们通过回顾我们在第七章中的 TensorFlow 和 Hugging Face 示例来尝试这两种方法,使用内置框架扩展机器学习服务

使用 SageMaker DDP 进行 TensorFlow 训练

我们最初的代码使用了高级的 Keras API:compile()fit()等。为了实现 DDP,我们需要重写这些代码,使用tf.GradientTape()并实现一个自定义训练循环。其实并不像听起来那么难,接下来让我们开始吧:

  1. 首先,我们需要导入并初始化 DDP:

    import smdistributed.dataparallel.tensorflow as sdp
    sdp.init()
    
  2. 然后,我们获取实例上存在的 GPU 列表,并为它们分配一个本地的 DDP 排名,这是一个简单的整数标识符。我们还允许内存增长,这是 DDP 所需的 TensorFlow 功能:

    gpus = tf.config.experimental.
                list_physical_devices('GPU')
    if gpus:
        tf.config.experimental.set_visible_devices(
            gpus[sdp.local_rank()], 'GPU')
    for gpu in gpus:
        tf.config.experimental.set_memory_growth(
            gpu, True)
    
  3. 根据文档的建议,我们根据训练集群中 GPU 的数量增加批量大小和学习率。这对任务的准确性至关重要:

    batch_size = args.batch_size*sdp.size()
    lr         = args.learning_rate*sdp.size()
    
  4. 接着,我们创建一个损失函数和优化器。标签在预处理过程中已经进行了独热编码,所以我们使用 CategoricalCrossentropy,而不是 SparseCategoricalCrossentropy。我们还在所有 GPU 上初始化模型和优化器变量:

    loss = tf.losses.CategoricalCrossentropy()
    opt = tf.optimizers.Adam(lr)
    sdp.broadcast_variables(model.variables, root_rank=0)
    sdp.broadcast_variables(opt.variables(), root_rank=0)
    
  5. 接下来,我们需要编写一个 training_step() 函数,并用 @tf.function 装饰它,以便 DDP 能识别它。顾名思义,这个函数负责在训练集群中的每个 GPU 上运行训练步骤:预测一个批次,计算损失,计算梯度并应用它们。它基于 tf.GradientTape() API,我们只是将其包装成 sdp.DistributedGradientTape()。在每个训练步骤的结束时,我们使用 sdp.oob_allreduce() 来计算平均损失,使用来自所有 GPU 的值:

    @tf.function
    def training_step(images, labels):
        with tf.GradientTape() as tape:
            probs = model(images, training=True)
            loss_value = loss(labels, probs)
        tape = sdp.DistributedGradientTape(tape)
        grads = tape.gradient(
            loss_value, model.trainable_variables)
        opt.apply_gradients(
            zip(grads, model.trainable_variables))
        loss_value = sdp.oob_allreduce(loss_value)
        return loss_value
    
  6. 然后,我们编写训练循环。没有特别之处。为了避免日志污染,我们仅打印主 GPU(rank 0)的消息:

    steps = len(train)//batch_size
    for e in range(epochs):
        if sdp.rank() == 0:
            print("Start epoch %d" % (e))
        for batch, (images, labels) in 
        enumerate(train.take(steps)):
            loss_value = training_step(images, labels)
            if batch%10 == 0 and sdp.rank() == 0:
                print("Step #%d\tLoss: %.6f" 
                      % (batch, loss_value))
    
  7. 最后,我们只在 GPU #0 上保存模型:

    if sdp.rank() == 0:
        model.save(os.path.join(model_dir, '1'))
    
  8. 接下来,在我们的笔记本中,我们使用两个 ml.p3.16xlarge 实例来配置此任务,并通过在估算器中添加额外参数启用数据并行:

    from sagemaker.tensorflow import TensorFlow
    tf_estimator = TensorFlow(
        . . .
        instance_count=2, 
        instance_type='ml.p3.16xlarge',
        hyperparameters={'epochs': 10, 
            'learning-rate': 0.0001, 'batch-size': 32},
        distribution={'smdistributed': 
            {'dataparallel': {'enabled': True}}}
    )
    
  9. 我们照常训练,可以在训练日志中看到步骤在进行:

    [1,0]<stdout>:Step #0#011Loss: 2.306620
    [1,0]<stdout>:Step #10#011Loss: 1.185689
    [1,0]<stdout>:Step #20#011Loss: 0.909270
    [1,0]<stdout>:Step #30#011Loss: 0.839223
    [1,0]<stdout>:Step #40#011Loss: 0.772756
    [1,0]<stdout>:Step #50#011Loss: 0.678521
    . . .
    

如你所见,使用 SageMaker DDP 扩展训练任务其实并不难,尤其是当你的训练代码已经使用了低级 API 时。我们这里使用了 TensorFlow,PyTorch 的过程非常相似。

现在,让我们看看如何使用这两个库训练大型 Hugging Face 模型。确实,最先进的 NLP 模型在不断变大和变得更加复杂,它们是数据并行和模型并行的理想候选者。

在 Hugging Face 上使用 SageMaker DDP 进行训练

由于 Hugging Face 的 Trainer API 完全支持 DDP,我们不需要修改训练脚本。太棒了!只需在估算器中添加一个额外的参数,设置实例类型和实例数量,就可以开始了:

huggingface_estimator = HuggingFace(
   . . . 
   distribution={'smdistributed': 
                    {'dataparallel':{'enabled': True}}
                }
)

在 Hugging Face 上使用 SageMaker DMP 进行训练

添加 DMP 也不难。我们的 Hugging Face 示例使用了一个 DistilBERT 模型,大小大约为 250 MB。这个大小足以在单个 GPU 上运行,但我们还是来尝试用 DMP 进行训练:

  1. 首先,我们需要将 processes_per_host 配置为小于或等于训练实例上 GPU 数量的值。在这里,我将使用一个带有 8 个 NVIDIA V100 GPU 的 ml.p3dn.24xlarge 实例:

    mpi_options = {
       'enabled' : True,
       'processes_per_host' : 8
    }
    
  2. 然后,我们配置 DMP 选项。这里,我设置了最重要的参数——我们希望的模型分区数(partitions),以及为了增加并行性而应复制多少次它们(microbatches)。换句话说,我们的模型将被分成四个部分,每个部分会被复制,并且这八个部分将分别运行在不同的 GPU 上。你可以在以下链接找到更多关于所有参数的信息:

    sagemaker.readthedocs.io/en/stable/api/training/smd_model_parallel_general.html

    smp_options = {
        'enabled': True,
        'parameters": {
            'microbatches': 2,
            'partitions': 4
        }
    }
    
  3. 最后,我们配置估算器并按常规进行训练:

    huggingface_estimator = HuggingFace(
        . . .
        instance_type='ml.p3dn.24xlarge',
        instance_count=1,
        distribution={'smdistributed': 
            {'modelparallel': smp_options},
             'mpi': mpi_options}
    )
    

    您可以在这里找到更多示例:

本章结束时,我们将查看您应考虑的针对大规模、高性能训练任务的存储选项。

使用其他存储服务

到目前为止,我们已经使用 S3 存储训练数据。在大规模情况下,吞吐量和延迟可能成为瓶颈,因此有必要考虑其他存储服务:

使用 SageMaker 和 Amazon EFS

EFS 是一个托管存储服务,兼容 NFS v4。它允许您创建可以附加到 EC2 实例和 SageMaker 实例的卷。这是一种方便的数据共享方式,您可以使用它来扩展大规模训练任务的 I/O。

默认情况下,文件存储在标准类中。您可以启用生命周期策略,自动将一段时间内未访问的文件移至低频访问,这种方式较慢,但更加具有成本效益。

您可以选择两种吞吐量模式中的一种:

  • 突发吞吐量:突发积分会随着时间积累,突发容量取决于文件系统的大小:100 MB/s,并且每个 TB 的存储会额外增加 100 MB/s。

  • 预配置吞吐量:您可以设置期望的吞吐量,范围从 1 到 1,024 MB/s。

您还可以选择两种性能模式中的一种:

  • 通用用途:对于大多数应用程序来说,这种模式足够了。

  • 最大 I/O:当数十个或数百个实例访问卷时,使用这个设置。吞吐量将最大化,但会牺牲延迟。

我们创建一个 8 GB 的 EFS 卷。然后,我们将在 EC2 实例上挂载它,复制之前准备好的Pascal VOC数据集,并训练一个目标检测任务。为了保持合理的成本,我们不会扩展该任务,但无论规模大小,整个过程都是相同的。

配置 EFS 卷

EFS 控制台使创建卷变得极其简单。你可以在 docs.aws.amazon.com/efs/latest/ug/getting-started.html 找到详细的操作说明:

  1. 我们将卷名称设置为 sagemaker-demo

  2. 我们选择我们的默认 VPC,并使用 区域 可用性。

  3. 我们创建了卷。一旦卷准备好,你应该会看到类似下面的截图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_9.jpg

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_8.jpg)

图 9.9– 创建 EFS 卷

EFS 卷已准备好接收数据。我们现在将创建一个新的 EC2 实例,挂载 EFS 卷,并复制数据集。

创建 EC2 实例

由于 EFS 卷位于 VPC 内部,它们只能由位于同一 VPC 中的实例访问。这些实例必须还拥有一个 安全组,该安全组允许进入的 NFS 流量:

  1. 在 VPC 控制台中(console.aws.amazon.com/vpc/#vpcs:sort=VpcId),我们记录下默认 VPC 的 ID。对我而言,它是 vpc-def884bb

  2. 仍然在 VPC 控制台中,我们转到 子网 部分(console.aws.amazon.com/vpc/#subnets:sort=SubnetId)。我们记录下默认 VPC 中所有子网的子网 ID 和可用区。

    对我来说,它们看起来像下图所示:

    https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_10.jpg

    ](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_9.jpg)

    图 9.10 – 查看默认 VPC 的子网

  3. 进入 EC2 控制台,我们创建一个 EC2 实例。我们选择 Amazon Linux 2 镜像和 t2.micro 实例类型。

  4. 接下来,我们设置 eu-west-1a 可用区。我们还将其分配给我们刚刚创建的安全组,IAM 角色分配给具有适当 S3 权限的角色,以及 文件系统分配给我们刚刚创建的 EFS 文件系统。我们还确保勾选了自动创建并附加所需安全组的选项。

  5. 在接下来的界面中,我们保持存储和标签不变,并附加一个允许 ssh 连接的安全组。最后,我们启动实例创建。

访问 EFS 卷

一旦实例准备好,我们可以通过 ssh 连接到它:

  1. 我们看到 EFS 卷已自动挂载:

    [ec2-user]$ mount|grep efs
    127.0.0.1:/ on /mnt/efs/fs1 type nfs4
    
  2. 我们移动到该位置,并从 S3 同步我们的 PascalVOC 数据集。由于文件系统以 root 身份挂载,我们需要使用 sudo

    [ec2-user] cd /mnt/efs/fs1
    [ec2-user] sudo aws s3 sync s3://sagemaker-ap-northeast-2-123456789012/pascalvoc/input input
    

工作完成。我们可以退出并关闭或终止实例,因为我们不再需要它。

现在,让我们使用这个数据集进行训练。

使用 EFS 训练物体检测模型

训练过程是相同的,唯一不同的是输入数据的位置:

  1. 我们不再使用 TrainingInput 对象来定义输入通道,而是使用 FileSystemInput 对象,传入我们 EFS 卷的标识符和卷内的绝对数据路径:

    from sagemaker.inputs import FileSystemInput
    efs_train_data = FileSystemInput(
                     file_system_id='fs-fe36ef34',
                     file_system_type='EFS',
                     directory_path='/input/train')
    efs_validation_data = FileSystemInput(
                          file_system_id='fs-fe36ef34',
                          file_system_type='EFS',
                          directory_path='/input/validation')
    data_channels = {'train': efs_train_data, 
                     'validation': efs_validation_data}
    
  2. 我们配置Estimator模块,传递托管 EFS 卷的 VPC 子网列表。SageMaker 将在那里启动训练实例,以便它们可以挂载 EFS 卷。我们还需要传递一个安全组,允许 NFS 流量。我们可以重用为 EC2 实例自动创建的那个安全组(不是允许 ssh 访问的那个)– 它在实例详情的Security标签中可见,如下图所示:https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_10.jpg

    from sagemaker import image_uris
    container = image_uris.retrieve('object-detection', 
                                    region)
    od = sagemaker.estimator.Estimator(
         container,
         role=sagemaker.get_execution_role(),
         instance_count=1,                                         
         instance_type='ml.p3.2xlarge',                                         
         output_path=s3_output_location,
         subnets=['subnet-63715206','subnet-cbf5bdbc',
                  'subnet-59395b00'],                                        
         security_group_ids=['sg-0aa0a1c297a49e911']
    )
    
  3. 为了测试目的,我们只训练一个周期。像往常一样,虽然这次数据是从我们的 EFS 卷加载的。

训练完成后,你可以在 EFS 控制台中删除 EFS 卷,以避免不必要的费用。

现在,让我们看看如何使用另一个存储服务——Amazon FSx for Lustre。

使用 SageMaker 和 Amazon FSx for Lustre

非常大规模的工作负载需要高吞吐量和低延迟存储——这正是 Amazon FSx for Lustre 所具备的两个特点。顾名思义,这项服务基于 Lustre 文件系统(lustre.org),这是一个流行的开源选择,适用于HPC应用。

你可以创建的最小文件系统是 1.2 TB(就像我说的,“非常大规模”)。我们可以为 FSx 文件系统选择两种部署选项之一:

  • 持久化:应用于需要高可用性的长期存储。

  • 临时存储:数据不会复制,如果文件服务器发生故障,数据将不会持久化。作为交换,我们获得了高爆发吞吐量,使其成为突发性短期作业的良好选择。

可选地,文件系统可以由 S3 桶支持。对象首次访问时,会自动从 S3 复制到 FSx。

就像 EFS 一样,文件系统存在于 VPC 中,我们需要一个安全组,允许传入 Lustre 流量(端口 988 和 1,021-2,023)。你可以在 EC2 控制台中创建它,它应该类似于以下截图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_11.jpg

图 9.12 – 为 FSx for Lustre 创建安全组

让我们创建文件系统:

  1. 在 FSx 控制台中,我们创建了一个名为sagemaker-demo的文件系统,并选择了临时存储部署类型。

  2. 我们设置存储容量为 1.2 TB。

  3. 在默认 VPC 的eu-west-1a子网中,我们将其分配给我们刚刚创建的安全组。

  4. s3://sagemaker-eu-west-1-123456789012)和前缀(pascalvoc)中。

  5. 在下一个屏幕上,我们检查我们的选择,如下图所示,然后创建文件系统。

    几分钟后,文件系统已投入使用,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_09_12.jpg

图 9.13 – 创建 FSx 卷

由于文件系统由 S3 桶支持,我们无需填充它。我们可以直接进行训练。

使用 FSx for Lustre 训练物体检测模型

现在,我们将使用 FSx 训练模型,具体如下:

  1. 类似于我们刚刚在 EFS 中所做的,我们使用FileSystemInput定义输入通道。一个区别是目录路径必须以文件系统挂载点的名称开头。您可以在 FSx 控制台中找到它作为挂载名称

    from sagemaker.inputs import FileSystemInput
    fsx_train_data = FileSystemInput(
      file_system_id='fs-07914cf5a60649dc8',
      file_system_type='FSxLustre',                            
      directory_path='/bmgbtbmv/pascalvoc/input/train')
    fsx_validation_data = FileSystemInput(
      file_system_id='fs-07914cf5a60649dc8',
      file_system_type='FSxLustre',                            
      directory_path='/bmgbtbmv/pascalvoc/input/validation')
    data_channels = {'train': fsx_train_data, 
                     'validation': fsx_validation_data }
    
  2. 所有其他步骤都是相同的。不要忘记更新传递给Estimator模块的安全组名称。

  3. 当我们完成训练后,在控制台中删除 FSx 文件系统。

这结束了我们对 SageMaker 存储选项的探索。总结一下,这里是我的建议:

  • 首先,您应尽可能使用 RecordIO 或 TFRecord 数据。它们便于移动,训练速度更快,并且可以同时使用文件模式和管道模式。

  • 对于开发和小规模生产,文件模式完全没问题。您的主要关注点应始终是您的机器学习问题,而不是无用的优化。即使在小规模下,EFS 也可以作为协作的有趣选择,因为它便于共享数据集和笔记本。

  • 如果您使用内置算法进行训练,管道模式是一个明智的选择,并且应该在每一个机会中使用它。如果您使用框架或自己的代码进行训练,实施管道模式将需要一些工作,可能不值得工程投入,除非您在进行大规模工作(数百 GB 或更多)。

  • 如果您有大规模、分布式的工作负载,涉及数十个实例或更多,性能模式下的 EFS 值得一试。不要接近令人惊叹的 FSx for Lustre,除非您有疯狂的工作负载。

总结

在本章中,您学习了何时以及如何扩展训练作业。您看到,要找到最佳设置,肯定需要进行一些仔细的分析和实验:扩展与扩展,CPU 与 GPU 与多 GPU 等。这应该帮助您为自己的工作负载做出正确的决策,并避免昂贵的错误。

您还学习了如何通过分布式训练、数据并行、模型并行、RecordIO 和管道模式等技术实现显著加速。最后,您学习了如何为大规模训练作业设置 Amazon EFS 和 Amazon FSx for Lustre。

在下一章中,我们将介绍用于超参数优化、成本优化、模型调试等高级功能。

第十章:高级训练技巧

在上一章中,你学习了如何使用Pipe 模式分布式训练等特性来扩展训练任务,以及使用替代的S3存储数据集。

在本章中,我们将结束对训练技巧的探索。在本章的第一部分,你将学习如何通过托管点训练大幅降低训练成本,如何通过自动模型调优从模型中挤出每一滴精度,并且如何使用SageMaker Debugger拆解模型。

在本章的第二部分,我们将介绍两个新的 SageMaker 功能,帮助你构建更高效的工作流和更高质量的模型:SageMaker Feature StoreSageMaker Clarify

本章涵盖以下主题:

  • 使用托管点训练优化训练成本

  • 使用自动模型调优优化超参数

  • 使用 SageMaker Debugger 探索模型

  • 使用 SageMaker Feature Store 管理特征和构建数据集

  • 使用 SageMaker Clarify 检测偏差并解释预测

技术要求

你需要一个 AWS 账户来运行本章中包含的示例。如果你还没有账户,请访问aws.amazon.com/getting-started/来创建一个。你还应该了解 AWS 免费层(aws.amazon.com/free/),它允许你在一定的使用限制内免费使用许多 AWS 服务。

你需要为你的账户安装并配置 AWS 命令行界面CLI)(aws.amazon.com/cli/)。

你将需要一个可用的pandasnumpy等库。

本书中包含的代码示例可在 GitHub 上获取,链接为github.com/PacktPublishing/Learn-Amazon-SageMaker-second-edition。你需要安装 Git 客户端来访问它们(git-scm.com/)。

使用托管点训练优化训练成本

在上一章中,我们在ImageNet数据集上训练了图像分类算法。这个任务运行了不到 4 小时。按照每小时$290 计算,这个任务大约花费了我们$1,160。那是一大笔钱……但真的那么贵吗?

比较成本

在你抛开双手,喊出“他在想什么?”之前,请考虑一下让你的组织拥有并运行这个训练集群需要花费多少成本:

  1. 一个粗略的资本支出计算(包括服务器、存储、GPU、100 Gbit/s 的网络设备)至少需要 150 万美元。就运营支出而言,托管成本不会便宜,因为每台等效的服务器需要 4-5 千瓦的电力。这足够填满你典型托管公司的一排机架,因此即使使用高密度机架,你也需要多个。再加上带宽、跨连接等,我的直觉告诉我每月大约需要 1.5 万美元(在某些地区可能更多)。

  2. 我们需要添加硬件支持合同(比如,每年 10%,即 15 万美元)。将这个集群的折旧期设为 5 年,总月成本为($1.5M + 60*$15K + 5*$150K)/60 = 52.5K 美元。为了计算维护服务器等的人工成本,我们将其四舍五入到 55K 美元。

使用保守估计,这笔开支相当于使用我们为 ImageNet 示例所用的大型每小时$290 集群进行 190 小时的训练。正如我们在本章稍后将看到的,托管的 Spot 训练通常能提供 70%的节省。因此,现在这笔开支相当于每月大约 633 小时的 ImageNet 训练。

这意味着每月 87%的使用率(633/720),而且很不可能你会让你的训练集群保持如此繁忙。再加上停机时间、硬件创新带来的加速折旧、硬件保险费用、未将 150 万美元投资于其他项目的机会成本等等,物理基础设施的商业案例每分钟都在变得更差。

财务问题固然重要,但最糟糕的是你只能拥有一个集群。如果一个潜在的商业机会需要另一个集群怎么办?你会再花 150 万美元吗?如果不会,是否需要共享现有的集群?当然,只有你能决定什么对你的组织最有利。只要确保你从全局角度来看问题。

现在,让我们来看一下如何轻松享受 70%的成本降低。

了解 Amazon EC2 Spot 实例

在任何时候,Amazon EC2的容量都超过了实际需求。这使得客户可以根据需要随时向他们的平台添加按需容量。可以通过 API 调用显式创建按需实例,或者在配置了Auto Scaling的情况下自动创建。一旦客户获得了按需实例,他们将保留它,直到他们决定释放它,无论是显式释放还是自动释放。

Spot 实例是利用这些未使用容量并享受非常显著折扣(通常为 50-70%)的简单方法。您可以以相同的方式请求它们,它们的行为也相同。唯一的区别是,如果 AWS 需要容量来构建按需实例,您的 Spot 实例可能会被回收。在被强制终止之前,它会收到两分钟的中断通知。

这并不像听起来那么糟糕。根据区域和实例系列的不同,抢占式实例可能不会被频繁回收,客户通常可以将它们保留几天甚至更长时间。此外,您可以为此需求架构您的应用程序,例如,在抢占式实例上运行无状态工作负载,并依赖托管服务进行数据存储。成本优势非常明显,不容错过!

查看过去三个月p3dn.24xlarge的情况,抢占式价格比按需价格便宜 60-70%:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_1.jpg

图 10.1 – 查看 p3dn.24xlarge 的抢占式价格

这些是 EC2 的价格,但相同的折扣率也适用于 SageMaker 价格。折扣因实例类型、区域,甚至可用区而异。您可以使用describe-spot-price-history API 以编程方式收集这些信息,并将其用于工作流中:

https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-spot-price-history.html

现在,让我们看看这对 SageMaker 意味着什么。

理解托管抢占式训练

使用抢占式实例进行训练在所有 SageMaker 配置中都可用:单实例训练、分布式训练、内置算法、框架以及您自己的算法。

只需设置几个估算器参数即可。您无需担心处理通知和中断,SageMaker 会自动为您处理。

如果训练作业被中断,SageMaker 会恢复足够的抢占式容量并重新启动训练作业。如果算法使用了检查点,训练将从最新的检查点继续。如果没有,作业将从头开始。

实现检查点所需的工作量取决于您使用的算法:

  • 三个用于计算机视觉和XGBoost的内置算法支持检查点功能。

  • 所有其他内置算法则没有此功能。您仍然可以使用抢占式实例进行训练。然而,最大运行时间限制为 60 分钟,以减少潜在的浪费。如果您的训练作业超过 60 分钟,您应该考虑进行扩展。如果仍然不够,您将不得不使用按需实例。

  • 深度学习容器适用于TensorFlowPyTorchApache MXNetHugging Face,并内置了检查点功能,您无需修改训练脚本。

  • 如果您使用其他框架或自己的自定义代码,您需要实现检查点功能。

在训练过程中,检查点会保存在训练容器内。默认路径为/opt/ml/checkpoints,您可以通过估算器参数进行自定义。SageMaker 还会自动将这些检查点持久化到用户定义的 S3 路径。如果您的训练作业被中断并重新启动,检查点会自动复制到容器内。您的代码可以检查它们的存在,并加载适当的检查点来继续训练。

注意

请注意,即使使用按需实例进行训练,检查点功能仍然可用。如果你希望将检查点存储在 S3 中以便进一步检查或进行增量训练,这将非常有用。唯一的限制是本地模式不支持检查点功能。

最后但同样重要的是,检查点功能确实会拖慢任务的速度,尤其是对于大模型来说。然而,这是一个值得支付的小代价,以避免从头开始重新启动长时间运行的任务。

现在,让我们将托管点训练添加到我们在第五章中运行的目标检测任务中,训练计算机视觉模型

使用托管点训练进行目标检测

从按需训练切换到托管点训练非常简单。我们只需设置训练任务的最大持续时间,包括等待 Spot 实例可用的时间。

我们设置了 2 小时的最大运行时间,加上 8 小时的任何点延迟。如果超出了这两个限制,任务将自动终止。这对于终止运行时间过长或因为等待点实例而卡住的任务非常有帮助:

od = sagemaker.estimator.Estimator(
     container,
     role,
     instance_count=2,                                 
     instance_type='ml.p3.2xlarge',                                 
     use_spot_instances=True,
     max_run=7200,                     # 2 hour
     max_wait=36000,                   # +8 hours
     output_path=s3_output_location)

我们使用与之前相同的配置进行训练:管道模式和dist_sync模式。当第一个 epoch 完成时,训练日志告诉我们检查点功能已经激活。每次验证指标改进时,都会自动保存一个新的检查点:

Updating the best model with validation-mAP=1.615789635726003e-05
Saved checkpoint to "/opt/ml/model/model_algo_1-0000.params"

一旦训练任务完成,训练日志会告诉我们节省了多少:

Training seconds: 7794
Billable seconds: 2338
Managed Spot Training savings: 70.0%

这个任务不仅比按需版本便宜 70%,而且价格还不到我们原来单实例任务的一半。这意味着我们可以使用更多的实例,并以相同的预算加速训练任务。实际上,托管点训练让你能够优化任务的持续时间和成本。你可以根据业务需求设定训练预算,然后尽可能地获取基础设施。

让我们尝试另一个例子,在Keras中实现检查点功能。

使用托管点训练和 Keras 中的检查点功能

在这个例子中,我们将在 TensorFlow 2.1 中构建一个简单的Sequential API。

Keras 中的检查点功能

首先,让我们来看一下 Keras 脚本本身。为了简洁起见,这里只展示了重要的步骤。你可以在本书的 GitHub 仓库中找到完整的代码:

  1. 使用脚本模式,我们存储数据集路径和超参数。

  2. 接下来,我们加载数据集并将像素值归一化到[0,1]范围内。我们还对类别标签进行独热编码。

  3. 我们构建了一个Sequential模型:两个卷积块(Conv2D / BatchNormalization / ReLU / MaxPooling2D / Dropout),然后是两个全连接块(Dense / BatchNormalization / ReLU / Dropout),最后是一个用于数据集中 10 个类别的softmax输出层。

  4. 我们使用分类交叉熵损失函数和Adam优化器来编译模型:

    model.compile(
        loss=tf.keras.losses.categorical_crossentropy,
        optimizer=tf.keras.optimizers.Adam(),
        metrics=['accuracy'])
    
  5. 我们定义一个 Keras 回调,每当验证准确率提高时就保存一个检查点:

    from tensorflow.keras.callbacks import ModelCheckpoint
    chk_dir = '/opt/ml/checkpoints'
    chk_name = 'fmnist-cnn-{epoch:04d}'
    checkpointer = ModelCheckpoint(
        filepath=os.path.join(chk_dir, chk_name),
        monitor='val_accuracy')
    
  6. 我们训练模型,添加我们刚刚创建的回调:

    model.fit(x=x_train, y=y_train, 
              validation_data=(x_val, y_val),
              batch_size=batch_size, epochs=epochs,
              callbacks=[checkpointer],
              verbose=1)
    
  7. 训练完成后,我们将模型保存为 TensorFlow Serving 格式,这是在 SageMaker 上部署时所需的格式:

    from tensorflow.keras.models import save_model
    save_model(model, os.path.join(model_dir, '1'),  
               save_format='tf')
    

现在,让我们看看我们的训练笔记本。

使用托管的 spot 训练和检查点保存进行训练

我们使用之前相同的工作流程:

  1. 我们下载 Fashion-MNIST 数据集并将其保存在本地目录。我们将数据集上传到 S3,并定义 SageMaker 应该将检查点复制到的 S3 位置。

  2. 我们配置一个 TensorFlow 估算器,启用托管的 spot 训练,并传递检查点的 S3 输出位置。这次,我们使用的是 ml.g4dn.xlarge 实例。这种非常具成本效益的 GPU 实例(在 eu-west-1 区域的价格为 $0.822)足以应对一个小模型:

    from sagemaker.tensorflow import TensorFlow
    tf_estimator = TensorFlow(
        entry_point='fmnist-1.py',
        role=sagemaker.get_execution_role(),
        instance_count=1,
        instance_type='ml.g4dn.xlarge',     
        framework_version='2.1.0',
        py_version='py3',
        hyperparameters={'epochs': 20},
        output_path=output_path,
        use_spot_instances=True,
        max_run=3600,
        max_wait=7200,
        checkpoint_s3_uri=chk_path)
    
  3. 我们像往常一样启动训练,任务达到了 93.11%的准确率。训练持续了 289 秒,我们只需为 87 秒支付费用, thanks to a 69.9%的折扣。总费用为 1.98 美分!谁说 GPU 训练必须昂贵?

  4. 在训练日志中,我们看到每当验证准确率提高时,都会创建一个检查点:

    INFO:tensorflow:Assets written to /opt/ml/checkpoints/fmnist-cnn-0001/assets
    

    在任务运行时,我们还看到检查点被复制到 S3:

    $ aws s3 ls s3://sagemaker-eu-west-1-123456789012/keras2
    fashion-mnist/checkpoints/
    PRE fmnist-cnn-0001/
    PRE fmnist-cnn-0002/
    PRE fmnist-cnn-0003/
    PRE fmnist-cnn-0006/
    . . .
    

如果我们的 spot 任务被中断,SageMaker 会在容器内复制检查点,以便我们可以使用它们来恢复训练。这需要在我们的 Keras 脚本中添加一些逻辑,以加载最新的检查点。让我们看看如何实现。

从检查点恢复训练

这是一个非常简单的过程——查找检查点,并从最新的检查点继续训练:

  1. 我们列出检查点目录:

    import glob
    checkpoints = sorted(
        glob.glob(os.path.join(chk_dir,'fmnist-cnn-*')))
    
  2. 如果有检查点,我们会找到最新的一个以及它的 epoch 编号。然后,我们加载模型:

    from tensorflow.keras.models import load_model
    if checkpoints :
        last_checkpoint = checkpoints[-1]
        last_epoch = int(last_checkpoint.split('-')[-1])
        model = load_model(last_checkpoint)
        print('Loaded checkpoint for epoch ', last_epoch)
    
  3. 如果没有检查点,我们像往常一样构建模型:

    else:
        last_epoch = 0
        model = Sequential()
        . . .
    
  4. 我们编译模型,启动训练,并传递最后一个 epoch 的编号:

    model.fit(x=x_train, y=y_train, 
              validation_data=(x_val, y_val), 
              batch_size=batch_size,
              epochs=epochs,
              initial_epoch=last_epoch,
              callbacks=[checkpointer],
              verbose=1)
    

我们怎么测试这个呢?没有办法故意造成一个 spot 中断。

诀窍是:用 checkpoint_s3_uri 路径中的现有检查点启动一个新的训练任务,并增加 epoch 的数量。这将模拟恢复一个中断的任务。

将 epoch 数设置为 25,并将检查点保存在 s3://sagemaker-eu-west-1-123456789012/keras2

fashion-mnist/checkpoints,我们再次启动训练任务。

在训练日志中,我们看到最新的检查点被加载,训练从第 21 个 epoch 继续:

Loaded checkpoint for epoch 20
. . .
Epoch 21/25

我们还看到每当验证准确率提高时,新的检查点会被创建,并被复制到 S3:

INFO:tensorflow:Assets written to: /opt/ml/checkpoints/fmnist-cnn-0021/assets

如你所见,在 SageMaker 中设置检查点并不困难,你应该也能在其他框架中做到这一点。得益于此,你可以享受由托管的按需训练提供的深度折扣,而在中断发生时也无需担心丢失任何工作。当然,你也可以单独使用检查点来检查中间训练结果,或用于增量训练。

在接下来的部分,我们将介绍另一个重要特性:自动模型调优。

使用自动模型调优优化超参数

超参数对训练结果有巨大的影响。就像混沌理论中所说的那样,一个单一超参数的微小变化就可能导致准确率的剧烈波动。在大多数情况下,我们无法解释“为什么?”,这让我们对接下来该尝试什么感到困惑。

多年来,已经设计了几种技术来尝试解决选择最佳超参数的问题:

  1. 手动搜索:这意味着依靠我们的最佳判断和经验来选择“最佳”超参数。说实话:这真的不起作用,尤其是在深度学习和众多训练及网络架构参数的情况下。

  2. 网格搜索:这意味着系统地探索超参数空间,集中在热点区域,然后重复这一过程。这比手动搜索要好得多。然而,这通常需要训练成百上千的任务。即使有可扩展的基础设施,时间和预算仍然可能是巨大的。

  3. 随机搜索:指的是随机选择超参数。虽然听起来不合常理,但詹姆斯·伯格斯特拉和约书亚·本吉奥(图灵奖得主)在 2012 年证明,这一技术在相同计算预算下能比网格搜索交付更好的模型。

  4. www.jmlr.org/papers/v13/bergstra12a.html

  5. 超参数优化(HPO):这意味着使用优化技术来选择超参数,例如贝叶斯优化高斯过程回归。在相同的计算预算下,HPO 通常能以比其他技术少 10 倍的训练周期交付结果。

了解自动模型调优

SageMaker 包含一个自动模型调优功能,可以让你轻松探索超参数范围,并通过有限的任务数快速优化任何训练指标。

模型调优支持随机搜索和超参数优化(HPO)。前者是一个有趣的基准,帮助你检查后者是否确实表现更好。你可以在这篇精彩的博客文章中找到非常详细的比较:

https://aws.amazon.com/blogs/machine-learning/amazon-sagemaker-automatic-model-tuning-now-supports-random-search-and-hyperparameter-scaling/

模型调优对你使用的算法完全无关。它适用于内置算法,文档中列出了可以调优的超参数。它也适用于所有框架和自定义容器,且超参数传递方式相同。

对于我们想要优化的每一个超参数,我们需要定义以下内容:

  • 一个名称

  • 一种类型(参数可以是整数、连续的或分类的)

  • 探索的值范围

  • 一种缩放类型(线性、对数、反对数或自动)——这让我们控制特定参数范围的探索方式

我们还定义了要优化的指标。它可以是任何数值,只要它在训练日志中可见,并且您可以传递正则表达式来提取它。

然后,我们启动调优作业,传递所有这些参数以及要运行的训练作业数量和并行度。使用贝叶斯优化,您可以通过顺序作业(无并行)获得最佳结果,因为优化可以在每个作业后应用。话虽如此,运行少量并行作业是可以接受的。随机搜索对并行性没有限制,因为作业之间完全不相关。

调用deploy() API 在调优器对象上部署最佳模型。如果调优仍在进行中,它将部署迄今为止的最佳模型,这对于早期测试非常有用。

让我们使用内置算法运行第一个示例,并了解模型调优 API。

使用自动模型调优进行目标检测

我们将优化我们的目标检测作业。查看文档,我们可以看到可调超参数的列表:

https://docs.aws.amazon.com/sagemaker/latest/dg/object-detection-tuning.html

让我们尝试优化学习率、动量和权重衰减:

  1. 我们使用管道模式设置输入通道。这里没有变化。

  2. 我们还像往常一样配置估算器,设置托管现货训练以最小化成本。我们将在单个实例上训练以获得最高准确度:

    od = sagemaker.estimator.Estimator(
         container,
         role,                                        
         instance_count=1,                                        
         instance_type='ml.p3.2xlarge',                                       
         output_path=s3_output_location,                                        
         use_spot_instances=True,
         max_run=7200,
         max_wait=36000,
         volume_size=1)       
    
  3. 我们使用与之前相同的超参数:

    od.set_hyperparameters(base_network='resnet-50',
                           use_pretrained_model=1,
                           num_classes=20,
                           epochs=30,
                           num_training_samples=16551,
                           mini_batch_size=90)
    
  4. 我们定义了我们希望调优的三个额外超参数。我们明确设置学习率的对数缩放,以确保探索不同数量级:

    from sagemaker.tuner import ContinuousParameter,
    hyperparameter_ranges = {
        'learning_rate': ContinuousParameter(0.001, 0.1, 
                         scaling_type='Logarithmic'), 
        'momentum': ContinuousParameter(0.8, 0.999), 
        'weight_decay': ContinuousParameter(0.0001, 0.001)
    }
    
  5. 我们设置要优化的指标:

    objective_metric_name = 'validation:mAP'
    objective_type = 'Maximize'
    
  6. 我们将所有内容整合在一起,使用HyperparameterTuner对象。我们决定运行 30 个作业,其中两个作业并行运行。我们还启用了早停,以淘汰表现较差的作业,从而节省时间和金钱:

    from sagemaker.tuner import HyperparameterTuner
    tuner = HyperparameterTuner(od,
                objective_metric_name,
                hyperparameter_ranges,
                objective_type=objective_type,
                max_jobs=30,
                max_parallel_jobs=2,
                early_stopping_type='Auto')
    
  7. 我们在调优器对象(而不是估算器)上启动训练,而不等待它完成:

    tuner.fit(inputs=data_channels, wait=False)
    
  8. 目前,SageMaker Studio 没有提供方便的调优作业查看界面。相反,我们可以在 SageMaker 控制台的 超参数调优作业 部分跟踪进度,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_2.jpg

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_2.jpg)

图 10.2 – 在 SageMaker 控制台中查看调优作业

作业运行了 17 小时(壁钟时间)。22 个作业完成,8 个作业提前停止。总训练时间为 30 小时 15 分钟。应用 70%的现货折扣,总成本为 25.25 * $4.131 * 0.3 = $37.48。

这个调优作业的表现如何?使用默认超参数,我们的独立训练作业达到了0.2453。我们的调优作业达到了0.6337,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_3.jpg

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_3.jpg)

图 10.3 – 调优作业结果

验证 mAP 的图表显示在下一张图片中。它告诉我我们可能需要再训练一段时间,以获得更高的准确度:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_4.jpg

图 10.4 – 查看 mAP 指标

一种方法是使用最佳超参数启动单个训练作业,并让它运行更多周期。我们也可以通过在调优器对象上使用deploy()来恢复调优作业,并像任何 SageMaker 模型一样测试我们的模型。

正如你所见,自动模型调优非常强大。通过运行少量作业,我们将指标提高了 158%!与花费时间尝试其他技术相比,成本几乎可以忽略不计。

实际上,使用随机策略运行相同的调优作业可以得到最高准确率 0.52。我们肯定需要运行更多的训练作业才能希望达到 0.6315。

现在,让我们尝试优化我们在本章前面使用的 Keras 示例。

使用 Keras 进行自动模型调优

自动模型调优可以轻松应用于 SageMaker 上的任何算法,当然也包括所有框架。让我们看看 Keras 是如何工作的。

在本章的前面,我们在 Fashion MNIST 数据集上训练了我们的 Keras CNN,训练了 20 个周期,得到了 93.11%的验证准确率。让我们看看能否通过自动模型调优来提高这个结果。在此过程中,我们还将学习如何优化训练日志中存在的任何指标,而不仅仅是 SageMaker 中预定义的指标。

在自定义指标上的优化

修改我们的训练脚本,安装keras-metrics包(github.com/netrack/keras-metrics),并将精度召回率f1 得分指标添加到训练日志中:

import subprocess, sys
def install(package):
    subprocess.call([sys.executable, "-m", "pip",
                     "install", package])
install('keras-metrics')
import keras_metrics
. . . 
model.compile(
    loss=tf.keras.losses.categorical_crossentropy,
    optimizer=tf.keras.optimizers.Adam(),
    metrics=['accuracy',
              keras_metrics.precision(),
              keras_metrics.recall(),
              keras_metrics.f1_score()])

经过 20 个周期后,当前的指标如下所示:

loss: 0.0869 - accuracy: 0.9678 - precision: 0.9072 - recall: 0.8908 - f1_score: 0.8989 - val_loss: 0.2301 - val_accuracy: 0.9310 - val_precision: 0.9078 - val_recall: 0.8915 - val_f1_score: 0.8996

如果我们想优化 f1 得分,可以像这样定义调优器指标:

objective_metric_name = 'val_f1'
objective_type = 'Maximize'
metric_definitions = [
    {'Name': 'val_f1',
     'Regex': 'val_f1_score: ([0-9\\.]+)'
    }]

就是这样。只要在训练日志中打印出某个指标,你就可以用它来调优模型。

优化我们的 Keras 模型

现在,让我们运行我们的调优作业:

  1. 我们像这样为HyperparameterTuner定义指标,优化准确度并同时显示 f1 得分:

    objective_metric_name = 'val_acc'
    objective_type = 'Maximize'
    metric_definitions = [
        {'Name': 'val_f1', 
         'Regex': 'val_f1_score: ([0-9\\.]+)'},
        {'Name': 'val_acc', 
         'Regex': 'val_accuracy: ([0-9\\.]+)'}
    ]
    
  2. 我们定义要探索的参数范围:

    from sagemaker.tuner import ContinuousParameter, IntegerParameter
    hyperparameter_ranges = {
        'learning_rate': ContinuousParameter(0.001, 0.2, 
                         scaling_type='Logarithmic'), 
        'batch-size': IntegerParameter(32,512)
    }
    
  3. 我们使用相同的估算器(20 个周期,使用 Spot 实例),并定义调优器:

    tuner = HyperparameterTuner(
        tf_estimator,
        objective_metric_name,
        hyperparameter_ranges,                          
        metric_definitions=metric_definitions,
        objective_type=objective_type,
        max_jobs=20,
        max_parallel_jobs=2,
        early_stopping_type='Auto')
    
  4. 我们启动调优作业。在作业运行时,我们可以使用SageMaker SDK来显示训练作业及其属性的列表:

    from sagemaker.analytics import HyperparameterTuningJobAnalytics
    exp = HyperparameterTuningJobAnalytics(
       hyperparameter_tuning_job_name=
       tuner.latest_tuning_job.name)
    jobs = exp.dataframe()
    jobs.sort_values('FinalObjectiveValue', ascending=0)
    

    这将打印出下一个截图中可见的表格:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_5.jpg

图 10.5 – 查看调优作业的信息

调优作业运行了 2 小时 8 分钟(墙时)。最高验证准确率为 93.46%——相比我们的基准,取得了不错的改进。

我们当然可以通过训练更长时间来做得更好。然而,训练时间越长,过拟合的风险越大。我们可以通过早停法来减轻这一问题,这可以通过 Keras 回调实现。然而,我们应该确保作业报告的是最佳周期的指标,而不是最后一个周期的指标。我们该如何在训练日志中显示这个信息?通过另一个回调!

为早期停止添加回调

为早期停止添加一个 Keras 回调非常简单:

  1. 我们添加了一个基于验证准确率的早期停止内置回调:

    from tensorflow.keras.callbacks import EarlyStopping
    early_stopping = EarlyStopping(
        monitor='val_accuracy',
        min_delta=0.0001,
        patience=10,
        verbose=1,
        mode='auto')
    
  2. 我们添加了一个自定义回调,以便在每个 epoch 结束时保存验证准确率,并在训练结束时显示最佳结果:

    from tensorflow.keras.callbacks import Callback
    class LogBestMetric(Callback):
        def on_train_begin(self, logs={}):
            self.val_accuracy = []
        def on_train_end(self, logs={}):
            print("Best val_accuracy:",
                  max(self.val_accuracy))
        def on_epoch_end(self, batch, logs={}):
            self.val_accuracy.append(
                logs.get('val_accuracy'))
            best_val_metric = LogBestMetric()
    
  3. 我们将这两个回调添加到训练 API 中:

    model.fit(. . . 
        callbacks=[checkpointer, early_stopping, 
                   best_val_metric])
    

    测试几个单独的作业时,训练日志的最后几行现在看起来是这样的:

    Epoch 00048: early stopping
    Best val_accuracy: 0.9259
    
  4. 在笔记本中,我们更新了指标定义,以便提取最佳验证准确率:

    objective_metric_name = 'val_acc'
    objective_type = 'Maximize'
    metric_definitions = [
        {'Name': 'val_acc', 
         'Regex': 'Best val_accuracy: ([0-9\\.]+)'}
    ]
    

这次训练了 60 个 epochs(大约 3 小时的壁钟时间),当前的最高验证准确率为 93.78%。看起来通过调整学习率和批大小,已经达到了最佳效果。

使用自动模型调优进行架构搜索

我们的神经网络还有很多超参数:卷积滤波器的数量、dropout 等等。让我们也尝试优化这些参数:

  1. 我们修改训练脚本,添加命令行参数,以便为模型中 Keras 层使用的以下网络参数提供支持:

    parser.add_argument(
        '--filters1', type=int, default=64)
    parser.add_argument(
        '--filters2', type=int, default=64)
    parser.add_argument(
        '--dropout-conv', type=float, default=0.2)
    parser.add_argument(
        '--dropout-fc', type=float, default=0.2)
    

    正如你猜测的那样,参数允许我们为每一层的卷积滤波器数量、卷积层的 dropout 值和全连接层的 dropout 值设置值。

  2. 相应地,在笔记本中,我们定义了这些超参数及其范围。对于学习率和批大小,我们使用了以之前调优作业发现的最佳值为中心的窄范围:

    from sagemaker.tuner import ContinuousParameter, 
                                IntegerParameter
    hyperparameter_ranges = {
        learning-rate': ContinuousParameter(0.01, 0.14), 
        'batch-size': IntegerParameter(130,160),
        'filters1': IntegerParameter(16,256),
        'filters2': IntegerParameter(16,256),
        'dropout-conv': ContinuousParameter(0.001,0.5, 
                        scaling_type='Logarithmic'),
        'dropout-fc': ContinuousParameter(0.001,0.5, 
                      scaling_type='Logarithmic')
    }
    
  3. 我们启动调优作业,运行 50 个作业,每次运行 2 个,总共训练 100 个 epochs。

调优作业运行了大约 12 小时,总费用约为 15 美元。最高验证准确率达到了 94.09%。与我们的基线相比,自动模型调优提高了模型的准确性近 1 个百分点——这是一个非常显著的提升。如果这个模型每天用于预测 100 万个样本,这将意味着多出 10,000 个准确的预测!

总的来说,我们在调优 Keras 模型上花费了不到 50 美元。无论是什么业务指标从额外的准确性中获益,可以公平地说,这笔支出很快就能收回。正如许多客户告诉我的那样,自动模型调优是自我盈利的,甚至会带来更多回报。

这结束了我们对自动模型调优的探索,这是 SageMaker 中我最喜欢的功能之一。你可以在 github.com/awslabs/amazon-sagemaker-examples/tree/master/hyperparameter_tuning 找到更多示例。

现在,让我们了解一下 SageMaker Debugger,以及它如何帮助我们理解模型内部发生了什么。

使用 SageMaker Debugger 探索模型

SageMaker Debugger 让您为训练作业配置调试规则。这些规则将检查作业的内部状态,并检测在训练过程中可能出现的特定不良条件。SageMaker Debugger 包含一长串内置规则(docs.aws.amazon.com/sagemaker/latest/dg/debugger-built-in-rules.html),并且您可以添加自定义的 Python 编写规则。

此外,您还可以保存和检查模型状态(如梯度、权重等)以及训练状态(如指标、优化器参数等)。在每个训练步骤中,存储这些值的张量可以在接近实时的情况下保存到 S3 桶中,使得在模型训练过程中就可以对其进行可视化。

当然,您可以选择希望保存的张量集合,以及保存的频率等。根据您使用的框架,可用的集合有所不同。您可以在github.com/awslabs/sagemaker-debugger/blob/master/docs/api.md中找到更多信息。最后但同样重要的是,您可以保存原始张量数据或张量的归约结果,以限制涉及的数据量。归约操作包括最小值、最大值、中位数等。

如果您使用的是支持版本的 TensorFlow、PyTorch、Apache MXNet 的内置容器,或者内置的 XGBoost 算法,您可以开箱即用地使用 SageMaker Debugger,而无需在脚本中更改一行代码。没错,您没有看错。您只需向估算器中添加额外的参数,就像我们接下来在示例中展示的那样。

对于其他版本,或者使用您自己的容器,仅需进行最小的修改。您可以在github.com/awslabs/sagemaker-debugger找到最新的信息和示例。

调试规则和保存张量可以在同一个训练作业中进行配置。为了清晰起见,我们将运行两个独立的示例。首先,让我们使用来自第四章的 XGBoost 和波士顿住房示例,训练机器学习模型

调试 XGBoost 作业

首先,我们将配置几个内置规则,训练我们的模型,并检查所有规则的状态:

  1. 查看内置规则列表后,我们决定使用overtrainingoverfit。每个规则都有额外的参数,我们可以进行调整。我们保持默认设置,并相应地配置Estimator

    from sagemaker.debugger import rule_configs, Rule
    xgb_estimator = Estimator(container,
      role=sagemaker.get_execution_role(),
      instance_count=1,
      instance_type='ml.m5.large',
      output_path='s3://{}/{}/output'.format(bucket, prefix),
      rules=[
        Rule.sagemaker(rule_configs.overtraining()),
        Rule.sagemaker(rule_configs.overfit())
      ]
    )
    
  2. 我们设置超参数并启动训练,而无需等待训练作业完成。训练日志将不会在笔记本中显示,但仍然可以在CloudWatch Logs中查看:

    xgb_estimator.set_hyperparameters(
      objective='reg:linear', num_round=100)
    xgb_estimator.fit(xgb_data, wait=False)
    
  3. 除了训练作业外,每个规则下还会运行一个调试作业,我们可以查看它们的状态:

    description = xgb_estimator.latest_training_job.rule_job_summary()
    for rule in description:
      rule.pop('LastModifiedTime')
      rule.pop('RuleEvaluationJobArn')
      print(rule)
    

    这告诉我们调试作业正在运行:

    {'RuleConfigurationName': 'Overtraining',  
     'RuleEvaluationStatus': 'InProgress'}
    {'RuleConfigurationName': 'Overfit', 
     'RuleEvaluationStatus': 'InProgress'}
    
  4. 在训练作业完成后重新运行相同的单元格时,我们看到没有任何规则被触发:

    {'RuleConfigurationName': 'Overtraining',
     'RuleEvaluationStatus': 'NoIssuesFound'}
    {'RuleConfigurationName': 'Overfit', 
     'RuleEvaluationStatus': 'NoIssuesFound'}
    

如果触发了规则,我们会收到错误信息,训练任务将被停止。检查存储在 S3 中的张量有助于我们理解出了什么问题。

检查 XGBoost 任务

让我们配置一个新的训练任务,保存 XGBoost 的所有张量集合:

  1. 我们配置Estimator,传入DebuggerHookConfig对象。在每个训练步骤中,我们保存三种张量集合:指标、特征重要性和平均SHAPgithub.com/slundberg/shap)值。这些有助于我们理解数据样本中每个特征如何影响预测值的增减。

    对于更大的模型和数据集,这可能会生成大量数据,加载和分析这些数据需要较长时间。我们可以增加保存间隔,或保存张量缩减值而非完整张量:

    from sagemaker.debugger import DebuggerHookConfig, CollectionConfig
    save_interval = '1'
    xgb_estimator = Estimator(container,
        role=role,
        instance_count=1,
        instance_type='ml.m5.large',
        output_path='s3://{}/{}/output'.format(bucket,  
                                               prefix),
    
        debugger_hook_config=DebuggerHookConfig(                
            s3_output_path=
            's3://{}/{}/debug'.format(bucket,prefix),
          collection_configs=[
            CollectionConfig(name='metrics',
              parameters={"save_interval": 
                          save_interval}),
            CollectionConfig(name='average_shap',  
              parameters={"save_interval": 
                          save_interval}),
            CollectionConfig(name='feature_importance', 
              parameters={"save_interval": save_interval})
          ]
        )
    )
    
  2. 一旦训练任务开始,我们可以创建一个试验并加载已保存的数据。由于该任务非常短,我们将在一分钟左右查看到所有数据:

    from smdebug.trials import create_trial
    s3_output_path = xgb_estimator.latest_job_debugger_artifacts_path()
    trial = create_trial(s3_output_path)
    
  3. 我们可以列出所有已保存张量的名称:

    trial.tensor_names()
    ['average_shap/f0','average_shap/f1','average_shap/f10','feature_importance/cover/f0','feature_importance/cover/f1','train-rmse','validation-rmse']
    
  4. 我们还可以列出给定集合中所有张量的名称:

    trial.tensor_names(collection="metrics")
    ['train-rmse', 'validation-rmse']
    
  5. 对于每个张量,我们可以访问训练步骤和数值。让我们绘制来自average_shapfeature_importance集合的特征信息:

    def plot_features(tensor_prefix):
        num_features = len(dataset.columns)-1
        for i in range(0,num_features):
        feature = tensor_prefix+'/f'+str(i)
        steps = trial.tensor(feature).steps()
        v = [trial.tensor(feature).value(s) for s in steps]
        plt.plot(steps, v, label=dataset.columns[i+1])
        plt.autoscale()
        plt.title(tensor_prefix)
        plt.legend(loc='upper left')
        plt.show()
    
  6. 我们构建average_shap图:

    plot_features('average_shap')
    
  7. 你可以在以下截图中看到——discrimnox的平均值最大:https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_6.jpg

    图 10.6 – 绘制 SHAP 值随时间变化的图

  8. 我们构建feature_importance/weight图:

    plot_features('feature_importance/weight')
    

    你可以在以下截图中看到——crimagedis的权重最大:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_7.jpg

图 10.7 – 绘制特征权重随时间变化的图

现在,让我们在 Keras 和 Fashion-MNIST 示例中使用 SageMaker Debugger。

调试和检查 Keras 任务

我们可以通过以下步骤检查和调试 Keras 任务:

  1. TensorFlow 2.x 的默认行为是急切模式(eager mode),此时梯度不可用。因此,我们在脚本中禁用急切模式,这是唯一需要修改的地方:

    tf.compat.v1.disable_eager_execution()
    
  2. 我们从相同的估计器开始。数据集包含 70,000 个样本(60,000 个用于训练,10,000 个用于验证)。通过 30 个 epoch 和批次大小 128,我们的训练任务将有大约 16,400 个步骤(70,000 * 30 / 128)。在每个步骤保存张量可能显得有些过头。我们改为每 100 步保存一次:

    from sagemaker.tensorflow import TensorFlow
    from sagemaker.debugger import rule_configs, Rule, DebuggerHookConfig, CollectionConfig
    save_interval = '100'
    tf_estimator = TensorFlow(entry_point='fmnist-5.py',
        role=role,
        instance_count=1,
        instance_type='ml.p3.2xlarge',
        framework_version='2.1.0', 
        py_version='py3',
        hyperparameters={'epochs': 30},
        output_path=output_path,
        use_spot_instances=True,
        max_run=3600,
        max_wait=7200,
    
  3. 查看 TensorFlow 的内建规则后,我们决定设置poor_weight_initializationdead_relucheck_input_images。我们需要指定输入张量中的通道信息索引。对于 TensorFlow 来说,它是 4(批次大小、高度、宽度和通道):

        rules=[      
    Rule.sagemaker(
        rule_configs.poor_weight_initialization()), 
    Rule.sagemaker(
        rule_configs.dead_relu()),
    Rule.sagemaker(
        rule_configs.check_input_images(), 
        rule_parameters={"channel": '3'})
        ],
    
  4. 查看 TensorFlow 的集合后,我们决定保存指标、损失、输出、权重和梯度:

        debugger_hook_config=DebuggerHookConfig(                
            s3_output_path='s3://{}/{}/debug'
                   .format(bucket, prefix),
            collection_configs=[
                CollectionConfig(name='metrics',  
                    parameters={"save_interval": 
                                save_interval}),
                CollectionConfig(name='losses', 
                    parameters={"save_interval": 
                                save_interval}),
                CollectionConfig(name='outputs', 
                    parameters={"save_interval": 
                                save_interval}),
                CollectionConfig(name='weights', 
                    parameters={"save_interval": 
                                save_interval}),
                CollectionConfig(name='gradients', 
                    parameters={"save_interval": 
                                save_interval})
            ],
        )
    )
    
  5. 当训练开始时,我们在训练日志中看到规则被触发:

    ********* Debugger Rule Status *********
    *
    * PoorWeightInitialization: InProgress        
    * DeadRelu: InProgress        
    * CheckInputImages: InProgress        
    *
    ****************************************
    
  6. 训练完成后,我们检查调试规则的状态:

    description = tf_estimator.latest_training_job.rule_job_summary()
    for rule in description:
        rule.pop('LastModifiedTime')
        rule.pop('RuleEvaluationJobArn')
        print(rule)
    {'RuleConfigurationName': 'PoorWeightInitialization', 
     'RuleEvaluationStatus': 'NoIssuesFound'}
    {'RuleConfigurationName': 'DeadRelu',
     'RuleEvaluationStatus': 'NoIssuesFound'}
    {'RuleConfigurationName': 'CheckInputImages', 
     'RuleEvaluationStatus': 'NoIssuesFound'}
    
  7. 我们使用保存在 S3 中的相同张量创建一个试验:

    from smdebug.trials import create_trial
    s3_output_path = tf_estimator.latest_job_debugger_artifacts_path()
    trial = create_trial(s3_output_path)
    
  8. 让我们检查第一层卷积层中的过滤器:

    w = trial.tensor('conv2d/weights/conv2d/kernel:0')
    g = trial.tensor(
    'training/Adam/gradients/gradients/conv2d/Conv2D_grad/Conv2DBackpropFilter:0')
    print(w.value(0).shape)
    print(g.value(0).shape)
    (3, 3, 1, 64)
    (3, 3, 1, 64)
    

    在我们的训练脚本中定义,第一层卷积层有 64 个过滤器。每个过滤器是 3x3 像素,具有单通道(2D)。因此,梯度具有相同的形状。

  9. 我们编写一个函数来绘制过滤器权重和梯度随时间变化的图,并绘制第一层卷积层中最后一个过滤器的权重:

    plot_conv_filter('conv2d/weights/conv2d/kernel:0', 63)
    

    你可以在以下截图中看到图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_8.jpg

图 10.8 – 绘制卷积过滤器随时间变化的权重

如你所见,SageMaker Debugger 使得检查训练作业变得非常简单。如果你使用内置的支持它的容器,你无需修改代码。所有配置都在估算器中完成。

你可以在github.com/awslabs/amazon-sagemaker-examples找到更多示例,包括一些高级用例,如实时可视化和模型修剪。

这部分内容结束了,我们学习了如何通过托管的临时训练来优化训练作业的成本,通过自动模型调优来提高准确性,以及如何使用 SageMaker Debugger 检查它们的内部状态。

在第二部分中,我们将深入探讨两项高级功能,帮助我们构建更好的训练工作流——SageMaker Feature Store 和 SageMaker Clarify。

使用 SageMaker Feature Store 管理特征和构建数据集

直到现在,我们一直在笔记本或 SageMaker Processing 脚本中工程化我们的训练和验证特征,然后将它们存储为 S3 对象。然后,我们直接使用这些对象来训练和评估模型。这是一个完全合理的工作流。然而,随着你的机器学习工作流的增长和成熟,可能会出现以下问题:

  • 我们如何对特征应用明确定义的模式?

  • 我们如何选择特征的子集来构建不同的数据集?

  • 我们如何存储和管理不同版本的特征?

  • 我们如何发现并重用其他团队的特征工程?

  • 我们如何在预测时访问工程化的特征?

SageMaker Feature Store 被设计用来回答这些问题。让我们将其添加到我们与 BlazingText 和 Amazon Reviews 一起构建的分类训练工作流中,见第六章训练自然语言处理模型

使用 SageMaker Processing 工程化特征

我们几乎可以直接重用之前的 SageMaker Processing 任务。唯一的区别是工程数据的输出格式。在原始任务中,我们将其保存为纯文本文件,按照 BlazingText 期望的输入格式。此格式对于 SageMaker Feature Store 来说不太方便,因为我们需要轻松访问每一列。CSV 格式也不行,因为评论中包含逗号,因此我们决定改用 TSV 格式:

  1. 因此,我们在处理脚本中添加了几行:

    fs_output_dir = '/opt/ml/processing/output/fs/'
    os.makedirs(fs_output_dir, exist_ok=True)
    fs_output_path = os.path.join(fs_output_dir, 'fs_data.tsv')  
    data.to_csv(fs_output_path, index=False,header=True, sep='\t')
    
  2. 和之前一样运行我们的 SageMaker Processing 任务,我们现在看到两个输出:一个是 BlazingText 的纯文本输出(如果我们想直接对完整数据集进行训练),另一个是我们将摄取到 SageMaker Feature Store 中的 TSV 输出:

    s3://sagemaker-us-east-1-123456789012/sagemaker-scikit-learn-2021-07-05-07-54-15-145/output/bt_data
    s3://sagemaker-us-east-1-123456789012/sagemaker-scikit-learn-2021-07-05-07-54-15-145/output/fs_data
    
  3. 让我们将 TSV 文件加载到pandas数据框中,并显示前几行:

    fs_training_output_path = 's3://sagemaker-us-east-1-123456789012/sagemaker-scikit-learn-2021-07-05-07-54-15-145/output/fs_data/fs_data.tsv'
    data = pd.read_csv(fs_training_output_path, sep='\t',
                       error_bad_lines=False, dtype='str')
    data.head()
    

    这将打印出下图所示的表格:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_9.jpg

图 10.9 – 查看前几行

现在,让我们创建一个特征组,以便我们摄取这些数据。

创建特征组

特征组是一种资源,用于存储相关特征的集合。特征组按行组织,每行都有一个唯一标识符和时间戳。每行包含键值对,其中每一对代表一个特征名称和特征值。

  1. 首先,让我们定义特征组的名称:

    from sagemaker.feature_store.feature_group import FeatureGroup
    feature_group_name = 'amazon-reviews-feature-group-' + strftime('%d-%H-%M-%S', gmtime())
    feature_group = FeatureGroup(
        name=feature_group_name,    
        sagemaker_session=feature_store_session)
    
  2. 接下来,我们设置包含唯一标识符的特征名称——review_id在这里完全适用,你可以使用数据源中任何唯一的值,如主键:

    record_identifier_feature_name = 'review_id'
    
  3. 然后,我们为pandas数据框中的所有行添加了时间戳列。如果你的数据源已经包含时间戳,你可以重用该值,无论是float64格式还是UNIX日期/时间格式:

    event_time_feature_name = 'event_time'
    current_time_sec = int(round(time.time()))
    data = data.assign(event_time=current_time_sec)
    

    现在我们的数据框看起来如下所示:

    https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_10.jpg

    图 10.10 – 查看时间戳

  4. 下一步是为特征组定义模式。我们可以将其显式提供为 JSON 文档,或者让 SageMaker 从 pandas 数据框中自动提取。我们使用第二种方式:

    data['review_id'] = data['review_id']
        .astype('str').astype('string')
    data['product_id'] = data['product_id']
        .astype('str').astype('string')
    data['review_body'] = data['review_body']
        .astype('str').astype('string')
    data['label'] = data['label']
        .astype('str').astype('string')
    data['star_rating'] = data['star_rating']
        .astype('int64')
    data['event_time'] = data['event_time']
        .astype('float64')
    

    接下来,我们加载特征定义:

    feature_group.load_feature_definitions(
        data_frame=data)
    
  5. 最后,我们创建特征组,传递将存储特征的 S3 位置。这是我们查询它们以构建数据集的地方。我们启用在线存储,这将使我们在预测时以低延迟访问特征。我们还添加了描述和标签,便于发现特征组:

    feature_group.create(
      role_arn=role,
      s3_uri='s3://{}/{}'.format(default_bucket, prefix),
      enable_online_store=True,
      record_identifier_name=
          record_identifier_feature_name,
      event_time_feature_name=
          event_time_feature_name,
      description="1.8M+ tokenized camera reviews from the   
                   Amazon Customer Reviews dataset",
      tags=[
          { 'Key': 'Dataset', 
            'Value': 'amazon customer reviews' },
          { 'Key': 'Subset',
            'Value': 'cameras' },
          { 'Key': 'Owner',
            'Value': 'Julien Simon' }
      ])
    

几秒钟后,特征组准备就绪,并在 SageMaker Studio 中可见,位于组件和注册表 / 特征存储下,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_11.jpg

图 10.11 – 查看特征组

现在,让我们开始数据摄取。

摄取特征

SageMaker Feature Store 允许我们通过三种方式摄取数据:

  • 调用PutRecord() API 以摄取单个记录。

  • 调用ingest() API 上传pandas数据框的内容。

  • 如果我们使用SageMaker 数据 Wrangler进行特征工程,可以使用自动生成的笔记本创建特征组并导入数据。

我们在这里使用第二个选项,它与以下代码一样简单:

feature_group.ingest(data_frame=data, max_workers=10, 
                     wait=True)

一旦数据导入完成,特征将存储在我们指定的 S3 位置以及专用的低延迟后端中。我们可以使用前者来构建数据集。

查询特征以构建数据集

当我们创建特征组时,SageMaker 会自动在 AWS Glue 数据目录 中为其添加一个新表。这使得使用 Amazon Athena 查询数据并按需构建数据集变得更加容易。

假设我们希望构建一个包含至少有 1,000 条评论的畅销相机的数据集:

  1. 首先,我们编写一个 SQL 查询,计算每台相机的平均评分,统计每台相机收到的评论数,仅保留至少有 1,000 条评论的相机,并按平均评分降序排列相机:

    query_string = 
    'SELECT label,review_body FROM 
    "'+ feature_group_table +'"'
    + ' INNER JOIN (
          SELECT product_id FROM (
              SELECT product_id, avg(star_rating) as  
                     avg_rating, count(*) as review_count
              FROM "'+ feature_group_table+ '"' + '
              GROUP BY product_id) 
          WHERE review_count > 1000) tmp 
    ON "'+feature_group_table+'"'
    + '.product_id=tmp.product_id;'
    
  2. 然后,我们使用 Athena 查询我们的特征组,将选中的行存储在 pandas 数据框中,并显示前几行:

    dataset = pd.DataFrame()
    feature_group_query.run(query_string=query_string, output_location='s3://'+default_bucket+'/query_results/')
    feature_group_query.wait()dataset = feature_group_query.as_dataframe()
    dataset.head()
    

这会打印出下一张图片中可见的表格:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_12.jpg

图 10.12 – 查看查询结果

从那时起,一切照常。我们可以将这个数据框保存为 CSV 文件,并用它来训练模型。你可以在 GitHub 仓库中找到一个完整的示例。

探索 SageMaker Feature Store 的其他功能

随着时间的推移,我们可以存储同一特征的不同版本——即具有相同标识符但时间戳不同的多个记录。这将允许我们通过简单的 SQL 查询来检索数据集的早期版本——在我们的数据中进行“时光旅行”。

最后但同样重要的是,功能也可以在在线商店中使用。我们可以通过 GetRecord() API 检索单个记录,并在预测时根据需要使用功能。

再次,你将在 GitHub 仓库中找到这两项功能的代码示例。

为了结束本章内容,让我们看看 Amazon SageMaker Clarify,这是一项通过检测数据集和模型中的潜在偏差,帮助我们构建更高质量模型的功能。

使用 SageMaker Clarify 检测数据集中的偏差并解释预测结果

机器学习ML)模型的好坏取决于其构建的 dataset。如果数据集不准确或无法公平地代表其应该捕捉的现实情况,那么相应的模型很可能会学习到这种偏差的表示,并在预测中延续这种偏差。作为机器学习实践者,我们需要意识到这些问题,理解它们如何影响预测,并尽可能地减少这种影响。

在这个示例中,我们将使用成人数据集,该数据集可在UCI 机器学习库中找到(archive.ics.uci.edu/ml,Dua, D.和 Graff, C.,2019)。这个数据集描述了一个二分类任务,我们尝试预测一个人是否年收入超过$50,000。这里,我们想检查这个数据集是否引入了性别偏差。换句话说,它是否有助于我们构建一个对男性和女性的预测效果一样好的模型?

注意

你在 GitHub 仓库中找到的数据集经过了轻微的处理。标签列已经根据 XGBoost 的要求被移到前面。类别变量已经进行了独热编码。

使用 SageMaker Clarify 配置偏差分析

SageMaker Clarify 计算训练前和训练后的指标,帮助我们理解模型的预测情况。

后训练指标显然需要一个已训练的模型,因此我们首先使用 XGBoost 训练一个二分类模型。这是我们已经看过很多次的内容,你可以在 GitHub 仓库中找到相关代码。这个模型的验证 AuC 达到了 92.75%。

一旦训练完成,我们就可以进行偏差分析:

  1. 偏差分析作为 SageMaker 处理任务运行。因此,我们创建一个SageMakerClarifyProcessor对象,指定我们的基础设施需求。由于任务规模较小,我们使用一个实例。对于更大的任务,我们可以使用更多实例,并且分析将自动在Spark上运行:

    from sagemaker import clarify
    clarify_processor = clarify.SageMakerClarifyProcessor(
        role=role,
        instance_count=1,
        instance_type='ml.m5.large',
        sagemaker_session=session)
    
  2. 然后,我们创建一个DataConfig对象,描述要分析的数据集:

    bias_report_output_path = 's3://{}/{}/clarify-bias'.format(bucket, prefix)
    data_config = clarify.DataConfig(
        s3_data_input_path=train_uri,
        s3_output_path=bias_report_output_path,
        label='Label',
        headers=train_data.columns.to_list(),
        dataset_type='text/csv')
    
  3. 同样地,我们创建一个ModelConfig对象,描述要分析的模型:

    model_config = clarify.ModelConfig(
        model_name=xgb_predictor.endpoint_name,
        instance_type='ml.t2.medium',
        instance_count=1,
        accept_type='text/csv')
    
  4. 最后,我们创建一个BiasConfig对象,描述要计算的指标。label_values_or_threshold定义了正向结果的标签值(1,表示年收入高于$50K)。facet_name定义了我们希望分析的特征(Sex_),而facet_values_or_threshold定义了潜在弱势群体的特征值(1,表示女性)。

    bias_config = clarify.BiasConfig(
        label_values_or_threshold=[1],
        facet_name='Sex_',
        facet_values_or_threshold=[1])
    

我们现在准备好运行分析了。

运行偏差分析

将所有内容整合在一起,我们使用以下命令启动分析:

clarify_processor.run_bias(
    data_config=data_config,
    model_config=model_config,
    bias_config=bias_config)

一旦分析完成,结果将在 SageMaker Studio 中可见。报告也会生成并以 HTML、PDF 和 Notebook 格式存储在 S3 中。

实验和试验中,我们找到我们的 SageMaker Clarify 任务,并右键点击打开试验详情。选择偏差报告,我们可以看到偏差指标,如下图所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_13.jpg

图 10.13 – 查看偏差指标

分析偏差指标

如果你想了解更多关于偏差指标的信息、它们的含义以及它们是如何计算的,我强烈推荐以下资源:

我们来看两个训练前的度量标准,类别不平衡CI)和标签中正类比例差异DPL),以及一个训练后的度量标准,预测标签中正类比例差异DPPL)。

CI 的非零值表明数据集是不平衡的。这里,男性和女性比例的差异是 0.35。确实,男性组大约占数据集的三分之二,女性组约占三分之一。这并不是一个非常严重的失衡,但我们也应该查看每个类别的正类标签比例。

DPL 衡量每个类别是否具有相同的正类标签比例。换句话说,数据集中男性和女性赚取$50K 的比例是否相同?DPL 的值为非零(0.20),这告诉我们男性的$50K 收入者比例更高。

DPPL 是一个训练后度量,类似于 DPL。它的值(0.18)表明模型不幸地拾取了数据集中的偏差,只是轻微地减少了它。实际上,模型为男性预测了一个更有利的结果(过度预测$50K 收入者),而为女性预测了一个不太有利的结果(低估$50K 收入者)。

这显然是一个问题。尽管模型有一个相当不错的验证 AuC(92.75%),但它并没有同样好地预测两种类别。

在我们深入分析数据并尝试缓解这个问题之前,先进行一次可解释性分析。

运行可解释性分析

SageMaker Clarify 可以计算局部和全局的 SHAP 值(github.com/slundberg/shap)。它们帮助我们理解特征的重要性,以及各个特征值如何影响正面或负面的结果。

偏差分析作为 SageMaker 处理作业运行,过程类似:

  1. 我们创建一个DataConfig对象,描述要分析的数据集:

    explainability_output_path = 's3://{}/{}/clarify-explainability.format(bucket, prefix)
    data_config = clarify.DataConfig(
        s3_data_input_path=train_uri,
        s3_output_path= explainability_output_path,
        label='Label',
        headers=train_data.columns.to_list(),
        dataset_type='text/csv')
    
  2. 我们创建一个SHAPConfig对象,描述我们希望如何计算 SHAP 值——即使用哪个基准(我使用了移除标签后的测试集),使用多少样本(特征数的两倍加 2048,这是一个常见的默认值),以及如何聚合值:

    shap_config = clarify.SHAPConfig(
        baseline=test_no_labels_uri,
        num_samples=2*86+2048,
        agg_method='mean_abs',
        save_local_shap_values=True
    )
    
  3. 最后,我们运行分析:

    clarify_processor.run_explainability(
        data_config=explainability_data_config,
        model_config=model_config,
        explainability_config=shap_config
    )
    

结果可以在 SageMaker Studio 中查看,Sex特征是最重要的,这确认了偏差分析的结果。抛开伦理考虑不谈,从商业角度来看,这似乎并不太合理。像教育程度或资本收益这样的特征应该更为重要。

https://example.org

](https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/lrn-amz-sgmk-2e/img/B17705_10_14.jpg)

图 10.14 – 查看特征重要性

本地 SHAP 值也已经计算并存储在 S3 中。我们可以使用这些值来了解特征值如何影响每个单独样本的预测。

现在,让我们看看如何尝试缓解我们在数据集中检测到的偏差。

缓解偏差

这个数据集结合了两个问题。首先,它包含更多的男性而非女性。其次,男性组的正向结果比例较高。这两个问题的结合导致数据集中$50K 收入的女性数量比例异常低。这使得模型更难以公平地学习,并且倾向于偏向多数类。

偏差缓解技术包括以下内容:

  • 通过删除多数样本来对多数类进行下采样,以重新平衡数据集

  • 通过复制现有样本来对少数类进行过采样,增加更多样本

  • 通过生成与现有样本具有相似统计属性的新样本,向少数类添加合成样本

    注意

    修改数据不应轻率进行,尤其是在受监管行业中运作的组织中。这可能会产生严重的业务、合规性和法律后果。在生产环境中进行此操作之前,请务必获得批准。

让我们尝试一种结合方法,基于imbalanced-learn开源库(https://imbalanced-learn.org)。首先,我们将使用合成少数类过采样技术SMOTE)算法向少数类添加合成样本,以匹配多数类样本中$50K 收入者的比例。接着,我们将对多数类进行下采样,使其样本数与少数类相匹配。结果将是一个完全平衡的数据集,两个类别的大小相同,$50K 收入者的比例也相同。让我们开始吧:

  1. 首先,我们需要计算两个类别的比例:

    female_male_not_50k_count = train_data['Sex_'].where(
        train_data['Label']==0).value_counts()
    female_male_50k_count = train_data['Sex_'].where(
        train_data['Label']==1).value_counts()
    ratios = female_male_50k_count / 
             female_male_not_50k_count
    print(ratios)
    

    这给出了以下结果,显示多数类(类别 0)拥有远高于$50K 收入者的比例:

    0.0    0.457002
    1.0    0.128281
    
  2. 然后,我们生成少数类的合成样本:

    from imblearn.over_sampling import SMOTE
    female_instances = train_data[train_data['Sex_']==1]
    female_X = female_instances.drop(['Label'], axis=1)
    female_Y = female_instances['Label']
    oversample = SMOTE(sampling_strategy=ratios[0])
    balanced_female_X, balanced_female_Y = oversample.fit_resample(female_X, female_Y)
    balanced_female=pd.concat([balanced_female_X, balanced_female_Y], axis=1)
    
  3. 接下来,我们使用原始多数类和重新平衡的少数类重新构建数据集:

    male_instances = train_data[train_data['Sex_']==0]
    balanced_train_data=pd.concat(
        [male_instances, balanced_female], axis=0)
    
  4. 最后,我们对原始多数类进行下采样,以重新平衡比例:

    from imblearn.under_sampling import RandomUnderSampler
    X = balanced_train_data.drop(['Sex_'], axis=1)
    Y = balanced_train_data['Sex_']
    undersample = RandomUnderSampler(
        sampling_strategy='not minority')
    X,Y = undersample.fit_resample(X, Y)
    balanced_train_data=pd.concat([X, Y], axis=1)
    
  5. 我们再次计算两个类别的样本数,并重新计算它们的比例:

    female_male_count= balanced_train_data['Sex_']    
        .value_counts()
    female_male_50k_count = balanced_train_data['Sex_']
        .where(balanced_train_data['Label']==1)
        .value_counts()
    ratios = female_male_50k_count/female_male_count
    print(female_male_count)
    print(female_male_50k_count)
    print(ratios)
    

    这显示了以下结果:

    1.0    0.313620
    0.0    0.312039
    

在使用这个重新平衡的数据集进行训练,并使用相同的测试集时,我们得到了 92.95%的验证 AuC,相比之下原始模型为 92.75%。进行新的偏差分析时,CI 为零,DPL 和 DPPL 接近零。

我们不仅构建了一个预测更公平的模型,而且还稍微提高了准确性。难得的是,这次我们似乎做到了两全其美!

总结

本章总结了我们对训练技术的探索。你学习了受管训练(managed spot training),这是一种通过减少 70%或更多的训练成本的简单方法。你还了解了检查点(checkpointing)如何帮助恢复被中断的任务。接着,你学习了自动模型调优(automatic model tuning),这是一种通过探索超参数范围从模型中提取更多准确性的有效方法。你了解了 SageMaker 调试器(SageMaker Debugger),这是一个高级功能,能够自动检查训练任务中的不良条件,并将张量集合保存到 S3,以便检查和可视化。最后,我们发现了两个有助于你构建更高质量工作流和模型的功能,SageMaker 特征存储(SageMaker Feature Store)和 SageMaker Clarify。

在下一章,我们将详细学习模型部署。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值