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

Эксперимент в расширении DVC для VSCode
Эксперимент в расширении DVC для VSCode

Изменим параметр test_size = 0.5 и снова вызовем команду make training:

Добавление нового эксперимента после вызова make training
Добавление нового эксперимента после вызова 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

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

Ссылки:

  1. Репозиторий с проектом

  2. Официальная документация DVC

  3. AI Talent Hub

Комментарии (3)


  1. tanella
    15.09.2023 11:08
    +1

    Спасибо за разбор! очень подробно. Но я бы не назвала пост "простым", тут все-таки для прочтения нужна подготовка :)


    1. smeyanoff Автор
      15.09.2023 11:08
      +1

      Спасибо!
      Перевел пост на "средний")


  1. nikolay_karelin
    15.09.2023 11:08

    Спасибо. Хорошее введение!