Проблема эффективного создания продуктов на базе Machine Learning в бизнесе не ограничивается подготовкой данных, разработкой и обучением нейросети или другого алгоритма. На итоговый результат влияют такие факторы, как: процессы верификации датасетов, организованные процессы тестирования, и размещение моделей в виде надежных Big Data приложений.
Бизнес-показатели зависят не только от решений Data Scientist’а, но и от того, как команда разработчиков реализует данную модель, а администраторы и инженеры развернут ее в кластерном окружении. Важно качество входных данных (Data Quality), периодичность их поступления, источники и каналы передачи информации, что является задачей дата-инженера. Организационные и технические препятствия при взаимодействии разнопрофильных специалистов приводят к увеличению сроков создания продукта и снижению его ценности для бизнеса. Для устранения таких барьеров и придумана концепция MLOps, которая, подобно DevOps и DataOps, стремится увеличить автоматизацию и улучшить качество промышленных ML-решений, ориентируясь на нормативные требованиям и выгоду для бизнеса. Применять подходы MLOps необходимо на всех этапах создания ML решений.

В статье мы поговорим об использовании принципов и практик MLOps на стадии разработки моделей, и расскажем как самим развернуть сервис самообслуживания по созданию сред разработки для дата-саентистов.

Инструменты и практики


Совместная работа над кодом проекта


Каждому специалисту в ходе создания аналитического приложения приходится работать с множеством документов: это могут быть модули python, интерактивные блокноты jupyter, конфигурационные файлы, файлы с данными, артефакты выполнения кода, файлы с заметками, комментариями, графиками и метриками.

Существуют как минимум две основные причины навести в здесь порядок.

Если договориться о структуре расположения и названиях файлов, то становится понятно, что-где находится, к чему относится, и помогает эффективнее работать даже над небольшими проектами. Если проект крупный, и в работе принимают участие большое количество специалистов, выяснить, где прячется определенный модуль и входные данные становится проблемой. Решение есть — следование определенным правилам. Ведь отсутствие структуры и соглашений о нейминге может привести (и часто приводит) к полной остановке работы над задачей — “до выяснения обстоятельств”.

Вторая причина организации проектов — возможность автоматизации. Если файлы называются и располагаются по определенным правилам — значит, с таким проектом готова работать машина. Сборка приложения или docker-контейнера на базе исходных данных, запуск тестов, корректная работа скриптов переобучения моделей, отображение результатов экспериментов в единой системе — это лишь некоторые задачи, которые можно выполнять без участия человека, избегая рутинной работы и ошибок.

Чтобы упростить процесс создания структуры проекта применяются специализированные утилиты, например, Coockie Cutter.

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

Проект, построенный на базе Python, обычно включает в себя:
  • файл с описанием (например — “readme.md”)
  • файл, содержащий список используемых библиотек/модулей (например — “requirements.txt”)
  • файл, содержащий информацию о версии интерпретатора (например — “.python-version”)
  • набор модулей, из которых, как правило, один — является основным (основной файл включает в себя либо цепочку вызовов функций из различных модулей, либо содержит код инференса, то есть получения результатов работы модели)
  • файлы, содержащие различного рода тесты
  • ссылки на данные и бинарные артефакты модели

На практике, структура и состав проекта также могут зависеть от тех инструментов совместной работы и автоматизации, которые используются, например, MLFlow, Kubflow, GitLab и т.д.

Git и плюшки в инструментах датасаентиста


Совместная работа над аналитическим приложением в современном мире невозможна без использования системы управления кодом. OpenSource стандартом в этой области является Git.

Но что делать, если вы пришли в мир Data Science без опыта разработки программного обеспечения и никогда не пользовались системой Git?

Ответ первый — изучать технологии и инструменты DevOps. Модели, которые остаются в песочнице аналитиков, никому не нужны. Бизнес хочет не просто услышать красивые слова, но и получить результаты в сжатые сроки. Следовательно, от датасаентистов начинают требовать знаний и навыков работы с инструментами, позволяющими выполнять работу эффективнее и доводить результат до коммерческого применения.

Ответ второй — применять плагины и надстройки для тех инструментов, которые уже хорошо знакомы. Примером здесь может стать расширение Git для JupyterLab. С его помощью можно управлять локальными и удаленными git-репозиториями, не прибегая к помощи командной строки.
image

Разработка. Кратко о воспроизводимости результатов.


Кто-то скажет: “Ну, хорошо, совместно работать с кодом мы научились, договорились о структуре файлов и папок. Можно мы пойдем делать модели?” — Конечно! Но и здесь для эффективного достижения результатов нужен системный подход.

Как правило, разработка модели включает в себя несколько этапов, таких как:
  • Загрузка и предварительная обработка / проверка данных;
  • Формирование и проверка признаков;
  • Обучение модели;
  • Валидация модели.

Эти этапы могут иметь собственные зависимости. Данные на каждом из них могут преобразовываться и использоваться по-разному.

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

Обеспечить связь между задачами, гарантировать воспроизводимость результатов поможет Data Pipelines и соответствующие инструменты, которые позволяют ими управлять.

Согласно одному из возможных определений: Data Pipelines — это множество элементов обработки данных, объединенных в одну последовательность. При этом, выход одного элемента является входом для последующего.

Существует множество инструментов для создания и управления Data Pipelines:
  • DVC Pipelines
  • Kubeflow Pipelines
  • Airflow

Еще одним элементом воспроизводимости является возможность сохранения, передачи и воссоздания окружения / среды (со всеми зависимостями), в рамках которого проводилась разработка. Здесь помогут контейнеры, которые обеспечат решение многих задач.

Использование контейнеризированной среды. Зачем это нужно?


Техническим специалистам контейнеры полюбились за возможность упаковать приложение вместе с его средой запуска, решая проблему зависимостей в разных окружениях. Например, различие версий языковых библиотек на ноутбуке разработчика, и в последующих окружениях, приведёт к сбоям, и нужно будет, как минимум, потратить время на их анализ, а как максимум — решать проблему проникших в продакшен багов. Использование контейнеров устраняет проблему «А на моей машине все работало!”

Также, контейнеры позволяют сократить время разработки приложения и упрощают управление им в продакшене благодаря легкости настройки и изменения конфигурации, возможности версионировать её вместе с кодом приложения. Плюсом является наличие удобных инструментов оркестрирования, позволяющих быстро масштабировать инфраструктуру.

Кроме того, фактическое отсутствие привязки контейнеров к хостинговой платформе даёт огромную гибкость при выборе или смене провайдера — вы можете запускать их без принципиальных отличий на личном компьютере, bare metal серверах и в облачных сервисах.

Что делать, если необходимо запустить выполнение кода для проведения процессной аналитики или другой задачи, но не хватает памяти и мощностей компьютера, или компьютер с необходимым софтом вообще не доступен?

Разворачиваем свой сервис: Self-Service IDE


Ответить на обозначенные выше вопросы применительно к созданию сред и запуску инструментов разработки позволяют решения на базе контейнеризации. Сейчас на рынке уже представлено несколько подобного рода систем: это и Kubeflow — платформа с открытым исходным кодом, развитие которой поддерживает Google, и разного рода платформы от компаний Cloudera (Data Science Workbench), IBM и так далее. Сегодня мы рассмотрим относительно простой вариант решения на базе контейнеров и JupyterHub.

JupyterHub


Платформа Jupyter отлично справляется со снижением порога входа в Питон для начинающих программистов, data scientist’ов. И вот команда растёт, в ней теперь не только программисты, но и менеджеры, аналитики, исследователи. Внезапно отсутствие совместного рабочего окружения и сложность настройки начинают тормозить работу. JupyterHub решает именно эту задачу: это многопользовательский сервер / хаб, предоставляющий возможность запускать Jupyter «одной кнопкой». Сервер отлично подходит для аналитиков и data scientist’ов, потому что пользователю нужен лишь браузер: никаких проблем с установкой ПО на ноутбук, совместимостью, пакетами.

Но, достаточно “лирики” и архитектурных отступлений. Давайте разберемся более подробно, что нужно сделать, чтобы развернуть собственный сервис самообслуживания для команды Data Science.

Разворачиваем решение на Swarm


Прежде всего, необходимо создать виртуальную машину, на образе CentOS 7. (Сама установка будет работать на bare-metal сервере). Также установим последнюю версию Docker ce 20.10.3., и настроим сеть так, чтобы порты 80 и 443 были доступны для HTTP и HTTPS. Свяжем Public IP-адрес с этим инстансом, для доступа в Интернет.

Добавьте своего пользователя в группу docker, чтобы вам не требовалось sudo для выполнения docker-команд. Убедитесь, что работает команда docker info.

Сервер, на котором будет выполнена команда по инициации docker swarm кластера, будет являться Manager node:
Скрытый текст
docker swarm init --advertise-addr INTERNAL_IP_ADDRESS


Лучше указывать внутренний IP-адрес, например, 192.xxx.xxx.xxx. Это адрес, который узлы Swarm будут использовать для подключения к кластеру.

Как это ни удивительно, у Swarm нет встроенного механизма — permanent storage, который автоматически перемещает данные с одного узла на другой в случае повторного создания пользовательского контейнера на другом сервере. Зато есть несколько volume plugins, но их настройка и конфигурация заставит вас попотеть, что заставит задуматься: “Не лучше переключиться на Kubernetes?”. Чтобы добиться более простой настройки, которая позволяет обрабатывать несколько десятков пользователей, можно использовать NFS сервер. Мы просто создаем volume на NFS директорию. Пользователи не будут иметь доступ к файлам своих коллег, потому что в их контейнер монтируется только их собственная папка, созданная под пользователя в NFS.
Скрытый текст
/var/nfs        *(rw,sync,no_subtree_check)


Для того, чтобы подключить все узлы Swarm к NFS, необходимо на каждом из узлов выполнить:
Скрытый текст
sudo mount 192.NFS.SRV.IP:/var/nfs /mnt


Для проверки, что nfs сервер настроен, создадим файл it_works на одном из узлов Swarm
Скрытый текст
touch /mnt/it_works


Проверим, что на остальных узлах Swarm этот файл также стал доступен:
Скрытый текст
ls /mnt


Для настройки Jupyterhub, чтобы он поднимал контейнеризированные пользовательские сервисы, будем использовать SwarmSpawner, создадим файл jupyterhub_config.py и добавим в него следующие строчки:
Скрытый текст
from dockerspawner import SwarmSpawner

c.JupyterHub.spawner_class = SwarmSpawner
c.JupyterHub.hub_ip = ‘0.0.0.0’
c.JupyterHub.hub_port = 8081
c.JupyterHub.port = 8000
c.SwarmSpawner.network_name = "demo_hub_network"
c.SwarmSpawner.extra_host_config = { 'network_mode': "demo_hub_network" }
c.SwarmSpawner.host_ip = "0.0.0.0"
c.SwarmSpawner.http_timeout = 600
c.SwarmSpawner.start_timeout = 600
c.SwarmSpawner.notebook_dir = notebook_dir
c.Spawner.default_url = '/lab'


В качестве оперативной базы данных JupyterHub будем использовать PostgreSQL, для примера добавим параметры подключения в конфиг JupyterHub (их также можно получать из переменных среды):
Скрытый текст

## Postgresql params
host = 'db'
port = '5432'
user = 'hub'
password = 'hub'
db = 'hub'

c.JupyterHub.db_url = f'postgresql://{user}:{password}@{host}:{port}/{db}'


Для использования Docker Volumes через NFS необходимо реализовать функцию, которая будет создавать пользовательскую директорию в случае, если ее нет и монтировать ее к пользовательскому контейнеру. А также добавим общую директорию share для всех пользователей JupyterHub:
Скрытый текст
## Create user dir if doesnt exist and mount it with configs in user'ss notebook
def user_dir_create(spawner):
    username = spawner.user.name 
    user_dir_path = os.path.join('/mnt', username)
    if not os.path.exists(user_dir_path):
            os.mkdir(user_dir_path, 0o755)
            os.chown(user_dir_path,1000,100)
    mounts = [
                    {'type': 'bind',
                    'source': f'/mnt/{username}',
                    'target': '/home/jovyan/work', },
                    {'type': 'bind',
                    'source': '/mnt/all_users',
                    'target': '/home/jovyan/work/all_users', },
                  ]

    spawner.extra_container_spec = {
        'mounts': mounts
    }
    
c.Spawner.pre_spawn_hook = create_homedir_hook


Для настройки доступности JupyterHub через Ldap, необходимо добавить в конфиг:
Скрытый текст
c.JupyterHub.authenticator_class = 'ldapauthenticator.LDAPAuthenticator'
c.LDAPAuthenticator.server_address = '<hostLdapServer>'
c.LDAPAuthenticator.bind_dn_template =  [
    'uid={username},ou=Peoples,dc=example,dc=com',
 ]
c.LDAPAuthenticator.use_ssl = False


Чтобы у пользователя была возможность выбора различных версий ноутбуков с предустановленными библиотеками, необходимо переопределить два метода в классе SwarmSpawner:
Скрытый текст

class Spawner(SwarmSpawner):
    def _options_form_default(self):
        return """
        <label for="stack">Choose containter</label>
        <select name="stack" size="1">
        <option value="jupyter/base-notebook">jupyter/base-notebook</option>
        <option value="jupyter/scipy-notebook">jupyter/scipy-notebook</option>
        <option value="jupyter/datascience-notebook">jupyter/datascience-notebook</option>
        </select>
        """

    def options_from_form(self, formdata):
        options = {}
        options['stack'] = formdata['stack']
        image = ''.join(formdata['stack'])
        self.container_image = image
        return options

c.JupyterHub.spawner_class = Spawner


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

c.JupyterHub.services = [
    {
        'name': 'idle-culler',
        'admin': True,
        'command': [sys.executable, '-m', 'jupyterhub_idle_culler', '--timeout=3000'],
    }
]


Теперь необходимо собрать образ непосредственно JupyterHub:
Скрытый текст
ARG JUPYTERHUB_VERSION=0.9.2
FROM jupyterhub/jupyterhub:$JUPYTERHUB_VERSION
RUN apt update -y && \
pip install --no-cache \
dockerspawner \
jupyterhub-ldapauthenticator \
jupyterhub-idle-culler
RUN python -m pip install --upgrade pip setuptools wheel


Осталось только задеплоить наш JupyterHub на Swarm кластере, для этого необходимо создать docker-compose.yml:
Скрытый текст
version: '3.7'

services:
  db:
    image: postgres
    networks:
      - hub_network
    environment:
      POSTGRES_USER: 'hub'
      POSTGRES_PASSWORD: 'hub'
      POSTGRES_DB: 'hub'
    volumes:
      - /mnt/data/postgres:/var/lib/postgresql/data

  hub:
    image: demo/jupyterhub:latest
    deploy:
      replicas: 1
      placement:
        constraints: [node.role == manager]
    configs:
      - source: jupyter_conf
        target: /srv/jupyterhub/jupyterhub_config.py
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
      - "/mnt:/mnt"
    networks:
      - hub_network
    ports:
      - "8000:8000"
    depends_on:
      - db

networks:
  hub_network:
    driver: overlay

configs:
  jupyter_conf:
    file: /mnt/jupyterhub_config.py


И запустить на менеджер узле Swarm следующую команду:
Скрытый текст
docker stack deploy -c docker-compose.yml demo


После успешного запуска всех сервисов, jupyerhub будет доступен по адресу менеджер ноды Swarm кластера на 8000 порту. В качестве прокси сервера можно добавить сервис nginx с ssl сертификатами для обеспечения большей безопасности и балансировки нагрузки, для этого необходимо сгенерировать самоподписанные сертификаты или подписать RSA запрос. Создать конфиг nginx.conf для nginx:
Скрытый текст
server {
    listen 80;
    server_name jupyterhub;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl default_server;
    server_name jupyterhub;
    client_max_body_size 50M;

    ssl_certificate /etc/nginx/certs/cert.pem;
    ssl_certificate_key /etc/nginx/certs/key.pem;


    location / {
        proxy_pass http://hub:8000;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_set_header X-NginX-Proxy true;
    }

    location ~* /(user/[^/]*)/(api/kernels/[^/]+/channels|terminals/websocket)/? {
        proxy_pass http://hub:8000;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_set_header X-NginX-Proxy true;

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 86400;

    }
}


Осталось добавить сервис nginx в docker-compose файл:
Скрытый текст
 nginx:
    image: nginx:latest
    deploy:
      replicas: 1
      placement:
        constraints: [node.role == manager ]
    configs:
      - source: nginx
        target: /etc/nginx/conf.d/default.conf
      - source: crt1
        target: /etc/nginx/certs/key.pem
      - source: crt2
        target: /etc/nginx/certs/cert.pem
    ports:
      - 80:80
      - 443:443
    networks:
      - hub_network

configs:
  nginx:
    file: /mnt/nginx.conf
  crt1:
    file: /mnt/key.key
  crt2:
    file: /mnt/cert.pem


Пользовательские образы ноутбуков можно расширять требуемым функционалом, добавлять коннекторы к различным базам данных, используя odbc/jdbc драйвера, добавлять snippet'ы и различные плагины для JupyterLab, и так далее, все зависит от потребностей пользователя. К примеру, если такой кластер находится во внутренней сети без доступа к интернету, то можно создать pypi репозиторий в nexus’е, и добавить в каждый пользовательский образ JupyterLab ноутбуков pip.conf, который ставит python библиотеки из данного репозитория.

Заключение


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

Про ModelOps/MLOps и другие вопросы вокруг применения ML и продвинутой аналитики в реальных бизнес-задачах мы регулярно общаемся в нашем сообществе NoML:

Подключайтесь!