Русский
Русский
English
Статистика
Реклама

Machinelearning

ИИ итоги уходящего 2020-го года в мире машинного обучения

01.01.2021 00:16:35 | Автор: admin

Подведем основные итоги уходящего года. Рассмотрим самые громкие открытия в мире компьютерного зрения, обработки естественного языка, генерации изображений и видео, а также крупный прорыв в области биологии. Коротко о самом главном за год!

Если тебе интересно машинное обучение, то приглашаю вМишин Лернинг мой субъективный телеграм-канал об искусстве глубокого обучения, нейронных сетях и новостях из мира искусственного интеллекта.

GPT-3

Ресерчеры из OpenAI представили GPT-3 сеть Generative Pre-trained Transformer 3, одну из лучших языковых моделей на сегодняшний день. Архитектура GPT-3 подобна GPT-2, параметров стало 175 миллиардов, и модель учили на 570 гигабайтах текста.

Более крупные модели позволяют более эффективно использовать контекстную информацию.Более крупные модели позволяют более эффективно использовать контекстную информацию.

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


NVIDIA MAXINE

Компания NVIDIA представила нейросетевые алгоритмы под общим названием Maxine, целями которых являются: улучшение качества видеосвязи, многократное снижение нагрузки на сеть при видеозвонках (нейросетевое сжатие видео), возможность реалистичной коррекции взгляда для создания ощущения, что собеседник смотрит вам в глаза, а не в свой монитор.

MuZero

Еще один зверь из мира RL от ресерчеров из DeepMind. MuZero подход, в котором при обучении на начальном этапе не требуется знаний правил игры.

Иллюстрация поиска по дереву Монте-Карло, используя MuZeroИллюстрация поиска по дереву Монте-Карло, используя MuZero

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

Алгоритм уже умеет играть в привычные нам для этой сферы игры: го, шахматы и кучу игр для приставки Atari.

AlphaFold

И опять DeepMind с их нейросетевым алгоритмом впредсказания трехмерной структуры белка по последовательности аминокислот AlphaFold 2. Точность такого алгоритма составляет 92,4 балла из 100, что является рекордом на сегодняшний день!

При считывании информации с матричной РНК (трансляция) молекула белка формируется как цепочка аминокислот. Потом, в зависимости от физических и химических свойств, цепочка начинает сворачиваться. Таким образом формируется третичная структура белка. Именно от этой структуры и зависят свойства конкретного белка.

Задача определения первичной аминокислотной структуры белка (самой последовательности) является элементарной. Это легко читается напрямую из ДНК. Кодон тройка нуклеотидных остатков (триплет) образуют 64 варианта, а именно 4 (аденин, гуанин, цитозин, тимин) в третей степени. Из них 61 комбинация кодирует определённые аминокислоты в будущем белке, а 3 оставшихся кодона сигнализируют об остановке трансляции и называются стоп-кодонами. При этом 61 комбинация кодирует всего 20 различный аминокислот белка. Знакомые с теорий информации и избыточностью уже улыбаются, ну, а биологи называют кодоны, кодирующие одинаковые аминокислоты, изоакцепторными кодонами.

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

Процесс предсказания третичной структуры белкаПроцесс предсказания третичной структуры белка

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

Чтобы сделать публикацию еще более полезной, я добавил образовательные материалы вышедшие в 2020м году:

Новая специализация TensorFlow: Advanced Techniques от deeplearning ai, основанной самим Andrew Ng

YouTube-Лекция: Нейронные сети: как их создают и где применяют? Два часа о самом главном

Поздравляю всех с Новым годом! Больше ресерча в наступающем году!

Подробнее..

DALL E от OpenAi Генерация изображений из текста. Один из важнейших прорывов ИИ в начале 2021 года

06.01.2021 06:14:44 | Автор: admin

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

Итак, исследователи в области искусственного интеллекта из openai создали нейронную сеть под названием DALL E, которая генерирует изображения из текстового описания на естественном языке.

Если тебе интересно машинное обучение, то приглашаю вМишин Лернинг мой субъективный телеграм-канал об искусстве глубокого обучения, нейронных сетях и новостях из мира искусственного интеллекта.

DALL E представляет собой версиюGPT-3с 12 миллиардами параметров,обученную генерировать изображения из текстовых описаний на датасете из пар текст-изображение.Исследователи обнаружили, что DALL E обладает огромным репертуаром генеративных возможностей, включая возможность создания антропоморфных животных и других необычных объектов, комбинирующих совершенно нетривиальные свойства, например "кресло в форме авокадо."

Изображения, сгенерированные DALL E на основании текстового описания "кресло в форме авокадо"Изображения, сгенерированные DALL E на основании текстового описания "кресло в форме авокадо"

Можно сказать, что уже были все предпосылки к созданию DALL E: прошлогодний триумф GPT-3 и успешное создание Image GPT сети, способной к генерации изображений на основе текста, использующей языковую модель трансформер GPT-2. Все уже подходило к тому, чтобы создать новую модель, взяв в этот раз за основу GPT-3. И теперь DALL E показывает невиданные доселе чудеса манипулирования визуальными концепциями с помощью естественного языка!

Как и GPT-3, DALL E это языковая модель-трансформер, принимающая на вход текст и изображение, как последовательность размером до 1280 токенов. Модель обучена максимизировать правдоподобие при генерации токенов, следующих один за другим.

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

Давайте посмотрим на примеры, которые говорят сами за себя. Исследователи утверждают, что не использовали ручной "cherry picking". Примерами являются изображения, полученные при помощи DALL E, в которых используются 32 лучших примера из 512-ти сгенерированных, отобранных созданным ранее (теми же openai) нейронным ранжированиемCLIP.

Text: a collection of glasses sitting on the table

Изображения, сгенерированные DALL EИзображения, сгенерированные DALL E

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

Text: an emoji of a baby penguin wearing a blue hat, red gloves, green shirt, and yellow pants

Эмодзи пингвиненка, одетого в голубую шапку, красные перчатки, зеленую футболку и желтые штаны Эмодзи пингвиненка, одетого в голубую шапку, красные перчатки, зеленую футболку и желтые штаны

DALL E может не только генерировать изображение с нуля, но и регенерировать (достраивать) любую прямоугольную область существующего изображения, вплоть до нижнего правого угла изображения, в соответствии с текстовым описанием. В качестве примера за основу взяли верхнюю часть фотографии бюста Гомера. Модель принимает на вход это изображение и текст: a photograph of a bust of homer

Text: a photograph of a bust of homer

Фотография бюста ГомераФотография бюста Гомера

Особенно поражает то, что DALL E выучил исторический и географический контекст. Модель способна к обобщению тенденций в дизайне и технологиях. Вот пример того, как DALL E генерирует телефонные аппараты разных десятилетий двадцатого века.

Text: a photo of phone from the ...

Фотографии телефонов разных десятилетий XX векаФотографии телефонов разных десятилетий XX века

Название модели DALL E является словослиянием имени художника Сальвадора Дали и робота WALL E от Pixar. Вышел такой своеобразный Вали-Дали. Вообще в мире ИИ "придумывание" таких оригинальных названий это некий тренд. Что определенно радует, и делает эту область еще более оригинальной.

Старый добрый перенос стиля WALL E в DalСтарый добрый перенос стиля WALL E в Dal

Для пущего сюрреализма и оправдания своего названия DALL E "попросили" сгенерировать животных, синтезированных из множества понятий, включая музыкальные инструменты, продукты питания и предметы домашнего обихода. Хотя это не всегда удавалось, исследователи обнаруживали, что DALL E иногда принимает во внимание формы двух объектов при решении о том, как их объединить. Например, когда предлагается нарисовать улитку-арфу.

Text: a snail made of harp

Улитка-Арфа. Фантастические твари и где они обитают..Улитка-Арфа. Фантастические твари и где они обитают..

Вывод

DALL E это декодер-трансформер, который принимает и текст, и изображение в виде единой последовательности токенов (1280 токенов = 256 для текста + 1024 для изображения) и далее генерирует изображения авторегрессивном режиме.

Что можно сказать? Наступает эра "великого объединения" языковых моделей, компьютерного зрения и генеративных сетей. То что мы видим сейчас, уже поражает воображение своими результатами, не говоря уже о том, насколько подобные подходы могут изменить процесс генерации контента.

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

Что ты думаешь о DALL E и подобных генеративных нейронных моделях, способных создавать визуальный контент по текстовому описанию? Где может быть полезна такая технология? Насколько тебя впечатлили результаты? Давай обсудим в комментариях.

Подробнее..

Перевод 12 примеров улучшения кода с помощью dataclass

10.02.2021 18:20:17 | Автор: admin

В рамках курса Python Developer. Basic подготовили для вас перевод полезного материала.

Также приглашаем всех желающих на
открытый вебинар по теме Три кита: map(), filter() и zip(). Можно ли писать код, требующий циклов, но без циклов? Можно. Может ли он быть быстрее, чем, если бы мы использовали циклы в Python? Может. Для реализации задуманного понадобится знание слов "callback", "iterator" и "lambda". Будет сложно, но интересно. Присоединяйтесь.


Мы добавляем алгоритмы кластеризации с помощью пакетов scikit-learn, Keras и других в пакет Photonai. На 12 примерах мы покажем, как @dataclass улучшает код на Python. Для этого мы используем код из пакета Photonai для Machine Learning.

Обновитесь до Python 3.7 или более поздней версии

Декоратор @dataclass был добавлен в Python 3.7. Можно использовать Python 3.7 из Docker-образа, добавив в файл следующие команды /.bashrc_profile или /bashrc.txt.

devdir='<path-to-projects>/photon/photonai/dockerSeasons/dev/'testdir='<path-to-projects>/photon/photonai/dockerSeasons/test/'echo $devdirecho $testdirexport testdirexport devdir#alias updev="(cd $devdir; docker-compose up) &"alias downdev="(cd $devdir; docker-compose down) &"alias builddev="(cd $devdir; docker-compose build) &"#alias uptest="(cd $testdir; docker-compose up) & "alias downtest="(cd $testdir; docker-compose down) &"alias buildtest="cd $testdir; docker-compose build) &"

Если вы не можете найти файл /bashrc.txt создайте его самостоятельно с помощью touch/bashrc.txt. (в случае MacOS или одной из разновидностей операционных систем Linux или Unix.)

Примечание: Не забудьте указать в качестве исходника /.bashrc_profile или /bashrc.txt, когда закончите их редактировать.

Здесь вы найдете более подробную информацию о реализации Docker, которой я пользуюсь.

Примечание: вы можете добавить код Docker в свой проект из клонируемого репозитория на GitHub.

Добавьте подсказки типов

Python язык с динамической типизацией. В версиях Python от 3.5 есть подсказки типов (PEP 484). Я подчеркиваю, что именно подсказки, поскольку они не влияют на работу интерпретатора Python. Насколько вам известно, интерпретатор Python вообще их игнорирует.

Подсказки типов (обратите внимание, не строгая проверка типов) позволяют искать ошибки, находить дыры в безопасности и статически проверять типы после первого выполнения и при модульном тестировании.

В Python 3.7 подсказки типов нужны для полей в определении класса при использовании декоратора @dataclass.

Я добавляю подсказки типов во все приведенные примеры @dataclass. Если вы хотите узнать о них больше, рекомендую почитать:

  1. https://medium.com/swlh/future-proof-your-python-code-20ef2b75e9f5

  2. https://realpython.com/python-type-checking/

  3. https://docs.python.org/3/library/typing.html

Декоратор @dataclass уменьшает шаблонность

@dataclass был добавлен в Python 3.7. Основной движущей силой было желание избавиться от шаблонности, связанной с состоянием в определении класса def.

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

Примечание: Если вы не используете pandas, можно ускорить выполнение этих функции, с помощью быстрой вставки @jit из пакета numba.

@dataclass декорирует определение класса def и автоматически генерирует 5 методов init(), repr(), str, eq(), и hash().

Примечание: он генерирует и другие методы, но об этом позже.

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

Пример коротенького класса в photon/photonai/base/hyperpipe.py, декорированный с помощью @dataclass.

### Example #1class Data:    def __init__(self, X=None, y=None, kwargs=None):        self.X = X        self.y = y        self.kwargs = kwargs

Пример 1, после декорации =>

from dataclasses import dataclassfrom typing import Dictimport numpy as np@dataclassclass Data:    X: np.ndarray = None  # The field declaration: X    y: np.array = None    # The field declaration: y    kwargs: Dict = None   # The field declaration: kwargs

Примечание: Если тип не является частью объявления, то поле игнорируется. Используйте тип any для подстановки типа, если он меняется или во время выполнения неизвестен.

Сгенерировался ли код eq()?

### Example #2data1 = Data()data2 = Data()data1 == data1

Пример 2, вывод =>

True

Да! А что насчет методов repr() и str?

### Example #3print(data1)data1

Пример , вывод =>

Data(X=None, y=None, kwargs=None)Data(X=None, y=None, kwargs=None)

Да! А методы hash() и init?

Example #4@dataclass(unsafe_hash=True)class Data:    X: np.ndarray = None    y: np.array = None    kwargs: Dict = None        data3 = Data(1,2,3){data3:1}

Пример 4, вывод =>

{Data(X=1, y=2, kwargs=3): 1}

Да!

Примечание: У сгенерированного метода init все еще сигнатура (X, y, kwargs). Кроме того, обратите внимание, что подсказки типов были проигнорированы интерпретатором Python 3.7.

Примечание: У init(), repr(), strи eq() значение ключевого слова по умолчанию True, тогда как у hash() по умолчанию False.

Вы можете использовать inspect также, как и для любого другого экземпляра.

### Example #5from inspect import signatureprint(signature(data3.__init__))

Пример 5, вывод =>

(X: numpy.ndarray = None, y: <built-in function array> = None, kwargs: Dict = None) -> None

Круто!

Более длинный пример из photon/photonai/base/hyperpipe.py

### Example #6class CrossValidation:    def __init__(self, inner_cv, outer_cv,                 eval_final_performance, test_size,                 calculate_metrics_per_fold,                 calculate_metrics_across_folds):        self.inner_cv = inner_cv        self.outer_cv = outer_cv        self.eval_final_performance = eval_final_performance        self.test_size = test_size        self.calculate_metrics_per_fold = calculate_metrics_per_fold        self.calculate_metrics_across_folds =            calculate_metrics_across_folds        self.outer_folds = None        self.inner_folds = dict()Example #6 Output=>

Пример 6, после декорации =>

from dataclasses import dataclass@dataclassclass CrossValidation:    inner_cv: int    outer_cv: int    eval_final_performance: bool = True    test_size: float = 0.2    calculate_metrics_per_fold: bool = True    calculate_metrics_across_folds: bool = FalseNote:(Example #6) As any signature, keyword arguments fields with default values must be declared last.Note:(Example #6)  class CrossValidation: Readability has increased substantially by using @dataclass and type hinting.
### Example #7cv1 = CrossValidation()

Пример 7, вывод =>

TypeError: __init__() missing 2 required positional arguments: 'inner_cv' and 'outer_cv'Note:(Example #7) inner_cv and outer_cv are positional arguments. With any signature, you declare a non-default field after a default one. (Hint: If this were allowed, inheritance from a parent class breaks.)((Why? Goggle interview question #666.))
### Example #8cv1 = CrossValidation(1,2)cv2 = CrossValidation(1,2)cv3 = CrossValidation(3,2,test_size=0.5)print(cv1)cv3

Пример 8, вывод =>

CrossValidation(inner_cv=1, outer_cv=2, eval_final_performance=True, test_size=0.2, calculate_metrics_per_fold=True, calculate_metrics_across_folds=False)CrossValidation(inner_cv=3, outer_cv=2, eval_final_performance=True, test_size=0.5, calculate_metrics_per_fold=True, calculate_metrics_across_folds=False)
### Example #9cv1 == cv2

Пример 9, вывод =>

True
### Example #10cv1 == cv3

Пример 10, вывод =>

False
### Example #11from inspect import signatureprint(signature(cv3.__init__))cv3

Пример 11, вывод =>

(inner_cv: int, outer_cv: int, eval_final_performance: bool = True, test_size: float = 0.2, calculate_metrics_per_fold: bool = True, calculate_metrics_across_folds: bool = False) -> NoneCrossValidation(inner_cv=3, outer_cv=2, eval_final_performance=True, test_size=0.5, calculate_metrics_per_fold=True, calculate_metrics_across_folds=False)Note: (Example #11) The inspect function shows the signature of the class object while the__str__ default shows the instance state variables and their values.

Очень круто!

Упс, а что насчет:

self.outer_folds = Noneself.inner_folds = dict()

У нас есть переменные состояния, но они не создаются при вызове. Не волнуйтесь, @dataclass справится и с этим. Покажу в следующем разделе.

Обработка после инициализации

Существует такой метод, как post-init, который является частью определения @dataclass. Метод post_init выполняется после init, сгенерированного @dataclass. Он включает обработку после установки состояния сигнатуры.

Мы завершаем преобразование установив оставшееся состояние CrossValidation:

### Example 12from dataclasses import dataclass@dataclassclass CrossValidation:    inner_cv: int    outer_cv: int    eval_final_performance: bool = True    test_size: float = 0.2    calculate_metrics_per_fold: bool = True    calculate_metrics_across_folds: bool = False    def __post_init__(self):        self.outer_folds = None        self.inner_folds = dict()

Источники

Здесь вы найдете отличные варианты использования декоратора @dataclass:

  1. https://realpython.com/python-data-classes/

  2. https://blog.usejournal.com/new-buzzword-in-python-is-here-dataclasses-843dd1d372a5

Заключение

На 12 примерах до и после я показал, как @dataclass преобразует классы в пакете Photonai Machine Learning. Мы видели, как @dataclass повысил производительность и читаемость кода.

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

Добавление @dataclass и подсказок типов демонстрирует, что Python продолжает расти и развиваться.

Примечание: Вы можете добавить обновленный код Photonai в свой проект из клонируемого репозитория на GitHub.

Я показал далеко не все возможности @dataclass. Поскольку мы только добавляем кластеризацию, я продолжу документировать изменения в photonai.


Узнать подробнее о курсе Python Developer. Basic.

Смотреть открытый вебинар по теме Три кита: map(), filter() и zip().

Подробнее..

Датасет о мобильных приложениях

25.05.2021 12:05:50 | Автор: admin

Вступление

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

Данные

Датасет опубликован на сайте Kaggle.

DOI: 10.34740/KAGGLE/DSV/2107675.

Для 293392 приложений (наиболее популярных) собраны токены описаний и сами данные приложений, включая оригинальное описание. В наборе данных нет имен приложений; их идентифицируют уникальные идентификаторы. Перед токенизацией большинство описаний были переведены на английский язык.

В датасете 4 файла:

  • bundles_desc.csvсодержит только описания;

  • bundles_desc_tokens.csvсодержит токены и жанры;

  • bundles_prop.csv, bundles_summary.csvсодержат рпзличные характеристики приложений и даты релиза/обновления.

EDA

Прежде всего, давайте посмотрим, как данные распределяются по операционным системам.

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

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

histnorm ='probability' # type of normalization

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

Основные данные были собраны за короткий период времени в январе 2021 года.

Добавим новую фичу - количество месяцев между датой выпуска и последним обновлением.

df['bundle_update_period'] = \    (pd.to_datetime(        df['bundle_updated_at'], utc=True).dt.tz_convert(None).dt.to_period('M').astype('int') -      df['bundle_released_at'].dt.to_period('M').astype('int'))у

Интересно посмотреть, как распределены жанры приложений. Принимая во внимание дисбаланс ОС, я нормализую данные для гистограммы.

Мы видим, что жанры полностью не пересекаются. Особенно это заметно в играх. Для анализа такая ситуация крайне неприятна. Что мы можем с этим поделать? Самое очевидное - уменьшить количество жанров для Android и привести их к тому же виду, что и для iOS путем сведения всех игровых жанров к одному Games. Но я полагаю, что это не лучший вариант, так как будет потеря информации. Попробуем решить обратную задачу. Для этого нужно построить модель, которая будет предсказывать жанры приложений по их описанию.

Модель

Я создал несколько дополнительных фичей, используя длину описания и количество токенов.

def get_lengths(df, columns=['tokens', 'description']):    lengths_df = pd.DataFrame()    for i, c in enumerate(columns):        lengths_df[f"{c}_len"] = df[c].apply(len)        if i > 0:            lengths_df[f"{c}_div"] = \                lengths_df.iloc[:, i-1] / lengths_df.iloc[:, i]            lengths_df[f"{c}_diff"] = \                lengths_df.iloc[:, i-1] - lengths_df.iloc[:, i]    return lengths_dfdf = pd.concat([df, get_lengths(df)], axis=1, sort=False, copy=False)

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

Для обучения используются данные Android-приложений.

android_df = df[df['store_os']=='android']ios_df = df[df['store_os']=='ios']

Окончательный список фичей модели выглядит следующим образом:

columns = [    'genre', 'tokens', 'bundle_update_period', 'tokens_len',    'description_len', 'description_div', 'description_diff',    'description', 'rating', 'reviews', 'score',    'released_at_month']

Я разделил данные на две части - train и validation. Обратите внимание, что разделение должно быть стратифицировано.

train_df, test_df = train_test_split(    android_df[columns], train_size=0.7, random_state=0, stratify=android_df['genre'])y_train, X_train = train_df['genre'], train_df.drop(['genre'], axis=1)y_test, X_test = test_df['genre'], test_df.drop(['genre'], axis=1)

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

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

!pip install -U catboost

При работе с CatBoost рекомендую использовать Pool. Это удобная оболочка, объединяющая метки и другие метаданные, такие как категориальные и текстовые фичи. Бонусом идет снижение затрат памяти, так как не происходит дополнительная конвертация внутри библиотеки.

train_pool = Pool(    data=X_train,     label=y_train,    text_features=['tokens', 'description'])test_pool = Pool(    data=X_test,     label=y_test,     text_features=['tokens', 'description'])

Напишем функцию для инициализации и обучения модели. Я не подбирал оптимальные параметры; пусть это будет еще одним домашним заданием.

def fit_model(train_pool, test_pool, **kwargs):    model = CatBoostClassifier(        random_seed=0,        task_type='GPU',        iterations=10000,        learning_rate=0.1,        eval_metric='Accuracy',        od_type='Iter',        od_wait=500,        **kwargs    )return model.fit(        train_pool,        eval_set=test_pool,        verbose=1000,        plot=True,        use_best_model=True    )

Текстовые фичи используются для создания новых числовых фичей. Но для этого необходимо объяснить CatBoost, что именно мы хотим от него получить.

CatBoostClassifier имеет несколько параметров:

  • tokenizersиспользуемые для предварительной обработки фичей текстового типа перед созданием словаря;

  • dictionariesиспользуется для предварительной обработки фичей текстового типа;

  • feature_calcersиспользуется для расчета новых фичей;

  • text_processingобщий JSON-конфиг для токенизаторов, словарей и вычислителей, который определяет, как текстовые фичи преобразуются в фичи с плавающей точкой.

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

tpo = {    'tokenizers': [        {            'tokenizer_id': 'Sense',            'separator_type': 'BySense',        }    ],    'dictionaries': [        {            'dictionary_id': 'Word',            'token_level_type': 'Word',            'occurrence_lower_bound': '10'        },        {            'dictionary_id': 'Bigram',            'token_level_type': 'Word',            'gram_order': '2',            'occurrence_lower_bound': '10'        },        {            'dictionary_id': 'Trigram',            'token_level_type': 'Word',            'gram_order': '3',            'occurrence_lower_bound': '10'        },    ],    'feature_processing': {        '0': [            {                'tokenizers_names': ['Sense'],                'dictionaries_names': ['Word'],                'feature_calcers': ['BoW']            },            {                'tokenizers_names': ['Sense'],                'dictionaries_names': ['Bigram', 'Trigram'],                'feature_calcers': ['BoW']            },        ],        '1': [            {                'tokenizers_names': ['Sense'],                'dictionaries_names': ['Word'],                'feature_calcers': ['BoW', 'BM25']            },            {                'tokenizers_names': ['Sense'],                'dictionaries_names': ['Bigram', 'Trigram'],                'feature_calcers': ['BoW']            },        ]    }}

Запустим обучение:

model_catboost = fit_model(    train_pool, test_pool,    text_processing = tpo)
AccuracyAccuracyLossLoss
bestTest = 0.6454657601

Только две фичи имеют большое влияние на модель. Скорее всего, качество можно повысить за счет использования summary, но, поскольку этих данных нет в приложениях iOS, быстро применить не удастся. Можно использовать модель, которая может получить короткий абзац текста из описания. Я оставлю это задание в качестве домашнего задания читателям.

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

Чтобы получить такой вектор, нам нужно усложнить процесс, используя предсказания OOF (Out-of-Fold). Не будем использовать сторонние библиотеки; попробуем написать простую функцию.

def get_oof(n_folds, x_train, y, x_test, text_features, seeds):        ntrain = x_train.shape[0]    ntest = x_test.shape[0]              oof_train = np.zeros((len(seeds), ntrain, 48))    oof_test = np.zeros((ntest, 48))    oof_test_skf = np.empty((len(seeds), n_folds, ntest, 48))    test_pool = Pool(data=x_test, text_features=text_features)     models = {}    for iseed, seed in enumerate(seeds):        kf = StratifiedKFold(            n_splits=n_folds,            shuffle=True,            random_state=seed)                  for i, (tr_i, t_i) in enumerate(kf.split(x_train, y)):            print(f'\nSeed {seed}, Fold {i}')            x_tr = x_train.iloc[tr_i, :]            y_tr = y[tr_i]            x_te = x_train.iloc[t_i, :]            y_te = y[t_i]            train_pool = Pool(                data=x_tr, label=y_tr, text_features=text_features)            valid_pool = Pool(                data=x_te, label=y_te, text_features=text_features)            model = fit_model(                train_pool, valid_pool,                random_seed=seed,                text_processing = tpo            )            x_te_pool = Pool(                data=x_te, text_features=text_features)            oof_train[iseed, t_i, :] = \                model.predict_proba(x_te_pool)            oof_test_skf[iseed, i, :, :] = \                model.predict_proba(test_pool)            models[(seed, i)] = model    oof_test[:, :] = oof_test_skf.mean(axis=1).mean(axis=0)    oof_train = oof_train.mean(axis=0)    return oof_train, oof_test, models

Обучение трудозатратно, но в результате получили:

  • oof_trainOOF-предсказания для Android приложений

  • oof_testOOF-предсказания для iOS приложений

  • modelsall OOF-модели для каждого фолда и сида

from sklearn.metrics import accuracy_scoreaccuracy_score(    android_df['genre'].values,    np.take(models[(0,0)].classes_, oof_train.argmax(axis=1)))

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

OOF accuracy: 0.6560790777135628

Я созданную фичу android_genre_vec, копируем значения из oof_train для приложений Android и oof_test для приложений iOS.

idx = df[df['store_os']=='ios'].indexdf.loc[df['store_os']=='ios', 'android_genre_vec'] = \    pd.Series(list(oof_test), index=idx)idx = df[df['store_os']=='android'].indexdf.loc[df['store_os']=='android', 'android_genre_vec'] = \    pd.Series(list(oof_train), index=idx)

Дополнительно был добавлен android_genre, в котором находится жанр с максимальной вероятностью.

df.loc[df['store_os']=='ios', 'android_genre'] = \    np.take(models[(0,0)].classes_, oof_test.argmax(axis=1))df.loc[df['store_os']=='android', 'android_genre'] = \    np.take(models[(0,0)].classes_, oof_train.argmax(axis=1))

После всех манипуляций, можно наконец-то посмотреть и сравнить распределение приложений по жанрам.

Итоги

В статье:

  • представлен новый бесплатный датасет;

  • сделан небольшой EDA;

  • созданы несколько новых фичей;

  • создана модель для предсказания жанров приложений по описаниям.

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

Код из статьи можно посмотреть здесь.

Подробнее..

Чего хотят конференции воспроизводимость экспериментов в data science

11.06.2021 22:17:41 | Автор: admin

Ведущие научные конференции просят воспроизводимости экспериментов. И это нужно для повышения доверия к работам, для извлечения пользы (повторной используемости и цитируемости), ну и тренд (согласно опросу журнала Nature).

Ожидания растут, в 2021 уже 9 из 10 конференций предлагают авторам провериться на воспроизводимость. Сдать тест, заполнить опросник, привести свидетеля и т. д.

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

Эксперименты в машинном обучении

Под экспериментом понимают процедуру для проверки и опровержения гипотез. Будь то гипотезы о наличии закономерностей в данных или выборе параметров модели.

Провести эксперимент не так просто, как может показаться. При желании, даже в типовом эксперименте со случайным перемешиванием и разделением данных на train, test и повторением несколько раз можно обнаружить применение рандомизации в постановке эксперимента.

Структура эксперимента

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

Проведенные измерения подвергаются анализу и делается вывод об успешности эксперимента, и можно ли принять или отвергнуть гипотезы.

Так вот, анализ представленных статей на конференции AAAI 2014, AAAI 2016, IJCAI 2013 и IJCAI 2016 показывает, что доля экспериментальных работ составляет более 80% в разы выше чисто теоретических!

Ожидается, что результаты экспериментов будут повторяемы

Вот список конференций и требования по повторяемости результато на апрель 2021 года. Список получен по GuideToResearch (Top 100), поиском по категории Machine Learning, Data Mining & Artificial Intelligence. Уровни и ссылки собраны вручную.

В таблице указаны вот такие уровни требований:

  • Not found требования по воспроизводимости не найдены в CFP или инструкциях автору.

  • Reminder организаторы просят авторов предоставлять воспроизводимые результаты.

  • Encouraged авторам предоставлен чеклист и настоятельно рекомендуется предоставить оценку соответствия ему, будет влиять на оценку работ.

  • Required выполнение требований по воспроизводимости обязательно для подачи работы.

Что это значит и что такое воспроизводимость?

Пока не заметно, чтобы от рецензентов требовали воспроизводить результаты авторов. На это, скорее всего, им не хватит времени, если только эксперимент изначально не автоматизирован. Могут попросить опубликовать код, заполнить чеклист, или уже отдельно проводят трек с повторением экспериментов. Наиболее распространены два чеклиста (пример раз от NeurIPS, пример два по мотивам исследований Gundersen et al.).

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

При этом гипотеза, что соответствие чеклистам или устаналиваемым правилам повышает воспроизводимость все еще не подтверждена. Более того, по состоянию на 2020-2021 в области машинного обучения нет устоявшейся терминологии и, тем более, ее перевода на русский. Вот что используют в ACM при выдаче бейджей о воспроизводимости, в вольном переводе:

Повторяй за мной (repeatable experiment / повторяемый эксперимент)
Авторы могут получить те же результаты в пределах погрешности, используя заготовленные артефакты эксперимента.

Артефактами (artifact) здесь называются статья, экспериментальный стенд или его части, исходный код, инструкции к его запуску, данные или способ их получения и так далее, что можно идентифицировать и связать с экспериментом.

Делай вместо меня (reproducible / повторяемые результаты)
Другие исследователи могут получить те же результаты, используя артефакты от авторов.

Думай как я (replicable / воспроизводимые исследования)
По описанию эксперимента другие исследователи могут реализовать артефакты и сделать те же выводы из результатов.

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

Минутка философии.

Считается, что воспроизводимость - одна из основ научного метода познания https://en.wikipedia.org/wiki/Reproducibility

Постпозитивист и автор теории потенциальной фальсифицируемости Карл Поппер пишет в середине XX века:

non-reproducible single occurrences are of no significance to science

Popper, K. R. 1959. The logic of scientific discovery. Hutchinson, London, United Kingdom.

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

Для чего еще повторное использование

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

Примеры: для своей новой работы нужна часть кода из прошлой статьи, откуда его брать? По-умолчанию, это копирование тетрадки, ячеек или блокнотик с заготовками и сниппетами (отпишитесь, кто так делает). В каких условиях использовался этот код ранее, какую задачу решал и с каким результатом?

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

Если мы повторно используем часть эксперимента или эксперимент целиком, получим ли мы ожидаемые результаты?

С другой стороны. Например, нашли очень интересную статью на том же arxiv.org или paperswithcode.org. Будет ли полезно для проекта? Есть ли код? Есть ли данные? Могу ли повторить? Не могу. Без кода вообще не смотрю, цитата с круглого стола по воспроизводимости экспериментов.

И еще provenance

То есть прослеживаемость. Вообще важная вещь! Есть даже Prov-ML модель предметной области проведения экспериментов в науках о данных. Корректность UML 2 подтверждена (кроме указания стереотипов в двойных угловых скобках, вместо << нужно ).

Примеры вопросов про прослеживаемость. Откуда у вас это значение? Кто получил результаты? На каком сервере были расчеты? Сколько ресурсов потрачено? Какая точность измерений?

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

А в чем сложности мой исследовательский код идеален?!

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

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

Исследовательский код демонстрирует результат статьи/отчета/анализа, не используется/не требуется для понимания (есть же статья), воспринимается как дополнение к ней. При этом часто нужные компетенции для написания индастриал кода отсутствуют в команде. Действительно, рецензенты же не код смотрят (см. выше).

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

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

Повторяемость результатов на практике

Что позволено Юпитеру, не позволено быку

В статье про воспроизводимость Jupyter Notebooks, найденных на GitHub, указывается, что без ошибок и с повторением сохраненных результатов выполнились только 4% из полутора миллионов тетрадок.

4%, Карл!

Среди запусков тетрадок, завершившихся с ошибкой, топ ошибок такой:

График из статьи A Large-scale Study about Quality and Reproducibility of Jupyter Notebooks.

Первое место проблемы с зависимостями в библиотеках и зависимостями в зависимостях. Часть репозиториев использовали requirements.txt, часть setup.py. Не всегда помогает из-за транзитивных зависимостей.

Второе место порядок исполнения. Тетрадка сохранена без прогона вчистую, порядок не сохранен и некоторые переменные объявлены или иницилизированы после использования.

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

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

  • Неуправляемая случайность в данных или алгоритмах (40%)

  • Зависимость вывода и результатов от функций времени (13%)

  • Различия отображения на графиках (некорректное использование matplotlib в том числе) (52%)

  • Недоступны внешние данные (3%)

  • Различия в выводе чисел с плавающей запятой (3%)

  • Непостоянный порядок обхода словарей и др. контейнеров в python (4%)

  • Различия в среде исполнения (27%)

А что с неЮпитером? Например, исследование повторяемости на R показывает примерно 44% воспроизводимости результатов (это как сравнивать красное с квадратным, но тем не менее). Аналогично указывается список причин, почему не удалось повторить результаты.

Как быть?

Никак. Ждать пока появятся гайд и подробные инструкции.

Причем похоже, что для воспроизводимости уровня Думай как я нужно уметь писать хороший текст и еще его уметь читать (см. навык чтения статей по DL вакансия в сами знаете какой компании, google it).

Или учиться проведению экспериментов. Повышать качество своей работы.

Если в индустрии см. построение пайплайнов и версионирование (a.k.a. MLOps), разобраться с исследовательским анализом данных (Exploratory Data Analysis, EDA) и делать его автоматическим, изучать тему, помогая опен-сорс проектам.

Если в академической среде см. постановка эксперимента, оформление статьи, архивирование и публикация результатов.

Что еще стоит узнать структуры репозиториев, работа с системой контроля версий, внимание - тестирование (обзор раз, рассказ два)!

Если в начале изучения ML обсудить с научным руководителем или наставником и изучить курсы по статистической проверке гипотез, и познакомиться с правилами проведения экспериментов в доказательной медицине (hardcore).

P.S. FAIR принципы

Несмотря на то, что принципы находимых (Findable), доступных (Accessible), переносимых (Interoperable) и повторно используемых (Reusable) результатов исследований (FAIR) были представлены еще в 2016 году, большинство опрошенных исследователей все еще не понимают или не применяют их в своих работах.

Ссылки и полезности

[1] Круглый стол по вопроизводимости экспериментов в науках о данных с научной конференции МФТИ.

[2] Статья про скрытый технический долг в системах машинного обучения.

[3] Статья по тестированию наукоемкого ПО.

[5] Доклад по анализу кода Jupyter Notebooks.

[6] Полезные слайды про проведение экспериментов и воспроизводимость.

[7]Статья по разработке исследовательского кода, Best Practices for Scientific Computing.

[8] Top Ten Reasons (not) to Share your Research Code .

[9] Статья с результатами опроса, что больше влияет на воспроизводимость, Understanding experiments and research practices for reproducibility: an exploratory study

Подробнее..

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

03.02.2021 00:17:16 | Автор: admin

Знакомство с проблемой

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

В качестве примера можно рассмотреть прогнозирование закупок и спроса на какие-либо продукты с помощью машинного обучения. Если вы управляете сетью розничных магазинов, вы можете использовать данные истории покупок клиентов и итогового спроса на эти продукты для прогнозирования расходов и объёмов закупок для каждого магазина по отдельности.

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

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

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

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

Решение

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

Обученная модель ML это просто файл на диске, поэтому нам нужно сохранить файл и сопоставление: идентификатор пользователя -> идентификатор модели.

Компоненты решения

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

  • Model абстрактная модель, предоставляющая прогноз; его реализация может быть SklearnModel, TensorFlowModel, MyCustomModel и т. д.

  • ModelInfoRepository абстрактный репозиторий, который предоставляет сопоставления userid -> modelid. Например, он может быть реализован как SQAlchemyModelInfoRepository.

  • ModelRepository абстрактный репозиторий, который может возвращать модель по ее ID. Это может быть FileSystemRepository, S3Repository или любая другая реализация репозитория.

from abc import ABCclass Model(ABC):    @abstractmethod    def predict(self, data: pd.DataFrame) -> np.ndarray:        raise NotImplementedError class ModelInfoRepository(ABC):    @abstractmethod    def get_model_id_by_user_id(self, user_id: str) -> str:        raise NotImplementedError class ModelRepository(ABC):    @abstractmethod    def get_model(self, model_id: str) -> Model:        raise NotImplementedError

Реализация

Теперь предположим, что мы обучили модель sklearn, которая хранится в Amazon S3 с сопоставлениями userid -> modelid, определенными в базе данных.

class SklearnModel(Model):    def __init__(self, model):        self.model = model     def predict(self, data: pd.DataFrame):        return self.model.predict(data) class SQAlchemyModelInfoRepository(ModelInfoRepository):    def __init__(self, sqalchemy_session: Session):        self.session = sqalchemy_session     def get_model_id_by_user_id(user_id: str) -> str:        # implementation goes here, query a table in any Database      class S3ModelRepository(ModelRepository):    def __init__(self, s3_client):        self.s3_client = s3_client     def get_model(self, model_id: str) -> Model:        # load and deserialize pickle from S3, implementation goes here

Это делает реализацию сервера чрезвычайно простой:

def make_app(model_info_repository: ModelInfoRepository,     model_repsitory: ModelRepository) -> Flask:    app = Flask("multi-model-server")        @app.predict("/predict/<user_id>")    def predict(user_id):        model_id = model_info_repository.get_model_id_by_user_id(user_id)         model = model_repsitory.get_model(model_id)         data = pd.DataFrame(request.json())         predictions = model.predict(data)         return jsonify(predictions.tolist())     return app

Обратите внимание, что благодаря абстракциям сервер Flask полностью независим от конкретной модели и реализации хранилища; мы можем заменить sklearn на TensorFlow и S3 на локальную папку, при этом в коде сервера Flask не меняются строки.

Замечание о кешировании

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

from cachetools import Cache class CachedModelRepository(ModelRepository):    def __init__(self, model_repository: ModelRepository, cache: Cache):        self.model_repository = model_repository        self.cache = cache     @abstractmethod    def get_model(self, model_id: str) -> Model:        if model_id not in self.cache:            self.cache[model_id] = self.model_repository.get_model(model_id)        return self.cache[model_id]

Пример использования:

from cachetools import LRUCache model_repository = CachedModelRepository(    S3ModelRepository(s3_client),    LRUCache(max_size=10))

Перед выходом в продакшен

Такой многомодельный сервер - одна из многих частей, необходимых для запуска приложений производственного уровня с возможностями машинного обучения. Безопасность корпоративного уровня, масштабируемость, MLOps и т. д. могут быть даже более важными для успеха и надежности проекта, чем немного более точная модель машинного обучения. Всегда помните о гениальном правиле 4 от Google: пусть первая модель будет простой, а инфраструктура - правильной.

Подробнее..

Чем грозит Москве британский штамм COVID-19? Отвечаем с помощью Python и дифуров

21.04.2021 18:22:57 | Автор: admin

Всем привет! Меня зовут Борис, я выпускник программы Науки о данных ФКН ВШЭ, работаю ML Инженером и преподаю в OTUS на курсах ML Professional, DL Basic, Computer Vision.

В первых числах января 2021 я узнал про британский штамм коронавируса,прогнозы о новой волне в США. Я подумал: аналитик данных я или кто? Мне захотелось забить гвоздик своим микроскопом и узнать, вызовет ли британский штамм волну заражений в Москвеи стоит ли покупать авиабилеты на лето.

Выглядело как приключение на две недели, но превратилось в исследование на три месяца. В процессе я выяснил, что хороших материалов по созданию эпидемиологических моделей практически нет. Банально авторы статей по моделированию COVID-19 в топовых журналах даже не делают train-test split.

Я предлагаю туториал на основе своего исследования. В нём я постарался передать все важные детали, которые сэкономили бы мне много недель, если бы о них кто-то писал.

Ноутбук к туториалу.

Краткая презентация с результатами исследования.

Препринт статьи: TBA


Какой британский штамм?

COVID-19 в представлении не нуждается, к нему мы уже привыкли. Однако новые мутации (штаммы) вируса способны изменить ход эпидемии. Сейчас ВОЗвыделяеттри таких штамма (variants of concern). Среди них штамм B.1.1.7, так называемый британский штамм.

Почему меня заинтересовал именно он? Во-первых,исследования показывают, что этот штамм распространяется на 40% - 90% быстрее, чем обычный коронавирус (для продвинутых: R0 на 40% - 90% больше). Во-вторых, случай заражения этим штаммом уже былобнаруженв России. В-третьих, непохоже, чтобы кто-то в России готовился к его появлению.

По результатам исследования пришел к выводу, что B.1.1.7 действительно способен привести к новой волне заражений COVID-19 и может унести 30 тысяч жизней.

Исходные данные

Нам понадобится статистика по заражениям, смертям и выздоровлениям в Москве. Её можно скачать со страницы.
На момент исследования был доступен промежуток дат: 2020.03.10 - 2021.03.23.

Изначально данные даны для всей России. Модель, которую мы будем использовать, работает лучше для замкнутых популяций, так что ограничимся моделированием для Москвы. Кроме того поправим индексы и переименуем колонки.

df.columns = ['date', 'region',             'total_infected', 'total_recovered', 'total_dead',            'deaths_per_day', 'infected_per_day', 'recovered_per_day']df = df[df.region == 'Москва'].reset_index()df['date'] = pd.to_datetime(df['date'], format='%d.%m.%Y')df['infected'] = df['total_infected'] - df['total_recovered'] - df['total_dead']df = df.drop(columns=['index', 'region'])df = df.sort_values(by='date')df.index = df.datedf['date'] = df.date.asfreq('D')

Добавим сглаженные скользящим окном в 7 дней версии всех колонок. Они пригодятся нам позже.

df_smoothed = df.rolling(7).mean().round(5)df_smoothed.columns = [col + '_ma7' for col in df_smoothed.columns]full_df = pd.concat([df, df_smoothed], axis=1)for column in full_df.columns:    if column.endswith('_ma7'):        original_column = column.strip('_ma7')        full_df[column] = full_df[column].fillna(full_df[original_column])

В итоге работаем со следующими колонками, а так же их сглаженными версиями:

  • infected_per_day новых заражений в данный день.

  • recovered_per_day новых выздоровлений в данный день.

  • deaths_per_day погибших от инфекции в данный день.

  • total_infected всего заражений к этому моменту, кумулятивная суммаinfected_per_day.

  • total_dead всего погибших к этому моменту, кумулятивная сумма отdeaths_per_day.

  • total_recovered всего выздоровлений к этому моменту, кумулятивная сумма отrecovered_per_day.

Основа модели: SEIRD

SIRэто очень простая модель, которая симулирует развитие эпидемии во времени. Популяция разделяется на три группы: Susceptible, Infected, Recovered.
Идея на пальцах:

  • Есть замкнутая популяция и изначальное количество зараженных (Infected).

  • В течение дня каждый зараженный с некоторой вероятностью инфицирует кого-то из группы Susceptible. Новые зараженные переходят в группу Infected.

  • Находящиеся в группе Infected выздоравливают, когда проходит срок длительности болезни и переходят в группу Recovered.

  • Запускаем этот процесс на много дней вперед и смотрим, как меняется численность групп.

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

  • Susceptible могут быть заражены.

  • Exposed заражены вирусом, но не распространяют его, то есть проходят инкубационный период.

  • Infectious распространяют вирус.

  • Recovered переболели и не могут быть заражены.

  • Dead погибли.

Изменение численности групп за день определяют дифференциальные уравнения:

Например, третье уравнение описывает изменение группы Infected за день. С вероятностьюу человека из группыEзаканчивается инкубационный период и он переходит в группуI. Так же с вероятностьючеловек из группыIвыздоравливает или погибает, переходя в группуRилиD. Куда он попадет определяется параметром, который соответствует доле смертности (Infection Fatality Rate). Таким образом, каждый день группу пополняют новые зараженные, а переболевшие уходят из группы.

Параметры модели:
case fatality rate.
количество человек, которых один переносчик заражает за день.
1 делить на среднюю длину инкубационного периода.
y 1 делить на среднюю длительность болезни.
R0 = /y базовое репродуктивное число, то есть количество человек, которых один переносчик заражает за время своей болезни.

Есть три причины использовать именно SEIRD. Во-первых, параметры SEIRD это просто характеристики болезни, а значит мы сможем прикинуть их значения на основании медицинских исследований COVID-19 как болезни. Во-вторых, эту модель очень легко модифицировать, что мы и будем делать дальше, добавляя меры карантина, неполноту статистики и второй штамм. В-третьих, эта модель позволяет легко покрутить разные сценарии. Например, можно смоделировать ситуацию: Что будет, если в этой же популяции появится второй штамм и параметрR0у него на 90% больше? Сделать подобное с помощью, например, градиентного бустинга, гораздо сложнее.

Главный минус SEIRD: это очень простая и полностью детерминированная модель. Она чувствительна к изначальным значениям и параметрам. Предсказания такой модели могут на порядки отличаться от реальных значений. Для нас это не страшно: мы ведь не пытаемся сделать самый точный прогноз по количеству зараженных, мы просто пытаемся понятьможет ли штамм B.1.1.7 вызвать новую волну. Интересно, что, несмотря на это, лучшая на текущий момент модель по прогнозированию COVID-19 этоочень навороченная SEIR модель.

Реализуем классический SEIRD

Привожу минимальную реализацию SEIRD.

class BarebonesSEIR:    def init(self, params=None):        self.params = params<span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">def</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(121, 93, 163);">get_fit_params</span>(<span class="hljs-params" style="box-sizing: border-box;">self</span>):</span>    params = lmfit.Parameters()    params.add(<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">"population"</span>, value=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">12_000_000</span>, vary=<span class="hljs-literal" style="box-sizing: border-box; color: rgb(0, 134, 179);">False</span>)    params.add(<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">"epidemic_started_days_ago"</span>, value=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">10</span>, vary=<span class="hljs-literal" style="box-sizing: border-box; color: rgb(0, 134, 179);">False</span>)    params.add(<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">"r0"</span>, value=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">4</span>, <span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">min</span>=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">3</span>, <span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">max</span>=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">5</span>, vary=<span class="hljs-literal" style="box-sizing: border-box; color: rgb(0, 134, 179);">True</span>)    params.add(<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">"alpha"</span>, value=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">0.0064</span>, <span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">min</span>=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">0.005</span>, <span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">max</span>=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">0.0078</span>, vary=<span class="hljs-literal" style="box-sizing: border-box; color: rgb(0, 134, 179);">True</span>)  <span class="hljs-comment" style="box-sizing: border-box; color: rgb(150, 152, 150);"># CFR</span>    params.add(<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">"delta"</span>, value=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>/<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">3</span>, <span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">min</span>=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>/<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">14</span>, <span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">max</span>=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>/<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">2</span>, vary=<span class="hljs-literal" style="box-sizing: border-box; color: rgb(0, 134, 179);">True</span>)  <span class="hljs-comment" style="box-sizing: border-box; color: rgb(150, 152, 150);"># E -&gt; I rate</span>    params.add(<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">"gamma"</span>, value=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>/<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">9</span>, <span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">min</span>=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>/<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">14</span>, <span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">max</span>=<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>/<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">7</span>, vary=<span class="hljs-literal" style="box-sizing: border-box; color: rgb(0, 134, 179);">True</span>)  <span class="hljs-comment" style="box-sizing: border-box; color: rgb(150, 152, 150);"># I -&gt; R rate</span>    params.add(<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">"rho"</span>, expr=<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'gamma'</span>, vary=<span class="hljs-literal" style="box-sizing: border-box; color: rgb(0, 134, 179);">False</span>)  <span class="hljs-comment" style="box-sizing: border-box; color: rgb(150, 152, 150);"># I -&gt; D rate</span>    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">return</span> params<span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">def</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(121, 93, 163);">get_initial_conditions</span>(<span class="hljs-params" style="box-sizing: border-box;">self, data</span>):</span>    <span class="hljs-comment" style="box-sizing: border-box; color: rgb(150, 152, 150);"># Simulate such initial params as to obtain as many deaths as in data</span>    population = self.params[<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'population'</span>]    epidemic_started_days_ago = self.params[<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'epidemic_started_days_ago'</span>]    t = np.arange(epidemic_started_days_ago)    (S, E, I, R, D) = self.predict(t, (population - <span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>, <span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">0</span>, <span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>, <span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">0</span>, <span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">0</span>))    I0 = I[-<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>]    E0 = E[-<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>]    Rec0 = R[-<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>]    D0 = D[-<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>]    S0 = S[-<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>]    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">return</span> (S0, E0, I0, Rec0, D0)<span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">def</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(121, 93, 163);">step</span>(<span class="hljs-params" style="box-sizing: border-box;">self, initial_conditions, t</span>):</span>    population = self.params[<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'population'</span>]    delta = self.params[<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'delta'</span>]    gamma = self.params[<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'gamma'</span>]    alpha = self.params[<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'alpha'</span>]    rho = self.params[<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'rho'</span>]        rt = self.params[<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'r0'</span>].value    beta = rt * gamma    S, E, I, R, D = initial_conditions    new_exposed = beta * I * (S / population)    new_infected = delta * E    new_dead = alpha * rho * I    new_recovered = gamma * (<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span> - alpha) * I    dSdt = -new_exposed    dEdt = new_exposed - new_infected    dIdt = new_infected - new_recovered - new_dead    dRdt = new_recovered    dDdt = new_dead    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">assert</span> S + E + I + R + D - population &lt;= <span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1e10</span>    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">assert</span> dSdt + dIdt + dEdt + dRdt + dDdt &lt;= <span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1e10</span>    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">return</span> dSdt, dEdt, dIdt, dRdt, dDdt<span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">def</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(121, 93, 163);">predict</span>(<span class="hljs-params" style="box-sizing: border-box;">self, t_range, initial_conditions</span>):</span>    ret = odeint(self.step, initial_conditions, t_range)    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">return</span> ret.T

Давайте разберемся.

Методget_fit_paramsпросто создает хранилище парамтеров. Мы используем библиотекуlmfit, но пока что не оптимизируем параметры, а просто используем структуруParametersкак продвинутый словарь. Для задания диапазонов параметров я использовалмета-анализы характеристик COVID-19.

Параметрepidemic_started_days_agoпозволяет задать дату появления первого зараженного. В моём предположении это2 марта 2020.

Методstepсодержит дифференциальные уравнения модели. Он получает на вход изначальные численности всех групп в виде кортежаinitial_conditionsи номер дняt, возвращает изменения всех групп за день.

Методpredictполучает на вход начальные условия и массивt_range. Он просто запускаетscipy.integrate.odeintна методеstep, чтобы проинтегрировать дифференциальные уравнения и получить численность групп в каждый день из массиваt_range.

Методget_initial_conditionsзапускает модель с момента появления первого зараженнного наepidemic_started_days_agoдней и возвращает полученные численности групп. Далее эти численности используются как начальные значения для моделирования последующих дней.

Запустим модель и сравним её предсказания с реальностью:

model = BarebonesSEIR()model.params = model.get_fit_params()train_initial_conditions = model.get_initial_conditions(train_subset)train_t = np.arange(len(train_subset))(S, E, I, R, D) = model.predict(train_t, train_initial_conditions)plt.figure(figsize=(10, 7))plt.plot(train_subset.date, train_subset['total_dead'], label='ground truth')plt.plot(train_subset.date, D, label='predicted', color='black', linestyle='dashed' )plt.legend()plt.title('Total deaths')plt.show()

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

Взглянем на динамику всех групп SEIRD:

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

Добавляем сдерживающие меры

Одна из причин появления нескольких волн это сдерживающие меры. Предположим, что карантинные меры в деньt, снижаютR0болезни на процентq(t). Тогда для каждого дня можно расчитать репродуктивное число с учетом карантина:Rt = R0 - R0 * q(t). Отсюда мы можем вычислить(t) = Rt * yи использовать её в уравнениях.

Осталось только задать функцииq(t). В своём исследовании я использовал простую ступенчатую функцию. Делим весь промежуток на отрезки по 60 дней, для каждого отрезка добавляем новый параметр: уровень карантина в данный период. Переходы между отрезками стоит сделать плавными с помощью, иначе функция не будет гладкой и подбирать параметры будет сложно.

Реализуем небольшой пример для наглядности.

def sigmoid(x, xmin, xmax, a, b, c, r):    x_scaled = (x - xmin) / (xmax - xmin)    out = (a * np.exp(c * r) + b * np.exp(r * x_scaled)) / (np.exp(c * r) + np.exp(x_scaled * r))    return outdef stepwise_soft(t, coefficients, r=20, c=0.5):    t_arr = np.array(list(coefficients.keys()))min_index = np.<span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">min</span>(t_arr)max_index = np.<span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">max</span>(t_arr)<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">if</span> t &lt;= min_index:    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">return</span> coefficients[min_index]<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">elif</span> t &gt;= max_index:    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">return</span> coefficients[max_index]<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">else</span>:    index = np.<span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">min</span>(t_arr[t_arr &gt;= t])<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">if</span> <span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">len</span>(t_arr[t_arr &lt; index]) == <span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">0</span>:    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">return</span> coefficients[index]prev_index = np.<span class="hljs-built_in" style="box-sizing: border-box; color: rgb(0, 92, 197);">max</span>(t_arr[t_arr &lt; index])<span class="hljs-comment" style="box-sizing: border-box; color: rgb(150, 152, 150);"># sigmoid smoothing</span>q0, q1 = coefficients[prev_index], coefficients[index]out = sigmoid(t, prev_index, index, q0, q1, c, r)<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">return</span> outt_range = np.arange(100)coefficients = {    0: 0,    30: 0.5,    60: 1,    100: 0.4,}plt.title('Пример функции уровня карантина')plt.scatter(coefficients.keys(), coefficients.values(), label='Моменты изменения уровня карантина')plt.plot(t_range, [stepwise_soft(t, coefficients, r=20, c=0.5) for t in t_range], label='Ступенчатая функция с плавными переходами')plt.xlabel('t')plt.ylabel('Уровень канартина')plt.legend(loc='lower center', bbox_to_anchor=(0.5, -0.6),)plt.show()

Чтобы добавить это в нашу SEIRD модель нужно сделать несколько шагов:

  1. Добавляем мультипликаторы карантина вget_fit_params. Изначально карантин находится на уровне нуля, но для остальных отрезков его уровень будет определен оптимизацией. Так же появляется новый гиперпараметр, задающий длину отрезков:stepwise_size, по умолчанию 60 дней.

def get_fit_params(self, data):    ...    params.add(f"t0_q", value=0, min=0, max=0.99, brute_step=0.1, vary=False)    piece_size = self.stepwise_size    for t in range(piece_size, len(data), piece_size):        params.add(f"t{t}_q", value=0.5, min=0, max=0.99, brute_step=0.1, vary=True)    return params
  1. Добавляемновый методget_step_rt_beta, который вычисляет(t)иRtиспользуя ступенчатую функцию.

  2. В методеstepиспользуемget_step_rt_beta,(t)с учетом карантина в деньt.

Учитываем неполноту статистики

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

Изменим группы Infected, Recovered и Dead следующим образом:

  • Iv(t) распространяют вирус, видны в статистике.

  • I(t) распространяют вирус, не видны в статистике.

  • Rv(t) переболели, видны в статистике.

  • R(t) переболели, не видны в статистике.

  • Dv(t) погибли, видны в статистике.

  • D(t) погибли, не видны в статистике.

Добавим два новых параметра:

  • pi вероятность попадания зараженного в статистику, то есть в группу Iv.

  • pd вероятность регистрации смерти зараженного, невидимого в статистике, в группу Dv. Зараженные из группы Iv всегда попадают в Dv, но этот параметр позволяет учесть, что часть смертей незамеченных зараженных так же попадает в статистику по смертности от COVID-19.

В конце концов мы приходим к такой схеме модели, где вершины графа это группы, а стрелки определяют как люди перемещаются между группами:

Для удобства будем называть её SEIRD со скрытыми состояниями. Она отличается от обычного SEIRD только дифференциальными уравнениями. Подробности реализации можно посмотретьздесь.

Оптимизируем параметры

Модель готова, осталось только подобрать оптимальные значения параметров так, чтобы выдаваемые моделью Iv(t), Rv(t) и Dv(t) были как можно ближе к историческим данным. Это можно сделать с помощью метода наименьших квадратов.

Конкретнее, используем для этогоlmfit.minimize. Эта функция получает на вход callable, возвращающий список отклонений предсказаний от истины (остатки, residuals), и подбирает такие параметры, чтобы сумма отклонений была минимальна. Используем алгоритм Levenberg-Marquardt (method=leastsq), который двигает каждый параметр на очень маленькое значение и проверяет, уменьшается ли ошибка. Важно знать, что по этой причине алгоритм не может оптимизировать дискретные параметры, поэтому, например, не сможет подобратьstepwise_size.

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

Рассмотрим получение остатков подробнее:

def smape_resid_transform(true, pred, eps=1e-5):    return (true - pred) / (np.abs(true) + np.abs(pred) + eps)class HiddenCurveFitter(BaseFitter):...    def residual(self, params, t_vals, data, model):        model.params = params    initial_conditions = model.get_initial_conditions(data)    (S, E, I, Iv, R, Rv, D, Dv), history = model.predict(t_vals, initial_conditions, history=<span class="hljs-literal" style="box-sizing: border-box; color: rgb(0, 134, 179);">False</span>)    (new_exposed,     new_infected_invisible, new_infected_visible,     new_recovered_invisible,     new_recovered_visible,     new_dead_invisible, new_dead_visible) = model.compute_daily_values(S, E, I, Iv, R, Rv, D, Dv)    new_infected_visible = new_infected_visible    new_dead_visible = new_dead_visible    new_recovered_visible = new_recovered_visible    true_daily_cases = data[self.new_cases_col].values[<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>:]    true_daily_deaths = data[self.new_deaths_col].values[<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>:]    true_daily_recoveries = data[self.new_recoveries_col].values[<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>:]    resid_I_new = smape_resid_transform(true_daily_cases, new_infected_visible)    resid_D_new = smape_resid_transform(true_daily_deaths, new_dead_visible)    resid_R_new = smape_resid_transform(true_daily_recoveries, new_recovered_visible)    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">if</span> self.weights:        residuals = np.concatenate([            self.weights[<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'I'</span>] * resid_I_new,            self.weights[<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'D'</span>] * resid_D_new,            self.weights[<span class="hljs-string" style="box-sizing: border-box; color: rgb(223, 80, 0);">'R'</span>] * resid_R_new,        ]).flatten()    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">else</span>:        residuals = np.concatenate([            resid_I_new,            resid_D_new,            resid_R_new,        ]).flatten()    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">return</span> residuals

Основная идея простая. Для промежуткаt_valsполучаем предсказания заражений в день, смертей в день и выздоровлений в день. Вычисляем отклонения предсказанных значений от реальных и кладем всё это в один массив.

Также применяется несколько трюков, до которых пришлось дойти методом эмпирического тыка.

Во-первых, количество новых зараженных и количество новых смертей имеют абсолютно разный масштаб: нормально видеть 1000 новых зараженных за день и лишь 1 смерть в тот же день. Если ориентироваться на абсолютные остатки, то оптимизация будет обращать внимание только на остатки по зараженным. Чтобы привести все остатки к одному масштабу я применил трансформацию котносительной ошибкес помощью функцииsmape_resid_transform.

Во-вторых, можно предположить, что статистика по смертям достовернее статистики по заражениям и выздоровлениям. Чтобы внести это предположение в оптимизацию вводятся веса (self.weights) для остатков. Хорошо сработали такие веса:0.5для остатков по смертям и0.25для остальных.

Тренируем модель

Наконец, натренируем SEIRD со скрытыми состояниями. Статистика вносится с некоторым лагом по времени. Чтобы это меньше влияло на нашу модель будем использовать сглаженные версии колонок для расчета остатков.

from sir_models.fitters import HiddenCurveFitterfrom sir_models.models import SEIRHiddenstepwize_size = 60weights = {    'I': 0.25,    'R': 0.25,    'D': 0.5,}model = SEIRHidden(stepwise_size=stepwize_size)fitter = HiddenCurveFitter(     new_deaths_col='deaths_per_day_ma7',     new_cases_col='infected_per_day_ma7',     new_recoveries_col='recovered_per_day_ma7',     weights=weights,     max_iters=1000,)fitter.fit(model, train_subset)result = fitter.resultresult

Полученные параметры:

Проверим фит модели на тренировочных данных:

Вот это другое дело! Модель показывает, что почти половина смертей не попадают в статистику. По заражениям другая картина: в статистику попадает примерно одна пятая.

Полезно взглянуть на все предсказываемые моделью компоненты: ежедневные смерти, заражения и выздоровления. Модель может выдавать абсолютно верные предсказания по смертям, но при этом прогнозировать в 10 раз больше выздоровлений, или наоборот.

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

Верифицируем модель c помощью кросс-валидации

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

В статьях по моделированию COVID-19 стандартом является проверять модель на предсказаниях кумулятивного числа погибших, поэтому сделаем так же.

Процесс верификации:

  • Выбираем несколько дат из тренировочных данных, например каждый 20-ый день.

  • Для каждой даты:

    • Обучаем модель на всех данных до этой даты.

    • Делаем прогноз суммарного количества погибших на 30 дней вперед.

    • Считаем ошибку предсказания.

  • Вычисляем среднюю ошибку модели.

  • Сравниваем ошибку с ошибкой бейзлайна: предсказанием предыдущего дня.

from sir_models.utils import eval_on_select_dates_and_k_days_aheadfrom sir_models.utils import smapefrom sklearn.metrics import mean_absolute_errorK = 30last_day = train_subset.date.iloc[-1] - pd.to_timedelta(K, unit='D')eval_dates = pd.date_range(start='2020-06-01', end=last_day)[::20]def eval_hidden_moscow(train_df, t, train_t, eval_t):    weights = {        'I': 0.25,        'R': 0.25,        'D': 0.5,    }    model = SEIRHidden()    fitter = HiddenCurveFitter(        new_deaths_col='deaths_per_day_ma7',        new_cases_col='infected_per_day_ma7',        new_recoveries_col='recovered_per_day_ma7',        weights=weights,        max_iters=1000,        save_params_every=500)    fitter.fit(model, train_df)train_initial_conditions = model.get_initial_conditions(train_df)train_states, history = model.predict(train_t, train_initial_conditions, history=<span class="hljs-literal" style="box-sizing: border-box; color: rgb(0, 134, 179);">False</span>)test_initial_conds = [compartment[-<span class="hljs-number" style="box-sizing: border-box; color: rgb(0, 134, 179);">1</span>] <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">for</span> compartment <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">in</span> train_states]test_states, history = model.predict(eval_t, test_initial_conds, history=<span class="hljs-literal" style="box-sizing: border-box; color: rgb(0, 134, 179);">False</span>)    <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(167, 29, 93);">return</span> model, fitter, test_states(models, fitters, model_predictions, train_dfs, test_dfs) = eval_on_select_dates_and_k_days_ahead(train_subset,                                        eval_func=eval_hidden_moscow,                                        eval_dates=eval_dates,                                        k=K)model_pred_D = [pred[7] for pred in model_predictions]true_D = [tdf.total_dead.values for tdf in test_dfs]baseline_pred_D = [[tdf.iloc[-1].total_dead]*K for tdf in train_dfs]overall_errors_model = [mean_absolute_error(true, pred) for true, pred in zip(true_D, model_pred_D)]overall_errors_baseline = [mean_absolute_error(true, pred) for true, pred in zip(true_D, baseline_pred_D)]print('Mean overall error baseline', np.mean(overall_errors_baseline).round(3))print('Mean overall error model', np.mean(overall_errors_model).round(3))overall_smape_model = [smape(true, pred) for true, pred in zip(true_D, model_pred_D)]np.median(overall_smape_model)

Результат:

  • Mean Absolute Arror бейзлайна: 714.

  • Mean Absolute Arror модели: 550.

  • Symmetric Mean Absolute Percentage Error: 4.6%

Модель побила бейзлайн и ошибка в пределах 5% Можно заключить, что модель подходит для оценки ситуации с двумя штаммами.

Расширение модели для двух штаммов

Ситуацию с двумя штаммами можно смоделировать как две параллельные эпидемии, которые заражают людей из одной популяции. При этом зараженные одним штаммом не могут заразиться другим. В контексте SEIR моделей это означает, например, что вместо группыI(t)для всех зараженных у нас теперь будет две группы для разных штаммов:I1(t)иI2(t).

Модель эпидемии обычного SARS-CoV-2 мы только что обучили. Осталось получить модель для британского штамма. СогласноисследованиямB.1.1.7 отличается только параметромR0: он в 1.4 - 1.9 раз больше. Значит мы можем взять обученную модель для обычного SARS-CoV-2, увеличитьR0и получить модель для B.1.1.7.

Полная схема модели с двумя штаммами выглядит так:

Главное отличиереализации этой модели в кодеот предыдущих в том, что эта модель не обучается, а создается на основе обученной модели для одного штамма. Добавляется параметрbeta2_mult, который задает, на сколько надо умножить1(t)первого штамма, чтобы получить2(t)второго.

class SEIRHiddenTwoStrains(SEIRHidden):    ...    @classmethod    def from_strain_one_model(cls, model):        strain1_params = model.params        strain1_params.add("beta2_mult", value=1.5, min=1, max=2, vary=False)        return cls(params=deepcopy(strain1_params))

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

Сценарии развития эпидемии при появлении B.1.1.7

Наконец, мы получили модель для двух штаммов. Неизвестно какое именноR0у B.1.1.7, а так же сколько сейчас в Москве носителей этого штамма. Рассмотрим несколько сценариев.

ЗначениеR0влияет на дату и высоту пика заражений новым штаммом. В лучшем случае B.1.1.7 просто продлевает эпидемию в текущем состоянии на полгода-год. В худшем случае случается большая волна заражений.

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

Ослабление карантина приводит к значительному количеству заражений.

Автор статьи: Борис Цейтлин


Узнайте подробнее о курсе "Machine Learning. Professional".

Также приглашаем всех желающих на бесплатный интенсив по теме: Деплой ML модели: от грязного кода в ноутбуке к рабочему сервису.

Подробнее..

Перевод Топ 6 библиотек Python для визуализации какую и когда лучше использовать?

20.05.2021 20:12:40 | Автор: admin

Перевод подготовлен в рамках курса "Machine Learning. Basic".

Всех желающих приглашаем на открытый онлайн-интенсив Data Science это проще, чем кажется. Поговорим об истории и основных вехах в развитии ИИ, вы узнаете, какие задачи решает DS и чем занимается ML. И уже на первом занятии вы сможете научить компьютер определять, что изображено на картинке. А именно, вы попробуете обучить свою первую модель машинного обучения для решения задачи классификации изображений. Поверьте, это проще, чем кажется!


Не знаете, какой инструмент визуализации использовать? В этой статье мы подробно расскажем о плюсах и минусах каждой библиотеки.

Это руководство было дополнено несколькими подробными примерами. Вы также можете отслеживать актуальные версии этой статьи здесь.

Мотивация

Если вы только собираетесь начать работу с визуализацией в Python, количество библиотек и решений вас определенно поразит:

  • Matplotlib

  • Seaborn

  • Plotly

  • Bokeh

  • Altair

  • Folium

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

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

Интерактивность

Хотите ли вы, чтобы ваша визуализация была интерактивной?

Визуализация в некоторых библиотеках, таких как Matplotlib, является простым статичным изображением, что хорошо подходит для объяснения концепций (в документе, на слайдах или в презентации).

Другие библиотеки, такие как Altair, Bokeh и Plotly, позволяют создавать интерактивные графики, которые пользователи могут изучать, взаимодействуя с ними.

Синтаксис и гибкость

Чем отличается синтаксис каждой библиотеки? Библиотеки низкого уровня, такие как Matplotlib, позволяют делать все, что вы захотите, но за счет более сложного API. Некоторые библиотеки, такие как Altair, очень декларативны, что упрощает построение графиков по вашим данным.

Тип данных и визуализации

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

Данные

Чтобы было проще сравнивать библиотеки, здесь представлены реальные данные с Github из этой статьи:

I Scraped more than 1k Top Machine Learning Github Profiles and this is what I Found

В статью включены визуализации из каждой библиотеки с помощью Datapane, который представляет собой Python фреймворк и API для публикации и совместного использования Python-отчетов. Больше реальных примеров вы можете найти в пользовательских отчетах в галереи Datapane.

Вы можете скачать файл csv здесь, либо получите данные напрямую из Datapane Blob.

import datapane as dpdp.Blob.get(name='github_data', owner='khuyentran1401').download_df()

Не забудьте залогиниться со своим токеном авторизации в Datapane, если вы хотите использовать Blob. Это займет менее минуты.

Matplotlib

Matplotlib, вероятно, является самой популярной библиотекой Python для визуализации данных. Все, кто интересуется data science, наверняка хоть раз сталкивались с Matplotlib.

Плюсы

  1. Четко отображены свойства данных

При анализе данных возможность быстро посмотреть распределение может быть очень полезной.

Например, если я хочу быстро посмотреть распределение топ 100 пользователей с наибольшим количеством подписчиков, обычно Matplotlib мне будет вполне достаточно:

import matplotlib.pyplot as plttop_followers = new_profile.sort_values(by='followers', axis=0, ascending=False)[:100]fig = plt.figure()plt.bar(top_followers.user_name,       top_followers.followers)

Даже что-то вроде этого:

fig = plt.figure()plt.text(0.6, 0.7, "learning", size=40, rotation=20.,         ha="center", va="center",         bbox=dict(boxstyle="round",                   ec=(1., 0.5, 0.5),                   fc=(1., 0.8, 0.8),                   )         )plt.text(0.55, 0.6, "machine", size=40, rotation=-25.,         ha="right", va="top",         bbox=dict(boxstyle="square",                   ec=(1., 0.5, 0.5),                   fc=(1., 0.8, 0.8),                   )         )plt.show()

Минусы

Matplotlib может создать любой график, но с его помощью может быть сложно построить или подогнать сложные графики, чтобы они выглядели презентабельно.

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

correlation = new_profile.corr()fig, ax = plt.subplots()im = plt.imshow(correlation)ax.set_xticklabels(correlation.columns)ax.set_yticklabels(correlation.columns)plt.setp(ax.get_xticklabels(), rotation=45, ha="right",         rotation_mode="anchor")

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

Seaborn

Seaborn - это библиотека Python для визуализации данных, построенная на базе Matplotlib. Она более высокоуровневая, что упрощает ее использование.

Плюсы

  1. Меньше кода

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

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

correlation = new_profile.corr()sns.heatmap(correlation, annot=True)

Мы получаем лучший график пользовательской активности без возни x и y!

2. Делает стандартные графики красивее

Многие люди выбирают seaborn для создания широко используемых графиков, таких как столбчатые и прямоугольные диаграммы, расчетные графики, гистограммы и т. д., но не только потому, что это потребует меньше кода, они еще и визуально приятнее. Как видно на примере выше, цвета выглядят лучше, чем цвета по умолчанию в Matplotlib.

sns.set(style="darkgrid")titanic = sns.load_dataset("titanic")ax = sns.countplot(x="class", data=titanic)

Минусы

Seaborn более ограничен и не имеет такой широкой коллекции графиков, как matplotlib.

Вывод: Seaborn это версия Matplotlib более высокого уровня. Несмотря на то, что коллекция графиков не настолько большая, как в Matplotlib, созданные с помощью seaborn широко используемые графики (например, столбчатая диаграмма, прямоугольная диаграмма, график пользовательской активности и т. д.), при меньшем количестве кода будет выглядеть визуально приятнее.

Plotly

Python библиотека Plotly упрощает создание интерактивных графиков типографского качества. Он также может создавать диаграммы, аналогичные Matplotlib и seaborn, такие как линейные графики, точечные диаграммы, диаграммы с областями, столбчатые диаграммы и т. д.

Плюсы

  1. Похож на R

Если вы поклонник графиков в R и вам не хватает его функционала при переходе на Python, Plotly даст вам такое же качество графиков с использованием Python!

Мой любимая версия - Plotly Express, потому что с ней можно легко и быстро создавать отличные графики одной строчкой в Python.

import plotly.express as pxfig = px.scatter(new_profile[:100],          x='followers',          y='total_stars',          color='forks',          size='contribution')fig.show()

2. Простота создания интерактивных графиков

Plotly также упрощает создание интерактивных графиков. Интерактивные графики не только красиво выглядят, но и позволяют публике более внимательно изучить каждую точку на графике.

Помните столбчатую диаграмму, которую мы показывали ранее в matplotlib? Давайте посмотрим, как она получится с помощью Plotly

import plotly.express as pxtop_followers = new_profile.sort_values(by='followers', axis=0, ascending=False)[:100]fig = px.bar(top_followers,              x='user_name',              y='followers',            )fig.show()

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

3. Легко делать сложные графики

С помощью Plotly достаточно легко создавать сложные графики.

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

import plotly.express as pximport datapane as dplocation_df = dp.Blob.get(name='location_df', owner='khuyentran1401').download_df()m = px.scatter_geo(location_df, lat='latitude', lon='longitude',                 color='total_stars', size='forks',                 hover_data=['user_name','followers'],                 title='Locations of Top Users')m.show()

И, написав всего несколько строк кода, местоположения всех пользователей красиво представлены на карте. Цвет окружностей представляет количество форков, а размер - общее количество звезд.

Вывод: Plotly отлично подходит для создания интерактивных и качественных графиков при помощи всего нескольких строк кода.

Altair

Altair - это библиотека Python декларативной статистической визуализации, которая основана на vega-lite, что идеально подходит для графиков, требующих большого количества статистических преобразований.

Плюсы

1. Простая грамматика визуализации

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

Например, для данных о Титанике выше мы хотели бы подсчитать количество людей в каждом классе. Все, что нам нужно, это использовать count() в y_axis

import seaborn as snsimport altair as alt titanic = sns.load_dataset("titanic")alt.Chart(titanic).mark_bar().encode(    alt.X('class'),    y='count()')

2. Простота преобразования данных

Altair также упрощает преобразование данных при создании диаграммы.

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

hireable = alt.Chart(titanic).mark_bar().encode(    x='sex:N',    y='mean_age:Q').transform_aggregate(    mean_age='mean(age)',    groupby=['sex'])hireable

Логика здесь состоит в том, чтобы использовать transform_aggregate() для взятия среднего значения возраста (mean(age)) каждого пола (groupby=['sex']) и сохранить его в переменной mean_age). За ось Y мы берем переменную.

Мы также можем убедиться, что класс - это номинальные данные (категорийные данные в произвольном порядке), используя :N, или что mean_age - это количественные данные (меры значений, такие как числа), используя :Q.

Полный список преобразований данных можно найти здесь.

3. Связывание нескольких графиков

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

Например, мы хотим визуализировать количество людей из каждого класса в пределах значений, ограниченных выделенным интервалом в точечной диаграмме по возрасту и плате за проезд. Тогда нам нужно написать что-то вроде этого:

brush = alt.selection(type='interval')points = alt.Chart(titanic).mark_point().encode(    x='age:Q',    y='fare:Q',    color=alt.condition(brush, 'class:N', alt.value('lightgray'))).add_selection(    brush)bars = alt.Chart(titanic).mark_bar().encode(    y='class:N',    color='class:N',    x = 'count(class):Q').transform_filter(    brush)points & bars

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

Минусы

Если вы не задаете пользовательский стиль, простые диаграммы, такие как, например, столбчатые, не будут оформлены стилистически так же хорошо, как в seaborn или Plotly. Altair также не рекомендует использовать наборы данных с более чем 5000 экземплярами и рекомендует вместо этого агрегировать данные перед визуализацией.

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

Bokeh

Bokeh - это интерактивная библиотека для визуализации, предназначенная для презентации данных в браузерах.

Плюсы

  1. Интерактивная версия Matplotlib

Если мы будем будем составлять топы интерактивных библиотек для визуализации, Bokeh, вероятно, займет первое место в категории сходства с Matplotlib.

Matplotlib позволяет создать любой график, так как эта библиотека предназначена для визуализации на достаточно низком уровне. Bokeh можно использовать как с высокоуровневым, так и низкоуровневым интерфейсом; таким образом, она способна создавать множество сложных графиков, которые создает Matplotlib, но с меньшим количеством строк кода и более высоким разрешением.

Например, круговой график Matplotlib,

import matplotlib.pyplot as pltfig, ax = plt.subplots()x = [1, 2, 3, 4, 5]y = [2, 5, 8, 2, 7]for x,y in zip(x,y):     ax.add_patch(plt.Circle((x, y), 0.5, edgecolor = "#f03b20",facecolor='#9ebcda', alpha=0.8))#Use adjustable='box-forced' to make the plot area square-shaped as well.ax.set_aspect('equal', adjustable='datalim')ax.set_xbound(3, 4)ax.plot()   #Causes an autoscale update.plt.show()

который, в Bokeh, может быть создан с лучшим разрешением и функциональностью:

from bokeh.io import output_file, showfrom bokeh.models import Circlefrom bokeh.plotting import figurereset_output()output_notebook()plot = figure(plot_width=400, plot_height=400, tools="tap", title="Select a circle")renderer = plot.circle([1, 2, 3, 4, 5], [2, 5, 8, 2, 7], size=50)selected_circle = Circle(fill_alpha=1, fill_color="firebrick", line_color=None)nonselected_circle = Circle(fill_alpha=0.2, fill_color="blue", line_color="firebrick")renderer.selection_glyph = selected_circlerenderer.nonselection_glyph = nonselected_circleshow(plot)

2. Связь между графиками

В Bokeh также можно достаточно просто связывать графики. Изменение, примененное к одному графику, будет применено к другому графику с этой же переменной.

Например, если мы создаем 3 графика рядом и хотим наблюдать их взаимосвязь, мы можем связанное закрашивание

from bokeh.layouts import gridplot, rowfrom bokeh.models import ColumnDataSourcereset_output()output_notebook()source = ColumnDataSource(new_profile)TOOLS = "box_select,lasso_select,help"TOOLTIPS = [('user', '@user_name'),            ('followers', '@followers'),            ('following', '@following'),            ('forks', '@forks'),             ('contribution', '@contribution')]s1 = figure(tooltips=TOOLTIPS, plot_width=300, plot_height=300, title=None, tools=TOOLS)s1.circle(x='followers', y='following', source=source)s2 = figure(tooltips=TOOLTIPS, plot_width=300, plot_height=300, title=None, tools=TOOLS)s2.circle(x='followers', y='forks', source=source)s3 = figure(tooltips=TOOLTIPS, plot_width=300, plot_height=300, title=None, tools=TOOLS)s3.circle(x='followers', y='contribution', source=source)p = gridplot([[s1,s2,s3]])show(p)

Минусы

Поскольку Bokeh - это библиотека, которая имеет интерфейс среднего уровня, она часто требует меньше кода, чем Matplotlib, но требует больше кода для создания того же графика, чем Seaborn, Altair или Plotly.

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

Если мы не добавим ширину столбцов графика, то он будет выглядеть так:

from bokeh.transform import factor_cmapfrom bokeh.palettes import Spectral6p = figure(x_range=list(titanic_groupby['class']))p.vbar(x='class', top='survived', source = titanic_groupby,      fill_color=factor_cmap('class', palette=Spectral6, factors=list(titanic_groupby['class'])      ))show(p)

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

from bokeh.transform import factor_cmapfrom bokeh.palettes import Spectral6p = figure(x_range=list(titanic_groupby['class']))p.vbar(x='class', top='survived', width=0.9, source = titanic_groupby,      fill_color=factor_cmap('class', palette=Spectral6, factors=list(titanic_groupby['class'])      ))show(p)

Если вы хотите создать красивую столбчатую диаграмму, используя меньшее количеством кода, то для вас это может быть недостатком Bokeh по сравнению с другими библиотеками

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

Folium

Folium позволяет легко визуализировать данные на интерактивной встраиваемой карте. В библиотеке есть несколько встроенных тайлсетов из OpenStreetMap, Mapbox и Stamen

Плюсы

  1. Очень легко создавать карты с маркерами

Несмотря на то, что Plotly, Altair и Bokeh также позволяют нам создавать карты, Folium использует открытую уличную карту, что-то близкое к Google Map, с помощью минимального количества кода

Помните, как мы создавали карту для визуализации местоположения пользователей Github с помощью Plotly? Мы могли бы сделать карту еще лучше с помощью Folium:

import folium# Load datalocation_df = dp.Blob.get(name='location_df', owner='khuyentran1401').download_df() # Save latitudes, longitudes, and locations' names in a listlats = location_df['latitude']lons = location_df['longitude']names = location_df['location']# Create a map with an initial locationm = folium.Map(location=[lats[0], lons[0]])for lat, lon, name in zip(lats, lons, names):      # Create marker with other locations    folium.Marker(location=[lat, lon],                  popup= name,                  icon=folium.Icon(color='green')).add_to(m)    m

Живой вариант карты можно посмотреть в оригинале: https://towardsdatascience.com/top-6-python-libraries-for-visualization-which-one-to-use-fe43381cd658

2. Добавление потенциального местоположения

Если мы хотим добавить потенциальные местоположения других пользователей, Folium упрощает это, позволяя пользователям добавлять маркеры:

# Code to generate map here#....# Enable adding more locations in the mapm = m.add_child(folium.ClickForMarker(popup='Potential Location'))

Живой вариант карты можно посмотреть в оригинале: https://towardsdatascience.com/top-6-python-libraries-for-visualization-which-one-to-use-fe43381cd658

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

3. Плагины

У Folium есть ряд плагинов, которые вы можете использовать со своей картой, в том числе плагин для Altair. Что, если мы хотим увидеть карту пользовательской активности общего количества звездных пользователей Github в мире, чтобы определить, где находится большое количество пользователей Github с большим количеством звезд? Карта пользовательской активности в плагинах Folium позволяет вам это сделать:

from folium.plugins import HeatMapm = folium.Map(location=[lats[0], lons[0]])HeatMap(data=location_df[['latitude', 'longitude', 'total_stars']]).add_to(m)

Живой вариант карты можно посмотреть в оригинале: https://towardsdatascience.com/top-6-python-libraries-for-visualization-which-one-to-use-fe43381cd658

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

Вывод: Folium позволяет создавать интерактивную карту в несколько строк кода. Он дает вам ощущения близкие к использованию Google Map.

Заключение

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

Если вы все еще не знаете, какую библиотеку использовать для ваших данных, просто выберите ту, которая вам больше нравится. Затем, если код слишком громоздкий или график не так хорош, как вы думаете, просто попробуйте другую библиотеку!

Не стесняйтесь форкать и использовать код для этой статьи из этого репозитория на Github.

Мне нравится писать об основных концепциях data science и пробовать различные алгоритмы и инструменты анализа данных. Вы можете связаться со мной в LinkedIn и Twitter.

Отметьте этот репозиторий, если хотите изучить код всех статей, которые я писал. Следите за мной на Medium, чтобы быть в курсе моих последних статей по data science.


Узнать подробнее о курсе "Machine Learning. Basic"

Смотреть онлайн-интенсив Data Science это проще, чем кажется

Подробнее..

Перевод Линейная алгебра для исследователей данных

15.06.2021 14:10:21 | Автор: admin
Иллюстрация: UCIИллюстрация: UCI

Наша [Ирвинга Капланского и Пола Халмоша] общая философия в отношении линейной алгебры такова: мы думаем в безбазисных терминах, пишем в безбазисных терминах, но когда доходит до серьезного дела, мы запираемся в офисе и вовсю считаем с помощью матриц.

Ирвинг Капланский

Для многих начинающих исследователей данных линейная алгебра становится камнем преткновения на пути к достижению мастерства в выбранной ими профессии.

kdnuggetskdnuggets

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

Произведения векторов

Для двух векторов x, y их скалярным или внутренним произведением xy

называется следующее вещественное число:

Как можно видеть, скалярное произведение является особым частным случаем произведения матриц. Также заметим, что всегда справедливо тождество

.

x^Ty = y^Tx

Для двух векторов x , y (не обязательно одной размерности) также можно определить внешнее произведение xy . Это матрица, значения элементов которой определяются следующим образом: (xy) = xy, то есть

След

Следом квадратной матрицы A , обозначаемым tr(A) (или просто trA), называют сумму элементов на ее главной диагонали:

След обладает следующими свойствами:

  • Для любой матрицы A : trA = trA.

  • Для любых матриц A,B : tr(A + B) = trA + trB.

  • Для любой матрицы A и любого числа t : tr(tA) = t trA.

  • Для любых матриц A,B, таких, что их произведение AB является квадратной матрицей: trAB = trBA.

  • Для любых матриц A,B,C, таких, что их произведение ABC является квадратной матрицей: trABC = trBCA = trCAB (и так далее данное свойство справедливо для любого числа матриц).

TimoElliottTimoElliott

Нормы

Норму x вектора x можно неформально определить как меру длины вектора. Например, часто используется евклидова норма, или норма l:

Заметим, что x=xx.

Более формальное определение таково: нормой называется любая функция f : n , удовлетворяющая четырем условиям:

  1. Для всех векторов x : f(x) 0 (неотрицательность).

  2. f(x) = 0 тогда и только тогда, когда x = 0 (положительная определенность).

  3. Для любых вектора x и числа t : f(tx) = |t|f(x) (однородность).

  4. Для любых векторов x, y : f(x + y) f(x) + f(y) (неравенство треугольника)

Другими примерами норм являются норма l

и норма l

Все три представленные выше нормы являются примерами норм семейства lp, параметризуемых вещественным числом p 1 и определяемых как

Нормы также могут быть определены для матриц, например норма Фробениуса:

Линейная независимость и ранг

Множество векторов {x,x,...,x} называют линейно независимым, если никакой из этих векторов не может быть представлен в виде линейной комбинации других векторов этого множества. Если же такое представление какого-либо из векторов множества возможно, эти векторы называют линейно зависимыми. То есть, если выполняется равенство

для некоторых скалярных значений ,, - , то мы говорим, что векторы x,...,x линейно зависимы; в противном случае они линейно независимы. Например, векторы

линейно зависимы, так как x = 2x + x.

Столбцовым рангом матрицы A называют число элементов в максимальном подмножестве ее столбцов, являющемся линейно независимым. Упрощая, говорят, что столбцовый ранг это число линейно независимых столбцов A. Аналогично строчным рангом матрицы является число ее строк, составляющих максимальное линейно независимое множество.

Оказывается (здесь мы не будем это доказывать), что для любой матрицы A столбцовый ранг равен строчному, поэтому оба этих числа называют просто рангом A и обозначают rank(A) или rk(A); встречаются также обозначения rang(A), rg(A) и просто r(A). Вот некоторые основные свойства ранга:

  • Для любой матрицы A : rank(A) min(m,n). Если rank(A) = min(m,n), то A называют матрицей полного ранга.

  • Для любой матрицы A : rank(A) = rank(A).

  • Для любых матриц A , B np: rank(AB) min(rank(A),rank(B)).

  • Для любых матриц A,B : rank(A + B) rank(A) + rank(B).

Ортогональные матрицы

Два вектора x, y называются ортогональными, если xy = 0. Вектор x называется нормированным, если ||x|| = 1. Квадратная м

атрица U называется ортогональной, если все ее столбцы ортогональны друг другу и нормированы (в этом случае столбцы называют ортонормированными). Заметим, что понятие ортогональности имеет разный смысл для векторов и матриц.

Непосредственно из определений ортогональности и нормированности следует, что

Другими словами, результатом транспонирования ортогональной матрицы является матрица, обратная исходной. Заметим, что если U не является квадратной матрицей (U , n < m), но ее столбцы являются ортонормированными, то UU = I, но UU I. Поэтому, говоря об ортогональных матрицах, мы будем по умолчанию подразумевать квадратные матрицы.

Еще одно удобное свойство ортогональных матриц состоит в том, что умножение вектора на ортогональную матрицу не меняет его евклидову норму, то есть

для любых вектора x и ортогональной матрицы U .

TimoElliottTimoElliott

Область значений и нуль-пространство матрицы

Линейной оболочкой множества векторов {x,x,...,x} является множество всех векторов, которые могут быть представлены в виде линейной комбинации векторов {x,...,x}, то есть

Областью значений R(A) (или пространством столбцов) матрицы A называется линейная оболочка ее столбцов. Другими словами,

Нуль-пространством, или ядром матрицы A (обозначаемым N(A) или ker A), называют множество всех векторов, которые при умножении на A обращаются в нуль, то есть

Квадратичные формы и положительно полуопределенные матрицы

Для квадратной матрицы A и вектора x квадратичной формой называется скалярное значение x Ax. Распишем это выражение подробно:

Заметим, что

  • Симметричная матрица A называется положительно определенной, если для всех ненулевых векторов x справедливо неравенство xAx > 0. Обычно это обозначается как

    (или просто A > 0), а множество всех положительно определенных матриц часто обозначают

    .

  • Симметричная матрица A называется положительно полуопределенной, если для всех векторов справедливо неравенство x Ax 0. Это записывается как

    (или просто A 0), а множество всех положительно полуопределенных матриц часто обозначают

    .

  • Аналогично симметричная матрица A называется отрицательно определенной

  • , если для всех ненулевых векторов x справедливо неравенство xAx < 0.

  • Далее, симметричная матрица A называется отрицательно полуопределенной (

    ), если для всех ненулевых векторов x справедливо неравенство xAx 0.

  • Наконец, симметричная матрица A называется неопределенной, если она не является ни положительно полуопределенной, ни отрицательно полуопределенной, то есть если существуют векторы x, x такие, что

    и

    .

Собственные значения и собственные векторы

Для квадратной матрицы A комплексное значение и вектор x будут соответственно являться собственным значением и собственным вектором, если выполняется равенство

На интуитивном уровне это определение означает, что при умножении на матрицу A вектор x сохраняет направление, но масштабируется с коэффициентом . Заметим, что для любого собственного вектора x и скалярного значения с справедливо равенство A(cx) = cAx = cx = (cx). Таким образом, cx тоже является собственным вектором. Поэтому, говоря о собственном векторе, соответствующем собственному значению , мы обычно имеем в виду нормализованный вектор с длиной 1 (при таком определении все равно сохраняется некоторая неоднозначность, так как собственными векторами будут как x, так и x, но тут уж ничего не поделаешь).


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


Подробнее..

Вкусовщина и AI как мы в Prisma Labs делали объективно субъективный автоматический улучшатель фотографий

09.03.2021 10:13:46 | Автор: admin

Привет, Хабр! Меня зовут Андрей, я занимаюсь R&D в Prisma Labs. В своё время наша команда провела весьма интересное исследование на тему автоматического улучшения фотографии, результатом которого стала фича AutoAdjustment в приложении Lensa, позволяющая в один клик сделать цветокоррекцию фото. В этом посте я хочу поделиться полученным в ходе проекта опытом. Расскажу, в чём заключается сложность этой задачи, где вас могут поджидать нежеланные грабли. Также покажу, на что способен разработанный нашей командой искусственный интеллект. Прочитав этот пост, вы вместе с нами пройдёте тернистый путь от красивой идеи до одной из киллер-фичей популярного приложения. Ну что, погнали?

Зачем вообще всё это нужно?

Почти каждый хоть раз делился собственными фотографиями в социальных сетях. Выкладывая свои фото, мы, конечно же, хотим, чтобы они набрали как можно больше лайков. Для этого часто приходится прибегать к различным техникам и инструментам для коррекции изображений. Так, например, перед тем как выложить фотографию в Instagram, мы можем наложить какой-то фильтр и исправить некоторые настройки фотографии: экспозицию, контраст, температуру, резкость и т.д. Как итог, мы тратим много собственного времени, при том что основная проблема кроется даже не в этом. Корректируя фотографию, мы, к сожалению, не всегда можем объективно оценить, насколько нам получилось её улучшить (особенно если мы с вами не профессиональные фотографы). Так, после наложения какого-либо фильтра вам может показаться, что фотография стала выглядеть лучше, а друзья могут не согласиться, сказав, что она потеряла естественность, и лучше бы вы вообще выложили оригинал. Получается, время было потрачено напрасно! Конечно же, всегда есть возможность обратиться к профессиональному фотографу или ретушеру, и тогда, скорее всего, обработка будет на высоте, но это тоже непросто и не бесплатно. Из этих мыслей и предположений родилась идея: как бы нам помочь пользователям сэкономить их время предложить решение, которое в один клик сделает их фотографии "лучше" (при помощи искусственного интеллекта, разумеется).

Теперь чуть больше по существу, что же мы хотели получить?

В нашем приложении Lensa присутствуют стандартные инструменты для редактирования фотографий: настройка экспозиции, контраста, теней, температуры, светлых участков и т.д. Нашей задачей было построить решение, которое позволит всего в один клик автоматически подобрать значения для каждой из этих настроек так, чтобы после их применения фотография стала нравиться аудитории сильнее, чем оригинал (по крайней мере, большинству). Мы не претендуем на новизну идеи автоматического улучшения фотографии, методов, которые делают что-то похожее, к моменту начала проекта уже существовало множество. Главной особенностью нашего подхода стала идея предсказывать значения настроек для инструментов, доступных через UI в нашем приложении.

Такой подход был выбран по нескольким причинам:

  • Наша модель только рекомендует пользователю изменения, которые он сам после вправе "донастроить". Юзер сможет скорректировать только те значения, которые ему кажутся ошибочными.

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

На рисунке ниже представлена схема нашей модели.

Схема модели автоматического улучшения фотографииСхема модели автоматического улучшения фотографии

Одна из основных "фишек" нашего редактора состоит в том, что после "автоулучшения" фотография должна остаться естественной. По этой причине некоторые из инструментов вроде зернистости или выцветания (grain/fade) мы исключили из нашего подхода, так как они делают фотографию менее реалистичной (на рисунке выше предсказанные значения нулевые).

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

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

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

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

  • Ко всему прочему, мы не хотим, чтобы модель предсказывала специфичный стиль, например, настройки для перевода в чёрно-белые тона. Многие профессиональные фотографы любят делать нестандартную авторскую обработку. В связи с этим поставить чёткое тз для разметки не самая тривиальная задача.

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

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

Что означают эти названия у нас в Lensa?

Ответ основан на понимании того, что ожидает пользователь, когда выставляет значение теней на 50/100, а значение контрастности на -20/100. Как оказалось, опытный юзер предполагает, что тёмные области (тени) станут светлее, и при этом вся фотография станет более серой (из-за уменьшения контрастности). Ровно так, опираясь на то, чего ждут от нас пользователи, и к чему они привыкли, мы выстраивали эти инструменты.

Подбросим в топку чуть больше математики. Когда инженеры пытаются делать какие-то манипуляции с картинкой, в первую очередь вспоминают про различные цветовые пространства. Самое распространённое RGB, но в нём гораздо сложнее реализовать наши инструменты, чем в тех, где за цвет и яркость отвечают раздельные компоненты. Все операции с изображением мы выполняем в пространстве LAB (L канал отвечает за интенсивность).

Рассмотрим пару примеров наших инструментов, которые направлены в основном на L канал изображения: контраст и тени. На рисунке ниже представлены кривые инструментов (adjustments) контраста и теней для L канала, по OX исходное значение, по OY значение после применения инструмента.

Кривые для контраста и тенейКривые для контраста и теней

Каждая кривая (красная, серая, зелёная) соответствует некоторому значению этого инструмента.

adjusted=A(x, \alpha)

здесьx исходное значение L канала для некоторого пикселя,adjusted во что перейдетx после применения инструмента,\alpha значение "ползунка" инструмента (сила применения),A кривая (функция) инструмента (adjustment). В случае изображения кривые применяются попиксельно.

Теперь про кривые: зеленая кривая соответствует максимальному значению инструмента тому, как будет преобразован L канал для максимального значения ползунка (+100). Красная кривая, напротив, соответствует минимальному (-100). Пунктирная линия отражает преобразование при нулевом значении\alpha(тождественное).

Инжиниринг кривых не самая тривиальная задача!

Разберёмся с формой кривых на примере контраста. Когда вы увеличиваете контраст у картинки (зелёная кривая), то хотите, чтобы пиксели, которые были светлыми, становились еще более светлыми, а пиксели, которые были тёмными, получались еще более тёмными, при этом серые особо не менялись. Ровно это и позволяет нам сделать кривая контраста: значения ближе к 50 (серый) меняются слабо, зато области, которые находятся ближе к квантилям 1/4 и 3/4, изменяются сильнее всего. В случае с красной кривой всё наоборот: белые и чёрные участки тянутся к серому, поэтому зелёная и красная кривые проходят по разные стороны от пунктирной. С тенями всё еще проще, когда вы поднимаете тени, то хотите, чтобы высветились тёмные участки, а светлые участки почти не менялись. К каналам a и b также применяются некоторые преобразования, но для простоты опустим эти детали.

А что с промежуточными значениями?

Можно линейно проинтерполировать результат максимума или минимума с оригиналом (в зависимости от знака альфы). Так, например, если мы хотим узнать, чему будет равен результат при значение ползунка в 50/100, то сначала считаем, чему будет равен результат при максимальном значении, а затем смешиваем с оригиналом с весами 0.5 (формула ниже).

\dfrac{1}{2}x+\dfrac{1}{2}A(x, \alpha_{max})

Обратимость кривых и предлагаемый подход

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

Обратимость кривой инструментов (adjusts)Обратимость кривой инструментов (adjusts)

Если не вдаваться в детали, то обратимость функции (кривой)Aозначает, что для неё можно построить такую функциюA^{-1}, что для каждого значенияxи каждого допустимого значения\alphaверно следующее утверждение:

A(A^{-1}(x, \alpha), \alpha)=x

Допустим, мы построили такие функции, что все они являются обратимыми, что же дальше? Для упрощения задачи давайте представим, что нам нужно предсказать значение для одного инструмента, например, контраста. То есть наша модель должна принимать на вход исходную фотографию юзераxс "плохим" контрастом и предсказывать такое значение\alpha, чтобы применённая с силой\alphaфункция контраста давала на выходе "идеально контрастную" фотографиюI.В каком случае вообще представляется возможным предсказать такое значение\alphaчтобы получитьI? Это выполнимо, если существует некоторое\alpha, для которого справедливо следующее утверждение:

x=A^{-1}(I, \alpha)

Это предположение является одним из самых важных моментов в нашем подходе.

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

Допустим, у нас есть сет таких "идеальных" фотографийI. Тогда, "испортив" их обратной функцией для данного инструмента (в нашем случае контраста), мы получим обучающую выборку с триплетами\big(I,\alpha,x=A^{-1}(x,\alpha)\big)и будем учить нашу модель предсказывать\alphaпо входуx.

Но у нас же несколько инструментов!

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

\begin{cases}x_{0}=I\\x_{i}=A^{-1}_{N-i+1}(x_{i-1}, \alpha_{N-i+1}),\ \ \ i=1...N\end{cases}

Если у насNинструментов, тоx_{N} результат применения композиции их обратных функций.

Ниже представлена иллюстрация парадигмы с триплетами для случая одного инструмента.

Схема обучения модели с триплетамиСхема обучения модели с триплетами

На рисунке выше CI=InvA(I, alpha) искажение "идеальной" фотографии путём наложения некоторой обратной функции со значением alpha. M(CI) предсказание обучаемой модели.

Мы выбрали подход к обучению с ухудшением идеального фото с помощью обратных преобразований по нескольким причинам:

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

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

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

Важнейшая составляющая. Откуда мы брали данные для обучения?

Есть же размеченные данные, что с ними не так?

В начале работы над проектом мы пытались использовать некоторые open-source данные от фотографов вроде MIT-Adobe FiveK Dataset (его используют в большинстве статей по автоулучшению фото). Достаточно быстро мы поняли, что все найденные open-source датасеты нам не подходят по нескольким причинам:

  • Наш таргетинг селфи и портретные фото, во всех открытых датасетах таких изображений малая доля.

  • В датасетах вроде MIT FiveK данные размечены очень специфично множество фотографий выглядят хоть и эффектно, но весьма неестественно заметно, что они были отредактированы.

Мы пробовали размечать данные командой, но столкнулись с проблемой. Каждый выбирал настройки, исходя из собственных вкусов, поэтому в данных не было видно явного тренда, и получившаяся разметка была очень разнородной и даже противоречивой. В связи с этим мы выбрали одного асессора, и в итоге весь датасет был размечен одним человеком с экспертизой в области фото (будем называть его "судьёй"). После этого нашей задачей было уже не обучить объективный "улучшатель" фотографий, а научить модель размечать фотографии так же, как наш судья.

По итогу в датасет попало порядка 1000 фотографий (оригиналы, идеалы и конфиги). Немного, но этого оказалось достаточно.

Одна за всех или все за ...

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

  • Эффекты от наложения некоторых инструментов очень скоррелированы. Существует множество конфигов, применяя которые, мы получаем почти одинаковый выход. Обучаться модели с таким условием тяжелее нужно учить все инструменты одновременно. Более того, нужно выучить не только правильный конфиг, а ещё понять, как применение всего этого конфига (всех инструментов одновременно) влияет на фотографию.

  • Модель почти всегда видит сложные сэмплы если мы сразу портим фотографию композицией всех обратных инструментов, то в большинстве случаев модели на вход будет приходить изображение, на котором плохо абсолютно всё: плохая экспозиция, плохой контраст, плохие тени и т.д. Сэмплировать искажения так же, как "in the wild", мы не можем, так как не знаем, как выглядит их априорное распределение в реальном мире.

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

У такого подхода тоже есть свои проблемы:

  • Во время обучения определённой модели, например, контраста, сеть не видит фотографии с испорченными тенями, что не является правдой для in the wild. Мы пробовали добавлять аугментациями искажения других инструментов (кроме того, который учим), но опять столкнулись с проблемой корреляции эффектов и усложнили каждой модели задачу.

  • Данный подход сложнее в интеграции на девайс и гораздо более трудозатратен в плане обучения: вам нужно последовательно обучить N сеток вместо всего одной.

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

Функция потерь. Картинки укажут вам более правильный путь, чем конфиги

Сначала мы пробовали, используя регрессионный лосс на паре (predicted_alpha, gt_alpha), учить модель предсказывать верное значение инструмента, но наступили на очередные грабли. У такого подхода есть как минимум два минуса.

Во-первых, как упоминалось выше, может существовать несколько различных конфигов для инструментов, которые приводят к одному и тому же результату. Но, если мы учим отдельно по одной модели на каждый инструмент при фиксированных остальных инструментах (которые не участвуют в обучении), найдётся ровно один правильный ответ для восстановления искажений инструмента, под который мы обучаем модель. Так что для подхода "одна модель под один инструмент" это не стало серьёзным препятствием.

Вторая проблема оказалась более серьёзной, и здесь нас уже не спас подход с обучением своей сетки под каждый инструмент. Допустим, мы хотим исправить насыщенность картинки. Мы знаем, что оригинальное изображение было испорчено со значением -90, а мы предсказали 50. Правда ли, что вне зависимости от исходного изображения мы всегда должны одинаково штрафовать нашу модель? Оказывается, что это не так.

За наглядным примером проблемы и ходить далеко не нужно!
Искажение насыщенности на -90 для двух разных оригиналовИскажение насыщенности на -90 для двух разных оригиналов

В приведённом примере абсолютная разница между предсказанным и истинным значениями одинакова для обоих случаев, при этом мы явно должны штрафовать модель для Case 2 сильнее (мы не восстановим оригинал с предсказанием 50). В то же время, в случае Case 1 для любого предсказания модели мы получаем одинаковый результат в точности оригинал (изменение насыщенности не меняет чёрно-белую фотографию). Получается, что мы должны штрафовать не за разницу в конфигах, а за то, насколько результат применения предсказанного конфига отличается от желанного изображения.

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

Получается, что функция потерь должна штрафовать разницу между изображениями, а не между конфигами. Например, мы можем использовать L2-норму между изображениями:

Loss=MSE(I, A(M(CI))

Важно отметить, что предложенная функция потерь MSE между изображениями, дифференцируема по предсказанному значению, так как каждая наша функция каждого инструмента дифференцируема по нему.

Мы обучили модель. Как понять, что она делает что-то адекватное?

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

Первая эвристика основана на идее о том, что модель не должна корректировать свои же предсказания. Мы назвали такое свойство "сходимостью".

Cходимость первый признак адекватности модели!

Допустим, мы обучили модель для улучшения контрастности. Подаём некоторую фотографию (лучше ту, на которой есть явные проблемы с контрастом) в нашу модель. Модель предсказывает некоторое число P1. Затем применяем к этой фотографии инструмент контраста со значением P1 и получаем новую исправленную фотографию. Подаём её в модель,получаем число P2 и т.д. Так вот, мы считали, что модель является "адекватной", если эта последовательность (P1, P2, P3, ) достаточно быстро сходится. В идеале значения (P2, P3, ) должны быть около нуля.

Иллюстрация сходимости моделиИллюстрация сходимости модели

Второе наблюдение чем сильнее мы испортили фотографию, тем большее (по абсолютной величине) значение наша модель должна предсказывать.

Хорошая модель монотонная модель

На рисунке выше по оси X отложено значение, с которым мы портили фотографию, а по оси Y предсказание модели. Видно, что модель не идеально предсказывает значения, с которыми портится фотография, но отношение порядка сохраняет. Связано это с тем, что модели достаточно просто понять, что на картинке нужно поднять или опустить контрастность и масштаб этого значения (относительно своего априорного идеала), а вот точное абсолютное значение угадать сложно (ещё один пунктик в сторону того, что обучать с функцией потерь именно на конфиги очень сложно). Также во время обучения мы старались подбирать такие распределения для параметров искажения (на основе размеченных данных), чтобы фотографии всё ещё выглядели более-менее естественными, поэтому модель не видела совсем испорченные фотографии (хвосты кривой).

Пристегнитесь, сдедующая остановка используемые метрики!

Долгое время в качестве метрик мы использовали только визуальный тест. Заключался он в следующем: мы показывали нашему судье две фотографии и просили выбрать, какая выглядит лучше. При этом мы также просили дать оценку от 1 до 5 (насколько выбранный вариант выглядит лучше конкурента). Это позволяло нам как сравнивать обученные модели, так и понимать, насколько мы далеки от разметки судьи. Важнейшей проблемой такой метрики является то, что этот процесс совершенно не автоматизирован и требует слишком много времени и усилий каждый раз нам нужно было строить несколько кандидатов (обучать несколько моделей) и для каждого нового кандидата просить нашего судью проходить такой визуальный тест. По этой причине нам пришлось думать над альтернативными метриками хотя бы для уменьшения количества проводимых визуальных тестов (для отсеивания заведомо неудачных моделей).

Итак, мы хотим придумать метрики, которые помогут нам ответить на вопрос, насколько наши новые модели стали ближе к поведению судьи.

Первая метрика была построена с целью понять, насколько сильно изменится результат обработки, если в настройках судьи (gt config) заменить значение одного инструмента, например, контраста, на предсказанное моделью значение. Схема расчёта метрики приведена на рисунке ниже.

Схема подсчёта первой используемой метрикиСхема подсчёта первой используемой метрики

Спустя некоторое время мы поняли, что у такой метрики есть один важный недостаток. Допустим, наша модель на схеме выше (при расчёте метрики) предсказала\alphaдля контраста. У наших инструментов есть четкий порядок применения, и проблема состоит в том, что, на самом деле, контраст идёт вторым в очереди (то есть до теней), а не последним. Даже с учётом того, что мы сохраняем порядок применения всех инструментов после предсказания, мы "подсовываем" модели не совсем истинный вход. В реальном мире (в самом редакторе), даже если обученные модели для экспозиции и теней будут всегда предсказывать правильные значения, модель для контраста, скорее всего, не предскажет то же самое значение\alpha, так как на вход поступит изображение с ещё не исправленными тенями (исправление теней влияет на контраст фотографии).

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

Предположим, мы хотим настраивать экспозицию, контраст и светлые участки. Мы знаем, что сначала идёт экспозиция, затем контраст и только после настройка для светлых участков. Пусть у нас уже есть какое-то приближение для нашего решения модели, которые предсказывают значения для всех указанных инструментов (в самом начале это могут быть просто константные нулевые предсказания). Допустим, мы выучили новую модель для светлых участков. Чтобы проверить, сможет ли только она улучшить качество всего имеющегося пайплайна, мы заменим в лучшем наборе ровно одну модель по светлым участкам и проверим, стал ли предсказанный результат ближе к разметке судьи. Если да, то мы обновляем в лучшем множестве старую модель на новую и переходим к контрасту уже с новой зафиксированной моделью по светлым участкам (затем только к экспозиции).Схема одного предлагаемого шага обновления лучшего набора моделей представлена ниже.

Схема подсчёта второй используемой метрикиСхема подсчёта второй используемой метрики

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

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

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

Везде грабли-грабли-грабли, а что же мы получили в итоге?

Если долго мучиться, всё обязательно получится! Нам удалось построить модели, которые устроили нас по визуальному качеству, удовлетворяли нашим эвристикам и показывали позитивные результаты на приведённых метриках. Сейчас эти модели встроены в наш инструмент автоулучшения фотографии (Autoadjustments) в приложении Lensa. Ниже парочка примеров селфи с результатами работы пайплайна. Ждём вас в гости в приложении, если подумали, что это "черри-пики" :)

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

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

Я рассказал вам про одну из наших интереснейших технологий. На собственном опыте показал, как уберечь себя от лишних шишек (мы уже набили их за вас). Берясь за проекты, связанные с обработкой и улучшением фото, формулируйте задачи как можно точнее и понятнее, чтобы лишний раз не натыкаться на проблему субъективизма.

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

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

Подробнее..

Перевод Глубокие нейронные деревья принятия решений

20.01.2021 00:15:59 | Автор: admin

Описание

Глубокие нейронные сети доказали свою эффективность при обработке данныхот органов чувств, таких,как изображения и аудио. Однако для табличных данных более популярны древовидные модели. Хорошим свойством древовидных моделей является их естественная интерпретируемость. В этой работе мы представляемDeepNeuralDecisionTrees(DNDT)древовидныемодели, реализованные нейронными сетями. DNDT внутренне интерпретируем, так как это дерево. Тем не менее, поскольку это также нейронная сеть (NN), ее можно легко реализоватьс помощьюинструментарияNN и обучитьпо алгоритмуградиентногоспуска, а непо жадному алгоритму (алгоритмужадного разбиения). Мы проводим оценку DNDT на нескольких табличных наборах данных, проверяем его эффективность и исследуем сходства и различия между DNDT и обычными деревьями решений. Интересно, что DNDT самообучаетсякак на разделенном, так и на функциональном уровне.

  1. Введение

Интерпретируемость прогностических моделей важна, особенно в тех случаях, когда речь идет об этикеправовой, медицинскойи финансовой,критически важных приложениях, где мы хотим вручную проверитьрелевантностьмодели. Глубокие нейронные сети (Lecunetal., 2015 [18];Schmidhuber, 2015 [25]) достигли превосходных результатов во многих областях, таких как компьютерное зрение, обработка речи и языковое моделирование. Однако отсутствие интерпретируемости не позволяет использоватьв приложенияхэто семейство моделейкак черныйящик, для которогомы должны знатьпроцедурупрогноза, чтобыверифицироватьпроцесс принятия решения. Более того, в некоторых областях, таких как бизнес-аналитика (BI), часто более важно знать, как каждый фактор влияет на прогноз, а не сам вывод. Методы, основанные на дереве решений (DT), такие как C4.5 (Quinlan, 1993 [23]) и CART (Breimanetal., 1984 [5]), имеют явное преимущество в этом аспекте, поскольку можно легко проследить структуру дерева и точно проверить, как делается прогноз.

В этой работе мы предлагаем новую модель на пересечении этих двух подходов глубокое нейронное дерево решений (DNDT),исследуем егосвязи с каждым из них. DNDT- это нейронные сети со специальной архитектурой, где любой выборвесов DNDT соответствует определенному дереву решений и поэтому интерпретируем. Однако, поскольку DNDT реализуется нейронной сетью (NN), она наследует несколько интересных свойств, отличныхоттрадиционныхDT: DNDT может быть легкореализованнесколькимистрокамикода в любом программном фреймворке NN; все параметры одновременно оптимизируются с помощью стохастического градиентного спуска, а не более сложной и потенциально неоптимальной процедурыжадногорасщепления.DNDTготов к крупномасштабной обработке с обучением на основе мини-патчейи ускорением GPUот коробочного решения, его можно подключить к любой более крупной модели NN в качестве строительного блока для сквозного обучения с обратным распространением (back-propagation).

2.Похожие работы

Модели на основе деревьев решений.Древовидные модели широко используются в обучении под наблюдением, например, взадачахклассификации. Они рекурсивно разбивают входное пространство и присваивают метку/оценку конечному узлу. Хорошо известные древовидные моделииспользуютC4. 5 (Quinlan, 1993 [23]) и CART (Breimanetal., 1984 [5]). Ключевым преимуществом древовидных моделей является то, что они легко интерпретируются, поскольку предсказания задаются набором правил. Также часто используется ансамбль из нескольких деревьев, таких какслучайный лес(Breiman, 2001 [6]) иXGBoost(Chen&Guestrin, 2016 [8]), чтобы повысить производительность за счет интерпретируемости. Такие древовидные модели часто конкурируют или превосходят нейронные сети в задачах прогнозирования с использованием табличных данных.

Интерпретируемые модели. По мере того как предсказания, основанные на машинном обучении,используютсяповсеместнои затрагивают многие аспекты нашей повседневной жизни, фокус исследований смещается от производительности модели (например, эффективности и точности) к другим факторам, таким как интерпретируемость (Weller, 2017 [26];Doshi-Velez, 2017 [11]). Это особеннонеобходимов приложениях, где существуютпроблемыэтические (Bostrom&Yudkowsky, 2014 [4]) или безопасности, и предсказания моделей должны быть объяснимы, чтобы проверить правильность процесса рассуждения или обосновать решениядля них. В настоящее время предпринимается ряд попыток сделать моделиобъяснимыми.Некоторые из них являются модельно-агностическими (Ribeiroetal., 2016 [24]), в то время как большинство из них связаны с определенным типом модели, например, классификаторамина основе правил (Dashetal., 2015 [10];Malioutovetal., 2017 [19]), моделями ближайших соседей (Kimetal., 2016 [15]) и нейроннымисетями (Kimetal., 2017 [16]).

Нейронные сети и деревья решений. В некоторых исследованиях предлагалось унифицировать модели нейронной сети и деревьев решений.Bul&Kontschieder(2014) [7] предложили нейронныелеса решений( Neural Decision Forests NDF) как ансамбль нейронных деревьев решений, где расщепленные функции реализуютсяслучайнымимногослойными персептронами.Deep-NDF (Kontschiederetal., 2015 [17]) использовал стохастическую и дифференцируемую модель дерева решений, которая совместно изучает представления (черезCNNs) и классификацию (через деревья решений). Предлагаемый нами DNDT во многом отличается от этих методов. Во-первых, у нас нет альтернативной процедуры оптимизации для изучения структуры (разделения) и обучения параметров (матрица оценок). Вместо этого мы изучаем их все с помощьюоднопроходногообратного распространения (back propagation). Во-вторых, мы не ограничиваем разбиение двоичным (левым или правым), поскольку мы применяем дифференцируемую функцию разбиения, которая может разбивать узлы на несколько ( 2) листьев. Наконец, что наиболее важно, мы разрабатываем нашу модель специально для интерпретируемости, особенно для приложенийк табличным данным, где мы можем интерпретировать каждую входную функцию. Напротив, модели в (Bul&Kontschieder, 2014 [7];Kontschiederetal., 2015 [17]) предназначены для прогнозирования и применяются к необработанным данным изображения. Некоторые проектные решения делают их непригодными для табличных данных. Например, вKontschiederetal. (2015 [17]), они используют менее гибкое дерево, в котором структуражесткофиксируется, пока изучается разбиение узла.

Несмотря на похожее название, наша работа кардинально отличается от работыБалестриеро(2017 [2]), которая разработала своего рода наклонное дерево решений, реализованное нейронной сетью. В отличие от обычных одномерных деревьев решений, каждый узел внаклонномдереве решений включает в себя все функции, а не одну функцию, что делает модель не интерпретируемой.

Альтернативныеактиваторыдереварешений.ОбычныеDT изучаются рекурсивнымжаднымрасщеплением признаков (Quinlan, 1993;Breimanetal., 1984 [23]). Это эффективно и имеет некоторые преимущества для выбора признаков, однако такойжадныйпоиск может быть неоптимальным (Norouzietal., 2015 [20]). В некоторых недавних работах исследуются альтернативные подходы к обучению деревьев решений, которые направлены на достижение лучшей производительности при менеетребовательнойоптимизации, например с помощью латентного структурированного прогнозирования переменных (Norouzietal., 2015 [20]) или обучения контроллера расщепления RNN с использованием обучения с подкреплением (Xiongetal., 2017 [28]). Напротив,нашDNDT намного проще, чемуказанные, но все же потенциально может найти лучшие решения, чем обычные индукторы DT,содновременным поискомструктурыи параметровдерева с SGD. Наконец, также отмечаем, что в то время как обычные активаторы DT используют только двоичные расщепления(для простоты), наша модель DNDT может одинаково легко работать с расщеплениями произвольной мощности, что иногда может привести к более интерпретируемым деревьям.

3.Методология

3.1.Функция мягкого контейнера

Основной модуль, который мы здесь реализуем, - это функция мягкой ячейки(Doughertyetal., 1995)или объединения множества точечных объектов в динамические полигоны (бины),которуюмы будем использовать для принятия решений о разделении в DNDT. Как правило, функциябиннингапринимает в качестве входных данныхвещественныйскаляр x и выдает индексячейки, которойонпринадлежит.Жесткоеразделение по ячейкамнедифференцируемое, поэтому мы предлагаем дифференцируемую аппроксимацию этой функции.

Предположим, что у нас есть непрерывная переменная x, которую мы хотим разбить на N + 1 интервалов. Это приводит к необходимости n точек среза, которые в данном контексте являются обучаемыми переменными. Обозначим точки среза [1, 2,, n]какмонотонно возрастающие, то есть 1 < 2 < < n. Во времяобученияпорядок может быть перетасован после обновления, поэтому мы должны сначала сортировать их в каждом прямом проходе. Однако это не повлияет на дифференцируемость, потому что сортировка просто меняет местами позиции .

Теперь построим однослойную нейронную сеть с функцией активацииsoftmax.

Здесь w-это константа, а не обучаемая переменная, и ее значение задается как w = [1; 2;:: : ; n + 1]. b строится как,

> 0 - фактор напряженности. При 0 выход стремится ктекущемувектору.

Мы можем проверить это, проверив три последовательныхлогита

o_{i-1},o_i, o_{i+1}.

Когда у нас есть как

o_i> o_{i-1} (при \quad x > _i), так \quad и \quad o_i> o_{i+1} (при \quad x < _{i+1}),

x должен попасть в интервал

(_i, _{i+1}).

Таким образом, нейронная сеть в уравнении 1 будет производить почти однократноегорячеекодированиеячейкиx, особенно при более низкой напряженности. При желании,мы можем применить трюкотжига наклона(Chungetal., 2017 [9]), который постепенно снижает напряжение приобучении, чтобы,в конце концов,получить более детерминированную модель.

Если кто-то предпочитает фактическийгорячий (текущийкодируемый)вектор, можно применитьStraight-Through(ST)Gumbel-Softmax(Jangetal., 2017): для прямого прохода мысэмплируемоднократный вектор, используя хитрость с Gumbel-Max, тогда как для обратного прохода (backward pass) мы используемGumbel-Softmaxпривычисленииградиента (см.Bengio(2013 [3]) для более подробного анализа.

На рис.1 показан конкретный пример, где мы имеем скаляр x в диапазоне [0, 1] и две точки среза в 0.33 и 0.66 соответственно. Основываясь на уравнениях1 и 2, мы имеем трилогитаo1 = x, o2 = 2x 0.33, o3 = 3x 0.99.

Рисунок 1. Конкретный пример нашей функции мягкого биннинга с использованием точек среза в 0.33 и 0.66. Ось x - это значение непрерывной входной переменной x2 [0; 1]. Вверху слева: исходные значения логитов; вверху справа: значения после применения функции softmax с т = 1; Внизу слева: т= 0.1; внизу справа: т = 0.01.Рисунок 1. Конкретный пример нашей функции мягкого биннинга с использованием точек среза в 0.33 и 0.66. Ось x - это значение непрерывной входной переменной x2 [0; 1]. Вверху слева: исходные значения логитов; вверху справа: значения после применения функции softmax с т = 1; Внизу слева: т= 0.1; внизу справа: т = 0.01.

3.2Построениепрогнозов

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

x \in R^D \, c \, функциями \,D

Связывая каждый признакxd со своей собственной нейронной сетьюfd(xd), мы можем исчерпывающе найти все конечные узлы с помощью,

Здесь z теперь также является почтигорячимвектором, который указывает индекс листового узла, куда поступает экземпляр x. Наконец, мы предполагаем, что линейный классификатор на каждом листе z классифицирует поступающие туда экземпляры. DNDT проиллюстрирован на Рис. 2.

Рисунок 2. Изученный DNDT для набора данных Iris (сокращенная версия с двумя функциями). Вверху: DNDT - показано, где красным шрифтом указаны обучаемые переменные, а черным константы. Внизу: DT визуализация той же сети, что и обычное дерево решений. Дроби указывают маршрут случайно выбранных 6 классифицируемых экземпляров.Рисунок 2. Изученный DNDT для набора данных Iris (сокращенная версия с двумя функциями). Вверху: DNDT - показано, где красным шрифтом указаны обучаемые переменные, а черным константы. Внизу: DT визуализация той же сети, что и обычное дерево решений. Дроби указывают маршрут случайно выбранных 6 классифицируемых экземпляров.

3.3Обучение дерева

С помощью описанного выше метода мы можем направлять входные экземпляры в конечные узлы и классифицировать их. Таким образом, обучение дерева решений теперь становится вопросом обученияв узловыхточкахсрезаи листьевклассификаторов. Поскольку все шаги нашего прямого прохода дифференцируемы, все параметры (Рис. 2, Красный) теперь могут бытьнапрямуюи одновременно обучены с помощью SGD.

Обсуждение. DNDT хорошо масштабируется с количеством экземпляров благодаря мини-пакетному обучению в стиле нейронной сети. Однако ключевым недостатком этогостилядо сих пор является то, что из-за использования продуктаKroneckerон не масштабируется по количеству функций. В нашей текущей реализации мы избегаем этой проблемы с "широкими" наборами данных, обучаялесслучайногоподпространства(Ho, 1998 [13]) - за счет нашей интерпретируемости. То есть вводится несколько деревьев, каждое из которых обучается на случайном подмножестве признаков. Лучшим решением, которое не требует неинтерпретируемоголеса, является использование разреженности конечногоразделения на ячейкиво время обучения: количество непустых листьев растет намного медленнее, чем общее количество листьев. Но это несколько усложняет простую в остальном реализацию DNDT.

4.Эксперименты

4.1Реализация

DNDT концептуально прост и легок в реализации с помощью 20 строк кода вTensorFlowилиPyTorch. Поскольку он реализован как нейронная сеть, DNDT поддерживает "из коробки" ускорение GPU и мини-пакетное обучение наборов данных, которые не помещаются в память, благодаря современным фреймворкам глубокого обучения.

4.2Наборы данных и конкуренты

Мы сравниваем DNDT с нейронными сетями (реализованнымиTensorFlow(Abadietal., 2015) [1]) и деревом решений (отScikit-learn(Pedregosaetal., 2011 [22])) на 14 наборах данных, собранных изKaggleи UCI (подробности набора данных см. В табл. 1).

Для базовой линии дерева решений (DT) мы установили два ключевых критериягиперпараметров: критерий'gini' иразделитель 'best'. Для нейронной сети (NN) мы используем архитектуру из двух скрытых слоев по 50 нейронов в каждом для всех наборов данных. DNDT также имеетгиперпараметр-количество точек среза для каждого объекта (коэффициент ветвления), который мы устанавливаемравным1 для всех объектов и наборов данных. Подробный анализ эффекта этогогиперпараметраможно найти в разделе 4.4.Длянаборовданных с более чем 12 признаками,мы используем ансамбль DNDT, где каждое дерево выбирает 10 признаков случайным образом, и у нас есть 10уровнейв общей сложности. Окончательный прогноз дается большинством голосов.

4.3Точность

Мы оцениваем производительность DNDT, дерева решений инейросетевыхмоделей на каждом из наборов данных в Табл. 1. точность тестового набора представлена в Табл.2.

В целом наиболее эффективной моделью является DT. Хорошая производительность DT неудивительна, поскольку эти наборы данных в основном табличные, а размерность объектов относительно невелика.

Таблица 1. Коллекция из 14 наборов данных от Kaggle (обозначается буквой (K)) и UCI: количество экземпляров (#inst.), количество объектов (#feat.) и количество классов (#cl.)Таблица 1. Коллекция из 14 наборов данных от Kaggle (обозначается буквой (K)) и UCI: количество экземпляров (#inst.), количество объектов (#feat.) и количество классов (#cl.)Таблица 2. Точность тестового набора каждой модели: DT: дерево решений. NN: нейронная сеть. DNDT: наше глубокое нейронное дерево решений, где ( * ) указывает, что используется ансамблевая версия.Таблица 2. Точность тестового набора каждой модели: DT: дерево решений. NN: нейронная сеть. DNDT: наше глубокое нейронное дерево решений, где ( * ) указывает, что используется ансамблевая версия.

Условно говоря, нейронные сети не имеют явного преимущества в отношении такого рода данных. Однако DNDT немного лучше, чемванильнаянейронная сеть, так как она ближе к дереву решений попостроению. Конечно, это только ориентировочный результат, так как все эти модели имеют настраиваемыегиперпараметры. Тем не менее интересно, что ни одна модель не обладает доминирующим преимуществом. Это напоминает теоремы об отсутствиибесплатного обеда(Wolpert, 1996[27]).

4.4Анализ активных точек среза

В DNDT количество точек среза на объект является параметром сложности модели. Мы не связываем значения точек среза, а это значит, что некоторые из них неактивны, например, они либо меньше минимальногоxd, либо больше максимальногоxd.

В этом разделе мы исследуем, сколько точек среза фактически используется после обучения DNDT. Точка среза активна, когда по крайней мере один экземпляр из набора данных попадает на каждую ее сторону. Для четырех наборов данных-CarEvaluation,Pima,IrisиHabermanмы устанавливаем количество точек среза на объект от 1 до 5 и вычисляем процент активных точек среза, как показано на рис. 3.Видно, что по мере увеличения числа точек среза их использование в целом уменьшается. Это означает, что DNDT несколько саморегулируется: он не использует все доступные ему параметры.

Рисунок 3. Доля (%) активных точек, используемых DNDT.Рисунок 3. Доля (%) активных точек, используемых DNDT.

Мы можем дополнительно исследовать, как количество доступных точек среза влияет на производительность этих наборов данных. Как видно из Рис. 4, производительность первоначально увеличивается с увеличением числа точек среза, а затем стабилизируется после определенного значения. Это обнадеживает, потому что это означает, что большие DNDT не слишком подходят к данным обучения, даже без явной регуляризации.

Рисунок 4. Точность тестирования DNDT для увеличения числа точек разреза (сложность модели).Рисунок 4. Точность тестирования DNDT для увеличения числа точек разреза (сложность модели).

4.5Анализ активных признаков

При обучении DNDT также возможно, что для определенного объекта все точки среза неактивны. Это соответствует отключению функции, чтобы она не влияла на прогнозирование,аналогично обычному ученику DT, который никогда не выбирает данную функцию, чтобызадать узел-распознавательв любом месте дерева. В этом разделе мы анализируем, как DNDT исключает функции таким образом. Мы запускаем DNDT 10 раз и записываем,сколькораз данный объект исключаетсяиз-за того,что все его точки среза неактивны.

Учитывая случайностьчерезинициализируемыевесадлямини-пакетнойвыборки, мы наблюдаем, что некоторые функции (например, функция индекса 0 вiris) последовательно игнорируются DNDT (см. табл. 3 для всех результатов). Это говорит о том, что DNDT делает некоторый неявныйвыбор объектов, выталкивая точки отсечениязаграницы данных для несущественных объектов. В качестве побочного продукта мы можем получить меру важности(веса)функции изнаборафункции в течение нескольких запусков: чем больше функция игнорируется, тем менее важной она, вероятно,ибудет.

Таблица 3. Процент ( % ) случаев, когда DNDT игнорирует каждую функциюТаблица 3. Процент ( % ) случаев, когда DNDT игнорирует каждую функцию

4.6Сравнение с деревом решений

Используя методы, разработанные в разделе 4.5, мы исследуем, благоприятствуют ли DNDT и DTсосходнымихарактеристиками. Мы сравниваем важность признака черезкритерийgini (Джини), используемыйв дереве решений (Рис. 5), с нашей метрикой скорости отбора (табл.3).

Рисунок 5. Рейтинг важности характеристик, произведенный DT (Gini).Рисунок 5. Рейтинг важности характеристик, произведенный DT (Gini).

Сравнивая эти результаты, мы видим, что иногда DNDT и DT предпочитаютвыбор признаков, например, дляIrisони оба оценивают Признак 3 как наиболее важный. Но бывает, что они также могут иметь разные взгляды, например, дляХаберманаDT выбрал функцию 0 как наиболее важную, тогда как DNDT полностью проигнорировал ее. На самом деле DNDT использует только функцию 2 для прогнозирования, которая занимает второе место поDT.Однако такого рода разногласия не обязательно могут привести к существенным различиям в производительности. Как видно из Табл. 2, дляХаберманаточность испытаний DNDT и DT составляет 70,9% и 66,1% соответственно.

Наконец, мы количественно оцениваем сходстворанжированийпризнаков DNDT и признаков DT, вычисляяTauкритерияКендаллаподвумрейтинговымспискам. Результаты, приведенные в Табл.4, свидетельствуют об умеренной корреляции в целом.

Таблица 4. Рейтинг функций DNDT и DT по Кендаллу: большие значения означают большее сходство.Таблица 4. Рейтинг функций DNDT и DT по Кендаллу: большие значения означают большее сходство.

4.7Ускорение GPU

Наконец, мы проверяем легкость ускорения обучения DNDT с помощью обработки графическим процессором - возможность, которая не является обычной или простойдля DT. Увеличивая количество точек отсечения для каждой функции, мы можем получить более крупные модели, для которых режим графического процессора имеет значительно меньшее время работы (см. Рис. 6).

Рисунок 6. Иллюстрация ускорения GPU: время обучения DNDT включено. 3,6 ГГц CPU против GTX Titan GPU. В среднем за 5 прогонов.Рисунок 6. Иллюстрация ускорения GPU: время обучения DNDT включено. 3,6 ГГц CPU против GTX Titan GPU. В среднем за 5 прогонов.

5.Заключение

Мы представили древовидную модель на основе нейронной сети DNDT. Он имеет лучшую производительность, чем NN для определенных наборов табличных данных, при этом обеспечиваетинтерпретируемое дерево решений. Между тем, по сравнению с обычными DT, DNDT проще в реализации, одновременно выполняет поиск в древовидной структуре и параметрах с помощью SGD и легко ускоряется на GPU. Есть много возможностей для будущей работы. Мы хотим исследовать источник наблюдаемой нами саморегуляции; изучить подключение DNDT как модуля, подключенного к обычному элементу обучения CNN, для сквозного обучения; выяснить, можно ли использовать обучение на основе SGD целого дерева DNDT в качестве постобработки для точной настройки обычных,жаднообученных DT и повышения ихпроизводительности; выяснить, можно ли использовать многие подходы кадаптивномуобучению на основе NN для обеспечения возможности переносаобучения для DT.

Ссылки
  1. Abadi, Martn, Agarwal, Ashish, Barham, Paul, Brevdo, Eugene, Chen, Zhifeng, Citro, Craig, Corrado, Greg S., Davis, Andy, Dean, Jeffrey, Devin, Matthieu, Ghemawat, Sanjay, Goodfellow, Ian, Harp, Andrew, Irving, Geoffrey, Isard, Michael, Jia, Yangqing, Jozefowicz, Rafal, Kaiser, Lukasz, Kudlur, Manjunath, Levenberg, Josh, Mane, Dandelion, Monga, Rajat, Moore, Sherry, Murray, Derek, Olah, Chris, Schuster, Mike, Shlens, Jonathon, Steiner, Benoit, Sutskever, Ilya, Talwar, Kunal, Tucker, Paul, Vanhoucke, Vincent, Vasudevan, Vijay, Viegas, Fernanda, Vinyals, Oriol, Warden, Pete, Wattenberg, Martin, Wicke, Martin, Yu, Yuan, and Zheng, Xiaoqiang. TensorFlow: Large-scale machine learning on heterogeneous systems, 2015. URL https://www.tensorflow.org/.

  2. Balestriero, R. Neural Decision Trees. ArXiv e-prints, 2017.

  3. Bengio, Yoshua. Estimating or propagating gradients through stochastic neurons. CoRR, abs/1305.2982, 2013.

  4. Bostrom, Nick and Yudkowsky, Eliezer. The ethics of artificial intelligence, pp. 316334. Cambridge University Press, 2014.

  5. Breiman, L., H. Friedman, J., A. Olshen, R., and J. Stone, C. Classification and Regression Trees. Chapman & Hall, New York, 1984.

  6. Breiman, Leo. Random forests. Machine Learning, 45(1): 532, October 2001.

  7. Bul, S. and Kontschieder, P. Neural decision forests for semantic image labelling. In CVPR, 2014.

  8. Chen, Tianqi and Guestrin, Carlos. Xgboost: A scalable tree boosting system. In KDD, 2016.

  9. Chung, J., Ahn, S., and Bengio, Y. Hierarchical Multiscale Recurrent Neural Networks. In ICLR, 2017.

  10. Dash, S., Malioutov, D. M., and Varshney, K. R. Learning interpretable classification rules using sequential rowsampling. In ICASSP, 2015.

  11. Doshi-Velez, Finale; Kim, Been. Towards a rigorous science of interpretable machine learning. ArXiv e-prints, 2017.

  12. Dougherty, James, Kohavi, Ron, and Sahami, Mehran. Supervised and unsupervised discretization of continuous features. In ICML, 1995.

  13. Ho, Tin Kam. The random subspace method for constructing decision forests. IEEE Transactions on Pattern Analysis and Machine Intelligence, 20(8):832844, 1998.

  14. Jang, E., Gu, S., and Poole, B. Categorical Reparameterization with Gumbel-Softmax. In ICLR, 20

  15. Kim, B., Gilmer, J., Viegas, F., Erlingsson, U., and Wattenberg, M. TCAV: Relative concept importance testing with Linear Concept Activation Vectors. ArXiv e-prints, 2017.

  16. Kim, Been, Khanna, Rajiv, and Koyejo, Sanmi. Examples are not enough, learn to criticize! Criticism for interpretability. In NIPS, 2016.

  17. Kontschieder, P., Fiterau, M., Criminisi, A., and Bul, S. R. Deep neural decision forests. In ICCV, 2015.

  18. Lecun, Yann, Bengio, Yoshua, and Hinton, Geoffrey. Deep learning. Nature, 521(7553):436444, 5 2015.

  19. Malioutov, Dmitry M., Varshney, Kush R., Emad, Amin, and Dash, Sanjeeb. Learning interpretable classification rules with boolean compressed sensing. In Transparent Data Mining for Big and Small Data, pp. 95121. Springer International Publishing, 2017.

  20. Norouzi, Mohammad, Collins, Maxwell D., Johnson, Matthew, Fleet, David J., and Kohli, Pushmeet. Efficient non-greedy optimization of decision trees. In NIPS, 2015.

  21. Paszke, Adam, Gross, Sam, Chintala, Soumith, Chanan, Gregory, Yang, Edward, DeVito, Zachary, Lin, Zeming, Desmaison, Alban, Antiga, Luca, and Lerer, Adam. Automatic differentiation in pytorch. In NIPS Workshop on Autodiff, 2017.

  22. Pedregosa, F., Varoquaux, G., Gramfort, A., Michel, V., Thirion, B., Grisel, O., Blondel, M., Prettenhofer, P., Weiss, R., Dubourg, V., Vanderplas, J., Passos, A., Cournapeau, D., Brucher, M., Perrot, M., and Duchesnay, E. Scikit-learn: Machine learning in Python. Journal of Machine Learning Research, 12:28252830, 2011.

  23. Quinlan, J. Ross. C4.5: Programs for Machine Learning. Morgan Kaufmann Publishers Inc., 1993.

  24. Ribeiro, Marco Tulio, Singh, Sameer, and Guestrin, Carlos. why should i trust you?: Explaining the predictions of any classifier. In KDD, 2016.

  25. Schmidhuber, J. Deep learning in neural networks: An overview. Neural Networks, 61:85117, 2015.

  26. Weller, Adrian. Challenges for transparency. In ICML Workshop on Human Interpretability in Machine Learning, pp. 5562, 2017.

  27. Wolpert, David H. The lack of a priori distinctions between learning algorithms. Neural Computation, 8(7):13411390, 1996.

  28. Xiong, Zheng, Zhang, Wenpeng, and Zhu, Wenwu. Learning decision trees with reinforcement learning. In NIPS Workshop on Meta-Learning, 2017.

Подробнее..

Сколько зарабатывает специалист по машинному обучению обзор зарплат и вакансий в 2021

10.03.2021 20:17:40 | Автор: admin

Привет, Хабр! Мы продолжаем свою серию аналитических статей о рынке зарплат и вакансий в IT. И сегодня на очереди ML-инженер, или специалист по машинному обучению, тем более, что 23 марта Skillfactory запускает новый поток продвинутого курсаMachine Learning и Deep Learning.

Machine Learning Engineer специальность 1 в разработке и проектировании сложных систем, которая в декабре 2020 года занимала 38,54 % вакансий отрасли и примерно 9 % всех вакансий на российском рынке IT. Так давайте разберёмся, сколько на самом деле получают специалисты по машинному обучению, как попасть в ML и куда можно развиваться. Поехали!


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

Кто такой ML-инженер

Machine Learning Engineer это эксперт в области искусственного интеллекта. Именно он разрабатывает алгоритмы, по которым думает компьютер.

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

А ещё специалист по машинному обучению создает ботов, которые общаются с клиентами. Так что именно им нужно говорить спасибо за Бот, позови оператора. Алиса, Siri и голосовой помощник Олег, это, кстати, тоже детище ML-инженеров.

Профессия инженера машинного обучения это подраздел Data Science. И деятельность ML-инженера больше ориентирована на практические задачи. Он решает бизнес-задачи, используя алгоритмы машинного обучения. Он может использовать существующие наработки или писать каждый раз новые это не важно. Единственная цель качественное выполнение задачи с наименьшими затратами ресурсов.

Основные компетенции специалиста ML-EngineerОсновные компетенции специалиста ML-Engineer

Что требуют работодатели от ML-Engineer

Пул навыков довольно большой. Мы проанализировали свыше 350 вакансий и заметили, что в большинстве из них чётко разделены компетенции специалиста по Data Science и ML. Но требования к вакансиям всё равно очень схожие.

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

Работодатели в большинстве случаев знают, для чего им нужен специалист по ML и какими компетенциями он должен обладать. Вот те, что попадаются в вакансиях чаще всего:

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

Базис программирования. Python упоминается в абсолютном большинстве вакансий примерно 92 % всех, но также работодатели требуют знания R, Java, C++, Scala. Также необходимы навыки использования библиотек вроде pandas, OpenCV, Numpy, Eigen, NLTK, Spacy, scikit-learn или других.

Моделирование данных. Ещё один базовый навык, который необходим в большинстве вакансий. Именно от навыков моделирования данных зависит эффективность машинного обучения. В основном необходимо знать паттерны моделирования, алгоритмы итеративного обучения и стратегии оценки точности моделей.

SQL. В 73 % вакансий требуют знания SQL, но очень много вакансий, в которых необходимы навыки в NoSQL СУБД.

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

Гибкие методологии разработки проектов. Примерно в трети вакансий упоминаются Agile, Scrum, Kanban и другие гибкие методологии. Опыт работы с ними считается плюсом, но не обязателен.

В целом требования в вакансии ML-инженера и дата-сайентиста довольно сильно пересекаются. Компании малого и среднего бизнеса не делают между ними практически никакой разницы и часто ищут сразу Data Scientist / ML-Engineer.

Софт-скилы предсказуемы. Они с минимальными расхождениями копируют требования из вакансий Data Scientist и Data Analyst:

  • аналитический склад мышления, логика;

  • коммуникативность;

  • инициативность;

  • внимательность к деталям.

Но все же есть и любопытные наблюдения. В целом софтскиллы упоминают в два раза меньше компаний, чем, к примеру, в вакансиях Data Analyst. Здесь ключевую роль играют хард-скилы. Личностные навыки и особенности характера вторичны.

Зарплаты и вакансии в России и не только

Подходим к самому интересному. По состоянию на 04.03.2021 и данным с hh.ru, в России имеются 1052 вакансии, которые содержат упоминания ML или машинного обучения.

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

Непосредственно к машинному обучению относится чуть больше 500 вакансий. За 2020 год востребованность специальности выросла более чем в 2 раза и продолжает расти.

Большинство вакансий открыто в Москве 55 %. Примерно 17,5 % размещены для Санкт-Петербурга. Примерно 24,5 % разбросаны по другим крупным городам России с населением выше 500 000 человек. Количество вакансий в населённых пунктах меньше 200 000 жителей не превышает 2 %.

В целом локации и востребованность коррелируются с отраслями Data Science и Data Analyst. Основной работодатель московские компании и международные бренды, у которых есть офис в Москве.

Основная проблема 80 % компаний не указывает вилку зарплат или хотя бы примерный уровень, на который может рассчитывать соискатель.

Мы проанализировали 200 вакансий к открытыми предложениями по зарплате. Результаты довольно неплохие. Медиана проходит по точке в 165 000 рублей. Это уровень месячной зарплаты, на который реально может рассчитывать специалист с 12-летним опытом в ML.

Junior ML-engineer или специалист, который хочет попасть в машинное обучение из смежных отраслей, может рассчитывать на оплату от 80 тысяч рублей.

Опытный сеньор может зарабатывать от 200 000 рублей. И это далеко не предел. В крупных международных компаниях отделы ML сегодня растут очень быстро, и специалист топового уровня может рассчитывать на зарплату 330 000 рублей.

В регионах ситуация значительно хуже. Примерно половина вакансий вообще смешивает специалистов по ML, DS и DA в одного. А в остальных навыки машинного обучения это дополнительная компетенция, а не основная.

Даже если компания понимает, зачем ей нужен ML, то с зарплатами в регионах не очень. Есть предложения для джунов от 25 000 рублей, а выше 80 000 уже считается отличной зарплатой для профи.

Зато на международном рынке всё хорошо у специалистов по машинному обучению.

По данным salary.com, годовая зарплата ML-инженера в США составляет 120 000 долларов по медиане. Это 10 000 долларов в месяц или, в переводе на деревянные, 730 000 рублей.

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

С удалёнкой всё не очень радужно. Большинство компаний стремятся взять ML-инженера на фултайм в офис. Даже в условиях пандемии вакансий, которые допускают удалённую работу, это очень немного.

Откуда прийти и куда расти специалисту по машинному обучению

ML-инженер не очень дружелюбная специальность для новичков. В неё можно попасть с нуля, но для этого нужна как минимум крепкая математическая база. В идеале математическое или экономическое высшее образование. И даже в этом случае нужно быть готовым к сложностям изучать придётся очень много всего.

Для успешного продвижения и роста нужно понимать, как работает сфера Data Science целиком. Поэтому идеальные стартовые площадки для перехода в ML это дата-сайентист и дата-аналитик.

Также в машинное обучение могут перейти разработчики на Python. Для этого нужно будет разобраться с основными ML-библиотеками.

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

ML-инженер универсальный специалист, подобный швейцарскому ножу. Для желающих стать таким специалистов, у нас есть специальный продвинутый курс Machine Learning и Deep Learning А промокод HABR даст скидку 50%.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод Как управлять проектами машинного обучения и data science

21.03.2021 18:07:02 | Автор: admin

Управлять проектами машинного обучения (Machine learning) и data science сложно, поскольку проекты часто носят исследовательский характер, и трудно предсказать, сколько времени потребуется на их завершение. Часто всё начинается с одной идеи, а затем перетекает в новое направление, когда предложенный метод не срабатывает или если предположения относительно данных оказываются неверными.

Построение модели также является длительным процессом (по сравнению с работой в сфере программного обеспечения и аналитики), и data scientist нередко попадает в кроличью нору и тратит месяцы на проект, не имея четких представлений о прогрессе. Еще одно отличие от стандартных практик разработки программного обеспечения заключается в том, что построение модели обычно выполняется всего одним человеком, и это не совсем вписывается в традиционные командные рабочие процессы, такие как Kanban и Scrum.

Я потратил достаточно много времени, изучая существующие рабочие процессы (в основном в Jira) с точки зрения пригодности для управления проектами машинного обучения и data science, но безуспешно. Большая часть информации нацелена на разработку программного обеспечения и фокусируется на Agile методологиях. Обсуждая этот вопрос с коллегами и друзьями мне не удалось найти ничего, что было бы адаптировано для машинного обучения и data science. Я заметил, что часть коллег пытаются адаптировать свой рабочий процесс к стандартной инженерной практике, в других же случаях, они вообще не пытаются управлять проектами. Последнее особенно проблематично, по причине того, что проекты, которые требуют слишком много времени и замахиваются на слишком большую предметную область, вероятнее всего провалятся.

Поскольку мне не удалось найти подходящее решение, я решил разработать собственную рабочую схему для управления проектами машинного обучения и data science. Данный процесс может быть реализован в Jira и позволяет мне легко отслеживать статус проектов, вести отчетность, а также не давать раздуваться предметной области, избегая построения чересчур сложных моделей. У наших исследователей появляется рабочая схема, которая помогает им в построении модели, что повышает их успехи в проекте. Я пользуюсь этой системой уже несколько лет, и мы с моей командой очень довольны ею.

Этапы проекта машинного обучения четко определены

Независимо от того, создаете ли вы сложный алгоритм компьютерного зрения с использованием методов глубокого обучения, модель обучения ранжированию с помощью LightGBM или даже простую линейную регрессию, процесс построения модели машинного обучения имеет четко определенные этапы. На схеме показаны шаги построения модели от первоначального исследования до анализа результатов A/B-тестирования. Обратите внимание, что у каждого этапа есть конечные результаты (deliverable или майлстоун). Это та стадия процесса, когда вы обсуждаете получившийся результат с командой или стейкхолдерами.

1) Исследование (Research)

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

Предполагается, что на этом этапе исследователь в основном занимается изучением литературы и выбором методологии решения задачи. Сюда также входит коллективный поиск творческих решений с коллегами, если таковые имеются. Иногда для выбора методологии также требуется изучение существующих готовых решений и инструментов, и создания примитивных прототипов с помощью Jupyter notebook.

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

План также демонстрируется стейкхолдерам для получения их фидбека.

2) Исследование данных (Data Exploration)

Это традиционный этап исследования данных с помощью Pandas и Jupiter notebook (или иногда Tableau) с целью получения каких-либо выводов о используемых данных. Типичный анализ включает подсчет количества строк в данных, создание гистограмм для различных агрегатов функций, графиков тенденций с динамикой и нескольких графиков распределения. Исследователи также будут формировать запросы, которые станут ядром их ETL модели.

Конечный результат: подробный отчет об исследовании данных с помощью Jupiter notebook, содержащий графики и комментарии, дающий какое-либо представление о используемых данных. Отчет будет передан остальной части команды и стейкхолдерам проекта.

3) Моделирование (Modeling)

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

Конечные результаты: Результатами этого этапа являются:

  1. Прототип модели

  2. Отчет в Jupyter notebook с обширной оценкой модели

Отчет по результатам будет представлен команде и стейкхолдерам проекта.

4) Коммерческое внедрение (Productization)

На данном этапе происходит реализация кода окончательной версии. Распространенные задачи на этом этапе включают в себя добавление комментариев ко всем функциям и обеспечение правильного форматирования кода в соответствии со стандартами Python и соглашением рабочей команды. Код снабжается такими отчетными метриками, как количество извлеченных строк, количество строк в выходных данных, погрешность прогнозирования по нескольким метрикам и важность признаков, когда это возможно. Наконец, код проверяется одним data scientistом и одним инженером.

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

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

5) A/B-тестирование (A/B Testing)

Большинство моделей проходят этап A/B-тестирования. С его помощью специалисты и стейкхолдеры определяют детали теста: период проведения тестирования, процент трафика, контроль тестирования, интерпретация результатов и т. д. В период тестирования члены команды будут задействованы в других проектах, но им все же нужно будет уделять внимание и тестированию.

6) Анализ результатов (Results Analysis)

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

Конечные результаты:

  1. Подробный отчет о результатах в Jupyter notebook.

2. Гипотеза относительно того, почему все пошло не так, как ожидалось (если это возможно)

Окончательный отчет будет представлен команде и стейкхолдерам проекта.

Работа с Jira

Хотя этот фреймворк может выглядеть великолепно в теории, в действительности вышеуказанные этапы редко бывают строго последовательными. Например, очень часто происходят качели от исследования данных к моделированию и обратно к исследованию данных. Кроме того, этот процесс не вписывается в существующий фреймворк Jira, так как же его реализовать на практике?

На самом деле это довольно просто. Мы используем доску Kanban в Jira и горизонтальные колонки (swimlanes, по одной на члена команды) с несколькими кастомными полями и изменениями. Приведенные ниже рекомендации определяют суть этого процесса:

  • Для каждого проекта создается новый эпик (Epic) тикет, а работа разделяется на задачи (Tasks).

  • Каждая задача помечается этапом (Phase) - кастомным полем в Jira для выбора одного из 6 этапов, перечисленных выше. (Обратите внимание, что на одном этапе может быть несколько задач.)

  • Задачи не могут длиться больше 1 недели (5 дней). Это заставляет членов команды разбивать свою работу на более мелкие (но все же довольно значительные) части, что позволяет отслеживать прогресс с минимальными накладными расходами.

  • Одновременно в работе может быть только один тикет. Это гарантирует то, что мы всегда видим статус проекта.

  • Этапы не всегда являются последовательными, и можно перемещаться между ними по мере формирования новых задач.

Заключение

Управление проектами машинного обучения и data science не должно быть сложным. Сначала я тратил порядка 30 минут в день на мониторинг этого процесса, но как только команда привыкла к нему, мое время сократилось до 15 минут в неделю! Я знаю, на каком этапе находится каждый проект в любой момент времени, сколько времени он занял, и я могу быстро выявить проблемы, чтобы при необходимости вмешаться и помочь своей команде. Мои data scientistы имеют четкую схему работы для построения модели, и они стали в этом намного эффективнее.

Я надеюсь, что все это окажется таким же полезным для Вас, как и для меня.


Перевод статьи подготовлен в преддверии старта курса "Промышленный ML на больших данных".

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

Подробнее..

Перевод Перспективные архитектуры для современных инфраструктур данных

04.05.2021 18:23:50 | Автор: admin

На сегодняшний день базы данных класса Massive Parallel Processing это отраслевой стандарт для хранения Больших Данных и решения разнообразных аналитических задач на их основе.

Сценарии использования mpp-баз разнообразны: они могут быть и рабочей лошадкой корпоративного BI, и инструментом централизации данных из сотен источников в одном DWH, и даже использоваться для горячего взаимодействия с ML-моделями, работающими в продуктивном окружении.

Данный класс технологий необходимый элемент в инструментарии современного Data Engineer.

На демо-занятии мы подробно разберем, что же такое mpp-БД, какие решения есть сегодня на рынке и даже углубимся в практический пример использования одной их самых инновационных mpp-систем на сегодня: ClickHouse.

Приглашаем всех желающих присоединиться к участию в демо-уроке Введение в MPP-базы данных на примере ClickHouse.


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

Многие из самых быстрорастущих инфраструктурных стартапов сегодня создают продукты для управления данными. Эти системы позволяют принимать решения на основе данных (аналитические системы) и управлять продуктами на основе данных, в том числе с помощью машинного обучения (оперативные системы). Они варьируются от конвейеров, по которым передаются данные, до решений для их хранения, SQL-движков, которые анализируют данные, дашбордов для мониторинга, которые упрощают понимание данных от библиотек машинного обучения и data science до автоматизированных конвейеров данных, каталогов данных и т.д.

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

Инфраструктура данных включает

Этот доклад содержит эталонные архитектуры инфраструктуры данных, составленные в результате обсуждений с десятками практиков. Огромное спасибо всем, кто участвовал в этом исследовании!

Стремительный рост рынка инфраструктуры данных

Одной из основных причин, из-за которых был составлен этот доклад, является стремительный рост инфраструктуры данных за последние несколько лет. По данным Gartner, расходы на инфраструктуру данных достигли в 2019 году рекордного показателя в 66 миллиардов долларов, что составляет 24% и эта цифра растет всех расходов на программное обеспечение для инфраструктуры. По данным Pitchbook, 30 крупнейших стартапов по созданию инфраструктуры данных за последние 5 лет привлекли более 8 миллиардов долларов венчурного капитала на общую сумму 35 миллиардов долларов.

Венчурный капитал, привлеченный наиболее показательными стартапами в области инфраструктуры данных в 2015-2020 гг.

Примечание: Любые инвестиции или портфельные компании, упомянутые или описанные в этой презентации, не являются репрезентативными для всего объема инвестиций во все инвестиционные каналы, управляемые a16z, и нет никаких гарантий, что эти инвестиции будут прибыльными или что другие инвестиции, сделанные в будущем, будут иметь аналогичные характеристики или результаты. Список инвестиций, сделанных фондами под управлением a16z, доступен здесь: https://a16z.com/investments/.

Гонка за данными также отражается на рынке труда. Аналитики данных, инженеры по обработке данных и инженеры по машинному обучению возглавили список самых быстрорастущих специальностей Linkedin в 2019 году. По данным NewVantage Partners 60% компаний из списка Fortune 1000 имеют директоров по обработке и анализу данных, по сравнению с 12% в 2012 году, и согласно исследованию роста и прибыльности McKinsey эти компании значительно опережают своих коллег.

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

Унифицированная архитектура инфраструктуры данных

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

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

Результатом этих обсуждений стала следующая диаграмма эталонной архитектуры:

Unified Architecture for Data Infrastructure

Унифицированная архитектура для инфраструктуры данных

Примечание: Исключает транзакционные системы (OLTP), обработку логов и SaaS-приложения для аналитики. Перейдите сюда, чтобы просмотреть версию в высоком разрешении.

Столбцы диаграммы определены следующим образом:

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

Остальная часть этой статьи посвящена разбору этой архитектуры и того, как она чаще всего реализуется на практике.

Аналитика, AI/ML и грядущая конвергенция?

Инфраструктура данных на высоком уровне служит двум целям: помочь бизнес-лидерам принимать более обоснованные решения с помощью данных (аналитические варианты использования) и встроить аналитику данных в клиентские приложения, в том числе с помощью машинного обучения (оперативные варианты использования).

Вокруг этих вариантов использования выросли две параллельные экосистемы. Основу аналитической экосистемы составляют хранилища данных (data warehouse). Большинство хранилищ данных хранят данные в структурированном формате и предназначены для быстрого и простого получения выводов на основе обработки основных бизнес-метрик, обычно с помощью SQL (хотя Python становится все более популярным). Озеро данных (data lake) является основой оперативной экосистемы. Сохраняя данные в необработанном виде, он обеспечивает гибкость, масштабируемость и производительность, необходимые для специализированных приложений и более сложных задач обработки данных. Озера данных работают на широком спектре языков, включая Java/Scala, Python, R и SQL.

У каждой из этих технологий есть свои ярые приверженцы, а выбор одной из них оказывает значительное влияние на остальную часть стека (подробнее об этом позже). Но что действительно интересно, так это то, что у современных хранилищ и озер данных становится все больше общих черт обе предлагают стандартные хранилища, собственное горизонтальное масштабирование, полуструктурированные типы данных, ACID-транзакции, интерактивные SQL запросы и т. д.

Важный вопрос на будущее: находятся ли хранилища и озера данных на пути к конвергенции? То есть станут ли они взаимозаменяемыми в стеке? Некоторые эксперты считают, что это все-таки происходит и способствует упрощению технологического и вендорного многообразия. Другие считают, что параллельность экосистем сохранится из-за различий в языках, сценариях использования или других факторов.

Архитектурные сдвиги

Инфраструктура данных подвержена широким архитектурным сдвигам, происходящим в индустрии программного обеспечения, включая переход к облаку, бизнес-моделям с открытым исходным кодом, SaaS и так далее. Однако помимо них существует ряд изменений, которые уникальны для инфраструктуры данных. Они продвигают архитектуру вперед и часто дестабилизируют рынки (например, ETL-инструменты) в процессе.

Новые возможности

Формируется набор новых возможностей обработки данных, которые нуждаются в новых наборах инструментов и базовых систем. Многие из этих трендов создают новые категории технологий (и рынки) с нуля.

Схемы построения современной инфраструктуры данных

Чтобы сделать архитектуру как можно более действенной, мы попросили экспертов систематизировать набор общих схем (blueprints) руководств по внедрению для организаций, работающих с данными, в зависимости от размера, сложности, целевых вариантов использования и применения.

Здесь мы предоставим обзор трех обобщенных схем. Мы начнем схемы современной бизнес-аналитики, которая фокусируется на облачных хранилищах данных и аналитических вариантах использования. Во второй схеме мы рассматриваем мультимодальную обработку данных, охватывая как аналитические, так и оперативные варианты использования, построенные на основе озера данных. В окончательной схеме мы подробно рассмотрим оперативные системы и новые компоненты AI и ML стека.

Три обобщенных схемы

Схема 1: современная бизнес-аналитика

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

Перейдите сюда, чтобы просмотреть версию в высоком разрешении.

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

Основные варианты использования включают отчеты, дашборды и специальный анализ, в основном с использованием SQL (и некоторого количества Python) для анализа структурированных данных.

К сильным сторонам этой модели относятся низкие первоначальные инвестиции, скорость и простота начала работы, а также широкая доступность кадров. Этот план менее приемлем для команд, у которых есть более сложные потребности в данных, включая обширную data science, машинное обучение или приложения для потоковой передачи / с низкой задержкой.

Схема 2: мультимодальная обработка данных

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

Перейдите сюда, чтобы просмотреть версию в высоком разрешении.

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

Сценарии использования включают в себя как бизнес-аналитику, так и более продвинутые функции, включая оперативный AI/ML, аналитику, чувствительную к потоковой передаче / задержке, крупномасштабные преобразования данных и обработку различных типов данных (включая текст, изображения и видео) с использованием целого набора языков (Java/Scala, Python, SQL).

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

Схема 3: Искусственный интеллект и машинное обучение.

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

Перейдите сюда, чтобы просмотреть версию в высоком разрешении.

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

Основные варианты использования сосредоточены на возможностях работы с данными как для внутренних, так и для клиентских приложений - запускаемых либо онлайн (т. е. в ответ на ввод данных пользователем), либо в пакетном режиме.

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

Смотря в будущее

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

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


Перевод подготовлен в рамках онлайн-курса "Data Engineer".

Смотреть вебинар Введение в MPP-базы данных на примере ClickHouse.

Подробнее..

Перевод Топ 3 статистических парадокса в Data Science

05.05.2021 18:14:12 | Автор: admin

Перевод подготовлен в рамках курса "Machine Learning. Professional".

Также приглашаем всех желающих принять участие в двухдневном онлайн-интенсиве Деплой ML модели: от грязного кода в ноутбуке к рабочему сервису.


Ошибки наблюдения и различия в подгруппах вызывают статистические парадоксы

Ошибки наблюдения и различия в подгруппах могут легко привести к статистическим парадоксам в любом прикладном решении data science. Игнорирование этих элементов может полностью дискредитировать заключения нашего анализа.

Действительно, не так уж и редко можно наблюдать такие удивительные явления, как тенденции подгрупп, которые полностью изменяются в противоположную сторону в агрегированных данных. В этой статье мы рассмотрим топ 3 наиболее распространенных статистических парадокса, встречающихся в Data Science.

1. Парадокс Берксона

Первым ярчайшим примером является обратная корреляция между степенью тяжести заболевания COVID-19 и курением сигарет (см., например, обзор Европейской комиссии Wenzel 2020). Курение сигарет широко известный фактор риска респираторных заболеваний, так как же объяснить это противоречие?

Работа Griffith 2020, недавно опубликованная в Nature, предполагает, что это может быть случай ошибки коллайдера (Collider Bias), также называемой парадоксом Берксона. Чтобы понять этот парадокс, давайте рассмотрим следующую графическую модель, в которую мы включили третью случайную переменную: госпитализация.

Парадокс Берксона: госпитализация это переменная-коллайдер для курения сигарет, и для тяжести течения COVID-19. (Изображение автора)

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

Давайте рассмотрим следующий пример набора данных. На левом рисунке у нас есть данные по всему населению, а на правом рисунке мы рассматриваем только подмножество госпитализированных людей (то есть мы используем переменную-коллайдер).

Парадокс Берксона: если мы добавим условие в соответствии с коллайдером госпитализация, мы увидим обратную связь между курением и COVID-19! (Изображение автора)

На левом рисунке мы можем наблюдать прямую корреляцию между осложнениями от COVID-19 и курением сигарет, которую мы ожидали, поскольку мы знаем, что курение является фактором риска респираторных заболеваний.

Но на правом рисунке где мы рассматриваем только пациентов больниц мы видим противоположную тенденцию! Чтобы понять это, обратите внимание на следующие моменты.

1.Тяжелая форма COVID-19 увеличивает шансы на госпитализацию. То есть, если степень тяжести заболевания выше 1, то требуется госпитализация.

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

3. Таким образом, если у пациента легкая форма COVID-19, он имеет больше шансов оказаться курильщиком! Более того, в отличие от COVID-19 причиной для госпитализации станет наличие у пациента какого-либо заболевания, которое может быть вызвано курением (например, сердечно-сосудистые заболевания, рак, диабет).

Этот пример очень похож на оригинальную работу Berkson 1946, где автор заметил отрицательную корреляцию между холециститом и диабетом у пациентов больниц, несмотря на то, что диабет является фактором риска холецистита.

2. Скрытые (латентные) переменные

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

Рассмотрим, например, соотношение между количеством пожарных, задействованных для тушения пожара, и количеством людей, пострадавших в его результате. Мы ожидаем, что увеличение количества пожарных улучшит результат (в какой то степени см. закон Брукса), однако в агрегированных данных наблюдается прямая корреляция: чем больше пожарных задействовано, тем выше число раненых!

Чтобы понять этот парадокс, рассмотрим следующую графическую модель. Ключевым моментом является повторное рассмотрение третьей случайной переменной: степень тяжести пожара.

Парадокс скрытой переменной: степень тяжести пожара это скрытая переменная для n задействованных пожарных и для n пострадавших. (Изображение автора)

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

Давайте рассмотрим следующий пример с набором данных. На левом рисунке у нас отражены общие данные по всем видам пожаров, а на правом рисунке мы рассматриваем только сведения, соответствующие трем фиксированным степеням тяжести пожара (т.е. мы обусловливаем наши данные наблюдений скрытой переменной).

Скрытые переменные: если мы примем за условие скрытую переменную степень тяжести пожара, мы увидим обратную корреляцию между количеством задействованных пожарных и количеством раненых! (Изображение автора)

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

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

  • Если мы посмотрим на пожары высокой степени тяжести, мы увидим ту же тенденцию, даже несмотря на то, что количество задействованных пожарных и количество травм увеличиваются.

3. Парадокс Симпсона

Парадокс Симпсона это удивительное явление, когда мы постоянно наблюдаем какую-то тенденцию возникающую в подгруппах, и которая меняется на противоположную, если эти подгруппы объединить. Часто это связано с несбалансированностью классов в подгруппах данных.

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

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

  • С другой стороны, общий процент приема среди абитуриентов женского пола был ниже, чем у абитуриентов мужского пола.

Чтобы понять, как как такое может быть, давайте рассмотрим следующий набор данных с двумя факультетами: Факультет A и Факультет B.

  • Из 100 абитуриентов мужского пола: 80 подали заявки на Факультет A, из которых 68 были приняты (85%), а 20 подали заявки на Факультет В, из которых приняты были 12 человек (60%).

  • Из 100 абитуриентов женского пола: 30 подали заявки на Факультет А, из которых 28 были приняты (93%), в то время как 70 подали заявки на Факультет B, из которых были приняты 46 (66%).

Парадокс Симпсона: женщины-абитуриенты с большей вероятностью будут приняты в каждом факультете, но общий процент приема женщин в сравнении с мужчинами ниже! (Изображение автора)

Парадокс выражается следующими неравенствами.

Парадокс Симпсона: неравенство, лежащее в основе очевидного противоречия. (Изображение автора)

Теперь мы можем понять происхождение наших, казалось бы, противоречивых наблюдений. Дело в том, что существует ощутимый классовый гендерный дисбаланс среди абитуриентов на каждом из двух факультетов (Факультет A: 8030, Факультет B: 2070). Действительно, большинство студентов женского пола подали заявку на более конкурентный Факультет B (который имеет низкие показатели приема), в то время как большинство студентов мужского пола подали документы на менее конкурентный Факультет А (который имеет более высокие показатели приема). Это обусловливает противоречивые данные, которые мы получили.

Заключение

Скрытые переменные, переменные-коллайдеры, и дисбаланс классов могут легко привести к статистическим парадоксам во многих практических моментах data science. Поэтому этим ключевым моментам необходимо уделять особое внимание для правильного определения тенденций и анализа результатов.


Узнать подробнее о курсе "Machine Learning. Professional"

Участвовать в онлайн-интенсиве Деплой ML модели: от грязного кода в ноутбуке к рабочему сервису

Подробнее..

Перевод Полезные приемы и лучшие практики от Kaggle

27.05.2021 18:04:31 | Автор: admin

В преддверии старта курса "Machine Learning. Professional" делимся традиционным переводом полезного материала.


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

Об этом проекте

Kaggle прекрасное место. Это золотая жила для дата-сайентистов и инженеров машинного обучения. Не так много платформ, на которых вы можете найти высококачественные, эффективные, воспроизводимые, отобранные экспертами, потрясающие примеры коды в одном месте.

С момента запуска она провела более 164 соревнований. Эти соревнования привлекают на платформу экспертов и профессионалов со всего мира. В результате на каждом соревновании появляется множество высококачественных блокнотов и скриптов, а также огромное количество опенсорсных наборов данных, которые предоставляет Kaggle.

В начале своего пути в data science я приходил на Kaggle, чтобы найти наборы данных и оттачивать свои навыки. Когда бы я ни пытался разбираться с другими примерами и фрагментами кода, меня поражала сложность, и я сразу же терял мотивацию.

Но теперь я обнаружил, что провожу много времени за чтением чужих блокнотов и отправкой заявок на соревнования. Иногда там есть вещи, на которые стоит потратить все выходные. А иногда я нахожу простые, но невероятно эффективные приемы и передовой опыт, которые можно изучить, только наблюдая за другими профессионалами.

А в остальном дело за малым, мое ОКР практически вынуждает меня выкладывать все имеющиеся у меня знания в области науки о данных. Таким образом я представляю вам первый выпуск моего еженедельника Полезные приемы и лучшие практики от Kaggle. На протяжении всей серии я буду писать обо всем, что может быть полезно во время типичного рабочего процесса в области data science, включая фрагменты кода распространенных библиотек, передовые практики, которым следуют ведущие эксперты в области с Kaggle, и т. д. - все, что я узнал за прошедшую неделю. Наслаждайтесь!

1. Отображение только нижней части корреляционной матрицы

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

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

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

houses = pd.read_csv('data/melb_data.csv')# Calculate pairwise-correlationmatrix = houses.corr()# Create a maskmask = np.triu(np.ones_like(matrix, dtype=bool))# Create a custom diverging palettecmap = sns.diverging_palette(250, 15, s=75, l=40,                             n=9, center="light", as_cmap=True)plt.figure(figsize=(16, 12))sns.heatmap(matrix, mask=mask, center=0, annot=True,             fmt='.2f', square=True, cmap=cmap)plt.show();

Полученный в результате график намного легче интерпретировать, и он не так отвлекает избыточными данными. Сначала мы строим корреляционную матрицу, используя метод DataFrame .corr. Затем мы используем функцию np.ones_like с dtype, установленным в bool, чтобы создать матрицу значений True с той же формой, что и наш DataFrame:

>>> np.ones_like(matrix, dtype=bool)[:5]array([[ True, True, True, True, True, True, True, True, True, True, True, True, True], [ True, True, True, True, True, True, True, True, True, True, True, True, True], [ True, True, True, True, True, True, True, True, True, True, True, True, True], [ True, True, True, True, True, True, True, True, True, True, True, True, True], [ True, True, True, True, True, True, True, True, True, True, True, True, True]])

Затем мы передаем его в функцию Numpy .triu, которая возвращает двумерную логическую маску, которая содержит значения False для нижнего треугольника матрицы. Затем мы можем передать его функции Seaborn heatmap для построения подмножества матрицы в соответствии с этой маской:

sns.heatmap(matrix, mask=mask, center=0, annot=True,               fmt='.2f', square=True, cmap=cmap)

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

2. Добавление отсутствующих значений в value_counts

Небольшой удобный трюк с value_counts заключается в том, что вы можете увидеть долю пропущенных значений в любом столбце, установив dropna в False:

>>> houses.CouncilArea.value_counts(dropna=False, normalize=True).head()NaN           0.100810Moreland      0.085641Boroondara    0.085420Moonee Valley 0.073417Darebin       0.068778Name: CouncilArea, dtype: float64

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

>>> missing_props = houses.isna().sum() / len(houses)>>> missing_props[missing_props > 0].sort_values(ascending=False                                                 BuildingArea 0.474963YearBuilt    0.395803CouncilArea  0.100810Car          0.004566dtype: float64

Сначала найдите пропорции, разделив количество отсутствующих значений на длину DataFrame. Затем вы можете отфильтровать столбцы с 0%, т.е. выбирать только столбцы с пропущенными значениями.

3. Использование Pandas DataFrame Styler

Многие из нас никогда не осознают огромный неиспользованный потенциалpandas. Недооцененной и часто упускаемой из виду особенностью pandas является ее способность стилизовать свои DataFrameы. Используя атрибут .style для DataFrameов pandas, вы можете применять к ним условные конструкции и стили. В качестве первого примера давайте посмотрим, как можно изменить цвет фона в зависимости от значения каждой ячейки:

>>> diamonds = sns.load_dataset('diamonds')>>> pd.crosstab(diamonds.cut, diamonds.clarity).\                style.background_gradient(cmap='rocket_r')

Это практически тепловая карта без использования функции Seaborn heatmap. Здесь мы подсчитываем каждую комбинацию огранки и чистоты алмаза с помощью pd.crosstab. Используя .style.background_gradient с цветовой палитрой, вы можете легко определить, какие комбинации встречаются чаще всего. Только из приведенного выше DataFrame мы можем видеть, что большинство алмазов имеют идеальную огранку, а самая распространенная комбинация - с типом чистоты VS2.

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

>>> pd.crosstab(diamonds.cut, diamonds.clarity,          aggfunc=np.mean, values=diamonds.price).\          style.background_gradient(cmap='flare')

На этот раз мы агрегируем цены на бриллианты для каждой комбинации огранки и чистоты. Из стилизованного DataFrame мы видим, что самые дорогие бриллианты имеют чистоту VS2 или премиальную огранку. Но было бы лучше, если бы мы могли отображать агрегированные цены, округляя их. Мы также можем изменить это с помощью .style:

>>> agg_prices = pd.crosstab(diamonds.cut, diamonds.clarity,                         aggfunc=np.mean, values=diamonds.price).\                         style.background_gradient(cmap='flare')>>> agg_prices.format('{:.2f}')

Изменив в методе .format строку формата {:.2f} мы указываем точность в 2 числа после запятой.

С .style предел - ваше воображение. Имея базовые познания в CSS, вы можете создавать собственные функции стилизации под свои нужды. Ознакомьтесь с официальным руководством pandas для получения дополнительной информации.

4. Настройка глобальных конфигураций графиков с помощью Matplotlib

При выполнении EDA (Exploratory Data Analysis) вы обнаружите, что сохраняете некоторые настройки Matplotlib одинаковыми для всех ваших графиков. Например, вы можете захотеть применить настраиваемую палитру для всех графиков, использовать более крупные шрифты для меток, изменить расположение легенды, использовать фиксированные размеры фигур и т. д.

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

from matplotlib import rcParams

rcParams - это просто старый словарь Python, содержащий настройки по умолчанию для Matplotlib:

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

# Remove top and right spinesrcParams['axes.spines.top'] = FalsercParams['axes.spines.right'] = False# Set fixed figure sizercParams['figure.figsize'] = [12, 9]# Set dots per inch to 300, very high quality imagesrcParams['figure.dpi'] = 300# Enable autolayoutrcParams['figure.autolayout'] = True# Set global fontsizercParams['font.style'] = 16# Fontsize of ticklabelsrcParams['xtick.labelsize'] = 10rcParams['ytick.labelsize'] = 10

Вы можете избежать большого количества повторяющейся работы, установив все сразу после импорта Matplotlib. Просмотреть все другие доступные настройки можно, вызвав rcParams.keys().

5. Настройка глобальных конфигураций Pandas.

Как и в Matplotlib, у pandas есть глобальные конфигурации, с которыми вы можете поиграть. Конечно, большинство из них связано с опциями отображения. В официальном руководстве пользователя говорится, что всей системой опций pandas можно управлять с помощью 5 функций, доступных непосредственно из пространства имен pandas:

  • get_option() / set_option() - получить/установить значение одного параметра.

  • reset_option() - сбросить один или несколько параметров до значений по умолчанию.

  • description_option() - вывести описание одного или нескольких параметров.

  • option_context() - выполнить блок кода с набором параметров, которые после выполнения возвращаются к предыдущим настройкам.

Все параметры имеют имена без учета регистра и находятся с помощью регулярного выражения из под капота. Вы можете использовать pd.get_option, чтобы узнать, какое поведение используется по умолчанию, и изменить его по своему вкусу с помощью set_option:

>>> pd.get_option(display.max_columns)20

Например, указанный выше параметр управляет количеством столбцов, которые должны отображаться, когда в DataFrame очень много столбцов. Сегодня большинство наборов данных содержит более 20 переменных, и всякий раз, когда вы вызываете .head или другие функции отображения, pandas ставит раздражающее многоточие, чтобы обрезать результат:

>>> houses.head()

Я бы предпочел видеть все столбцы, прокручивая их. Давайте изменим это поведение:

>>> pd.set_option(display.max_columns, None)

Выше я полностью убираю ограничение:

>>> houses.head()

Вы можете вернуться к настройке по умолчанию с помощью:

pd.reset_option(display.max_columns)

Как и в столбцах, вы можете настроить количество отображаемых строк по умолчанию. Если вы установите для display.max_rows значение 5, вам не придется все время вызывать .head():

>>> pd.set_option(display.max_rows, 5)>>> houses

В настоящее время plotly становится очень популярным, поэтому было бы неплохо установить его как график по умолчанию для pandas. Поступая таким образом, вы будете получать интерактивные графические диаграммы всякий раз, когда вы вызываете .plot для DataFrameов pandas:

pd.set_option(plotting.backend, plotly)

Обратите внимание, что для этого вам необходимо установить plotly.

Если вы не хотите испортить поведение по умолчанию или просто хотите временно изменить определенные настройки, вы можете использовать pd.option_context в качестве диспетчера контекста. Временное изменение поведения будет применено только к блоку кода, который следует за оператором. Например, если есть большие числа, у pandas есть раздражающая привычка преобразовывать их в стандартную нотацию. Вы можете временно избежать этого, используя:

>>> df = pd.DataFrame(np.random.randn(5, 5))>>> pd.reset_option('display.max_rows')>>> with pd.option_context('float_format', '{:f}'.format):        df.describe()

Вы можете увидеть список доступных параметров в официальном руководстве пользователя pandas.


Узнать подробнее о курсе "Machine Learning. Professional"

Подробнее..

Мои machine learning тулы для инвестирования

29.03.2021 16:17:23 | Автор: admin

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

Выбор компаний в портфель

Дисклеймер: у автора нет экономического образования и все выводы и суждения в статье делаются на основе житейского опыта и здравого смысла.

Ниже приведены некоторые базовые моменты, на которые можно обращать внимание при формировании решения о покупке какой-либо акции.

Деятельность компании

Можно смотреть на то, чем занимается компания. Если есть вера в будущее электромобилей, то, например, можно посмотреть в сторону Tesla (далее из статьи будет понятно, что это может быть не самый лучший выбор). Если есть вера в биомед, то можно купить бумаги из соответствующего сектора. При этом, на мой взгляд, важно понимать, что конкретно делает компания, на чем основан ее бизнес. Например, если взять нефтяные компании, то они могут заниматься совершенно разными вещами - одни добывают сырье, другие его перерабатывают, третьи только транспортируют. Из-за этого некоторые события, происходящие в мире, сильно влияют на одних, но совсем не влияют на других. Условно, закрыли какой-то морской канал, и танкеру, чтобы доставить нефть, приходится делать крюк, следовательно лишние затраты для транспортной компании. При этом какому-нибудь переработчику нефти все равно.

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

Финансовые отчеты

Но помимо этого было бы неплохо знать - а как конкретно зарабатывает фирма? Откуда у нее основные источники дохода и как они распределены? К счастью, каждая компания, торгующаяся на бирже, обязана раз в квартал (четверть года) раскрывать информацию о своих финансах (так называемые финансовые отчеты). Чтобы найти такие отчеты, достаточно вбить в любом поисковике "company_name investor relations" и перейти на соответствующий раздел сайта компании. На картинке показан кусок такого отчета компании Apple за 4 квартал:

Из него можно понять, что выручка компании (net sales, часто её называют revenue) за квартал составила почти 111.5 миллиардов долларов, что больше, чем в аналогичном квартале год назад (91.8 млрд). Кроме того, 95.7 млрд из них приходится на продукцию компании (продажи iPhone, iPad и т.д.), а 15.7 - на сервисы (Apple Music, App Store и т.д.). Так же можно увидеть, что чистая прибыль компании составила 28.8 млрд, причем видно, как это число получилось :
Из net sales вычли cost of sales (непосредственные затраты на производство) и operating expenses (побочные затраты), а так же учли налоги (provision for income taxes).
111.4 - 67.1 - 10.8 - 4.8 = 28.7

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

Мультипликаторы

Часто бывает так, что нужно сравнить компании, понять, какая из них лучше. Например, было решено, что в портфель необходимо добавить компанию, производящую потребительские товары. Но таких компаний огромное количество, и все они разных размеров, демонстрируют совершенно разные revenue, net income и т.д. На помощь приходят мультпликаторы - производные величины, которые можно непосредственно сравнить.

P/E (price to earnings) - цена-прибыль. Вычисляется как отношение капитализации компании к её годовой прибыли. Другими словами, данный показатель говорит о том, сколько лет компания будет окупаться, если ее купить сейчас. Например, у Apple сейчас P/E~30. Значит, если (в теории) мы целиком купим эту компанию по текущей цене, то через 30 лет эта покупка себя отобьет. Итак, чем ниже P/E, тем "дешевле" компания, что, разумеется, хорошо (лучше я куплю бизнес, который окупится за 10 лет, чем за 20). При этом важно понимать, что для разных секторов средние P/E могут сильно разниться. Это объясняется тем, что от одних секторов ожидания выше, чем от других. Например, продуктовому ритейлеру почти нереально увеличить выручку в 10 раз, а поставщик какого-нибудь интернет-сервиса спокойно может кратно наращивать количество пользователей от года к году. Вот и выходит, что технологические компании по P/E стоят "дороже", чем, например, сырьевые.

D/E (Debt to Equity) - долг к собственному капиталу. Данный мультипликатор показывает, насколько высокая долговая нагрузка у компании. Понятно, что если долг слишком высокий, то выше риски банкротства компании и меньше у неё возможностей. Иногда компании со слишком высоким D/E называют "зомби", потому что результат их деятельности целиком идет на обслуживание долга.

ROE (Return On Equity) - рентабельность собственного капитала. Вычисляется как отношение чистой прибыли к собственному капиталу компании. Мультипликатор показывает, как компания способна генерировать прибыль за счет собственных средств.
Есть еще много других мультипликаторов, но я в основном пользуюсь только этими.

Машинное обучение

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

Данные

Понятно, что для написания нужных мне алгоритмов необходимы данные по фундаментальным показателям компаний за предыдущие кварталы (как в отчетах, только в одном месте и единообразно). Для американского рынка бесплатных источников я не нашел, но есть недорогой (около 30$) поставщик довольно качественных данных с удобным API. Кроме того, там же есть и посуточные базовые свечные данные.

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

downloader = QuandlDownloader(config, secrets, sleep_time=0.8) downloader.ticker_download('datatables/SHARADAR/SF1?ticker={ticker}',                           ticker_list,                           save_dirpath='data/core_fundamental',                           skip_exists=False,                           batch_size=10,                           n_jobs=2)downloader.ticker_download('datatables/SHARADAR/DAILY?ticker={ticker}',                           ticker_list,                           save_dirpath='data/daily',                           skip_exists=False,                           batch_size=5,                           n_jobs=2) 

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

class DataLoader:    def load_base_data(self) -> pd.DataFrame:        # returned pd.DataFrame should have ["ticker"] column    def load_quartely_data(self, tickers: List[str]) -> pd.DataFrame:        # returned pd.DataFrame should have ["ticker", "date"] columns    def load_daily_data(self, tickers: List[str]) -> pd.DataFrame:        # returned pd.DataFrame should have ["ticker", "date"] columns

Соответственно, предполагается, что load_base_data будет загружать основные данные про компании, которые не меняются со временем, вроде сектора, индустрии и т.д.
load_quartely_data будет загружать поквартальные данные (revenue, netincome и т.д.), при этом каждая строчка - отдельный квартал.

load_daily_data грузит дневную дату (например, ценовые свечи, дневную капитализацию). Для текущего поставщика данных используется класс SF1Data, реализующий данный интерфейс.

Честная стоимость компании

Итак, первая задача, которая у меня постоянно возникала и которую я не знал как решить - оценка адекватной стоимости компании. То есть, часто можно слышать фразы "эта компания переоценена", "слишком дорогая" или наоборот "сильно недооцененная, дешевая". Но как численно понять, сколько по-хорошему должна стоить компания? Кто-то может сказать "а разве не для этого и существует мультипликатор P/E?" и будет отчасти прав. Но. Часть компаний (особенно на ранних этапах) являются убыточными и для них мультипликатор вообще не определен. Или для какой-то компании сейчас мультипликатор высокий, но это не значит, что компания плохая, просто на данном этапе она может вкладываться в рост. И для меня, как для инвестора, это хорошо - да, компания могла бы получить высокую прибыль (а, соответственно, низкий P/E), если бы сократила расходы на маркетинг, например. Но тогда она не заполучила бы новых клиентов, не открыла новые точки и т.д. В результате это привело бы к тому, что в будущем прибыль компании была бы не такая большая, как если бы сейчас полученная прибыль направилась в развитие.

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

Признаки

Первым делом нужно соорудить некоторое представление компании по ее фундаментальным показателям. Логично предположить, что если компания зарабатывает по 100 млрд последние 10 кварталов, то она никак не может стоить 1 млрд (должна стоить гораздо дороже).

Аналогичная интуиция и с остальными показателями - если долг убывает со временем, значит дела в порядке (плюсик к капитализации). Если выручка растет за последние кварталы - значит компания развивается, это хорошо, и, соответственно, должно закладываться в цену. Все эти признаки легко покрываются с помощью подсчетов статистик вроде mean max min std и т.д. для последних, например, 2, 4, 10 кварталов. Кроме того, часто смотрят не только на то, растет выручка или нет, но и на темпы роста. Поэтому можно добавить статистики и по диффам - например, среднее значение того, на сколько процентов росла выручка. При этом логично, что все эти подсчеты можно делать для разных квартальных срезов компании: считать признаковое представление не только для текущего квартала, но и для предыдущих (параметр max_back_quarter), тем самым кратно увеличивая датасет. Ну и как результат, полученный класс для подсчета квартальных фичей и его использование:

fc1 = QuarterlyFeatures(columns=['revenue', 'netinc', 'debt'],  quarter_counts=[2, 4, 10],  max_back_quarter=5)fc1.calculate(data_loader, ['AAPL', 'INTC', 'F'])

Кроме того, при оценке стоимости компании важны и ее базовые характеристики. Например, как уже упоминал, технологические компании обычно оцениваются дороже. Аналогично, из разных индустрий средние оценки компаний тоже могут разниться. Поэтому был реализовал класс BaseCompanyFeatures, кодирующий подобные признаки с помощью лэйбл-энкодинга:

fc2 = BaseCompanyFeatures(cat_columns=['sector', 'sicindustry'])fc2.calculate(data_loader, ['AAPL', 'INTC', 'F'])

Ещё одним моментом, который бы хотелось учитывать при оценке компании, является подневная динамика движения цены акции компании за последнее время. Интуиция в том, что если стоимость компании стабильно растет, то инвесторы охотнее будут её покупать. При этом динамика должна представляться в нормализованном виде, чтобы избежать лика(нечестно определять стоимость компании, основываясь на стоимости компании). Соответствующий класс DailyAggQuarterFeatures (так же работает с квартальными слайсами компании и параметром max_back_quarter):

fc3 = DailyAggQuarterFeatures(    columns=['marketcap'],    agg_day_counts=[100, 200, 400, 800],    max_back_quarter=5)fc3.calculate(data_loader, ['AAPL', 'INTC', 'F'])

Для удобства комбинирования признаков был реализован класс FeatureMerger:

feature = FeatureMerger(fc1, fc2, on='ticker')feature = FeatureMerger(feature, fc3, on=['ticker', 'date'])feature.calculate(data_loader, ['AAPL', 'INTC', 'F'])

Таргет

С целевой переменной (той, что собираемся предсказывать) для этой модели более-менее понятно. Нужно просто взять капитализацию компании в момент отчета. Для согласования таргета и фичей было принято решение сделать следующий интерфейс подсчета таргета:

def calculate(self, data_loader, info_df: pd.DataFrame) -> pd.DataFrame:  '''  info_df:    pd.DataFrame containing information of tickers and dates    to calculate targets for. Should have columns: ["ticker", "date"].    '''

Ну и ниже сам класс QuarterlyTarget для подсчета таких квартальных таргетов. Его функционал чуть шире - данный класс позволяет брать значения переменных не только для нужного квартала, но и для соседних (например, если вдруг захотим предсказывать не текущую, а будущую капитализацию компании):

info_df = pd.DataFrame([{'ticker':'AAPL', 'date':'2020-10-30'}])target = QuarterlyTarget(col='marketcap', quarter_shift=0)target.calculate(data_loader, info_df)

На самом деле, тут можно было бы сделать и другой таргет - сгладить капитализацию за будущие 30 дней, например. Интуиция в том, что непосредственно в день отчета цена может сильно скакать, а за месяц все более-менее устаканится и средняя капитализация будет более стабильной. Как раз для таких целей у меня написан DailyAggTarget класс таргета:

target = DailyAggTarget(    col='marketcap',    horizon=30,    foo=np.mean)target.calculate(data_loader, info_df)

Модель

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

Теперь его распределение хоть как-то похоже на нормальное, что является комфортным для модели. Для удобства логарифмирование таргета и последующее возведение предсказаний в экспоненту были инкапсулированы в классе LogExpModel:

model = LogExpModel(lgbm.sklearn.LGBMRegressor())model.fit(X, y)model.predict(X)


Теперь главное - правильно построить валидацию. Дело в том, что если обучить модель на одном квартале, а валидировать на следующем, то модель просто запомнит эту компанию и возьмет капитализацию с предыдущего квартала, что не очень правильно. Модель должна по другим похожим компаниям понимать, сколько должна стоить текущая. Соответственно, нужно делать группировку по компаниям при валидации (разные квартальные срезы компании должны быть строго в одном фолде, т.е. не использоваться одновременно и для тренировки, и для валидации).

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

model = GroupedOOFModel(ansamble,    group_column='ticker',    fold_cnt=5)model.fit(X, y) # X should contain 'ticker' columnmodel.predict(X)

Кроме того, экспериментально было проверено, что одна модель ведет себя нестабильно при, например, дообучении на новых данных. Поэтому (и еще для увеличения итогового качества предсказаний) было решено использовать ансабль моделей. При этом в ансамбль входят как разные классы моделей, так и модели, обученные на разных сабсетах тренировочных данных (бэггинг). Во время инференса предсказания базовых моделей усредняются.

Данный ансамбль реализован с интерфейсом, типичным для всех моделей:

base_models = [lgbm.sklearn.LGBMRegressor(), ctb.CatBoostRegressor()]ansamble = EnsembleModel(  base_models=base_models,     bagging_fraction=0.7,    model_cnt=20)ansamble.fit(X, y)ansamble.predict(X)

Итого, финальная модель, являющаяся комбинацией всех описанных выше классов:

base_models = [LogExpModel(lgbm.sklearn.LGBMRegressor()),               LogExpModel(ctb.CatBoostRegressor())]ensemble = EnsembleModel(    base_models=base_models,     bagging_fraction=0.7,    model_cnt=20)model = GroupedOOFModel(ensemble,    group_column='ticker',    fold_cnt=5)

Пайплайн

Итак, почти все готово для обучения:

  • feature - класс, реализующий подсчет фичей

  • target - класс, реализующий вычисление таргета

  • model - модель(в том числе инкапсулирующая в себе разделение для валидации)

Осталось собрать все это воедино и обучить, используя наши данные (а именно, класс SF1Data). Для данных целей был написан класс BasePipeline. Он скрывает в себе всю логику с расчетом фичей, таргетов, обучением модели и подсчетом метрики (кроме того, поддерживается режим с мульти-таргетом и мульти-метриками). При инференсе производит pd.DataFrame с результатом в колонке out_name.

data_loader = SF1Data('path/to/data')pipeline = BasePipeline(    feature=feature,     target=target,     model=model,     metric=median_absolute_relative_error,    out_name=['fair_marketcap'])pipeline.fit(data_loader, ticker_list)pipeline.execute(data_loader, ['INTC'])

Результат:

ticker

date

fair_marketcap

INTC

2021-01-22

4.363793e+11

INTC

2020-10-23

2.924576e+11

INTC

2020-07-24

3.738603e+11

INTC

2020-04-24

3.766202e+11

INTC

2020-01-24

4.175332e+11


Еще раз код всего пайплайна целиком(он же на гитхабе https://github.com/fartuk/ml_investment/blob/main/train/fair_marketcap.py):

Код
SAVE_PATH = 'models_data/fair_marketcap'OUT_NAME = 'fair_marketcap'CURRENCY = 'USD'MAX_BACK_QUARTER = 10BAGGING_FRACTION = 0.7MODEL_CNT = 20FOLD_CNT = 5QUARTER_COUNTS = [2, 4, 10]AGG_DAY_COUNTS = [100, 200, 400, 800]SCALE_MARKETCAP = ["4 - Mid", "5 - Large", "6 - Mega"]DAILY_AGG_COLUMNS = ["marketcap", "pe"]CAT_COLUMNS = ["sector", "sicindustry"]QUARTER_COLUMNS = [            "revenue",            "netinc",            "ncf",            "assets",            "ebitda",            "debt",            "fcf",            "gp",            "workingcapital",            "cashneq",            "rnd",            "sgna",            "ncfx",            "divyield",            "currentratio",            "netinccmn"         ]data_loader = SF1Data('path/to/data')tickers_df = data_loader.load_base_data(    currency=CURRENCY,    scalemarketcap=SCALE_MARKETCAP)ticker_list = tickers_df['ticker'].unique().tolist()fc1 = QuarterlyFeatures(    columns=QUARTER_COLUMNS,    quarter_counts=QUARTER_COUNTS,    max_back_quarter=MAX_BACK_QUARTER)fc2 = BaseCompanyFeatures(cat_columns=CAT_COLUMNS)# Daily agss on marketcap and pe is possible here because it # normalized and there are no leakage.fc3 = DailyAggQuarterFeatures(    columns=DAILY_AGG_COLUMNS,    agg_day_counts=AGG_DAY_COUNTS,    max_back_quarter=MAX_BACK_QUARTER)feature = FeatureMerger(fc1, fc2, on='ticker')feature = FeatureMerger(feature, fc3, on=['ticker', 'date'])target = QuarterlyTarget(col='marketcap', quarter_shift=0)base_models = [LogExpModel(lgbm.sklearn.LGBMRegressor()),               LogExpModel(ctb.CatBoostRegressor(verbose=False))]ensemble = EnsembleModel(    base_models=base_models,     bagging_fraction=BAGGING_FRACTION,    model_cnt=MODEL_CNT)model = GroupedOOFModel(ensemble,                        group_column='ticker',                        fold_cnt=FOLD_CNT)pipeline = BasePipeline(feature=feature,                         target=target,                         model=model,                         metric=median_absolute_relative_error,                        out_name=OUT_NAME)result = pipeline.fit(data_loader, ticker_list)print(result)pipeline.export_core(SAVE_PATH)        

Итак, удалось построить пайплайн, который выдает для квартальных срезов компаний оценки честной капитализации. Поиграться с результатами и посмотреть на fair marketcap для компаний американского рынка можно на странице http://fattakhov.site/company?ticker=AAPL . Для этого нужно ввести тикер интересующей компании и нажать кнопку Analyze. Ну и, собственно, как можно использовать полученный инструмент - смотреть на реальную капитализацию (синий график), на предсказанную честную капитализацию (оранжевый график) и считать компанию недооцененный, если оранжевый график лежит ниже синего и переоцененной в противном случае. Сами точки, соответственно, находятся на датах выходов квартальных отчетов. Проверим модельку на некоторых примерах:

С теслой, как и ожидалось, модель говорит о сильной переоценке. И действительно, если глазами смотреть на показатели компании, то в данный момент цена кажется не очень оправданной.

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

Справедливое изменение капитализации после отчета

Идея следующего пайплайна, который я попробовал, заключается в следующем: пусть модель для честного определения капитализации может не учитывать некоторые скрытые причны того, почему та или иная компания стот столько, сколько она стоит. Но можно ли научить модель после выхода очередного отчета предсказывать, насколько должна была измениться стоимость компании? Кажется, что такая задача проще, потому что часто бывает, скрытые моменты, объясняющие рыночную стоимость, переходят из квартала в квартал. Ну вот нравится инвесторам покупать Теслу и ничего с этим не поделаешь.

Итак, вышел отчет компании, ее фундаментальные показатели как-то изменились (например, выручка выросла на 30%, прибыль выросла на 40% и т.д.). И по таким изменениям хотим предсказывать, а как по-хорошему должна была измениться капитализация.

Признаки

В принципе, все базовые фичи, которые были в первом пайплайне, можно оставить, они подходят и для текущей задачи. Но понятно, что нужно добавить и характеристики типа "изменение от квартала к кварталу". При этом стоит заметить, что важно сравнивать не только с предыдущим кварталом, но и кварталом год назад, потому что часто встречаются компании, например, с сезонными продажами. На графике ниже показана поквартальная выручка и прибыль компании Apple. Четко видны сезонные пики, связанные, думаю, с выходом новых моделей айфонов.

Реализовать подсчет нужных признаков можно с помощью класса QuarterlyDiffFeatures. В качестве параметра compare_quarter_idxs передаем [1, 4], так как хотим сравнить показатели с предыдущим кварталом и кварталом год назад. Класс посчитает относительные изменения показателей, находящихся в колонках columns:

fc = QuarterlyDiffFeatures(    columns=['revenue', 'netinc'],    compare_quarter_idxs=[1, 4],    max_back_quarter=5)fc.calculate(data_loader, ['AAPL', 'INTC', 'F'])

Таргет

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

target = QuarterlyDiffTarget(col='marketcap', norm=True)target.calculate(data_loader, info_df)

Модель

Навороты с ансамблем, разумеется, можно оставить, но нужно решить вопрос с валидацией. В данном случае совсем не обязательно группировать по компаниям, ведь предсказания относительные, а что будет с компанией в будущем по ее истории из прошлого - совсем непонятно. Однако тут есть другой момент - предсказывая изменение на текущий квартал, модель не должна знать про то, как в аналогичном квартале (и в будущем) изменились капитализации других компаний. Например, если вдруг случился мощный кризис, на фоне всеобщей паники капитализации всех компаний попадали даже при хороших отчетах. И модель по косвенным признакам поняла, что в данный момент кризис и занизила предсказания. Но во время инференса такой информации не будет, поэтому приходим к тому, что нужно делать time-series валидацию. А именно, сортируем все данные во времени, делим на N фолдов, обучаем модель на первом, предсказываем на второй. Обучаем модель на первых двух фолдах, предсказываем на третий. И так далее. Таким образом, делая предсказание на очередной квартал, мы не заглядываем в будущее.

Как и в случае с группировкой по фолдам, реализация time series валидации скрыта в классе модели TimeSeriesOOFModel:

model = TimeSeriesOOFModel(    base_model=lgbm.sklearn.LGBMRegressor(),    time_column='date',    fold_cnt=20)model.fit(X, y) # X should contain 'date' columnmodel.predict(X)

Пайплайн

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

код
SAVE_PATH = 'models_data/fair_marketcap_diff'OUT_NAME = 'fair_marketcap_diff'CURRENCY = 'USD'MAX_BACK_QUARTER = 10BAGGING_FRACTION = 0.7MODEL_CNT = 20FOLD_CNT = 5QUARTER_COUNTS = [2, 4, 10]COMPARE_QUARTER_IDXS = [1, 4]SCALE_MARKETCAP = ["4 - Mid", "5 - Large", "6 - Mega"]CAT_COLUMNS = ["sector", "sicindustry"]QUARTER_COLUMNS = [            "revenue",            "netinc",            "ncf",            "assets",            "ebitda",            "debt",            "fcf",            "gp",            "workingcapital",            "cashneq",            "rnd",            "sgna",            "ncfx",            "divyield",            "currentratio",            "netinccmn"         ]data_loader = SF1Data('path/to/data')tickers_df = data_loader.load_base_data(    currency=CURRENCY,    scalemarketcap=SCALE_MARKETCAP)ticker_list = tickers_df['ticker'].unique().tolist()fc1 = QuarterlyFeatures(    columns=QUARTER_COLUMNS,    quarter_counts=QUARTER_COUNTS,    max_back_quarter=MAX_BACK_QUARTER)fc2 = BaseCompanyFeatures(cat_columns=CAT_COLUMNS)fc3 = QuarterlyDiffFeatures(    columns=QUARTER_COLUMNS,    compare_quarter_idxs=COMPARE_QUARTER_IDXS,    max_back_quarter=MAX_BACK_QUARTER)feature = FeatureMerger(fc1, fc2, on='ticker')feature = FeatureMerger(feature, fc3, on=['ticker', 'date'])target = QuarterlyDiffTarget(col='marketcap')base_models = [lgbm.sklearn.LGBMRegressor(),               ctb.CatBoostRegressor(verbose=False)]ensemble = EnsembleModel(base_models=base_models,                          bagging_fraction=BAGGING_FRACTION,                         model_cnt=MODEL_CNT)model = GroupedOOFModel(ensemble,                        group_column='ticker',                        fold_cnt=FOLD_CNT)pipeline = BasePipeline(feature=feature,                         target=target,                         model=model,                         metric=median_absolute_relative_error,                        out_name=OUT_NAME)result = pipeline.fit(data_loader, ticker_list)print(result)pipeline.export_core(SAVE_PATH) 

Попробуем запустить полученный пайплайн на некоторых примерах. Опять же, посмотреть результат для других тикеров можно на странице http://fattakhov.site/company?ticker=AAPL (черный график). Итак, новый пайплайн выдет относительное предсказанное изменение для текущего квартала:

ticker

date

fair_marketcap_diff

INTC

2021-01-22

0.283852

INTC

2020-10-23

-0.021278

INTC

2020-07-24

-0.035124

INTC

2020-04-24

-0.098987

INTC

2020-01-24

0.198822

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

Видно, что получаются довольно точные предсказания (черный график), но расхождения все равно присутствуют, и как раз их и можно использовать при принятии решений о покупке/продажи очередной акции. То есть, например, вышел плохой отчет, компания сильно упала. Но модель говорит, что падение должно было быть не таким сильным -> потенциально хороший момент, чтобы купить.

С теслой, однако, даже такая модель не может объяснить столь стремительный рост :)

Характеристика потенциальных рисков

Может быть случай, что все предыдущие модели в один голос говорят: "покупай, дёшево!", но из истории понятно, что стоимость данной акции сильно скачет вверх-вниз, что не совсем приятно. Совершая сделку, хочется понимать, а какой есть риск? До каких пределов может опуститься цена и с какой вероятностью? Можно было бы из истории сделать статистические оценки, но у нас же ML-тулы :) Поэтому сделаем еще один машинно-обученный пайплайн (в этом подходе есть надежда, что модель сможет самостоятельно детектировать неблагоприятные/сильно рискованные моменты)

Фичи

Не мелочимся и используем сразу все признаки, которые использовались ранее.

Таргет

С выбором таргета тут сложнее. Основная идея в том, что он должен показывать, насколько вероятно падение в ближайшее время и каким оно может быть по величине. В голову приходит что-то вроде стандартного отклонения, std-вниз, максимальной просадки и т.п. за какой-то будущий промежуток времению. Пока для примера можно ограничиться std-вниз. Формула расчета представлена ниже (x_down - все x, меньшие среднего):

\sqrt{\sum{\frac{(x_{down} - \overline{x})^2}{N-1}}}

Класс таргета для таких целей уже описывался ранее:

target = DailyAggTarget(    col='marketcap',    horizon=90,    foo=down_std_norm)

Модель

С моделью все понятно, берем максимальный ансамбль. При этом так же, как и в предыдущем примере, нужно использовать time-series валидацию (более того, это первая модель, которая должна оценивать будущее).

Пайплайн

Все тот же класс BasePipeline позволяет реализовать данный пайплайн (гитхаб версия).

код
SAVE_PATH = 'models_data/marketcap_down_std'OUT_NAME = 'marketcap_down_std'CURRENCY = 'USD'TARGET_HORIZON = 90MAX_BACK_QUARTER = 10BAGGING_FRACTION = 0.7MODEL_CNT = 20FOLD_CNT = 20QUARTER_COUNTS = [2, 4, 10]COMPARE_QUARTER_IDXS = [1, 4]AGG_DAY_COUNTS = [100, 200, 400, 800]SCALE_MARKETCAP = ["4 - Mid", "5 - Large", "6 - Mega"]DAILY_AGG_COLUMNS = ["marketcap", "pe"]CAT_COLUMNS = ["sector", "sicindustry"]QUARTER_COLUMNS = [            "revenue",            "netinc",            "ncf",            "assets",            "ebitda",            "debt",            "fcf",            "gp",            "workingcapital",            "cashneq",            "rnd",            "sgna",            "ncfx",            "divyield",            "currentratio",            "netinccmn"         ]data_loader = SF1Data(config['path/to/data'])tickers_df = data_loader.load_base_data(    currency=CURRENCY,    scalemarketcap=SCALE_MARKETCAP)ticker_list = tickers_df['ticker'].unique().tolist()fc1 = QuarterlyFeatures(    columns=QUARTER_COLUMNS,    quarter_counts=QUARTER_COUNTS,    max_back_quarter=MAX_BACK_QUARTER)fc2 = BaseCompanyFeatures(cat_columns=CAT_COLUMNS)fc3 = QuarterlyDiffFeatures(    columns=QUARTER_COLUMNS,    compare_quarter_idxs=COMPARE_QUARTER_IDXS,    max_back_quarter=MAX_BACK_QUARTER)fc4 = DailyAggQuarterFeatures(    columns=DAILY_AGG_COLUMNS,    agg_day_counts=AGG_DAY_COUNTS,    max_back_quarter=MAX_BACK_QUARTER)feature = FeatureMerger(fc1, fc2, on='ticker')feature = FeatureMerger(feature, fc3, on=['ticker', 'date'])feature = FeatureMerger(feature, fc4, on=['ticker', 'date'])target = DailyAggTarget(    col='marketcap',    horizon=TARGET_HORIZON,    foo=down_std_norm)base_models = [LogExpModel(lgbm.sklearn.LGBMRegressor()),               LogExpModel(ctb.CatBoostRegressor(verbose=False))]ensemble = EnsembleModel(base_models=base_models,                          bagging_fraction=BAGGING_FRACTION,                         model_cnt=MODEL_CNT)model = TimeSeriesOOFModel(ensemble,                           time_column='date',                           fold_cnt=FOLD_CNT)pipeline = BasePipeline(feature=feature,                         target=target,                         model=model,                         metric=median_absolute_relative_error,                        out_name=OUT_NAME)result = pipeline.fit(data_loader, ticker_list)print(result)pipeline.export_core(SAVE_PATH) 

Итак, пайплайн готов и выдает на выход предсказанные std-вниз (нормированные):

ticker

date

marketcap_down_std

INTC

2021-01-22

0.043619

INTC

2020-10-23

0.057673

INTC

2020-07-24

0.061062

INTC

2020-04-24

0.053481

INTC

2020-01-24

0.039370

Если считать предсказанное std верным, то можно посчитать различные доверительные интервалы и отобразить их в виде ступенчатого графика. Для примера возьмём компанию Carnival, так как она известна своими скачками.

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

Заключение

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

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

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru