Привет Хабр, меня зовут Дмитрий Несмеянов, я являюсь руководителем направления разработки ML-инфраструктуры "ЛОКО-банка". Также я являюсь магистром самой крутой онлайн магистратуры на базе университета ИТМО @ai-talent.
Сегодня я хочу рассказать про DVC: инструмент, который многие, незаслуженно, обходят стороной. Была хорошая статья от Райффайзен Банк, в этой статье я постараюсь резюмировать мою и коллег экспертизу в работе с DVC.
DVC (Data Version Control) - это система версионирования датасетов и не только, которая является надстройкой над git. Если вы умеете работать с git, поздравляю, вы умеете работать с DVC. Кроме того, DVC позволяет логировать эксперименты, а также делать Auto-ML.
Я хочу разделить статью на несколько частей, чтобы было удобно ориентироваться в материале:
Установка DVC и привязка к хранилищу данных
DVC для DataScientist: Версионирование датасетов
DVC для DataScientist: Логирование экспериментов
DVC для DataScientist и MLOps: Автоматизация пайплайнов
Установка DVC и привязка к хранилищу данных
Начнем с настройки DVC. Я использую WSL Ubuntu с VSCode. Во всех примерах, в качестве менеджера пакетов, я буду использовать poetry. Заранее скажу, что не буду подробно разбирать команды poetry в этой статье.
Для начала создадим новый проект:
`dmitry@Dmitriy:~/projects$ poetry new dvc_example
Created package dvc_example in dvc_example
dmitry@Dmitriy:~/projects$ cd dvc_example/
poetry создаст виртуальное окружение в проекте, когда мы добавим в него первую библиотеку, в нашем случае dvc (обращу внимание, у меня в глобальных настройках poetry прописано создание виртуального окружения в директории с проектом):
dmitry@Dmitriy:~/projects/dvc_example$ poetry add dvc
Creating virtualenv dvc-example in /home/dmitry/projects/dvc_example/.venv
Using version ^3.19.0 for dvc
DVC может использовать разные хранилища. В т.ч. S3, S3-like (MinIO), GoogleDrive и т.д. Кроме того, можно версионировать файлы на вашем ПК. Мы в банке используем DVC c MinIO.
Как я уже упоминал раннее, DVC является надстройкой над Git. Поэтому, сначала необходимо инициализировать git-репозиторий. После этого инициализируем dvc-репозиторий командой dvc init
dmitry@Dmitriy:~/projects/dvc_example$ git init
dmitry@Dmitriy:~/projects/dvc_example$ dvc init
Initialized DVC repository.
Будем версионировать данные в локальном репозитории. Для этого создадим отдельную директорию dvc_local_storage
:
dmitry@Dmitriy:~/projects/dvc_example$ mkdir ~/projects/dvc_local_storage
dmitry@Dmitriy:~/projects/dvc_example$ dvc remote add local_storage ~/projects/dvc_local_storage/
DVC для DataScientist: Версионирование датасетов
Как уже говорил выше, с помощью DVC можно версионировать датасеты. Это является удобным иструментом, т.к. данные, обычно, не версионируются в удаленном репозитории.
Давайте посмотрим на игрушечный пример. Для работы будем использовать датасет с данными о заболеваниях диабетом.
На данном этапе дерево нашего проекта выглядит следующим образом:
├──.dvc
├──.venv
├── README.md
├── poetry.lock
├── pyproject.toml
└── tests
└── __init__.py
Для начала создадим несколько директорий в проекте с помощью:
dmitry@Dmitriy:~/projects/dvc_example$ mkdir -p data/initial_data
dmitry@Dmitriy:~/projects/dvc_example$ mkdir -p data/prepared_data
dmitry@Dmitriy:~/projects/dvc_example$ mkdir -p src/data
В директорию initial_data
будем складывать необработанные данные, в prepared_data
подготовленный датасет. В директорию src/data
будем складывать .py скрипты для манипуляций с данными.
Добавим скрип для загрузки данных src/data/data_download.py
, а также какой-то простой скрипт src/data/data_prepare.py
для подготовки данных. Например, будем в четных столбцах датасета уменьшать все значения на 1, а в нечетных на 2.
src/data/data_download.py:
import os
from sklearn import datasets
from dotenv import load_dotenv
import pandas as pd
load_dotenv()
if __name__ == "__main__":
dataset = datasets.load_diabetes()
features = pd.DataFrame(data=dataset.data,
columns=["feat%s" % x for x in range(dataset.data.shape[1])])
target = pd.DataFrame(data=dataset.target, columns=["target"])
features.to_csv("%s/initial_data.csv" % os.environ.get("INITIAL_DATA_PATH"))
target.to_csv("%s/target.csv" % os.environ.get("INITIAL_DATA_PATH"))
src/data/data_prepare.py:
import os
from dotenv import load_dotenv
import pandas as pd
load_dotenv()
def fillna(dataset: pd.DataFrame) -> pd.DataFrame:
prepare_dataset = dataset.copy()
for i, column in enumerate(dataset.columns):
if i % 2 == 0:
prepare_dataset[column] = prepare_dataset[column] - 1
else:
prepare_dataset[column] = prepare_dataset[column] - 2
return prepare_dataset
if __name__ == "__main__":
dataset = pd.read_csv("%s/initial_data.csv" % os.environ.get("INITIAL_DATA_PATH"))
prepared_dataset = fillna(dataset=dataset)
prepared_dataset.to_csv("%s/prepared_data.csv" % os.environ.get("PREPARED_DATA_PATH"))
Будем вызывать скрипты с помощью Makefile:
prepare_stage1:
poetry run python src/data/data_download.py
prepare_stage2:
poetry run python src/data/data_prepare.py
data_prepare: prepare_stage1 prepare_stage2
dmitry@Dmitriy:~/projects/dvc_example$ make data_prepare
poetry run python src/data/data_download.py
poetry run python src/data/data_prepare.py
На данном этапе мы загрузили датасет и сделали предобработку. Дерево проекта теперь выглядит так:
.
├── Makefile
├── README.md
├── data
│ ├── initial_data
│ │ ├── initial_data.csv
│ │ └── target.csv
│ └── prepared_data
│ └── prepared_data.csv
├── poetry.lock
├── pyproject.toml
├── src
│ ├── __init__.py
│ └── data
│ ├── __init__.py
│ ├── data_download.py
│ └── data_prepare.py
└── tests
└── __init__.py
Тут начинается магия. Добавим все .csv файлы в DVC с помощью команды dvc add
. Хотя DVC позволяет версионировать папки целиком, хорошим тоном считается добавлять файлы по отдельности.
poetry run dvc add data/initial_data/initial_data.csv
...
После dvc add
в директории с добавленным файлом появятся файлы .gitignore
и <file_name>.dvc
. С первым понятно, git теперь не будет версионировать эти файлы. Последний рассмотрим ближе на примере data/prepared_data/prepared_data.csv.dvc
:
outs:
- md5: 9baf10a3b757026d1f00c52dfbf7a718
size: 90325
hash: md5
path: prepared_data.csv
Файл записывает хэш файла, по нему DVC будет находить нужную версию наших данных, размер, тип хэширования и путь к файлу.
Дерево директории ./data
теперь выглядит так:
├── data
│ ├── initial_data
| | ├── .gitignore
│ │ ├── initial_data.csv
│ │ ├── initial_data.csv.dvc
│ │ ├── target.csv
│ │ └── target.csv.dvc
│ └── prepared_data
| ├── .gitignore
│ ├── prepared_data.csv
│ └── prepared_data.csv.dvc
Далее необходимо закоммитить изменения DVC и git, сделать push в удаленный репозиторий git и DVC.
dmitry@Dmitriy:~/projects/dvc_example$ poetry run dvc commit
dmitry@Dmitriy:~/projects/dvc_example$ git add .
dmitry@Dmitriy:~/projects/dvc_example$ git commit -m "add: dvc_staging"
dmitry@Dmitriy:~/projects/dvc_example$ poetry run dvc push -r local_storage
4 files pushed
dmitry@Dmitriy:~/projects/dvc_example$ git push -u origin main
И теперь, если по какой-то причине, мы удалим файл из проекта, можем легко восстановить его с помощью dvc pull
.
Для того, чтобы продемонстрировать, как работает версионирование в DVC, изменим файл src/data/data_prepare.py
. Для чётных столбцов будем прибавлять 1, а для нечетных вычитать 4.
dmitry@Dmitriy:~/projects/dvc_example$ make data_prepare
poetry run python src/data/data_download.py
poetry run python src/data/data_prepare.py
Закоммитим изменения dvc commit
и посмотрим на файл data/prepared_data/prepared_data.csv.dvc
:
outs:
- md5: eb82260e632fd286abf79718cc771b08
size: 87320
hash: md5
path: prepared_data.csv
Как видим, хэш изменился. Далее коммитим изменения git и пушим git и DVC.
dmitry@Dmitriy:~/projects/dvc_example$ git commit -am "fix: prepare_data"
dmitry@Dmitriy:~/projects/dvc_example$ git push
dmitry@Dmitriy:~/projects/dvc_example$ poetry run dvc push -r local_storage
Чтобы откатиться на предыдущую версию датасета, воспользуемся git checkout <id коммита>
. Команда dvc pull
откатит датасет до версии коммита:
dmitry@Dmitriy:~/projects/dvc_example$ git checkout aab115eb...
dmitry@Dmitriy:~/projects/dvc_example$ dvc pull
M data/prepared_data/prepared_data.csv
1 file modified
Хочу добавить, что DVC поддерживает версионирование по веткам. Работает также, как в git, командой dvc checkout
можно "прыгать" по веткам в проекте.
DVC для DataScientist: Логирование экспериментов
DVC позволяет логировать эксперименты. Поддерживает Jupyter Notebook, есть расширение для VSCode. Им я и буду пользоваться.
Для начала установим библиотеку dvc live
.
dmitry@Dmitriy:~/projects/dvc_example$ poetry add dvclive
Using version ^2.16.0 for dvclive
Создадим необходимы директории:
dmitry@Dmitriy:~/projects/dvc_example$ mkdir configs
dmitry@Dmitriy:~/projects/dvc_example$ mkdir models
dmitry@Dmitriy:~/projects/dvc_example$ mkdir -p src/models
Создадим конфиг обучения в configs/training_config.yml
со следующим содержанием:
train_config:
seed: 1
test_size: 0.4
validation_size: 0.1
Создадим файл обучения src/models/model_train.py
:
import os
import pickle
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
from sklearn.linear_model import LinearRegression
from dotenv import load_dotenv
from yaml import load, Loader
from dvclive import Live
load_dotenv()
if __name__ == "__main__":
with open("configs/train_config.yml", "r") as conf:
train_config = load(conf, Loader=Loader)["train_config"]
np.random.seed(train_config["seed"])
dataset = pd.read_csv("%s/prepared_data.csv" % os.environ.get("PREPARED_DATA_PATH"))
target = pd.read_csv("%s/target.csv" % os.environ.get("INITIAL_DATA_PATH"))
train_index, validation_index = train_test_split(dataset.index,
test_size=train_config["validation_size"])
train_index, test_index = train_test_split(train_index,
test_size=train_config["test_size"])
model = LinearRegression()
model.fit(dataset.loc[train_index], target.loc[train_index])
train_MSE = mean_squared_error(target.loc[test_index],
model.predict(dataset.loc[test_index]))
test_MSE = mean_squared_error(target.loc[train_index],
model.predict(dataset.loc[train_index]))
validation_MSE = mean_squared_error(target.loc[validation_index],
model.predict(dataset.loc[validation_index]))
with open("%s/linear_model.pickle" % os.environ.get("MODELS_PATH"), "wb") as mod:
mod.write(pickle.dumps(model))
with Live(save_dvc_exp=True) as live:
live.log_artifact("%s/linear_model.pickle" % os.environ.get("MODELS_PATH"))
live.log_metric("train_MSE", train_MSE)
live.log_metric("test_MSE", test_MSE)
live.log_metric("validation_MSE", validation_MSE)
В качестве метрики будем использовать MSE. Методами .log_artifact
логируем модель, .log_metric
- необходимые метрики. Если вы работали с ClearML, MLFlow или W&B, вы понимаете, как это работает. Все доступные методы можно посмотреть по ссылке.
Итак, чтобы использовать скрипт, добавим его в Makefile:
training:
poetry run python src/models/model_train.py
После вызова команды make training
, будут залогированы артефакты, метрики и модель:
dmitry@Dmitriy:~/projects/dvc_example$ make training
poetry run python src/models/model_train.py
100% Adding...|███████████████████████████████████████████████████████████████████████████████████|1/1 [00:00, 41.86file/s]
WARNING: The following untracked files were present in the workspace before saving but will not be included in the experiment commit:
configs/train_config.yml, src/models/__init__.py, src/models/model_train.py
Кроме того, появится директория dvclive
, в которой будут сохранятся результаты и графики. Также, так как я использую расширение DVC для VSCode, в таблице появятся метрики прошедшего эксперимента:
Изменим параметр test_size = 0.5 и снова вызовем команду make training:
Далее можно сравнить эксперименты, посмотреть графики, сделать checkout на лучший эксперимент или поделиться экспериментом.
DVC для DataScientist и MLOps: Автоматизация пайплайнов
Мы плавно подобрались к автоматизации пайплайнов с DVC. DVC, также как, например, AirFlow, позволяет писать DAG's для выполнения скриптов. Я не просто так записывал скрипты в Makefile
, пересоберем stage'и используя DVC-пайплайны. Для этого создадим в корне проекта файл dvc.yaml
со следующим содержанием:
stages:
data_download:
cmd: poetry run python src/data/data_download.py
deps:
- src/data/data_download.py
outs:
- data/initial_data/initial_data.csv
- data/initial_data/target.csv
data_prepare:
cmd: poetry run python src/data/data_prepare.py
deps:
- src/data/data_prepare.py
outs:
- data/prepared_data/prepared_data.csv
training:
cmd: poetry run python src/models/model_train.py
deps:
- src/models/model_train.py
outs:
- models/linear_model.pickle
Выполнение команды dvc exp run
создаст файл dvc.lock
, в котором будут хранится кэши для каждого stage`a в run`е.
Если попытаться запустить команду еще раз, DVC проверит, изменялись ли необходимые для stage`a файлы. Если не изменялись, stage`и не будут исполнятся:
dmitry@Dmitriy:~/projects/dvc_example$ dvc exp run
Reproducing experiment 'smoky-whin'
Stage 'data_download' didn't change, skipping
Stage 'data_prepare' didn't change, skipping
Stage 'training' didn't change, skipping
На этом у меня все, спасибо, что дочитали до этого момента. Надеюсь, материал оказался полезным.
Я выложил репозиторий с кодом в открытый доступ, можете ознакомится лично.
Ссылки: