Привет, хабр! Это уже 3-я статья про ClearML. В этой статье я рассказывал про базовый функционал ClearML, а в этой - про то, как настраивать и запускать эксперименты обучения и тестирования через веб-браузер. А теперь я бы хотел затронуть менее популярную тему — организацию датасетов.
Версионирование датасетов
В ML есть такой важный тезис: "Данные — это душа модели". Действительно, объем, чистота и разнообразие данных играют ключевую роль. Но почему-то тот факт, что версий данных может быть много, каждая со своими особенностями, и каждая может по-разному повлиять на исход обучения, часто оставляют за кулисами.
Например, у вас есть большой датасет А сомнительного качества. Есть еще другой датасет В более чистый, но меньше. Можно обучиться на А и В. А какие еще есть варианты?
1) объединить А+В;
2) еще можно очистить А;
3) а также расширить В.
4) объединить новые версии из п. 2 и 3
Из 2-ух исходных версий можно получить еще как минимум 4 новых (и это не считая различных комбинаций). А какая версия обучит лучшую модель? Этого мы не знаем. Нам остается только предполагать и экспериментировать.
С развитием проекта вариативность версий растет и растет, и повышается риск запутаться во всех них. И тут возникает потребность в использовании метода хранения, отслеживания и применения разных версий данных. Для таких целей существуют такие инструменты, как DVC или Pachyderm. Но, как вы поняли из названия статьи, речь пойдет о другом инструменте.
Реализация на примере
Версионирование
Установку ClearML мы пропустим, потому что она описана тут. Понимание материала лучше на конкретном примере. Представив проект "Распознавание объектов на изображении". Пусть имеется первая (исходная) версия датасета, скачанная из Kaggle. Я назову ее 1.0-kaggle
. Работа в ClearML делается с помощью тасок, и датасеты не исключение. Здесь и указывается название проекта.
from clearml import Task
from clearml import Dataset
Task.init(project_name="Dataset/Object Detection",
task_name="obj-detect",
task_type=Task.TaskTypes.data_processing,
auto_connect_frameworks=False,
auto_resource_monitoring=False,
auto_connect_streams=False)
Далее создадим непосредственно датасет с указанием версии, описания, свойств (user_properties) в виде словаря. А поскольку это первая версия, которая не имеет предшественников, то parent_datasets=None
. По итогу нужно зафиксировать изменения.
dataset = Dataset.create(parent_datasets=None,
dataset_version="1.0-kaggle",
description="http://kaggle.com/obj_detect",
use_current_task=True)
dataset._task.set_user_properties(data_path="/path/to/1.0-kaggle.json")
logger = dataset.get_logger()
dataset.finalize() # commit
Как это выглядит в ClearML

Аналогичным образом создадим еще одну исходную версию, в этот раз с YouTube - 1.0-youtube
. Но это версия содержит много дубликатов, поэтому нужно их почистить. Очищенную версию назовем 1.1-youtube
. ID исходной версии можно взять из поля, как на рисунке выше.
parent_id = "48b8d22437624e519f6a177eddcfcd36" # 1.0-youtube
dataset = Dataset.create(parent_datasets=[parent_id],
dataset_version="1.1-youtube",
description="drop duplicates",
use_current_task=True)
dataset._task.set_user_properties(data_path="/path/to/data-youtube.v1.1.json")
logger = dataset.get_logger()
dataset.finalize() # commit
Как это выглядит в ClearML

Обратите внимание, что в аргументе parent_datasets
указывается список родителей, т.е. их может быть несколько. Например, мы сейчас объединим версии 1.0-kaggle
и 1.1-youtube
, обозначив результат просто 1.0
.
parent1_id = "8ccc9f696ebb4ea1b1b5f79adf200851" # 1.1-youtube
parent2_id = "ada9f6d0fe0b4ee880b976988a09fdae" # 1.0-kaggle
dataset = Dataset.create(parent_datasets=[parent1_id, parent2_id],
dataset_version="1.0",
description="merged datasets from YouTube and Kaggle",
use_current_task=True)
dataset._task.set_user_properties(data_path="/path/to/1.0.json")
logger = dataset.get_logger()
dataset.finalize() # commit
Как это выглядит в ClearML

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



Если вы читали прошлую статью, то наверняка заметили сходство. Как я говорил, работа в ClearML делается с помощью тасок. Это касается обучения, тестирования и создания версии датасета. Эти таски могут хранить множество разнообразной информации.
Графики
Часто полезно хранить графическую информацию для отображения статистик. Например, распределения по классам или гистограммы. Это делается через logger.
Dataset.create(parent_datasets=None,
dataset_version="1.0",
description="Add table",
use_current_task=True)
logger = dataset.get_logger()
logger.report_table(title="classes",
series="names",
csv=csv_path)
logger.report_histogram("Class balance",
"data",
values=hist,
xaxis="classes",
yaxis="count")
logger.report_matplotlib_figure(title="Figure",
series="data",
figure=figure,
report_image=False,
report_interactive=True)
dataset.finalize() # commit
Как это выглядит в ClearML

Примеры медиа
Вдобавок ко всему, мне нравится возможность сохранять в отдельном окне некоторое количество примеров (картинок или аудио). Это называется debug samples. Полезно, если вы хотите самостоятельно просмотреть или прослушать пару примеров из датасета. Вдруг найдутся какие-то ошибки.
logger.report_media(title="examples",
series="1",
local_path=fpath,
stream=None,
delete_after_upload=False)

Загрузка самих данных в ClearML
Если ваши данные уже хранятся в заданном хранилище (прям на или в S3), то дублировать их в ClearML вам, возможно, и не нужно. Мы, например, так не делаем. Но если вы хотите использовать ClearML в качестве непосредственно хранилища и готовы отдать ему достаточно пространства на диске, то такая возможность тоже есть.
dataset = Dataset.create(parent_datasets=None,
dataset_version="1.0",
use_current_task=True)
dataset.add_files(path=dataset_path)
dataset.upload()
dataset.finalize()
Резюмируем
Организация датасетов действительно недооцененная возможность, которая может уберечь от потенциальной путаницей со всеми этими данными и их версиями. Подумайте, все ли у вас гладко в плане управления датасетами и посмотрите в сторону таких инструментов.