За 2 с лишним года, что я проработал в Upside, я постоянно боролся с производственной реализацией моделей машинного обучения. Как и большинство специалистов по данным, я предпочитаю выполнять проектирование функций и разрабатывать свои модели в блокноте Jupyter, сильно полагаясь на pandas и scikit-learn. В зависимости от приложения создаваемые мною функции могут поступать из нескольких внешних API и иметь несколько этапов предварительной обработки. Я редко думаю: Как я собираюсь воссоздать эту функцию в производственной среде и проверить, не является ли созданное мной значение абсолютно бессмысленным?

При создании API машинного обучения для обслуживания прогнозов я обычно определял некоторый вариант OrderedDict ¹, чтобы отслеживать имена и значения функций во время разработки функций. Я называю этот объект контейнером функций и использую его для передачи значений в виде массива NumPy методу .predict предварительно обученной модели машинного обучения. Это далеко не идеально по многим причинам и несколько раз сводило с ума инженеров Python, с которыми я работаю. Обычно я просто машу руками и говорю: Послушайте, у меня есть« модульный тест для вывода моего конвейера проектирования функций / прогнозирования моделей, и этого достаточно, чтобы сказать, что все работает, как ожидалось».

Мое утверждение выше не совсем неверно, но мы, специалисты по обработке данных, можем добиться большего!

Недавно я начал по-настоящему понимать концепцию типизированных переменных в Python - даже когда я разрабатываю модели в Jupyter Notebook! NamedTuples и dataclasses (доступны в Python 3.7+) - две мои любимые вещи, которые я использую при определении входов и выходов различных функций, которые я пишу. За последние несколько недель я полностью переписал серверную службу, которую построил около года назад, чтобы предсказывать задержки рейсов с использованием типизированных переменных. Но когда я добрался до той части кода, которая создавала и сохраняла функции, что-то меня все еще беспокоило. Я преобразовал свой контейнер функций из OrderedDict в dataclass², но процесс ввода каждого поля и типа данных казался слишком утомительным и многословным, особенно если мне когда-нибудь понадобится вернуться и что-то изменить в моей модели. Ему также не хватало возможности печатать и проверять значение, присвоенное полю, из коробки.

К счастью, мы можем использовать Pydantic ³ и некоторые простые вспомогательные функции, чтобы облегчить беспокойство, о котором я упоминал выше. Команда Upside Labs уже несколько месяцев использует Pydantic для проверки типов и валидации моделей данных в нескольких серверных сервисах, и это была еще одна чрезвычайно полезная библиотека. Давайте рассмотрим простой пример того, как мы можем использовать его для создания более надежного контейнера функций.

Пример

Допустим, я обучил модель, используя dataframe ниже (вероятно, это не очень хорошая модель). Функции cat_1 и cat_2 являются категориальными функциями горячего кодирования, а dog и bird - непрерывными функциями.

Приведенный ниже фрагмент кода показывает, как мы можем представить это dataframe как контейнер функций с помощью Pydantic.

Я не буду рассматривать каждую строку кода выше, но вот некоторые из основных моментов, которые я хотел бы выделить:

  1. Функции оболочки Pydantic conint и confloat используются для создания ограниченных целочисленных и ограниченных полей с плавающей запятой, соответственно, с использованием включающих границ, указанных параметрами ge (больше или равно) и le (меньше или равно). Если мы попытаемся присвоить значение полю, которое не соответствует определенному типу данных или выходит за указанные границы, возникает ошибка Pydantic ValidationError. Значения, которые я установил по умолчанию, получены при проверке исходного dataframe.
  2. Все поля определены как T.Optional со значением по умолчанию None, так что объект-контейнер функций может быть создан, а затем передан из одной функции в другую, а значения функции могут быть присвоены последовательно⁴.
  3. Вспомогательный метод set_categorical_features может использоваться для быстрого кодирования набора категориальных функций одновременно! Все, что вам нужно сделать, это указать функции prefix и positive_category (см. Фрагмент кода ниже, чтобы увидеть пример этого в действии). У меня часто есть категориальные функции с десятками или сотнями категорий, так что этот метод был чрезвычайно удобен.
  4. Вспомогательный метод set_bulk_features также можно использовать для одновременной установки нескольких значений функций. Просто передайте ему mapping словарь имен и значений полей, и метод позаботится обо всем остальном (включая проверку типов и валидацию).
  5. Свойство numpy_array можно использовать для создания массива NumPy с правильной формой и порядком значений полей, указанными в исходном обучении dataframe. Он имеет проверку, чтобы убедиться, что все значения были назначены, и вызовет пользовательскую FeatureIsNoneError ошибку, если какое-либо из полей будет None. Это свойство будет использоваться для передачи данных функции методу .predict (или .predict_proba) предварительно обученной модели машинного обучения.

Затем мы можем написать простой контейнер функций и конвейер прогнозирования модели, используя контейнер AnimalFeatures, который мы определили выше.

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

Наконец, мы можем преобразовать исходный dataframe в контейнер функций, используя простой скрипт для автоматической генерации имен полей, типов данных и границ, проверяя значения в каждом столбце данных. Я постоянно настраиваю свои модели и не хочу беспокоиться о том, как добавление или перемещение столбцов в моем обучении dataframe повлияет на конвейер производственных функций. Выходные данные функции ниже можно использовать для создания кода, который станет объектом контейнера функций - класс AnimalFeatures, определенный в приведенном выше фрагменте, был фактически сгенерирован с использованием этого сценария.

Закрытие

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

Ох ... и мы набираем сотрудников - загляните на нашу страницу вакансий!

Сноски:

[1]: Использование упорядоченного словаря вместо стандартного имеет то преимущество, что значения словаря возвращаются в определенном порядке. Это очень важно для приложений машинного обучения, потому что модель ожидает функции в том порядке, в котором они использовались во время обучения.

[2]: Изначально я использовал dataclass вместо NamedTuple для контейнера функций, потому что dataclasses изменяемы. Это позволило мне передать объект-контейнер через ряд функций, которые присваивают значения различным полям, которые я определил.

[3]: Огромный привет Rami Choudhury за то, что познакомил меня с Pydantic и за помощь в разработке кода в этом сообщении в блоге!

[4]: Это не является предписанием, это просто то, как я обычно пишу конвейеры производственных функций. Не стесняйтесь адаптировать любую из этой логики под свои нужды.