Воспроизводимость: seed, окружение, lineage
«Запустил тот же код — получил другую модель» — нормально для ML и решается дисциплиной воспроизводимости.
Воспроизводимость — свойство ML-системы давать тот же результат при повторном запуске за счёт фиксации случайности, окружения, данных и кода.
Почему ML невоспроизводим по умолчанию
Обычный код детерминирован. ML — нет: инициализация весов, перемешивание данных, dropout, сэмплирование — всё опирается на генератор случайных чисел. Запустите обучение дважды без фиксации — получите две разные модели. А ещё результат зависит от версии библиотеки, версии данных и даже железа. Воспроизводимость нужна, чтобы отлаживать, сравнивать честно и доказывать происхождение модели (особенно в регулируемых отраслях).
Четыре оси воспроизводимости
- Случайность (seed). Фиксируем seed всех генераторов: random, numpy, фреймворк.
- Окружение. Фиксируем версии пакетов (lock-файл) и системные зависимости (Docker-образ).
- Данные. Фиксируем версию датасета (DVC-хеш).
- Код. Фиксируем git-коммит.
Фиксация seed
Один seed детерминирует всю цепочку случайных решений. Проверим на чистом Python, что без seed результат пляшет, а с seed — повторяется.
import random
def sample_run(seed=None):
if seed is not None:
random.seed(seed)
return [random.randint(0, 99) for _ in range(5)]
print("Без seed (два запуска подряд могут отличаться):")
print(" ", sample_run())
print(" ", sample_run())
print("С seed=42 (всегда одно и то же):")
print(" ", sample_run(42))
print(" ", sample_run(42))
Вывод:
Без seed (два запуска подряд могут отличаться): [49, 97, 53, 5, 33] [65, 62, 51, 100, 38] С seed=42 (всегда одно и то же): [81, 14, 3, 94, 35] [81, 14, 3, 94, 35]
Первые две строки на вашем запуске будут другими (случайны), но две последние с seed=42 — всегда одинаковы. В реальном обучении так же фиксируют seed numpy и фреймворка; иногда дополнительно включают детерминированные операции GPU (ценой скорости).
Фиксация окружения
Версии библиотек влияют на результат: новый minor может изменить дефолт алгоритма. Поэтому фиксируют точные версии в lock-файле и собирают Docker-образ.
scikit-learn==1.3.2
numpy==1.26.4
pandas==2.1.4
Lineage: происхождение артефактов
Lineage отвечает на вопрос «как именно получился этот артефакт». Для прод-модели lineage — это цепочка: git-коммит кода → версия данных (DVC) → run эксперимента (параметры, seed) → окружение (образ) → версия в реестре. Если каждое звено зафиксировано, модель воспроизводима байт-в-байт, а инцидент можно расследовать до корня.
git sha + DVC hash + seed + образ --> run (MLflow) --> модель v7 --> Production
(что и из чего) (как обучали) (артефакт) (где живёт)
Как работает под капотом
Воспроизводимость — это сведение всей случайности и неявных зависимостей к явным, зафиксированным входам. Seed убирает стохастику алгоритма; lock-файл и образ убирают «дрейф окружения»; DVC-хеш убирает изменчивость данных; git-коммит фиксирует код. Трекинг-система записывает все эти входы в run, образуя проверяемую цепочку lineage. Повторный прогон с теми же входами обязан дать тот же выход.
Частые ошибки
- Фиксировать seed только одного генератора. Несколько источников случайности — фиксировать нужно все (random, numpy, фреймворк).
- Не пинить версии пакетов. «Работало вчера» ломается после тихого обновления зависимости.
- Хранить lineage в голове. Если связь код↔данные↔run не записана, воспроизвести модель спустя месяцы невозможно.
Итог
- ML невоспроизводим по умолчанию из-за случайности, окружения, данных и кода — все четыре оси нужно фиксировать.
- Seed детерминирует случайные решения; lock-файл и Docker фиксируют окружение; DVC и git — данные и код.
- Lineage связывает прод-модель со всеми входами, делая её воспроизводимой и расследуемой.