Создание и развертывание приложений для вывода ML (машинного обучения) с нуля с помощью Amazon SageMaker

Построение и развертывание ML (машинного обучения) приложений с нуля с использованием Amazon SageMaker

Поскольку машинное обучение (ML) становится все более популярным и получает все большее распространение, приложения, работающие на основе ML-инференции, становятся все более распространенными для решения широкого спектра сложных бизнес-проблем. Решение этих сложных бизнес-проблем часто требует использования нескольких ML-моделей и шагов. Этот пост показывает вам, как создать и разместить ML-приложение с помощью настраиваемых контейнеров на Amazon SageMaker.

Amazon SageMaker предлагает встроенные алгоритмы и предварительно созданные образы Docker встроенные алгоритмы и предварительно созданные образы SageMaker для развертывания моделей. Однако, если они не соответствуют вашим потребностям, вы можете использовать свои собственные контейнеры (BYOC) для размещения на Amazon SageMaker.

Есть несколько случаев, когда пользователям может потребоваться BYOC для размещения на Amazon SageMaker.

  1. Собственные фреймворки или библиотеки ML: Если вы планируете использовать фреймворк или библиотеки ML, которые не поддерживаются встроенными алгоритмами Amazon SageMaker или предварительно созданными контейнерами, вам потребуется создать настраиваемый контейнер.
  2. Специализированные модели: Для определенных областей или отраслей вам может потребоваться использование специфических архитектур моделей или настроенных предварительных обработок данных, которые недоступны во встроенных предложениях Amazon SageMaker.
  3. Собственные алгоритмы: Если вы разработали собственные собственные алгоритмы внутри компании, вам потребуется настраиваемый контейнер для развертывания их на Amazon SageMaker.
  4. Сложные конвейеры вывода: Если ваш рабочий процесс вывода МЛ включает пользовательскую бизнес-логику – ряд сложных шагов, которые должны выполняться в определенном порядке, то BYOC может помочь вам более эффективно управлять и оркестрировать этими шагами.

Обзор решения

В этом решении мы показываем, как разместить серийное ML-приложение на Amazon SageMaker с использованием контейнеров для реального времени с последними пакетами scikit-learn и xgboost.

Первый контейнер использует модель scikit-learn для преобразования исходных данных в признаковые столбцы. Он применяет StandardScaler для числовых столбцов и OneHotEncoder для категориальных.

Второй контейнер размещает предварительно обученную модель XGboost (то есть предиктор). Модель предиктора принимает признаковый ввод и предсказывает результаты.

Наконец, мы развертываем фиксатор и предиктор в последовательную инференционную конвейерную линию на конечной точке реального времени Amazon SageMaker.

Вот несколько разных соображений, почему вам может понадобиться иметь отдельные контейнеры в рамках вашего приложения вывода.

  • Разделение – Разные шаги конвейера имеют четко определенную цель и должны выполняться на отдельных контейнерах из-за включенных зависимостей. Это также помогает поддерживать хорошую структуру конвейера.
  • Фреймворки – Разные шаги конвейера используют специфические фреймворки, предназначенные для таких целей (например, scikit или Spark ML), и поэтому они должны выполняться на отдельных контейнерах.
  • Изоляция ресурсов – Разные шаги конвейера требуют различных требований к потреблению ресурсов и поэтому должны выполняться на отдельных контейнерах для большей гибкости и контроля.
  • Обслуживание и обновления – С операционной точки зрения это способствует функциональной изоляции, и вы можете продолжать обновлять или изменять отдельные шаги гораздо проще, не затрагивая другие модели.

Кроме того, локальная сборка отдельных контейнеров помогает в процессе итеративной разработки и тестирования с помощью избранных инструментов и интегрированных сред разработки (IDE). После того, как контейнеры готовы, вы можете использовать их для развертывания в облаке AWS для вывода с использованием конечных точек Amazon SageMaker.

Полная реализация, включая фрагменты кода, доступна в этом репозитории Github здесь.

Предварительные требования

Поскольку мы первоначально тестируем эти пользовательские контейнеры локально, на вашем компьютере должна быть установлена Docker Desktop. Вы должны быть знакомы с созданием контейнеров Docker.

Вам также понадобится учетная запись AWS со доступом к Amazon SageMaker, Amazon ECR и Amazon S3 для тестирования этого приложения в полном объеме.

Убедитесь, что у вас установлена последняя версия пакетов Amazon SageMaker Python и Boto3:

pip install --upgrade boto3 sagemaker scikit-learn

Обзор решения

Создание пользовательского контейнера для фильтра объектов

Для создания первого контейнера, контейнера фильтра объектов, мы обучаем модель scikit-learn для обработки исходных данных в наборе данных абалони. Скрипт предварительной обработки использует SimpleImputer для обработки отсутствующих значений, StandardScaler для нормализации числовых столбцов и OneHotEncoder для преобразования категориальных столбцов. После подгонки преобразователя мы сохраняем модель в формате joblib. Затем мы сжимаем и загружаем этот сохраненный артефакт модели в бакет сервиса хранения данных Amazon Simple Storage Service (Amazon S3).

Вот пример кода, демонстрирующего это. См. полную реализацию в ноутбуке featurizer.ipynb:

```pythonnumeric_features = list(feature_columns_names)numeric_features.remove("sex")numeric_transformer = Pipeline(    steps=[        ("imputer", SimpleImputer(strategy="median")),        ("scaler", StandardScaler()),    ])categorical_features = ["sex"]categorical_transformer = Pipeline(    steps=[        ("imputer", SimpleImputer(strategy="constant", fill_value="missing")),        ("onehot", OneHotEncoder(handle_unknown="ignore")),    ])preprocess = ColumnTransformer(    transformers=[        ("num", numeric_transformer, numeric_features),        ("cat", categorical_transformer, categorical_features),    ])# Вызовите fit у ColumnTransformer, чтобы применить все преобразователи к X, ypreprocessor = preprocess.fit(df_train_val)# Сохранение модели обработчика на дискjoblib.dump(preprocess, os.path.join(model_dir, "preprocess.joblib"))```

Затем, для создания пользовательского контейнера вывода для модели фильтра объектов, мы создаем образ Docker с пакетами nginx, gunicorn, flask и другими необходимыми зависимостями для модели фильтра объектов.

Nginx, gunicorn и Flask-приложение будут служить стеком моделей для обслуживания на реальных конечных точках Amazon SageMaker.

При использовании пользовательских контейнеров для размещения на Amazon SageMaker необходимо обеспечить, чтобы скрипт вывода выполнял следующие задачи после запуска внутри контейнера:

  1. Загрузка модели: Скрипт вывода (preprocessing.py) должен обращаться к каталогу /opt/ml/model для загрузки модели в контейнере. Артефакты модели в Amazon S3 будут загружены и смонтированы на контейнере в путь /opt/ml/model.
  2. Переменные окружения: Чтобы передать пользовательские переменные окружения в контейнер, вы должны указать их при создании Модели или при создании Конечной точки из задания обучения.
  3. API-требования: Скрипт вывода должен реализовывать маршруты /ping и /invocations в качестве приложения Flask. API /ping используется для проверки работоспособности, а API /invocations обрабатывает запросы вывода.
  4. Журналирование: Журналы вывода в скрипте вывода должны записываться в стандартные потоки вывода (stdout) и потоки стандартной ошибки (stderr). Затем эти журналы передаются в Amazon CloudWatch от Amazon SageMaker.

Вот фрагмент из preprocessing.py, который показывает реализацию /ping и /invocations.

Ссылка на полную реализацию находится в файле preprocessing.py в папке с фичуризатором.

```pythondef load_model():    # Строим путь к файлу модели фичуризатора    ft_model_path = os.path.join(MODEL_PATH, "preprocess.joblib")    featurizer = None    try:        # Открываем файл модели и загружаем фичуризатор с помощью joblib        with open(ft_model_path, "rb") as f:            featurizer = joblib.load(f)            print("Модель фичуризатора загружена", flush=True)    except FileNotFoundError:        print(f"Ошибка: Файл модели фичуризатора не найден по пути {ft_model_path}", flush=True)    except Exception as e:        print(f"Ошибка загрузки модели фичуризатора: {e}", flush=True)    # Возвращаем загруженную модель фичуризатора или None в случае ошибки    return featurizerdef transform_fn(request_body, request_content_type):    """    Преобразует тело запроса в массив numpy, который может быть использован в модели.    Эта функция принимает тело запроса и его тип контента в качестве входных данных, и    возвращает преобразованный массив numpy, который может быть использован в    модели для предсказания.    Параметры:        request_body (str): Тело запроса, содержащее входные данные.        request_content_type (str): Тип контента в теле запроса.    Возвращает:        data (np.ndarray): Преобразованные входные данные в виде массива numpy.    """    # Определяем имена столбцов для входных данных    feature_columns_names = [        "sex",        "length",        "diameter",        "height",        "whole_weight",        "shucked_weight",        "viscera_weight",        "shell_weight",    ]    label_column = "rings"    # Проверяем поддерживаемый тип контента запроса (text/csv)    if request_content_type == "text/csv":        # Загружаем модель фичуризатора        featurizer = load_model()        # Проверяем, является ли фичуризатор ColumnTransformer        if isinstance(            featurizer, sklearn.compose._column_transformer.ColumnTransformer        ):            print(f"Модель фичуризатора загружена", flush=True)        # Читаем входные данные из тела запроса как CSV-файл        df = pd.read_csv(StringIO(request_body), header=None)        # Присваиваем имена столбцов на основе количества столбцов во входных данных        if len(df.columns) == len(feature_columns_names) + 1:            # Это пример с метками, включает метку кольца            df.columns = feature_columns_names + [label_column]        elif len(df.columns) == len(feature_columns_names):            # Это пример без меток            df.columns = feature_columns_names        # Преобразуем входные данные с помощью фичуризатора        data = featurizer.transform(df)        # Возвращаем преобразованные данные в виде массива numpy        return data    else:        # Выдаем ошибку, если тип контента не поддерживается        raise ValueError("Неподдерживаемый тип контента: {}".format(request_content_type))@app.route("/ping", methods=["GET"])def ping():    # Проверяем, может ли модель быть загружена, устанавливаем соответствующий статус    featurizer = load_model()    status = 200 if featurizer is not None else 500    # Возвращаем ответ с определенным кодом статуса    return flask.Response(response="\n", status=status, mimetype="application/json")@app.route("/invocations", methods=["POST"])def invocations():    # Преобразуем из JSON в словарь    print(f"Фичуризатор: получен тип контента: {flask.request.content_type}")    if flask.request.content_type == "text/csv":        # Декодируем входные данные и преобразуем их        input = flask.request.data.decode("utf-8")        transformed_data = transform_fn(input, flask.request.content_type)        # Форматируем transformed_data в строку CSV        csv_buffer = io.StringIO()        csv_writer = csv.writer(csv_buffer)        for row in transformed_data:            csv_writer.writerow(row)        csv_buffer.seek(0)        # Возвращаем преобразованные данные в виде строки CSV в ответе        return flask.Response(response=csv_buffer, status=200, mimetype="text/csv")    else:        print(f"Получен: {flask.request.content_type}", flush=True)        return flask.Response(            response="Трансформер: Этот предиктор поддерживает только данные в формате CSV",            status=415,            mimetype="text/plain",        )```

Создание Docker-образа с фичуризатором и моделью для предоставления сервиса

Теперь создадим Dockerfile, используя пользовательский базовый образ и установим необходимые зависимости.

Для этого мы используем python:3.9-slim-buster в качестве базового образа. Вы можете изменить его на другой подходящий базовый образ в соответствии с вашими потребностями.

Затем мы копируем конфигурацию nginx, файл веб-сервера gateway gunicorn и скрипт вывода в контейнер. Мы также создаем скрипт на языке Python под названием serve, который запускает процессы nginx и gunicorn в фоновом режиме и устанавливает скрипт вывода (то есть приложение Flask preprocessing.py) в качестве точки входа для контейнера.

Вот фрагмент Dockerfile для размещения модели фичеризатора. Для полной реализации см. Dockerfile в папке featurizer.

```dockerFROM python:3.9-slim-buster…# Копирование requirements.txt в папку /opt/programCOPY requirements.txt /opt/program/requirements.txt# Установка пакетов, перечисленных в requirements.txtRUN pip3 install --no-cache-dir -r /opt/program/requirements.txt# Копирование содержимого папки code/ в папку /opt/programCOPY code/ /opt/program/# Установка рабочей директории в /opt/program, где находятся скрипты serve и inference.pyWORKDIR /opt/program# Открытие порта 8080 для обслуживанияEXPOSE 8080ENTRYPOINT ["python"]# Скрипт serve - это файл на языке Python в папке code/, который запускает процессы nginx и gunicornCMD [ "serve" ]```

Тестирование пользовательского образа вывода с локальным фичеризатором

Теперь создайте и протестируйте пользовательский контейнер вывода с локальным фичеризатором с использованием локального режима SageMaker от Amazon. Локальный режим идеально подходит для тестирования обработки, обучения и скриптов вывода без запуска каких-либо заданий в Amazon SageMaker. После подтверждения результатов локальных тестов вы можете легко адаптировать скрипты обучения и вывода для развертывания на Amazon SageMaker с минимальными изменениями.

Для тестирования пользовательского образа фичеризатора локально сначала создайте образ, используя ранее определенный Dockerfile. Затем запустите контейнер, смонтировав каталог, содержащий модель фичеризатора (preprocess.joblib), в каталог /opt/ml/model внутри контейнера. Кроме того, сопоставьте порт 8080 контейнера с хостом.

После запуска вы можете отправлять запросы вывода по адресу http://localhost:8080/invocations.

Чтобы создать и запустить контейнер, откройте терминал и выполните следующие команды.

Обратите внимание, что вы должны заменить <IMAGE_NAME>, как показано в следующем коде, именем образа вашего контейнера.

Кроме того, следующая команда предполагает, что обученная модель scikit-learn (preprocess.joblib) находится в каталоге с именем models.

```shelldocker build -t <IMAGE_NAME> .``````shelldocker run –rm -v $(pwd)/models:/opt/ml/model -p 8080:8080 <IMAGE_NAME>```

После запуска контейнера вы можете протестировать маршруты /ping и /invocations с помощью команд curl.

Выполните следующие команды из терминала

```shell# тестирование маршрута /ping на локальном конечной точкеcurl http://localhost:8080/ping# отправка необработанной строки csv на /invocations. Конечная точка должна вернуть преобразованные данныecurl --data-raw 'I,0.365,0.295,0.095,0.25,0.1075,0.0545,0.08,9.0' -H 'Content-Type: text/csv' -v http://localhost:8080/invocations```

Когда необработанные данные отправляются на http://localhost:8080/invocations, конечная точка отвечает преобразованными данными.

Вы должны увидеть ответ, похожий на следующий:

```shell* Попытка 127.0.0.1:8080...* Подключено к localhost (127.0.0.1) порт 8080 (#0)> POST /invocations HTTP/1.1> Host: localhost: 8080> User-Agent: curl/7.87.0> Accept: */*> Content-Type: text/csv> Content-Length: 47>* Отметить пакет, не поддерживающий многоразовое использование> HTTP/1.1 200 OK> Server: nginx/1.14.2> Date: Вс, 09 апреля 2023 г. 20:47:48 GMT> Content-Type: text/csv; charset=utf-8> Content-Length: 150> Connection: keep-alive-1.3317586042173168, -1.1425409076053987, -1.0579488602777858, -1.177706547272754, -1.130662184748842, * Соединение с хостом localhost остается нетронутым```

Теперь мы завершаем работу контейнера, а затем помечаем и загружаем локальное пользовательское изображение в частный репозиторий Amazon Elastic Container Registry (Amazon ECR).

См. следующие команды для входа в Amazon ECR, которые помечают локальное изображение с полным путем к образу Amazon ECR, а затем загружают изображение в Amazon ECR. Убедитесь, что заменяете переменные region и account, чтобы они соответствовали вашей среде.

```shell# Вход в ecr с вашими учетными даннымиaws ecr get-login-password - -region "${region}" |\docker login - -username AWS - -password-stdin ${account}".dkr.ecr."${region}".amazonaws.com# Пометить и загрузить изображение в частный Amazon ECRdocker tag ${image} ${fullname}docker push $ {fullname}```

См. создание репозитория и push an image to Amazon ECR команды для AWS Command Line Interface (AWS CLI) для получения дополнительной информации.

Дополнительный шаг

По желанию вы можете выполнить живой тест, развернув модель преобразования на реальный конечный пункт с использованием пользовательского docker-образа в Amazon ECR. См. образец featurizer.ipynb для полной реализации построения, тестирования и загрузки пользовательского образа в Amazon ECR.

Amazon SageMaker инициализирует конечный пункт вывода и копирует модельные артефакты в каталог /opt/ml/model внутри контейнера. См. статью How SageMaker Loads your Model artifacts.

Создание пользовательского контейнера прогнозирования XGBoost

При создании контейнера прогнозирования XGBoost мы следуем аналогичным шагам, как и при создании образа контейнера для фишеризации:

  1. Загрузите предварительно обученную модель XGBoost из хранилища Amazon S3.
  2. Создайте скрипт inference.py, который загружает предварительно обученную модель XGBoost, преобразует полученные от фишеризации преобразованные входные данные и преобразует их в формат XGBoost.DMatrix, запускает predict на бустере и возвращает прогнозы в формате json.
  3. Скрипты и файлы конфигурации, составляющие стек обслуживания моделей (т.е. nginx.conf, wsgi.py и serve), остаются неизменными и не требуют модификаций.
  4. Мы используем Ubuntu:18.04 в качестве базового образа для Dockerfile. Это не является обязательным условием. Мы используем базовый образ Ubuntu для демонстрации того, что контейнеры могут быть построены на базе любого базового образа.
  5. Шаги по созданию пользовательского docker-образа, тестированию изображения локально и отправке проверенного образа в Amazon ECR остаются теми же, что и раньше.

Для краткости мы показываем только измененный код в следующем примере, так как шаги аналогичны приведенным выше.

Во-первых, скрипт inference.py. Вот отрывок, показывающий реализацию /ping и /invocations. См. inference.py в папке predictor для полной реализации этого файла.

```python@app.route("/ping", methods=["GET"])def ping():    """    Проверка работоспособности сервера модели путем проверки загрузки модели.    Возвращает код состояния 200, если модель успешно загружена, и код состояния 500,    если произошла ошибка.    Возвращает:        flask.Response: Объект ответа, содержащий код состояния и тип содержимого.    """    status = 200 if model is not None else 500    return flask.Response(response="\n", status=status, mimetype="application/json")@app.route("/invocations", methods=["POST"])def invocations():    """    Обработка запросов на предсказание путем предварительной обработки входных данных, выполнения предсказаний    и возвращения предсказаний в виде JSON-объекта.    Эта функция проверяет, поддерживается ли тип содержимого запроса (text/csv; charset=utf-8),    и при наличии декодирует входные данные, выполняет предварительную обработку, делает предсказания и возвращает    предсказания в виде JSON-объекта. Если тип содержимого не поддерживается, возвращается код состояния 415.    Возвращает:        flask.Response: Объект ответа, содержащий предсказания, код состояния и тип содержимого.    """    print(f"Predictor: получен тип содержимого: {flask.request.content_type}")    if flask.request.content_type == "text/csv; charset=utf-8":        input = flask.request.data.decode("utf-8")        transformed_data = preprocess(input, flask.request.content_type)        predictions = predict(transformed_data)        # Возврат предсказаний в виде JSON-объекта        return json.dumps({"result": predictions})    else:        print(f"Получен: {flask.request.content_type}", flush=True)        return flask.Response(            response=f"XGBPredictor: Этот предиктор поддерживает только данные в формате CSV; Получен: {flask.request.content_type}",            status=415,            mimetype="text/plain",        )```

Вот отрывок из Dockerfile для размещения модели предиктора. Для полной реализации см. Dockerfile в папке предиктора.

```dockerFROM ubuntu:18.04…# установка необходимых зависимостей, включая flask, gunicorn, xgboost и т.д.,RUN pip3 --no-cache-dir install  flask  gunicorn  gevent  numpy  pandas  xgboost# Копирование содержимого директории code/ в /opt/programCOPY code /opt/program# Установка рабочей директории на /opt/program, где находятся скрипты serve и inference.pyWORKDIR /opt/program# Использование порта 8080 для обслуживанияEXPOSE 8080ENTRYPOINT ["python"]# serve - это питоновский скрипт в директории code/, который запускает процессы nginx и gunicornCMD ["serve"]```

Затем мы продолжаем создавать, тестировать и загружать этот пользовательский образ предиктора в частный репозиторий Amazon ECR. Для полной реализации см. записную книжку predictor.ipynb для создания, тестирования и загрузки пользовательского образа в Amazon ECR.

Развертывание серийного инферентного конвейера

После того, как мы протестировали оба образа фитеризатора и предиктора и загрузили их в Amazon ECR, мы теперь загружаем наши артефакты модели в бакет Amazon S3.

Затем создаем два объекта модели: один для фитеризатора (т.е. preprocess.joblib), и другой для предиктора (т.е. xgboost-модель), указывая URI пользовательского образа, который мы построили ранее.

Вот фрагмент, который это показывает. Для полной реализации см. записную книжку serial-inference-pipeline.ipynb для полной реализации.

```pythonsuffix = f"{str(uuid4())[:5]}-{datetime.now().strftime('%d%b%Y')}"# Модель фитеризатора (модель SKLearn)image_name = "<FEATURIZER_IMAGE_NAME>"sklearn_image_uri = f"{account_id}.dkr.ecr.{region}.amazonaws.com/{image_name}:latest"featurizer_model_name = f""<FEATURIZER_MODEL_NAME>-{suffix}"print(f"Создание модели фитеризатора: {featurizer_model_name}")sklearn_model = Model(    image_uri=featurizer_ecr_repo_uri,    name=featurizer_model_name,    model_data=featurizer_model_data,    role=role,)# Полное имя репозитория ECRpredictor_image_name = "<PREDICTOR_IMAGE_NAME>"predictor_ecr_repo_uri= f"{account_id}.dkr.ecr.{region}.amazonaws.com/{predictor_image_name}:latest"# Модель предиктора (модель XGBoost)predictor_model_name = f"""<PREDICTOR_MODEL_NAME>-{suffix}"print(f"Создание модели предиктора: {predictor_model_name}")xgboost_model = Model(    image_uri=predictor_ecr_repo_uri,    name=predictor_model_name,    model_data=predictor_model_data,    role=role,)```

Теперь, чтобы развернуть эти контейнеры последовательно, мы создаем объект PipelineModel и передаем модель featurizer и модель predictor в объект списка Python в том же порядке.

Затем вызываем метод .deploy() для PipelineModel, указывая тип экземпляра и количество экземпляров.

```pythonfrom sagemaker.pipeline import PipelineModelpipeline_model_name = f"Abalone-pipeline-{suffix}"pipeline_model = PipelineModel(    name=pipeline_model_name,    role=role,    models=[sklearn_model, xgboost_model],    sagemaker_session=sm_session,)print(f"Развертывание модели конвейера {pipeline_model_name}...")predictor = pipeline_model.deploy(    initial_instance_count=1,    instance_type="ml.m5.xlarge",)```

На этом этапе Amazon SageMaker развертывает последовательный конвейер вывода на реальный конечную точку. Мы ждем, пока конечная точка не станет InService.

Теперь мы можем протестировать конечную точку, отправив некоторые запросы вывода на эту активную конечную точку.

Смотрите полную реализацию в блокноте serial-inference-pipeline.ipynb.

Очистка

После завершения тестирования выполните инструкции в разделе очистка блокнота для удаления запрещенных ресурсов и избежания ненужных расходов. См. Amazon SageMaker Pricing для получения информации о стоимости экземпляров вывода.

```python# Удаление конечной точки, моделиtry:    print(f"Удаление модели: {pipeline_model_name}")    predictor.delete_model()except Exception as e:    print(f"Ошибка при удалении модели: {pipeline_model_name}\n{e}")    passtry:    print(f"Удаление конечной точки: {endpoint_name}")    predictor.delete_endpoint()except Exception as e:    print(f"Ошибка при удалении EP: {endpoint_name}\n{e}")    pass```

Заключение

В этой статье я показал, как мы можем создать и развернуть последовательное приложение для вывода МО, используя настраиваемые контейнеры вывода в режиме реального времени на Amazon SageMaker.

Это решение демонстрирует, как клиенты могут использовать собственные настраиваемые контейнеры для размещения на Amazon SageMaker в экономически эффективном режиме. С BYOC-вариантом клиенты могут быстро создавать и адаптировать свои приложения МО для развертывания на Amazon SageMaker.

Мы призываем вас попробовать это решение с набором данных, соответствующих ключевым показателям эффективности вашего бизнеса. Вы можете ознакомиться со всем решением в репозитории GitHub: GitHub repository.

Ссылки