使用 Amazon SageMaker 模型注册表 PlatoBlockchain 数据智能构建跨账户 MLOps 工作流程。 垂直搜索。 人工智能。

使用 Amazon SageMaker 模型注册表构建跨账户 MLOps 工作流程

设计良好的 CI/CD 管道对于有效扩展任何软件开发工作流程至关重要。 在设计生产 CI/CD 管道时,AWS 建议利用多个账户来隔离资源、遏制安全威胁并简化计费——数据科学管道也不例外。 在 AWS,我们不断创新以简化 MLOps 工作流程。

在这篇文章中,我们讨论了一些较新的跨账户功能 亚马逊SageMaker 这使您可以更好地共享和管理模型组以及管理模型版本。 要遵循的示例帐户结构 组织单位最佳做法 要跨账户使用 SageMaker 端点托管模型,请参阅 MLOps 工作负载协调器.

解决方案概述

下图说明了我们的共享模型注册表架构。

上述架构需要注意的几点:

以下步骤对应图:

  1. 数据科学家将数据科学帐户中的模型注册到共享服务 SageMaker 模型注册表中 PendingManualApproval 状态。 模型工件是在共享服务帐户中创建的 亚马逊简单存储服务 (Amazon S3)存储桶。
  2. 新模型版本注册后,有权根据指标批准模型的人应该批准或拒绝该模型。
  3. 模型通过后,部署账户中的CI/CD流水线为 触发部署 在 QA 帐户中更新模型详细信息并将阶段更新为 QA。
  4. 通过测试过程后,您可以选择在 CI/CD 过程中进行手动批准步骤,或者让 CI/CD 管道直接将模型部署到生产环境并将阶段更新为 Prod。
  5. 生产环境引用批准的模型和代码,也许做一个 生产中的 A/B 测试. 如果模型有审计或任何问题,您可以使用 Amazon SageMaker ML 沿袭跟踪. 它创建并存储有关机器学习 (ML) 工作流从数据准备到模型部署的步骤的信息。 使用跟踪信息,您可以重现工作流步骤、跟踪模型和数据集沿袭,并建立模型治理和审计标准。

在整个过程中,共享模型注册表保留了旧模型版本。 这允许团队回滚更改,甚至托管 生产变体.

先决条件

确保您具备以下先决条件:

  • 一个配置的多账户结构 – 有关说明,请参阅 AWS Organizations 组织单位的最佳实践. 出于本博客的目的,我们使用以下帐户:
    • 数据科学账户 – 数据科学家可以访问训练数据并创建模型的帐户。
    • 共享服务帐户 – 用于存储模型工件(如架构图中所示)的中央帐户,以便跨不同的工作负载帐户访问。
    • 部署账户 – 负责将更改部署到各个帐户的帐户。
    • 工作负载帐户 – 这些通常是 QA 和生产环境,软件工程师可以在其中构建应用程序以使用 ML 模型。
  • 具有适当权限的部署帐户 – 有关多账户 OU 结构最佳实践的更多信息,请参阅 部署 OU. 此帐户负责将工作负载帐户指向共享服务帐户的模型注册表中的所需模型。

定义跨账户策略

遵循最小权限原则,首先我们需要为共享服务资源添加跨账户资源策略,以允许其他账户访问。

由于模型构件存储在共享服务账户的 S3 存储桶中,因此数据科学账户需要 Amazon S3 读/写访问权限才能将经过训练的模型推送到 Amazon S3。 下面的代码说明了这个策略,但还没有将它添加到共享服务帐户:

#Data Science account's policy to access Shared Services' S3 bucket
 {
    'Version': '2012-10-17',
    'Statement': [{
        'Sid': 'AddPerm',
        'Effect': 'Allow',
        'Principal': {
            'AWS': 'arn:aws:iam:::root'
        }, 
        "Action": [ 
            's3:PutObject', 
            's3:PutObjectAcl',
            's3:GetObject', 
            's3:GetObjectVersion'
        ], #read/write
        'Resource': 'arn:aws:s3:::/*'
    }]
}

部署账户只需要被授予对 S3 存储桶的读取访问权限,这样它就可以使用模型构件部署到 SageMaker 端点。 我们还需要将以下策略附加到共享服务 S3 存储桶:

#Deployment account's policy to access Shared Services' S3 bucket
 {
    'Version': '2012-10-17',
    'Statement': [{
        'Sid': 'AddPerm',
        'Effect': 'Allow',
        'Principal': {
            'AWS': 'arn:aws:iam:::root'
        },
        'Action': [ 
            's3:GetObject', 
            's3:GetObjectVersion'
        ], #read
        'Resource': 'arn:aws:s3:::/*'
    }]
}

我们将这两种策略结合起来得到以下最终策略。 替换适当的帐户 ID 后,在共享服务帐户中创建此策略:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "AddPerm",
    "Effect": "Allow",
    "Principal": {
      "AWS": "arn:aws:iam:::root"    
    },
    "Action": [
      "s3:PutObject",
      "s3:PutObjectAcl",
      "s3:GetObject",
      "s3:GetObjectVersion"    ],
    "Resource": "arn:aws:s3:::/*"  
    },
    {
      "Sid": "AddPermDeployment",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam:::root"      
      },
      "Action": [
        "s3:GetObject",
        "s3:GetObjectVersion"      ], 
      "Resource": "arn:aws:s3:::/*"    
    }
  ]
}

为了能够部署在不同账户中创建的模型,用户必须具有有权访问 SageMaker 操作的角色,例如具有 AmazonSageMakerFullAccess 托管政策。 参考 从不同的帐户部署模型版本 了解更多详细信息。

我们需要定义包含我们要部署的模型版本的模型组。 此外,我们想授予数据科学帐户权限。 这可以通过以下步骤完成。 我们参考以下帐户:

  • 共享服务帐户 ID –模型注册表所在的帐户以及我们希望模型所在的位置
  • 数据科学帐户 ID – 我们将在其中进行培训并因此创建实际模型工件的帐户
  • 部署帐户 ID – 我们要托管此模型端点的帐户

首先我们需要确保模型包组存在。 您可以使用 Boto3 API,如以下示例所示,或者您可以使用 AWS管理控制台 创建模型包。 参考 创建模型包组 更多细节。 这假设您已经安装了 Boto3。

model_package_group_name = "cross-account-example-model"
sm_client = boto3.Session().client("sagemaker")

create_model_package_group_response = sm_client.create_model_package_group(
    ModelPackageGroupName=model_package_group_name,
    ModelPackageGroupDescription="Cross account model package group",
    Tags=[
          {
              'Key': 'Name',
              'Value': 'cross_account_test'
          },
      ]

)

print('ModelPackageGroup Arn : {}'.format(create_model_package_group_response['ModelPackageGroupArn']))

对于此模型包组的权限,您可以创建类似于以下代码的 JSON 文档。 将实际帐户 ID 和模型包组名称替换为您自己的值。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AddPermModelPackageGroupCrossAccount",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam:::root"      
      },
      "Action": [
        "sagemaker:DescribeModelPackageGroup"      
        ],
      "Resource": "arn:aws:sagemaker:::model-package-group/"    
    },
    {
      "Sid": "AddPermModelPackageVersionCrossAccount",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam:::root"      
      },
      "Action": [
        "sagemaker:DescribeModelPackage",
        "sagemaker:ListModelPackages",
        "sagemaker:UpdateModelPackage",
        "sagemaker:CreateModelPackage",
        "sagemaker:CreateModel"      
      ],
      "Resource": "arn:aws:sagemaker:::model-package//*"    
    }
  ]
}

最后,将策略应用于模型包组。 您无法通过控制台将此策略与程序包组相关联。 您需要 SDK 或 AWS命令行界面 (AWS CLI) 访问。 例如,以下代码使用 Boto3:

# Convert the policy from JSON dict to string
model_package_group_policy = dict( )
model_package_group_policy = json.dumps(model_package_group_policy)

# Set the new policy
sm_client = boto3.Session().client("sagemaker")
response = sm_client.put_model_package_group_policy(
    ModelPackageGroupName = model_package_group_name,
    ResourcePolicy = model_package_group_policy)

我们还需要定制 AWS密钥管理服务 (AWS KMS) 密钥,用于在将模型存储在 Amazon S3 中时对其进行加密。 这需要使用数据科学帐户来完成。 在 AWS KMS 控制台上,导航到 定义密钥使用权限 页。 在里面 其他 AWS 账户 部分中,选择 添加另一个 AWS 账户. 输入部署账户的 AWS 账号。 您将此 KMS 密钥用于 SageMaker 训练作业。 如果您没有为训练作业指定 KMS 密钥,SageMaker 默认使用 Amazon S3 服务器端加密密钥。 默认的 Amazon S3 服务器端加密密钥不能与其他 AWS 账户共享或由其使用。

策略和权限遵循以下模式:

  • 中指定的 Amazon S3 策略 shared_services_account 授予数据科学帐户和部署帐户权限
  • 中指定的 KMS 密钥策略 shared_services_account 授予数据科学帐户和部署帐户权限

我们需要确保共享服务帐户和部署帐户可以访问用于训练模型的 Docker 映像。 这些图像通常托管在 AWS 账户中,如果您还没有访问权限,您的账户管理员可以帮助您获得访问权限。 对于本文,我们不会在训练模型后创建任何自定义 Docker 映像,因此我们不需要针对映像的任何特定 Amazon ECR 策略。

在工作负载帐户(QA 或 prod)中,我们需要创建两个 AWS身份和访问管理 (IAM) 策略类似于以下内容。 这些是 内联政策,这意味着它们嵌入在 IAM 身份中。 这使这些帐户可以访问模型注册表。

第一个内联策略允许角色访问包含模型构件的共享服务账户中的 Amazon S3 资源。 提供 S3 存储桶的名称和您的模型:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::/sagemaker//output/model.tar.gz"
        }
    ]
}

第二个内联策略允许我们稍后创建的角色使用共享服务帐户中的 KMS 密钥。 指定共享服务帐户的帐户 ID 和 KMS 密钥 ID:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowUseOfTheKey",
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "arn:aws:kms:us-east-1::key/{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}"
            ]
        }
    ]
}

最后,我们需要 创建 IAM 角色 对于 SageMaker。 这个角色有 AmazonSageMakerFullAccess 附加政策。 然后,我们将这两个内联策略附加到我们创建的角色。 如果您使用现有的 SageMaker 执行角色,请将这两个策略附加到该角色。 有关说明,请参阅 创建角色和附加策略(控制台).

现在我们已经定义了每个帐户的策略,让我们使用一个示例来查看它的实际操作。

使用 SageMaker 管道构建和训练模型

我们首先在数据科学账户中创建一个 SageMaker 管道,用于进行数据处理、模型训练和评估。 我们使用从 StatLib 库获得的加州住房数据集。 在下面的代码片段中,我们使用自定义预处理脚本 preprocess.py 执行一些简单的特征转换,例如特征缩放,可以使用以下方法生成 笔记本. 该脚本还将数据集拆分为训练和测试数据集。

我们创建一个 SKLearnProcessor 对象来运行这个预处理脚本。 在 SageMaker 管道中,我们创建一个处理步骤 (ProcessingStep) 使用运行处理代码 SKLearnProcessor. 初始化 SageMaker 管道时调用此处理代码。 创建的代码 SKLearnProcessorProcessingStep 如下代码所示。 请注意,本节中的所有代码都在数据科学帐户中运行。

# Useful SageMaker variables - Create a Pipeline session which will lazy init resources
session = PipelineSession()

framework_version = "0.23-1"

# Create SKlearn processor object,
# The object contains information about what instance type to use, the IAM role to use etc.
# A managed processor comes with a preconfigured container, so only specifying version is required.
sklearn_processor = SKLearnProcessor(
    framework_version=framework_version,
    role=role,
    instance_type=processing_instance_type,
    instance_count=1,
    base_job_name="tf2-california-housing-processing-job",
    sagemaker_session=session
)

# Use the sklearn_processor in a SageMaker pipelines ProcessingStep
step_preprocess_data = ProcessingStep(
    name="Preprocess-California-Housing-Data",
    processor=sklearn_processor,
    inputs=[
        ProcessingInput(source=input_data, destination="/opt/ml/processing/input"),
    ],
    outputs=[
        ProcessingOutput(output_name="train", source="/opt/ml/processing/train"),
        ProcessingOutput(output_name="test", source="/opt/ml/processing/test"),
    ],
    code="preprocess.py",
)

我们需要自定义 KMS 密钥来加密模型,同时将其存储到 Amazon S3。 请参见以下代码:

kms_client = boto3.client('kms')
response = kms_client.describe_key(
    KeyId='alias/sagemaker/outkey',
)
key_id = response['KeyMetadata']['KeyId']

为了训练模型,我们创建了一个 TensorFlow 估计器对象。 我们将 KMS 密钥 ID 与我们的训练脚本一起传递给它 train.py、训练实例类型和计数。 我们还创建了一个 TrainingStep 将其添加到我们的管道中,并将 TensorFlow 估算器添加到其中。 请参见以下代码:

model_path = f"s3://{bucket}/{prefix}/model/"

hyperparameters = {"epochs": training_epochs}
tensorflow_version = "2.4.1"
python_version = "py37"

tf2_estimator = TensorFlow(
    source_dir="code",
    entry_point="train.py",
    instance_type=training_instance_type,
    instance_count=1,
    framework_version=tensorflow_version,
    role=role,
    base_job_name="tf2-california-housing-train",
    output_path=model_path,
    output_kms_key=key_id,
    hyperparameters=hyperparameters,
    py_version=python_version,
    sagemaker_session=session
)

# Use the tf2_estimator in a SageMaker pipelines ProcessingStep.
# NOTE how the input to the training job directly references the output of the previous step.
step_train_model = TrainingStep(
    name="Train-California-Housing-Model",
    estimator=tf2_estimator,
    inputs={
        "train": TrainingInput(
            s3_data=step_preprocess_data.properties.ProcessingOutputConfig.Outputs[
                "train"
            ].S3Output.S3Uri,
            content_type="text/csv",
        ),
        "test": TrainingInput(
            s3_data=step_preprocess_data.properties.ProcessingOutputConfig.Outputs[
                "test"
            ].S3Output.S3Uri,
            content_type="text/csv",
        ),
    },
)

除了训练之外,我们还需要进行模型评估,在本例中我们使用均方误差(MSE)作为指标。 这 较早的笔记本 也产生 evaluate.py,我们用它来评估我们使用 MSE 的模型。 我们还创建了一个 ProcessingStep 使用一个初始化模型评估脚本 SKLearnProcessor 目的。 以下代码创建此步骤:

from sagemaker.workflow.properties import PropertyFile

# Create SKLearnProcessor object.
# The object contains information about what container to use, what instance type etc.
evaluate_model_processor = SKLearnProcessor(
    framework_version=framework_version,
    instance_type=processing_instance_type,
    instance_count=1,
    base_job_name="tf2-california-housing-evaluate",
    role=role,
    sagemaker_session=session
)

# Create a PropertyFile
# A PropertyFile is used to be able to reference outputs from a processing step, for instance to use in a condition step.
# For more information, visit https://docs.aws.amazon.com/sagemaker/latest/dg/build-and-manage-propertyfile.html
evaluation_report = PropertyFile(
    name="EvaluationReport", output_name="evaluation", path="evaluation.json"
)

# Use the evaluate_model_processor in a SageMaker pipelines ProcessingStep.
step_evaluate_model = ProcessingStep(
    name="Evaluate-California-Housing-Model",
    processor=evaluate_model_processor,
    inputs=[
        ProcessingInput(
            source=step_train_model.properties.ModelArtifacts.S3ModelArtifacts,
            destination="/opt/ml/processing/model",
        ),
        ProcessingInput(
            source=step_preprocess_data.properties.ProcessingOutputConfig.Outputs[
                "test"
            ].S3Output.S3Uri,
            destination="/opt/ml/processing/test",
        ),
    ],
    outputs=[
        ProcessingOutput(output_name="evaluation", source="/opt/ml/processing/evaluation"),
    ],
    code="evaluate.py",
    property_files=[evaluation_report],
)

在模型评估之后,如果模型性能满足要求,我们还需要一个步骤将我们的模型注册到模型注册表中。 这在以下代码中使用 RegisterModel 步。 这里我们需要指定我们在共享服务帐户中声明的模型包。 用您的值替换区域、帐户和模型包。 这里使用的模型名称是 modeltest, 但您可以使用您选择的任何名称。

# Create ModelMetrics object using the evaluation report from the evaluation step
# A ModelMetrics object contains metrics captured from a model.
model_metrics = ModelMetrics(
    model_statistics=MetricsSource(
        s3_uri=evaluation_s3_uri,
        content_type="application/json",
    )
)

# Create a RegisterModel step, which registers the model with SageMaker Model Registry.
model = Model(
    image_uri=tf2_estimator.training_image_uri(),
    model_data=training_step.properties.ModelArtifacts.S3ModelArtifacts,
    source_dir=tf2_estimator.source_dir,
    entry_point=tf2_estimator.entry_point,
    role=role_arn,
    sagemaker_session=session
)

model_registry_args = model.register(
    content_types=['text/csv'],
    response_types=['application/json'],
    inference_instances=['ml.t2.medium', 'ml.m5.xlarge'],
    transform_instances=['ml.m5.xlarge'],
    model_package_group_name=model_package_group_name,
    approval_status='PendingManualApproval',
    model_metrics=model_metrics
)

 step_register_model= ModelStep(
    name='RegisterModel',
    step_args=model_registry_args
)

我们还需要创建模型工件,以便可以部署它(使用其他帐户)。 为了创建模型,我们创建了一个 CreateModelStep,如以下代码所示:

from sagemaker.inputs import CreateModelInput 
from sagemaker.workflow.model_step import ModelStep 
step_create_model = ModelStep( 
    name="Create-California-Housing-Model", 
    step_args=model.create(instance_type="ml.m5.large",accelerator_type="ml.eia1.medium"),
 )

向管道添加条件是通过 ConditionStep. 在这种情况下,如果新模型满足精度条件,我们只想在模型注册表中注册新模型版本。 请参见以下代码:

from sagemaker.workflow.conditions import ConditionLessThanOrEqualTo
from sagemaker.workflow.condition_step import (
    ConditionStep,
    JsonGet,
)

# Create accuracy condition to ensure the model meets performance requirements.
# Models with a test accuracy lower than the condition will not be registered with the model registry.
cond_lte = ConditionLessThanOrEqualTo(
    left=JsonGet(
        step=step_evaluate_model,
        property_file=evaluation_report,
        json_path="regression_metrics.mse.value",
    ),
    right=accuracy_mse_threshold,
)

# Create a SageMaker Pipelines ConditionStep, using the preceding condition.
# Enter the steps to perform if the condition returns True / False.
step_cond = ConditionStep(
    name="MSE-Lower-Than-Threshold-Condition",
    conditions=[cond_lte],
    if_steps=[step_register_model, step_create_model],
    else_steps=[step_higher_mse_send_email_lambda],
)

最后,我们要编排所有流水线步骤,以便可以初始化流水线:

from sagemaker.workflow.pipeline import Pipeline

# Create a SageMaker Pipeline.
# Each parameter for the pipeline must be set as a parameter explicitly when the pipeline is created.
# Also pass in each of the preceding steps.
# Note that the order of execution is determined from each step's dependencies on other steps,
# not on the order they are passed in.
pipeline = Pipeline(
    name=pipeline_name,
    parameters=[
        processing_instance_type,
        training_instance_type,
        input_data,
        training_epochs,
        accuracy_mse_threshold,
        endpoint_instance_type,
    ],
    steps=[step_preprocess_data, step_train_model, step_evaluate_model, step_cond],
)

从不同的帐户部署模型版本

现在模型已在共享服务帐户中注册,我们需要使用部署帐户中的 CI/CD 管道将其部署到我们的工作负载帐户中。 我们已经在前面的步骤中配置了角色和策略。 我们使用模型包 ARN 从模型注册表部署模型。 以下代码在部署帐户中运行,用于将批准的模型部署到 QA 和 prod:

from sagemaker import ModelPackage
from time import gmtime, strftime

sagemaker_session = sagemaker.Session(boto_session=sess)

model_package_arn = 'arn:aws:sagemaker:::/modeltest/version_number'
model = ModelPackage(role=role, 
                     model_package_arn=model_package_arn, 
                     sagemaker_session=sagemaker_session)
model.deploy(initial_instance_count=1, instance_type='ml.m5.xlarge')

结论

在本文中,我们演示了如何根据最小权限原则为 ML 设置多帐户设置所需的策略。 然后我们展示了在数据科学帐户中构建和训练模型的过程。 最后,我们使用部署账户中的 CI/CD 管道将最新版本的已批准模型部署到 QA 和生产账户。 此外,您可以 查看模型的部署历史构建触发器 in AWS 代码构建.

您可以将这篇文章中的概念扩展到托管模型 亚马逊弹性计算云 (Amazon EC2)或 Amazon Elastic Kubernetes服务 (Amazon EKS),以及构建批量推理管道。

要了解有关在 AWS 中使用单独账户构建 ML 模型的更多信息,请参阅 AWS Organizations 组织单位的最佳实践在生产中安全地更新模型.


作者简介

使用 Amazon SageMaker 模型注册表 PlatoBlockchain 数据智能构建跨账户 MLOps 工作流程。 垂直搜索。 人工智能。桑迪普维尔玛 是 AWS 的高级原型架构师。 他喜欢深入研究客户面临的挑战并为客户构建原型以加速创新。 他拥有 AI/ML 背景,是 New Knowledge 的创始人,并且对技术充满热情。 在空闲时间,他喜欢和家人一起旅行和滑雪。

玛尼哈努加  玛尼哈努加 是 Amazon Web Services (AWS) 的人工智能和机器学习专家 SA。 她帮助客户使用机器学习解决他们使用 AWS 的业务挑战。 她大部分时间都花在与计算机视觉、自然语言处理、预测、边缘 ML 等相关的 AI/ML 项目上进行深入研究和教授客户。 她对 ML at edge 充满热情,因此,她创建了自己的实验室,配备了自动驾驶套件和原型制造生产线,她在那里度过了很多空闲时间。

使用 Amazon SageMaker 模型注册表 PlatoBlockchain 数据智能构建跨账户 MLOps 工作流程。 垂直搜索。 人工智能。索米特拉·维克拉姆 是 Amazon SageMaker 团队的一名软件开发人员,常驻印度金奈。 工作之余,他喜欢花时间跑步、徒步旅行和骑摩托车穿越喜马拉雅山。

使用 Amazon SageMaker 模型注册表 PlatoBlockchain 数据智能构建跨账户 MLOps 工作流程。 垂直搜索。 人工智能。斯里德维·斯里尼瓦桑 是 AWS SageMaker 的工程领导者。 她对将 ML 作为一个旨在改变日常生活的平台充满热情和兴奋。 她目前专注于 SageMaker Feature Store。 在空闲时间,她喜欢与家人共度时光。

使用 Amazon SageMaker 模型注册表 PlatoBlockchain 数据智能构建跨账户 MLOps 工作流程。 垂直搜索。 人工智能。 鲁宾德·格鲁瓦尔 是 AWS 的高级人工智能/机器学习专家解决方案架构师。 他目前专注于在 SageMaker 上提供模型和 MLOps。 在担任此职务之前,他曾担任机器学习工程师构建和托管模型。 工作之余,他喜欢打网球和在山路上骑自行车。

使用 Amazon SageMaker 模型注册表 PlatoBlockchain 数据智能构建跨账户 MLOps 工作流程。 垂直搜索。 人工智能。法鲁克萨比尔 是 AWS 的高级人工智能和机器学习专家解决方案架构师。 他拥有德克萨斯大学奥斯汀分校的电气工程博士和硕士学位,以及佐治亚理工学院的计算机科学硕士学位。 在 AWS,他帮助客户制定和解决他们在数据科学、机器学习、计算机视觉、人工智能、数值优化和相关领域的业务问题。 他拥有超过 16 年的工作经验,还是德克萨斯大学达拉斯分校的兼职教员,教授应用机器学习的研究生课程。 他和他的家人居住在得克萨斯州达拉斯,喜欢旅行和长途旅行。

时间戳记:

更多来自 AWS机器学习