Сравнить коммиты

...

28 Коммитов

Автор SHA1 Сообщение Дата
syropiatovvv 00e0ce4b78
добавить ipynb-версию блокнота research по указанию преподавателя
21 часов назад
syropiatovvv 25444818cc
добавить README к research, обновить общий README (расположение файлов `requirements`), вынести документацию по использованию Jupyter в отдельный файл
21 часов назад
syropiatovvv 4665a66473
добавить скрипты для вызов mlflow gc, вынести общую часть конфигурации скриптов вызова mlflow в отдельный sourceable скрипт
21 часов назад
syropiatovvv 59897fbe61
в блокнот research добавить логирование списков выбранных признаков в MLFlow (через новый коллбек в mlflow_log_model), закомментировать логирование global_comment_file
21 часов назад
syropiatovvv bb1796e081
в блокнот research добавлено логирование Python requirements в MLFlow
21 часов назад
syropiatovvv d39d8f98d6
в блокнот research добавлена часть о выбранной лучшей модели
21 часов назад
syropiatovvv a7a0780f1a
refactor: в блокноте research, использовать mlflow nested runs
21 часов назад
syropiatovvv c16caf2e6a
сделать прогон optuna в блокноте research опциональным
21 часов назад
syropiatovvv 22ef4d303c
добавить в блокнот research часть про оптимизацию гиперпараметров, обобщить некоторый код (в особенности фильтры параметров моделей)
21 часов назад
syropiatovvv 543a4c6571
добавить в блокнот research часть про фильтрацию признаков с помощью SequentialFeatureSelector
21 часов назад
syropiatovvv ce02a6966b
изменить гиперпараметры RandomForestRegressor по умолчанию
21 часов назад
syropiatovvv 8c3e3c1588
добавить в блокнот research часть о feature engineering с sklearn, выделить в блокноте некоторые общие функции, убрать 'transformer_input' из сохраняемых параметров Pipeline
21 часов назад
syropiatovvv 2831ff4e81
fix: использовать правильное ядро Jupyter (локальное) в блокноте research
21 часов назад
syropiatovvv 6038a1c566
рефакторинг блокнота research в части логирования в mlflow
21 часов назад
syropiatovvv 070688dc68
рефакторинг блокнота research в плане использования MLFlow
21 часов назад
syropiatovvv 2b2241b2ab
разделение requirements.txt по частям проекта
21 часов назад
syropiatovvv 3ee22c3f0e
комментарии в блокнот research (до MLFlow)
21 часов назад
syropiatovvv af9340eda2
рефакторинг скрипта запуска сервера MLFlow, добавить скрипт запуска MLFlow для PowerShell
21 часов назад
syropiatovvv a3e8ebc030
убрать ненужную проверку существования файла БД в скрипте запуска MLFlow
4 дней назад
syropiatovvv 2f1b884d4b
fix: в блокноте research убрать лишние параметры для papermill
4 дней назад
syropiatovvv b66aed2636
добавить логирование прогонов в MLFlow
3 недель назад
syropiatovvv 462ab85b18
ввести MLFlow в работу
3 недель назад
syropiatovvv 41497aa039
блокнот research
3 недель назад
syropiatovvv f6714c0918
добавить scikit-learn в зависимости
3 недель назад
syropiatovvv 0d66f73f7f
fix: откатить версии зависимостей ради Python 3.10
3 недель назад
syropiatovvv 105e06f7b4
refactor,docs: ввести использование локальных ядер Jupyter, переписать инструкцию по использованию Jupyter
4 недель назад
syropiatovvv addc173d75
refactor: мелкое изменение в алгоритме suggest_bins_num
1 месяц назад
syropiatovvv ca3d34795d
удалить ненужный код (существует numpy.geomspace)
1 месяц назад

@ -51,16 +51,20 @@
#### Общие зависимости
Зависимости — пакеты Python — записаны в файле `requirements.txt` (см. **Пакеты Python**).
Зависимости — пакеты Python — записаны в файле `requirements/requirements.txt` (см. **Пакеты Python**).
#### Пакеты Python
Установка/обновление пакетов Python в активное окружение из файла `requirements.txt`:
Установка/обновление пакетов Python в активное окружение из файла `requirements/requirements.txt`:
```sh
pip install -U -r requirements.txt
pip install -U -r requirements/requirements.txt
```
## Разведочный анализ данных (EDA)
См. `eda/README.md`.
## Исследование и настройка предсказательной модели
См. `research/README.md`.

@ -0,0 +1,2 @@
$BACKEND_STORE_DB_PATH = "./mlflow/mlruns.sqlite"
$BACKEND_URI = "sqlite:///$BACKEND_STORE_DB_PATH"

@ -0,0 +1,4 @@
set -eu
BACKEND_STORE_DB_PATH="${BACKEND_STORE_DB_PATH:-./mlflow/mlruns.sqlite}"
BACKEND_URI="sqlite:///$BACKEND_STORE_DB_PATH"

@ -1,73 +0,0 @@
# Обоснование порядка работы с блокнотами Jupyter
[Архитектура](https://docs.jupyter.org/en/latest/projects/architecture/content-architecture.html) экосистемы Jupyter очень сложна, особенно если учитывать сторонние дополнения; данное обоснование может содержать неточности.
Концептуально, важные для темы компоненты в архитектуре Jupyter:
* *Ядро* ([kernel](https://docs.jupyter.org/en/latest/projects/kernels.html#kernels)) — независимый процесс, непосредственно исполняющий код на конкретном языке программирования (основываясь на конкретном экземпляре интерпретатора) и предоставляющий API для передачи исполняемого кода и получения результатов. (Самое популярное ядро — `ipykernel` — использует [IPython](https://ipython.org/), программный пакет, реализующий Python с расширенными возможностями интерактивной работы.)
* *Сервер* (server) и *приложение* (application) — компоненты, обеспечивающие выполнение прикладных задач. Так, стандартный `jupyter_server` и использующие его популярные [веб-приложения](https://jupyter.org/) (Notebook, JupyterLab) реализуют редактирование и исполнение блокнотов и других файлов (делегируя только непосредственное исполнение кода ядрам) и все пользовательские интерфейсы.
Чтобы пользователь могу выбрать ядро для использования (а значит, выбрать язык программирования и конкретный экземпляр интерпретатора), сервер Jupyter выполняет поиск доступных *спецификаций ядер* (kernelspec).
В случае Python, выбор конкретного экземпляра интерпретатора важен в частности потому, что это означает и выбор конкретного виртуального окружения. И здесь начинаются проблемы.
* Сервер Jupyter реализует только примитивный механизм поиска спецификаций ядер. "**Ядра [устанавливается](https://ipython.readthedocs.io/en/stable/install/kernel_install.html#installing-the-ipython-kernel)**" (install) глобально в систему либо для отдельного пользователя, что означает размещение спецификаций ядер под глобально уникальными именами (с учётом приоритета ядер, установленных для конкретного пользователя). Механизма локального размещения спецификаций ядер просто не предусмотрено. (Сервер Jupyter, возможно, реализует и другой механизм поиска *активных* ядер, который здесь нерелевантен.)
* Расширение [Jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) для Visual Studio Code самостоятельно реализует ограниченные функции сервера и приложения. Однако оно имеет [продвинутый механизм](https://code.visualstudio.com/docs/datascience/jupyter-kernel-management#_python-environments) поиска спецификаций ядер. Спецификации ядер, размещённые в виртуальном окружении (в директории данных (например, `share`), далее `jupyter/kernels`), также подбираются и могут быть использованы пользователем. Они должны иметь *локально* (в отношении виртуального окружения) уникальные имена, **однако** они делят пространство имён с глобально установленными ядрами, просто с более высоким приоритетом.
* Блокнот Jupyter сохраняет имя нужного для его исполнения ядра в своих метаданных (`kernelspec.name`) (даже при использовании Jupytext).
Таким образом, отдельный блокнот требует ядро с определённым именем, это имя ищется в системе глобально, и при использовании оригинальной системы Jupyter нет возможности даже определить это имя локально для отдельного виртуального окружения с приоритетом выше, чем у ядер, установленных глобально. Т.е. блокнот существенно привязывается к окружению конкретной системы, что мешает переносимости и контролю версий, а также захламляет систему глобально установленными ядрами.
Найденные варианты смягчения проблемы:
1. Разворачивать Jupyter целиком внутри виртуального окружения.
При этом подходе проблемы с выбором ядра, вероятно, нет вообще.
**\*** Нет полной уверенности, что Jupyter в виртуальном окружении действительно будет использовать его экземпляр интерпретатора для ядра по умолчанию.
**+** Простота работы, по крайней мере в пределах одного виртуального окружения.
**−** Повышенный расход ресурсов хранилища; пакет `notebook` версии 7.4 со всеми зависимостями занимает порядка 264 МБ на диске 12 тысячами файлов.
**−** Возможные проблемы из-за изоляции среды Jupyter от других проектов.
2. Использовать только Visual Studio Code с расширением Jupyter и пакетом `ipykernel` в каждом проекте; не использовать оригинальную систему (сервер Jupyter и веб-приложения) вообще.
В каждом виртуальном окружении также нужен пакет `ipykernel` для работы с ядром.
**+** Довольно надёжная локальная связь ядер с виртуальными окружениями.
**−** Большая проблема с переносимостью блокнотов из-за необходимости использовать конкретную IDE с расширением вместо оригинальной системы.
**−** Ограниченный функционал и нестабильность расширения Jupyter для Visual Studio Code.
**−** Потенциальная необходимость ручных хаков в случае использования более одного виртуального окружения в одном проекте.
3. Использовать единственное определённое и зафиксированное для проекта имя ядра; устанавливать ядро с указанным именем глобально либо, для использования нескольких ядер (например, для нескольких виртуальных окружений для одного проекта), править каждый блокнот локально.
**+** Относительно простой порядок работы в случае с единственным виртуальным окружением для каждого проекта.
**−** Необходимо обеспечить уникальность имени ядра для проекта; имя ядра может стать нечитаемым.
**−** Если появляется необходимость иметь несколько ядер для одного проекта (например, для нескольких виртуальных окружений) или переименовать единственное ядро, необходимо применять локальные правки к каждому релевантному блокноту, что существенно усложняет контроль версий.
**−** Установленные спецификации ядер остаются в системе глобально и захламляют систему.
4. Запускать ядро самостоятельно из виртуального окружения и использовать механизм поиска активных ядер.
Есть интерфейсы, намекающие на возможность этого варианта (см. пакет `ipykernel_launcher`), но неясно, насколько это было бы практично.
**\*** Непонятно, возможно ли это.
**\*** Непонятно, практично ли это.
**+** Довольно надёжная локальная связь ядер с виртуальными окружениями.
**−** Довольно много телодвижений для обычной работы с Jupyter.
**−** Очень сложно в реализации, может потребовать разработки собственного ПО.
Вариант **3** выбран на данный момент для данного проекта из-за экономии ресурсов, переносимости и относительной простоты порядка работы с блокнотами в тривиальном рабочем процессе.

@ -0,0 +1,143 @@
# Использование среды Jupyter
Для исследовательских задач в проекте используется среда [Jupyter](https://jupyter.org/). Т.к. блокноты хранятся в текстовом формате под контролем версий, нужно также дополнение [Jupytext](https://jupytext.readthedocs.io/en/latest/) (как минимум для ручной конвертации блокнотов; см. ниже).
Опционально можно использовать дополнение [papermill](https://papermill.readthedocs.io/en/latest/) для простого параметризованного исполнения блокнотов.
## Установка
### Общий порядок
**Внимание**: Оптимальный порядок установки и конфигурации Jupyter для работы с проектом неоднозначен. См. обоснование выбранного здесь порядка работы с блокнотами Jupyter и возможные альтернативные варианты в статье [Использование Jupyter с виртуальными окружениями Python](https://asrelo.hashnode.dev/using-jupyter-with-python-virtual-environments-ru).
1. Jupyter и дополнения должны быть установлены в систему, а **не** в виртуальное окружение. При необходимости деактивируйте виртуальное окружение.
```sh
deactivate
```
2. [Установите Jupyter](https://jupyter.org/install) и Jupytext в систему (**не** в виртуальное окружение).
```sh
pip install -U notebook jupytext
```
Полная инструкция по установке Jupytext: [Installation — Jupytext documentation](https://jupytext.readthedocs.io/en/latest/install.html).
3. **Опционально**, установите papermill в систему (**не** в виртуальное окружение).
```sh
pip install -U papermill
```
Полная инструкция по установке: [Installation - papermill 2.4.0 documentation](https://papermill.readthedocs.io/en/stable/installation.html).
4. Активируйте **виртуальное окружение** повторно.
5. Установите ядро Jupyter, связанное с данным виртуальным окружением, в директорию этого виртуального окружения. Укажите следующее имя ядра: `python3_venv`.
```sh
python -m ipykernel --sys-prefix --name python3_venv
```
6. **Опционально**, **заранее** сохраните в переменную окружения `JUPYTER_PATH` путь к данным Jupyter в виртуальном окружении `<path>` &mdash; см. п. 1 в инструкции по использованию.
* Windows (PowerShell):
```ps
[System.Environment]::SetEnvironmentVariable('JUPYTER_PATH', "<path>;$env:JUPYTER_PATH", 'User')
```
* Windows (cmd):
```bat
setx JUPYTER_PATH "<path>;%PATH%;JUPYTER_PATH"
```
* UNIX (sh):
```sh
echo 'export JUPYTER_PATH="<path>:$JUPYTER_PATH"' >> ~/.profile
```
**Внимание**: На данном этапе могут отсутствовать пригодные для прямого использования блокноты `.ipynb` (например, если проект развёртывается с нуля). Об использовании спаренных блокнотов и конвертации форматов см. [Использование Jupytext](#использование-jupytext).
### Зависимости
*Используемые при работе с Jupyter зависимости &mdash; пакеты Python &mdash; на данный момент включены в общие зависимости (см. выше), дополнительных действий не требуется.*
## Работа с блокнотами Jupyter
### Jupyter
1. **Если** при выполнении инструкции по установке Вы **не** сохранили в переменную окружения JUPYTER_PATH путь к данным Jupyter в виртуальном окружении, этот путь нужно добавить в переменную окружения сейчас.
Добавьте в переменную окружения `JUPYTER_PATH` абсолютный путь (далее обозначаемый `<path>`) `$VIRTUAL_ENV/share/jupyter`, где следует заменить `$VIRTUAL_ENV` на путь к директории, где развёрнуто виртуальное окружение. Для инструментов [`venv`](https://docs.python.org/3/library/venv.html), [`virtualenv`](https://virtualenv.pypa.io/en/stable/) можно просто в активном виртуальном окружении использовать подстановку переменной окружения `VIRTUAL_ENV` (активное виртуальное окружение не повлияет на дальнейшие шаги).
* Windows (PowerShell):
```ps
$env:JUPYTER_PATH = "<path>;$env:JUPYTER_PATH"
```
* Windows (cmd):
```bat
set "JUPYTER_PATH=<path>;%JUPYTER_PATH%"
```
* UNIX (sh):
```sh
export JUPYTER_PATH="<path>:$JUPYTER_PATH"
```
2. Запустите глобальный установленное приложение Jupyter (**не** из виртуального окружения).
* Например, запустите Jupyter Notebook:
```sh
jupyter notebook
```
Веб-приложение Notebook должно открыться в веб-браузере автоматически. Если этого не произошло, найдите в сообщениях сервера Jupyter строку примерно следующего содержания:
[I 08:58:24.417 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/
Откройте веб-браузер и перейдите по ссылке, выведенной в конце указанного сообщения.
См. также [документацию Jupyter](https://docs.jupyter.org/en/stable/running.html).
2. Используйте приложение для навигации по файловой системе (в частности, по каталогу `eda/`), редактирования и исполнения кода в блокнотах.
3. Если приложение Jupyter запрашивает **выбор ядра** Jupyter (**kernel**) или Вы сталкиваетесь с необъяснимыми **ошибками импортов**, выберите для текущего блокнота ядро с именем `python3_venv`.
* **Jupyter Notebook**: Может понадобиться выбор вручную; кнопка для выбора ядра для открытого блокнота находится в верхнем правом углу веб-страницы.
### Расширение Jupyter для Visual Studio Code
1. Запустите Visual Studio Code.
2. Откройте корневую директорию проекта в VS Code (*File* -> *Open Folder...*).
3. Если Вы открыли директорию проекта и VS Code запрашивает выбор автоматически обнаруженного виртуального окружения, согласитесь.
3. **Если** VS Code запрашивает выбор автоматически обнаруженного виртуального окружения, согласитесь.
**Иначе** [укажите](https://code.visualstudio.com/docs/python/environments#_working-with-python-interpreters) своё виртуальное окружение самостоятельно.
4. Используйте VS Code с расширением Jupyter для навигации по файловой системе (в частности, по каталогу `eda/`), редактирования и исполнения кода в блокнотах. **Не забывайте** при открытии любого блокнота проверять, что выбрано корректное ядро Jupyter (принадлежащее корректному виртуальному окружению). (Кнопка для выбора ядра для открытого блокнота находится в верхнем правом углу области содержимого вкладки; по умолчанию Вы увидите название выбранного виртуального окружения; если ядро не выбрано, на кнопке написано *Select Kernel*.)
### Использование Jupytext
Описанные ниже команды `jupytext` используют глобальной установленный экземпляр Jupytext (однако его можно запускать и изнутри виртуального окружения).
Для автоматической синхронизации связанных блокнотов (включая создание блокнотов отсутствующих, но ожидаемых форматов):
```sh
jupytext --sync eda/cars_eda.py
```
Jupytext довольно удобно работает в оригинальной среде Jupyter, синхронизируя изменения связанных файлов на лету при работе в Jupyter, **ориентируясь на метки времени на файлах**. См. документацию [Jupytext](https://jupytext.readthedocs.io/en/latest/index.html).
**Внимание**: С расширением Jupyter для Visual Studio Code Jupytext **не работает напрямую**. Для использования блокнотов `.ipynb` с расширением Jupyter для VS Code нужно синхронизировать текстовый файл под контролем версий и файл `.ipynb` вручную указанными выше командами. Однако заметьте, что это же расширение может исполнять блокнот в текстовом формате самостоятельно, посредством автоматизированного ведения временного блокнота; и оно даже автоматически создаёт/подхватывает локальное ядро Jupyter в виртуальном окружении.

@ -30,125 +30,14 @@
## Установка
Для EDA необходимы общие зависимости, см. **Общие зависимости** в `README.md`.
Для EDA необходимы общие зависимости, см. [Общие зависимости](../README.md#общие-зависимости) в `README.md`.
Для EDA используется среда [Jupyter](https://jupyter.org/). Т.к. блокноты хранятся в текстовом формате под контролем версий, нужно также дополнение [Jupytext](https://jupytext.readthedocs.io/en/latest/) (как минимум для ручной конвертации блокнотов; см. ниже).
Опционально можно использовать дополнение [papermill](https://papermill.readthedocs.io/en/latest/) для простого параметризованного исполнения блокнотов.
### Общий порядок
**Внимание**: Оптимальный порядок установки и конфигурации Jupyter для работы с проектом неоднозначен. См. обоснование выбранного здесь порядка работы с блокнотами Jupyter и возможные альтернативные варианты в `eda/docs/jupyter_workflow_motivation.md`.
1. Выполните установку общих зависимостей, если это ещё не выполнено, см. **Общие зависимости** в `README.md`.
2. Jupyter и дополнения должны быть установлены в систему, а НЕ в виртуальное окружение. При необходимости деактивируйте виртуальное окружение.
```sh
deactivate
```
3. [Установите Jupyter](https://jupyter.org/install) и Jupytext в систему (НЕ в виртуальное окружение).
```sh
pip install -U notebook
```
4. Установите Jupytext в систему (НЕ в виртуальное окружение).
```sh
pip install -U jupytext
```
Полная инструкция по установке: [Installation &#8212; Jupytext documentation](https://jupytext.readthedocs.io/en/latest/install.html).
5. **Опционально**, установите papermill в систему (НЕ в виртуальное окружение).
```sh
pip install -U papermill
```
Полная инструкция по установке: [Installation - papermill 2.4.0 documentation](https://papermill.readthedocs.io/en/stable/installation.html).
**Шаги 6&ndash;7** **необходимо** выполнить **только** если Вы желаете использовать что-то кроме расширения Jupyter для Visual Studio Code для работы с блокнотами **или собираетесь** коммитить писать блокноты под контроль версий.
6. Активируйте виртуальное окружение вновь.
7. Установите ядро Jupyter, связанное с данным виртуальным окружением, в систему. Используйте следующее имя для ядра: `mpei-iis-99040779`. (*Да, это странно, но это признано лучшим подходом на данный момент; см. обоснование в `docs/dev/jupyter_workflow_motivation.md`.*)
```sh
ipython kernel install --user --name=mpei-iis-99040779
```
(Эта команда устанавливает виртуальное окружение глобально для текущего пользователя; для установки глобально в систему, уберите флаг `--user`).
**Внимание**: На данном этапе могут отсутствовать пригодные для прямого редактирования блокноты `.ipynb` (например, если проект развёртывается с нуля). Об использовании спаренных блокнотов и конвертации форматов см. **Использование Jupytext**.
#### Удаление ядра из системы
Во избежание захламления системы ядро можно удалить из системы **позднее**, вызвав глобально установленный Jupyter (НЕ в виртуальном окружении) и передав имя ядра.
```sh
jupyter kernelspec uninstall mpei-iis-99040779
```
**Примечание**: Установленное ядро НЕ занимает значительные ресурсы хранилища. На самом деле в систему устанавливается крайне лёгкая *спецификация ядра*, ограниченная коротким текстовым файлом и иконками.
Для EDA используется среда [Jupyter](https://jupyter.org/). См. об установке и использовании Jupyter в проекте в `docs/jupyter.md`.
### Зависимости
Используемые непосредственно кодом проекта зависимости для разведочного анализа данных (EDA) (директория `eda/`) &mdash; пакеты Python &mdash; на данный момент включены в общие зависимости (см. выше).
### Использование Jupytext
Описанные ниже команды `jupytext` используют глобальной установленный экземпляр Jupytext (однако его можно запускать и изнутри виртуального окружения).
Для автоматической синхронизации связанных блокнотов (включая создание блокнотов отсутствующих, но ожидаемых форматов):
```sh
jupytext --sync eda/cars_eda.py
```
Jupytext довольно удобно работает в оригинальной среде Jupyter, синхронизируя изменения связанных файлов на лету при работе в Jupyter, **ориентируясь на метки времени на файлах**. См. документацию [Jupytext](https://jupytext.readthedocs.io/en/latest/index.html).
**Внимание**: С расширением Jupyter для Visual Studio Code Jupytext **не работает напрямую**. Для использования блокнотов `.ipynb` с расширением Jupyter для VS Code нужно синхронизировать текстовый файл под контролем версий и файл `.ipynb` вручную указанными выше командами. Однако заметьте, что это же расширение может исполнять блокнот в текстовом формате самостоятельно, посредством автоматизированного ведения временного блокнота; и оно даже автоматически создаёт/подхватывает локальное ядро Jupyter в виртуальном окружении.
Дополнительные зависимости, необходимые для EDA, &mdash; пакеты Python &mdash; записаны в файле `requirements/requirements-eda.txt` (см. **Пакеты Python**). См. об установке пакетов Python в **Пакеты Python** в `README.md`.
## Работа с блокнотами Jupyter
### Jupyter
1. Запустите глобальный глобально установленный сервер Jupyter и приложение (НЕ из виртуального окружения).
* Например, запустите сервер Jupyter и веб-приложение Notebook в браузере:
```sh
jupyter notebook
```
Веб-приложение Notebook должно открыться в веб-браузере автоматически. Если этого не произошло, найдите в сообщениях сервера Jupyter строку примерно следующего содержания:
[I 08:58:24.417 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/
Откройте веб-браузер и перейдите по ссылке, выведенной в конце указанного сообщения.
См. также [документацию Jupyter](https://docs.jupyter.org/en/stable/running.html).
2. Используйте приложение для навигации по файловой системе (в частности, по каталогу `eda/`), редактирования и исполнения кода в блокнотах.
3. Если приложение Jupyter запрашивает **выбор ядра** Jupyter (**kernel**) или Вы сталкиваетесь с необъяснимыми **ошибками импортов**, выберите для текущего блокнота ядро с именем `mpei-iis-99040779` (которое Вы установили в систему раньше).
* **Notebook**: Может понадобиться выбор вручную; кнопка для выбора ядра для открытого блокнота находится в верхнем правом углу веб-страницы.
### Расширение Jupyter для Visual Studio Code
1. Запустите Visual Studio Code.
2. **Если** Вы НЕ установили ядро Jupyter, связанное с виртуальным окружением для проекта, в систему, обязательно откройте корневую директорию проекта в VS Code (*File* -> *Open Folder...*). **Иначе** это необязательный, но удобный шаг.
3. Если Вы открыли директорию проекта и VS Code запрашивает выбор автоматически обнаруженного виртуального окружения, согласитесь.
4. **При открытии любого блокнота** убедитесь, что выбрано корректное ядро Jupyter. (Кнопка для выбора ядра для открытого блокнота находится в верхнем правом углу области содержимого вкладки; если ядро не выборано, на кнопке написано *Select Kernel*.)
* **Если** Вы установили ядро Jupyter в систему, рекомендуется выбрать установленное в систему ядро с именем `mpei-iis-99040779`.
* **Если** Вы совсем **не собираетесь** использовать Jupyter для работы с проектом **и не собираетесь** записывать блокноты под контроль версий, можно выбрать локальное ядро, связанное с виртуальным окружением (по умолчанию имеет название виртуального окружения &mdash; `.venv`).
5. Используйте IDE с расширением для навигации по файловой системе (в частности, по каталогу `eda/`), редактирования и исполнения кода в блокнотах.
См. об установке и использовании Jupyter в проекте в `docs/jupyter.md`.

@ -8,9 +8,9 @@
# format_version: '1.3'
# jupytext_version: 1.17.3
# kernelspec:
# display_name: mpei-iis-project-99040779
# display_name: python3_venv
# language: python
# name: mpei-iis-project-99040779
# name: python3_venv
# ---
# %% [markdown]

@ -0,0 +1,5 @@
$ErrorActionPreference = "Stop"
. $PSScriptRoot\_mlflow_config_common.ps1
& mlflow gc --backend-store-uri="$BACKEND_URI" @args

@ -0,0 +1,7 @@
#!/bin/sh
set -eu
. _mlflow_config_common.sh
& mlflow gc --backend-store-uri="$BACKEND_URI" "$@"

@ -0,0 +1,3 @@
SEQUENTIAL_FEATURE_SELECTOR_PARAMS_COMMON_INCLUDE = [
'k_features', 'forward', 'floating', 'scoring', 'cv', 'fixed_features', 'feature_groups',
]

@ -1,8 +0,0 @@
from functools import wraps
from math import log
from numpy import logspace as numpy_logspace
@wraps(numpy_logspace, assigned=('__annotations__', '__type_params__'))
def logspace(start, stop, *args_rest, **kwargs):
return numpy_logspace(log(start), log(stop), *args_rest, **kwargs)

@ -3,7 +3,12 @@ from typing import Optional
from scipy.stats import norm
SND_QUARTILE: float = 0.674490
# XXX: может, заменить вызов scipy.stats.norm на аппроксимацию?
SND_QUARTILE: float = (
#0.674490
norm.ppf(1 - 0.25)
)
def suggest_iqr_to_range_to_suggest_bins_num(n: int) -> float:
p = 1 / (n + 1)

@ -0,0 +1,60 @@
from collections.abc import Container, Sequence, Mapping
from typing import TypeAlias, TypeVar
ParamsFilterSpec: TypeAlias = (
bool
| Container[str]
| tuple[bool, Container[str]]
| Mapping[str, 'ParamsFilterSpec']
| tuple[bool, 'ParamsFilterSpec']
)
V = TypeVar('V')
def _split_param_key(key: str) -> tuple[str, ...]:
return tuple(key.split('__'))
def _match_key_to_filter_spec(
key: Sequence[str], spec: ParamsFilterSpec, empty_default: bool,
) -> bool:
if isinstance(spec, Sequence) and (len(spec) == 2) and isinstance(spec[0], bool):
if (len(key) == 0) and (not spec[0]):
return empty_default
spec = spec[1]
if isinstance(spec, Mapping):
if len(key) == 0:
return empty_default
spec_nested = spec.get(key[0])
if spec_nested is None:
return False
return _whether_to_include_param(key[1:], spec_nested)
elif isinstance(spec, Container):
if len(key) == 0:
return True
return (key[0] in spec)
return bool(spec)
def _whether_to_include_param(
key: Sequence[str], include: ParamsFilterSpec = True, exclude: ParamsFilterSpec = False,
) -> bool:
return (
(not _match_key_to_filter_spec(key, exclude, empty_default=False))
and _match_key_to_filter_spec(key, include, empty_default=True)
)
def filter_params(
params: Mapping[str, V],
include: ParamsFilterSpec = True,
exclude: ParamsFilterSpec = False,
) -> Mapping[str, V]:
return {
k: v
for k, v in params.items()
if _whether_to_include_param(_split_param_key(k), include, exclude)
}

@ -0,0 +1,3 @@
COLUMN_TRANSFORMER_PARAMS_COMMON_INCLUDE = [
'remainder', 'sparse_threshold', 'transformer_weights',
]

@ -0,0 +1 @@
RANDOM_FOREST_REGRESSOR_PARAMS_COMMON_EXCLUDE = ['n_jobs', 'verbose', 'warm_start']

@ -0,0 +1,5 @@
from pandas import DataFrame
def pandas_dataframe_from_transformed_artifacts(matrix, transformer) -> DataFrame:
return DataFrame(matrix, columns=transformer.get_feature_names_out())

@ -0,0 +1 @@
STANDARD_SCALER_PARAMS_COMMON_EXCLUDE = ['copy']

2
mlflow/.gitignore поставляемый

@ -0,0 +1,2 @@
mlruns.sqlite
mlartifacts/

@ -0,0 +1,4 @@
mlflow >=2.16,<2.22
mlxtend ~=0.23.4
optuna ~=4.5
scikit-learn >=1.7.2,<2

@ -1,8 +1,7 @@
bokeh >=3.7.2,<4
ipykernel >=6.30.1,<7
ipympl ~=0.9.6
matplotlib >=3.10.1,<4
numpy >=2.3.1,<3
numpy >=2.2.6,<3
pandas >=2.3.1,<3
scipy >=1.16.1,<2
scipy >=1.15.3,<2
seaborn ~=0.13.2

@ -0,0 +1,59 @@
# Исследование и настройка предсказательной модели
## Блокноты Jupyter
* `research` &mdash; Создание множества разных моделей, с использованием разных создаваемых признаков и оптимизацией гиперпараметров.
Использует файл аугментированных данных датасета о подержанных автомобилях, создаваемый блокнотом `eda/cars_eda.py`. См. `eda/README.md`.
Если параметр блокнота `mlflow_do_log` установлен в `True`, блокнот логирует в MLFlow создаваемые модели в отдельные вложенные (nested) прогоны под одним (новым) общим прогоном с именем, определяемым параметром `mlflow_experiment_name`.
Точность предсказания текущей цены автомобиля оценивается в первую очередь по показателю MAPE (из-за наличия в выборке значений цены разных порядков), во вторую очередь учитывается MSE (ради отслеживания систематических ошибок на подвыборках). Исследованные модели:
1. baseline (MAPE = 0.35, MSE = 1.18);
2. с использованием добавленных признаков (feature engineering с помощью scikit-learn) &mdash; точность неоднозначна по сравнению с baseline (MAPE = 0.31, MSE = 1.50);
3. с использованием добавленных и выбранных (SFS) признаков &mdash; точность существенно лучше baseline (MAPE = 0.20, MSE = 1.02);
4. с использованием добавленных и выбранных признаков и оптимизированными гиперпараметрами (optuna) &mdash; точность немного лучше модели 3 по MAPE (MAPE = 0.20, MSE = 0.94).
Модель 4 выбрана как финальная модель для последующего развёртывания. Она использует следующие признаки (такие же, как и модель 3):
* `extend_features_as_polynomial__selling_price` (исходная цена продажи, нормализована `StandardScaler`),
* `extend_features_as_polynomial__selling_price^2`,
* `extend_features_as_spline__age_sp_1` (значение базисной функции 2/5 однородного сплайна, нормализованного к крайним значениям возраста автомобилей),
* `extend_features_as_spline__age_sp_2` (то же, но базисная функция 3/5),
* `scale_to_standard__age` (исходный возраст автомобиля, нормализован `StandardScaler`).
По указанию преподавателя, скриншоты пользовательского интерфейса MLFlow сохранены в директории `./mlflow_ui_figures`.
По указанию преподавателя, ID финального прогона: `4c7f04ad9ee94237b44f60b6eb14b41e` (вложен в прогон `4e4a9094cb3c4eed9d4a056a27cadcd9`).
## Установка
Для исследования и настройки предсказательной модели необходимы общие зависимости, см. [Общие зависимости](../README.md#общие-зависимости) в `README.md`.
Для исследования и настройки предсказательной модели используется среда [Jupyter](https://jupyter.org/). См. об установке и использовании Jupyter в проекте в `docs/jupyter.md`.
### Зависимости
Дополнительные зависимости, необходимые для исследования и настройки предсказательной модели, &mdash; пакеты Python &mdash; записаны в файле `requirements/requirements-research.txt` (см. **Пакеты Python**). См. об установке пакетов Python в **Пакеты Python** в `README.md`.
## Работа с блокнотами Jupyter
См. об установке и использовании Jupyter в проекте в `docs/jupyter.md`.
## Работа с MLFlow
Для управления жизненным циклом моделей машинного обучения используется платформа [MLFlow](https://mlflow.org/).
Запуск локального сервера MLFlow (**выполнять в корневой директории проекта**):
run_mlflow_server
Для остановки сервера MLFlow пошлите ему сигнал `SIGINT` (`Ctrl+C` в терминале).
Очистка локальной tracking БД MLFlow от удалённых прогонов (**выполнять в корневой директории проекта**):
gc_mlflow
Очистка локальной tracking БД MLFlow от конкретных удалённых экспериментов по списку их ID, разделённым запятыми, `<ids>` (**выполнять в корневой директории проекта**):
gc_mlflow --experiment-ids=<ids>

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 43 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 37 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 93 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 62 KiB

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

@ -0,0 +1,905 @@
# ---
# jupyter:
# jupytext:
# formats: py:percent,ipynb
# text_representation:
# extension: .py
# format_name: percent
# format_version: '1.3'
# jupytext_version: 1.17.3
# kernelspec:
# display_name: python3_venv
# language: python
# name: python3_venv
# ---
# %% [markdown]
# # Исследование и настройка предсказательной модели для цен подержанных автомобилях
# %% [markdown]
# Блокнот использует файл аугментированных данных датасета о подержанных автомобилях, создаваемый блокнотом `eda/cars_eda.py`. См. ниже параметры блокнота для papermill.
# %%
#XXX: разделить блокнот штук на 5
# %%
from typing import Optional
# %% tags=["parameters"]
data_aug_pickle_path: Optional[str] = None
# Полный путь к файлу (pickle) для сохранения очищенного датасета. Если не установлен, используется `data/<data_aug_pickle_relpath>`.
data_aug_pickle_relpath: str = 'cars.aug.pickle'
# Путь к файлу (pickle) для сохранения очищенного датасета относительно директории данных `data`. Игнорируется, если установлен data_aug_pickle_path.
#model_global_comment_path: Optional[str] = None
## Полный путь к текстовому файлу с произвольным комментарием для сохранения в MLFlow как артефакт вместе с моделью. Если не установлен, используется `research/<comment_relpath>`.
#model_comment_relpath: str = 'comment.txt'
## Путь к текстовому файлу с произвольным комментарием для сохранения в MLFlow как артефакт вместе с моделью относительно директории `research`. Игнорируется, если установлен comment_path.
mlflow_tracking_server_uri: str = 'http://localhost:5000'
# URL tracking-сервера MLFlow.
mlflow_registry_uri: Optional[str] = None
# URL сервера registry MLFlow (если не указан, используется `mlflow_tracking_server_uri`).
mlflow_do_log: bool = True
# Записывать ли прогоны (runs) в MLFlow.
mlflow_experiment_id: Optional[str] = None
# ID эксперимента MLFlow, имеет приоритет над `mlflow_experiment_name`.
mlflow_experiment_name: Optional[str] = 'Current price predicion for used cars'
# Имя эксперимента MLFlow (ниже приоритетом, чем `mlflow_experiment_id`).
mlflow_root_run_name: str = 'Models'
# Имя корневого прогона MLFlow (остальные прогоны будут созданы блокнотом внутри этого, как nested)
# %%
from collections.abc import Collection, Sequence
import os
import pathlib
import pickle
import sys
# %%
import matplotlib
import mlflow
import mlflow.models
import mlflow.sklearn
import mlxtend.feature_selection
import mlxtend.plotting
import optuna
import optuna.samplers
import sklearn.compose
import sklearn.ensemble
import sklearn.metrics
import sklearn.model_selection
import sklearn.pipeline
import sklearn.preprocessing
# %%
BASE_PATH = pathlib.Path('..')
# %%
CODE_PATH = BASE_PATH
sys.path.insert(0, str(CODE_PATH.resolve()))
# %%
from iis_project.mlxtend_utils.feature_selection import SEQUENTIAL_FEATURE_SELECTOR_PARAMS_COMMON_INCLUDE
from iis_project.sklearn_utils import filter_params
from iis_project.sklearn_utils.compose import COLUMN_TRANSFORMER_PARAMS_COMMON_INCLUDE
from iis_project.sklearn_utils.ensemble import RANDOM_FOREST_REGRESSOR_PARAMS_COMMON_EXCLUDE
from iis_project.sklearn_utils.pandas import pandas_dataframe_from_transformed_artifacts
from iis_project.sklearn_utils.preprocessing import STANDARD_SCALER_PARAMS_COMMON_EXCLUDE
# %%
MODEL_INOUT_EXAMPLE_SIZE = 0x10
# %%
mlflow.set_tracking_uri(mlflow_tracking_server_uri)
if mlflow_registry_uri is not None:
mlflow.set_registry_uri(mlflow_registry_uri)
# %%
if mlflow_do_log:
mlflow_experiment = mlflow.set_experiment(experiment_name=mlflow_experiment_name, experiment_id=mlflow_experiment_id)
mlflow_root_run_id = None # изменяется позже
# %%
DATA_PATH = (
pathlib.Path(os.path.dirname(data_aug_pickle_path))
if data_aug_pickle_path is not None
else (BASE_PATH / 'data')
)
# %%
def build_sequential_feature_selector(*args, **kwargs):
return mlxtend.feature_selection.SequentialFeatureSelector(*args, **kwargs)
def plot_sequential_feature_selection(feature_selector, *args_rest, **kwargs):
metric_dict = feature_selector.get_metric_dict()
return mlxtend.plotting.plot_sequential_feature_selection(metric_dict, *args_rest, **kwargs)
# %% [markdown]
# ## Загрузка и обзор данных
# %%
with open(
(
data_aug_pickle_path
if data_aug_pickle_path is not None
else (DATA_PATH / data_aug_pickle_relpath)
),
'rb',
) as input_file:
df_orig = pickle.load(input_file)
# %% [markdown]
# Обзор датасета:
# %%
len(df_orig)
# %%
df_orig.info()
# %%
df_orig.head(0x10)
# %% [markdown]
# ## Разделение датасета на выборки
# %% [markdown]
# Выделение признаков и целевых переменных:
# %%
feature_columns = (
'selling_price',
'driven_kms',
'fuel_type',
'selling_type',
'transmission',
#'owner',
'age',
)
target_columns = (
'present_price',
)
# %%
features_to_scale_to_standard_columns = (
'selling_price',
'driven_kms',
'age',
)
assert all(
(col in df_orig.select_dtypes(('number',)).columns)
for col in features_to_scale_to_standard_columns
)
features_to_encode_wrt_target_columns = (
'fuel_type',
'selling_type',
'transmission',
#'owner',
)
assert all(
(col in df_orig.select_dtypes(('category', 'object')).columns)
for col in features_to_encode_wrt_target_columns
)
# %%
df_orig_features = df_orig[list(feature_columns)]
df_target = df_orig[list(target_columns)]
# %% [markdown]
# Разделение на обучающую и тестовую выборки:
# %%
DF_TEST_PORTION = 0.25
# %%
df_orig_features_train, df_orig_features_test, df_target_train, df_target_test = (
sklearn.model_selection.train_test_split(
df_orig_features, df_target, test_size=DF_TEST_PORTION, random_state=0x7AE6,
)
)
# %% [markdown]
# Размеры обучающей и тестовой выборки соответственно:
# %%
tuple(map(len, (df_target_train, df_target_test)))
# %% [markdown]
# ## Модели
# %%
# XXX: один файл requirements для всех моделей
MODEL_PIP_REQUIREMENTS_PATH = BASE_PATH / 'requirements' / 'requirements-isolated-research-model.txt'
# %% [markdown]
# Сигнатура модели для MLFlow:
# %%
mlflow_model_signature = mlflow.models.infer_signature(model_input=df_orig_features, model_output=df_target)
mlflow_model_signature
# %% [raw] vscode={"languageId": "raw"}
# input_schema = mlflow.types.schema.Schema([
# mlflow.types.schema.ColSpec("double", "selling_price"),
# mlflow.types.schema.ColSpec("double", "driven_kms"),
# mlflow.types.schema.ColSpec("string", "fuel_type"),
# mlflow.types.schema.ColSpec("string", "selling_type"),
# mlflow.types.schema.ColSpec("string", "transmission"),
# mlflow.types.schema.ColSpec("double", "age"),
# ])
#
# output_schema = mlflow.types.schema.Schema([
# mlflow.types.schema.ColSpec("double", "present_price"),
# ])
#
# mlflow_model_signature = mlflow.models.ModelSignature(inputs=input_schema, outputs=output_schema)
# %%
def build_features_scaler_standard():
return sklearn.preprocessing.StandardScaler()
# %%
#def build_categorical_features_encoder_onehot():
# return sklearn.preprocessing.OneHotEncoder()
def build_categorical_features_encoder_target(*, random_state=None):
return sklearn.preprocessing.TargetEncoder(
target_type='continuous', smooth='auto', shuffle=True, random_state=random_state,
)
# %% [markdown]
# Регрессор &mdash; небольшой случайный лес, цель &mdash; минимизация квадрата ошибки предсказания:
# %%
def build_regressor(n_estimators, *, max_depth=None, max_features='sqrt', random_state=None):
return sklearn.ensemble.RandomForestRegressor(
n_estimators, criterion='squared_error',
max_depth=max_depth, max_features=max_features,
random_state=random_state,
)
def build_regressor_baseline(*, random_state=None):
return build_regressor(16, max_depth=8, max_features='sqrt')
# %%
def score_predictions(target_test, target_test_predicted):
return {
'mse': sklearn.metrics.mean_squared_error(target_test, target_test_predicted),
'mae': sklearn.metrics.mean_absolute_error(target_test, target_test_predicted),
'mape': sklearn.metrics.mean_absolute_percentage_error(target_test, target_test_predicted),
}
# %%
# использует глобальные переменные mlflow_do_log, mlflow_experiment, mlflow_root_run_name
def mlflow_log_model(
model,
model_params,
metrics,
*,
nested_run_name,
model_signature=None,
input_example=None,
pip_requirements=None,
#global_comment_file_path=None,
extra_logs_handler=None,
):
global mlflow_root_run_id
if not mlflow_do_log:
return
experiment_id = mlflow_experiment.experiment_id
start_run_root_kwargs_extra = {}
if mlflow_root_run_id is not None:
start_run_root_kwargs_extra['run_id'] = mlflow_root_run_id
else:
start_run_root_kwargs_extra['run_name'] = mlflow_root_run_name
with mlflow.start_run(experiment_id=experiment_id, **start_run_root_kwargs_extra) as root_run:
if root_run.info.status not in ('RUNNING',):
raise RuntimeError('Cannot get the root run to run')
if mlflow_root_run_id is None:
mlflow_root_run_id = root_run.info.run_id
# важно одновременно использовать nested=True и parent_run_id=...:
with mlflow.start_run(experiment_id=experiment_id, run_name=nested_run_name, nested=True, parent_run_id=mlflow_root_run_id):
if isinstance(pip_requirements, pathlib.PurePath):
pip_requirements = str(pip_requirements)
_ = mlflow.sklearn.log_model(
model,
'model',
signature=model_signature,
input_example=input_example,
pip_requirements=pip_requirements,
)
if model_params is not None:
_ = mlflow.log_params(model_params)
if metrics is not None:
_ = mlflow.log_metrics(metrics)
#if (global_comment_file_path is not None) and global_comment_file_path.exists():
# mlflow.log_artifact(str(global_comment_file_path))
if extra_logs_handler is not None:
if callable(extra_logs_handler) and (not isinstance(extra_logs_handler, Collection)):
extra_logs_handler = (extra_logs_handler,)
for extr_logs_handler_fn in extra_logs_handler:
extr_logs_handler_fn(mlflow)
# %% [markdown]
# ### Baseline модель
# %% [markdown]
# Пайплайн предобработки признаков:
# %%
preprocess_transformer = sklearn.compose.ColumnTransformer(
[
('scale_to_standard', build_features_scaler_standard(), features_to_scale_to_standard_columns),
(
#'encode_categoricals_one_hot',
'encode_categoricals_wrt_target',
#build_categorical_features_encoder_onehot(),
build_categorical_features_encoder_target(random_state=0x2ED6),
features_to_encode_wrt_target_columns,
),
],
remainder='drop',
)
# %%
regressor = build_regressor_baseline(random_state=0x016B)
regressor
# %% [markdown]
# Составной пайплайн:
# %%
pipeline = sklearn.pipeline.Pipeline([
('preprocess', preprocess_transformer),
('regress', regressor),
])
pipeline
# %%
model_params = filter_params(
pipeline.get_params(),
include={
'preprocess': (
False,
{
**{k: True for k in COLUMN_TRANSFORMER_PARAMS_COMMON_INCLUDE},
'scale_to_standard': True,
'encode_categorical_wrt_target': True,
},
),
'regress': (False, True),
},
exclude={
'preprocess': {'scale_to_standard': STANDARD_SCALER_PARAMS_COMMON_EXCLUDE},
'regress': RANDOM_FOREST_REGRESSOR_PARAMS_COMMON_EXCLUDE,
},
)
model_params
# %% [markdown]
# Обучение модели:
# %%
_ = pipeline.fit(df_orig_features_train, df_target_train.iloc[:, 0])
# %% [markdown]
# Оценка качества:
# %%
target_test_predicted = pipeline.predict(df_orig_features_test)
# %% [markdown]
# Метрики качества (MAPE, а также MSE, MAE):
# %%
metrics = score_predictions(df_target_test, target_test_predicted)
metrics
# %%
mlflow_log_model(
pipeline,
model_params=model_params,
metrics={k: float(v) for k, v in metrics.items()},
nested_run_name='Baseline model',
model_signature=mlflow_model_signature,
input_example=df_orig_features.head(MODEL_INOUT_EXAMPLE_SIZE),
pip_requirements=MODEL_PIP_REQUIREMENTS_PATH,
#global_comment_file_path=(
# model_comment_path
# if model_comment_path is not None
# else (BASE_PATH / 'research' / model_comment_relpath)
#),
)
# %% [markdown]
# ### Модель с дополнительными признаками
# %% [markdown]
# Пайплайн предобработки признаков:
# %%
features_to_extend_as_polynomial = ('selling_price', 'driven_kms')
features_to_extend_as_spline = ('age',)
# %%
def build_preprocess_augmenting_transformer():
assert set(features_to_extend_as_polynomial) <= {*features_to_scale_to_standard_columns}
assert set(features_to_extend_as_spline) <= {*features_to_scale_to_standard_columns}
return sklearn.compose.ColumnTransformer(
[
(
'extend_features_as_polynomial',
sklearn.pipeline.Pipeline([
(
'extend_features',
sklearn.preprocessing.PolynomialFeatures(2, include_bias=False),
),
('scale_to_standard', build_features_scaler_standard()),
]),
features_to_extend_as_polynomial,
),
(
'extend_features_as_spline',
sklearn.preprocessing.SplineTransformer(
4, knots='quantile', extrapolation='constant', include_bias=False,
),
features_to_extend_as_spline,
),
(
'scale_to_standard',
build_features_scaler_standard(),
tuple(filter(lambda f: f not in features_to_extend_as_polynomial, features_to_scale_to_standard_columns)),
),
(
'encode_categoricals_wrt_target',
build_categorical_features_encoder_target(random_state=0x2ED6),
features_to_encode_wrt_target_columns,
),
],
remainder='drop',
)
# %%
PREPROCESS_AUGMENTING_TRANSFORMER_PARAMS_COMMON_INCLUDE = {
**{k: True for k in COLUMN_TRANSFORMER_PARAMS_COMMON_INCLUDE},
'extend_features_as_polynomial': {
'extend_features': True,
'scale_to_standard': True,
},
'extend_features_as_spline': True,
'scale_to_standard': True,
'encode_categorical_wrt_target': True,
}
PREPROCESS_AUGMENTING_TRANSFORMER_PARAMS_COMMON_EXCLUDE = {
'extend_features_as_polynomial': {
'scale_to_standard': STANDARD_SCALER_PARAMS_COMMON_EXCLUDE,
},
'scale_to_standard': STANDARD_SCALER_PARAMS_COMMON_EXCLUDE,
}
# %%
preprocess_transformer = build_preprocess_augmenting_transformer()
preprocess_transformer
# %% [markdown]
# Демонстрация предобработки данных:
# %%
preprocess_transformer_tmp = build_preprocess_augmenting_transformer()
df_augd_features_matrix_train = preprocess_transformer_tmp.fit_transform(df_orig_features_train, df_target_train.iloc[:, 0])
df_augd_features_train = pandas_dataframe_from_transformed_artifacts(df_augd_features_matrix_train, preprocess_transformer_tmp)
del preprocess_transformer_tmp
# %% [markdown]
# Обзор предобработанного датасета:
# %%
df_augd_features_train.info()
# %%
df_augd_features_train.head(0x8)
# %%
regressor = build_regressor_baseline(random_state=0x3AEF)
regressor
# %% [markdown]
# Составной пайплайн:
# %%
pipeline = sklearn.pipeline.Pipeline([
('preprocess', preprocess_transformer),
('regress', regressor),
])
pipeline
# %%
model_params = filter_params(
pipeline.get_params(),
include={
'preprocess': (False, PREPROCESS_AUGMENTING_TRANSFORMER_PARAMS_COMMON_INCLUDE.copy()),
'regress': (False, True),
},
exclude={
'preprocess': PREPROCESS_AUGMENTING_TRANSFORMER_PARAMS_COMMON_EXCLUDE.copy(),
'regress': RANDOM_FOREST_REGRESSOR_PARAMS_COMMON_EXCLUDE,
},
)
model_params
# %% [markdown]
# Обучение модели:
# %%
_ = pipeline.fit(df_orig_features_train, df_target_train.iloc[:, 0])
# %% [markdown]
# Оценка качества:
# %%
target_test_predicted = pipeline.predict(df_orig_features_test)
# %% [markdown]
# Метрики качества (MAPE, а также MSE, MAE):
# %%
metrics = score_predictions(df_target_test, target_test_predicted)
metrics
# %%
mlflow_log_model(
pipeline,
model_params=model_params,
metrics={k: float(v) for k, v in metrics.items()},
nested_run_name='Model with engineered features',
model_signature=mlflow_model_signature,
input_example=df_orig_features.head(MODEL_INOUT_EXAMPLE_SIZE),
pip_requirements=MODEL_PIP_REQUIREMENTS_PATH,
#global_comment_file_path=(
# model_comment_path
# if model_comment_path is not None
# else (BASE_PATH / 'research' / model_comment_relpath)
#),
)
# %% [markdown]
# ### Модель с дополнительными и отфильтрованными признаками
# %%
def build_selected_columns_info_for_mlflow(names=None, indices=None):
info = {}
if names is not None:
info['names'] = names
if indices is not None:
info['indices'] = indices
return info
def build_extra_logs_handler_selected_columns(names=None, indices=None):
def extra_log(mlf):
if any((v is not None) for v in (names, indices)):
info = build_selected_columns_info_for_mlflow(names=names, indices=indices)
mlf.log_dict(info, 'selected_columns_info.json')
return extra_log
# %%
def build_selected_columns_info_for_mlflow_from_sequential_feature_selector(feature_selector, *, take_names=True, take_indices=True):
return build_selected_columns_info_for_mlflow(
names=(feature_selector.k_feature_names_ if take_names else None),
indices=(tuple(feature_selector.k_feature_idx_) if take_indices else None),
)
def build_extra_logs_handler_selected_columns_from_sequential_feature_selector(feature_selector):
def extra_log(mlf):
info = build_selected_columns_info_for_mlflow_from_sequential_feature_selector(feature_selector)
mlf.log_dict(info, 'selected_columns_info.json')
return extra_log
# %%
regressor = build_regressor_baseline(random_state=0x8EDD)
regressor
# %% [markdown]
# Выбор признаков среди дополненного набора по минимизации MAPE:
# %%
len(df_augd_features_train.columns)
# %%
FILTERED_FEATURES_NUM = (4, 8)
# %%
def build_feature_selector(*, verbose=0):
return build_sequential_feature_selector(
regressor, k_features=FILTERED_FEATURES_NUM, forward=True, floating=True, cv=4, scoring='neg_mean_absolute_percentage_error',
verbose=verbose,
)
# %%
FEATURE_SELECTOR_PARAMS_COMMON_INCLUDE = {
**{k: True for k in SEQUENTIAL_FEATURE_SELECTOR_PARAMS_COMMON_INCLUDE},
'estimator': False,
}
FEATURE_SELECTOR_PARAMS_COMMON_EXCLUDE = () # TODO: ай-яй-яй
# %%
feature_selector = build_feature_selector(verbose=1)
feature_selector
# %%
_ = feature_selector.fit(df_augd_features_train, df_target_train.iloc[:, 0])
# %% [markdown]
# Выбранные признаки (имена и индексы):
# %%
build_selected_columns_info_for_mlflow_from_sequential_feature_selector(feature_selector)
# %% [markdown]
# MAPE в зависимости от количества выбранных признаков (указан регион выбора, ограниченный `FILTERED_FEATURES_NUM`):
# %%
fig, ax = plot_sequential_feature_selection(feature_selector, kind='std_dev')
ax.grid(True)
if isinstance(FILTERED_FEATURES_NUM, Sequence):
_ = ax.axvspan(min(FILTERED_FEATURES_NUM), max(FILTERED_FEATURES_NUM), color=matplotlib.colormaps.get_cmap('tab10')(6), alpha=0.15)
# хотелось бы поставить верхнюю границу `len(df_augd_features_train.columns)`, но SequentialFeatureSelector до неё не досчитывает-то
_ = ax.set_xlim((1, (max(FILTERED_FEATURES_NUM) if isinstance(FILTERED_FEATURES_NUM, Sequence) else FILTERED_FEATURES_NUM)))
_ = ax.set_ylim((None, 0.))
# %% [markdown]
# Составной пайплайн:
# %%
pipeline = sklearn.pipeline.Pipeline([
('preprocess', build_preprocess_augmenting_transformer()),
('select_features', feature_selector),
('regress', regressor),
])
pipeline
# %%
model_params = filter_params(
pipeline.get_params(),
include={
'preprocess': (False, PREPROCESS_AUGMENTING_TRANSFORMER_PARAMS_COMMON_INCLUDE.copy()),
'select_features': (False, FEATURE_SELECTOR_PARAMS_COMMON_INCLUDE.copy()),
'regress': (False, True),
},
exclude={
'preprocess': PREPROCESS_AUGMENTING_TRANSFORMER_PARAMS_COMMON_EXCLUDE.copy(),
'select_features': FEATURE_SELECTOR_PARAMS_COMMON_EXCLUDE,
'regress': RANDOM_FOREST_REGRESSOR_PARAMS_COMMON_EXCLUDE,
},
)
model_params
# %% [markdown]
# Обучение модели:
# %%
# XXX: SequentialFeatureSelector обучается опять!?
_ = pipeline.fit(df_orig_features_train, df_target_train.iloc[:, 0])
# %% [markdown]
# Оценка качества:
# %%
target_test_predicted = pipeline.predict(df_orig_features_test)
# %% [markdown]
# Метрики качества (MAPE, а также MSE, MAE):
# %%
metrics = score_predictions(df_target_test, target_test_predicted)
metrics
# %%
mlflow_log_model(
pipeline,
model_params=model_params,
metrics={k: float(v) for k, v in metrics.items()},
nested_run_name='Model with filtered engineered features',
model_signature=mlflow_model_signature,
input_example=df_orig_features.head(MODEL_INOUT_EXAMPLE_SIZE),
pip_requirements=MODEL_PIP_REQUIREMENTS_PATH,
#global_comment_file_path=(
# model_comment_path
# if model_comment_path is not None
# else (BASE_PATH / 'research' / model_comment_relpath)
#),
extra_logs_handler=(build_extra_logs_handler_selected_columns_from_sequential_feature_selector(pipeline.named_steps['select_features']),),
)
# %% [markdown]
# ### Автоматический подбор гиперпараметров модели
# %% [markdown]
# Составной пайплайн:
# %%
def build_pipeline(regressor_n_estimators, regressor_max_depth=None, regressor_max_features='sqrt'):
return sklearn.pipeline.Pipeline([
('preprocess', build_preprocess_augmenting_transformer()),
('select_features', build_feature_selector()),
('regress', build_regressor(regressor_n_estimators, max_depth=regressor_max_depth, max_features=regressor_max_features)),
])
# %% [markdown]
# Целевая функция для оптимизатора гиперпараметров (подбирает параметры `RandomForestRegressor`: `n_estimators`, `max_depth`, `max_features`):
# %%
def regressor_hyperparams_objective(trial):
n_estimators = trial.suggest_int('n_estimators', 1, 256, log=True)
max_depth = trial.suggest_int('max_depth', 1, 16, log=True)
max_features = trial.suggest_float('max_features', 0.1, 1.)
# составной пайплайн:
pipeline = build_pipeline(n_estimators, regressor_max_depth=max_depth, regressor_max_features=max_features)
# обучение модели:
_ = pipeline.fit(df_orig_features_train, df_target_train.iloc[:, 0])
# оценка качества:
target_test_predicted = pipeline.predict(df_orig_features_test)
# метрика качества (MAPE):
mape = sklearn.metrics.mean_absolute_percentage_error(df_target_test, target_test_predicted)
return mape
# %% [markdown]
# optuna study:
# %%
optuna_sampler = optuna.samplers.TPESampler(seed=0x0A1C)
optuna_study = optuna.create_study(sampler=optuna_sampler, direction='minimize')
optuna_study.optimize(regressor_hyperparams_objective, n_trials=24)
# %% [markdown]
# Количество выполненных trials:
# %%
len(optuna_study.trials)
# %% [markdown]
# Лучшие найдённые гиперпараметры:
# %%
repr(optuna_study.best_params)
# %%
regressor_best_params = dict(optuna_study.best_params.items())
# %% [markdown]
# Составной пайплайн:
# %%
def build_pipeline_optimized_best():
return build_pipeline(
regressor_best_params['n_estimators'],
regressor_max_depth=regressor_best_params['max_depth'],
regressor_max_features=regressor_best_params['max_features'],
)
# %%
pipeline = build_pipeline_optimized_best()
pipeline
# %%
model_params = filter_params(
pipeline.get_params(),
include={
'preprocess': (False, PREPROCESS_AUGMENTING_TRANSFORMER_PARAMS_COMMON_INCLUDE.copy()),
'select_features': (False, FEATURE_SELECTOR_PARAMS_COMMON_INCLUDE.copy()),
'regress': (False, True),
},
exclude={
'preprocess': PREPROCESS_AUGMENTING_TRANSFORMER_PARAMS_COMMON_EXCLUDE.copy(),
'select_features': FEATURE_SELECTOR_PARAMS_COMMON_EXCLUDE,
'regress': RANDOM_FOREST_REGRESSOR_PARAMS_COMMON_EXCLUDE,
},
)
model_params
# %% [markdown]
# Обучение модели:
# %%
_ = pipeline.fit(df_orig_features_train, df_target_train.iloc[:, 0])
# %% [markdown]
# Оценка качества:
# %%
target_test_predicted = pipeline.predict(df_orig_features_test)
# %% [markdown]
# Метрики качества (MAPE, а также MSE, MAE):
# %%
metrics = score_predictions(df_target_test, target_test_predicted)
metrics
# %%
mlflow_log_model(
pipeline,
model_params=model_params,
metrics={k: float(v) for k, v in metrics.items()},
nested_run_name='Optimized model with filtered engineered features',
model_signature=mlflow_model_signature,
input_example=df_orig_features.head(MODEL_INOUT_EXAMPLE_SIZE),
pip_requirements=MODEL_PIP_REQUIREMENTS_PATH,
#global_comment_file_path=(
# model_comment_path
# if model_comment_path is not None
# else (BASE_PATH / 'research' / model_comment_relpath)
#),
extra_logs_handler=(build_extra_logs_handler_selected_columns_from_sequential_feature_selector(pipeline.named_steps['select_features']),),
)
# %% [markdown]
# ### И в продакшн
# %% [markdown]
# Лучшая выбранная модель &mdash; с автоматически подобранными гиперпараметрами.
# %%
pipeline = build_pipeline_optimized_best()
pipeline
# %%
model_params = filter_params(
pipeline.get_params(),
include={
'preprocess': (False, PREPROCESS_AUGMENTING_TRANSFORMER_PARAMS_COMMON_INCLUDE.copy()),
'select_features': (False, FEATURE_SELECTOR_PARAMS_COMMON_INCLUDE.copy()),
'regress': (False, True),
},
exclude={
'preprocess': PREPROCESS_AUGMENTING_TRANSFORMER_PARAMS_COMMON_EXCLUDE.copy(),
'select_features': FEATURE_SELECTOR_PARAMS_COMMON_EXCLUDE,
'regress': RANDOM_FOREST_REGRESSOR_PARAMS_COMMON_EXCLUDE,
},
)
model_params
# %%
_ = pipeline.fit(df_orig_features, df_target.iloc[:, 0])
# %%
mlflow_log_model(
pipeline,
model_params=model_params,
metrics=None,
nested_run_name='Final model',
model_signature=mlflow_model_signature,
input_example=df_orig_features.head(MODEL_INOUT_EXAMPLE_SIZE),
pip_requirements=MODEL_PIP_REQUIREMENTS_PATH,
#global_comment_file_path=(
# model_comment_path
# if model_comment_path is not None
# else (BASE_PATH / 'research' / model_comment_relpath)
#),
extra_logs_handler=(build_extra_logs_handler_selected_columns_from_sequential_feature_selector(pipeline.named_steps['select_features']),),
)
# %%

@ -0,0 +1,14 @@
$ErrorActionPreference = "Stop"
. $PSScriptRoot\_mlflow_config_common.ps1
$DEFAULT_ARTIFACT_ROOT = "./mlflow/mlartifacts/"
$MLFLOW_PORT = if ($env:MLFLOW_PORT) { $env:MLFLOW_PORT } else { 5000 }
New-Item -ItemType Directory -Force -Path $DEFAULT_ARTIFACTS_ROOT | Out-Null
& mlflow server `
--backend-store-uri="$BACKEND_URI" `
--default-artifact-root="$DEFAULT_ARTIFACT_ROOT" `
-p $MLFLOW_PORT

@ -0,0 +1,16 @@
#!/bin/sh
set -eu
. _mlflow_config_common.sh
DEFAULT_ARTIFACT_ROOT="./mlflow/mlartifacts/"
: "${MLFLOW_PORT:=5000}"
mkdir -p "${DEFAULT_ARTIFACT_ROOT}"
exec mlflow server \
--backend-store-uri="$BACKEND_URI" \
--default-artifact-root="$DEFAULT_ARTIFACT_ROOT" \
-p "$MLFLOW_PORT"
Загрузка…
Отмена
Сохранить