Привет Хабр, меня зовут Дмитрий Несмеянов, я являюсь руководителем направления разработки 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
На этом у меня все, спасибо, что дочитали до этого момента. Надеюсь, материал оказался полезным.
Я выложил репозиторий с кодом в открытый доступ, можете ознакомится лично.
Ссылки:
