ai_learning/modules/00_environment/notes/02_git_reproducibility.md

13 KiB
Raw Permalink Blame History

02. Git: коммиты, ветки, diff и исключение больших данных

Git сохраняет историю изменений текста и делает происхождение каждого результата проверяемым.

Мотивация

Архивы project_final_2.zip не отвечают на главные вопросы: что изменилось, зачем, кто это сделал и к какой версии кода относится модель. Git хранит направленную историю снимков и позволяет точно назвать состояние проекта идентификатором коммита.

Для воспроизводимого эксперимента нужны как минимум:

  • коммит исходного кода;
  • версия конфигурации и зависимостей;
  • идентификатор входных данных;
  • команда запуска и seed;
  • сохранённые метрики и окружение.

Git решает первую часть и связывает остальные части ссылками. Он не является универсальным хранилищем больших данных и не гарантирует научную воспроизводимость сам по себе.

Модель данных Git

У файла есть три практически важных состояния:

  1. рабочая директория — текущие файлы;
  2. индекс (staging area) — снимок, подготовленный к следующему коммиту;
  3. репозиторий — уже сохранённые коммиты.

Коммит содержит дерево файлов, метаданные и ссылку на родительский коммит. Его хеш зависит от содержимого. Ветка — подвижное имя, указывающее на коммит; HEAD указывает на текущую ветку или непосредственно на коммит.

A---B---C  main
     \
      D---E  experiment

Здесь обе ветки разделяют историю до B. Переключение ветки меняет снимок в рабочей директории, а слияние объединяет линии разработки.

Базовый цикл с осмысленным diff

git status
git diff
git add src/model.py tests/test_model.py
git diff --staged
git commit -m "Validate feature dimensions before training"

git diff показывает незастейдженные изменения, а git diff --staged — точное содержимое будущего коммита. Проверять второй diff важно: git add . может случайно включить данные, секрет или отладочный вывод.

Хороший коммит:

  • решает одну логическую задачу;
  • остаётся запускаемым или явно помечает промежуточное состояние;
  • включает связанные тесты;
  • объясняет причину изменения;
  • не содержит постороннего форматирования.

История — инструмент диагностики. Малые атомарные коммиты упрощают git bisect, review и отмену конкретного решения.

Ветки и интеграция

Создание отдельной ветки:

git switch -c feature/stable-normalization

Перед слиянием изменения проверяют тестами и чтением полного diff относительно базы. Merge создаёт коммит с двумя родителями и сохраняет форму истории. Rebase переносит коммиты на новую базу и переписывает их идентификаторы.

Переписывать опубликованную общую историю опасно: коллеги уже могли построить на ней свои ветки. Rebase удобен для локальной неопубликованной ветки; merge безопаснее как явная интеграция общей работы.

Конфликт — не ошибка Git, а отсутствие однозначного автоматического решения. Нужно понять намерения обеих сторон, отредактировать файл, запустить проверки, застейджить разрешение и продолжить операцию. Нельзя механически выбирать «ours» или «theirs» без проверки поведения.

Что отслеживать

В Git обычно помещают:

  • исходный код, тесты и небольшую документацию;
  • декларации зависимостей и конфигурации;
  • небольшие неизменяемые примеры данных;
  • скрипты получения и преобразования данных.

Не помещают:

  • .venv/, кэши, байткод и временные файлы;
  • секреты и локальные .env;
  • большие сырые датасеты, чекпойнты, модели;
  • легко воспроизводимые результаты сборки.

Пример .gitignore:

.venv/
__pycache__/
.pytest_cache/
.env
data/raw/
artifacts/
*.pyc

gitignore действует только на неотслеживаемые файлы. Если секрет уже закоммичен, добавление шаблона не удалит его из истории: ключ надо немедленно отозвать, затем отдельно очищать историю с координацией команды.

Проверка причины игнорирования:

git check-ignore -v data/raw/train.csv

Большие данные и происхождение артефактов

Обычный Git плохо работает с часто меняющимися бинарными файлами: каждый вариант увеличивает историю, а содержательный строковый diff невозможен. Возможные решения:

  • объектное хранилище и файл с URI, контрольной суммой и размером;
  • Git LFS для версионируемых больших бинарных объектов;
  • DVC или аналог для графа данных и пайплайнов;
  • скрипт скачивания для публичного неизменяемого источника.

Контрольная сумма подтверждает байтовую идентичность:

from hashlib import sha256
from pathlib import Path

path = Path("data/raw/sample.csv")
digest = sha256(path.read_bytes()).hexdigest()
print(path.stat().st_size, digest)

Она не объясняет смысл данных. Поэтому полезен манифест: источник, лицензия, время получения, схема, число строк, контрольная сумма и шаги обработки.

Воспроизводимый запуск как запись

Пусть обучение запускается на коммите c. Минимальная запись эксперимента:

commit: 4f16c2a...
dirty: false
command: python -m src.train --config configs/base.json
data_sha256: 9e1...
python: 3.12.4
seed: 42

Состояние dirty: true означает, что рабочая директория отличалась от коммита. Одного хеша тогда недостаточно: незакоммиченные изменения могут быть потеряны. Для значимого результата лучше коммитить код или сохранять diff как артефакт.

Самостоятельный фрагмент получает метаданные без сторонних библиотек:

import subprocess

def git(*args: str) -> str:
    return subprocess.check_output(
        ["git", *args], text=True, encoding="utf-8"
    ).strip()

commit = git("rev-parse", "HEAD")
dirty = bool(git("status", "--porcelain"))
print({"commit": commit, "dirty": dirty})

Запуск вне репозитория завершится ошибкой — это полезнее молчаливого значения unknown, если происхождение результата обязательно.

Поиск причины регрессии

История позволяет перейти от «раньше работало» к проверяемому поиску.

  1. Найти хороший и плохой коммиты.
  2. На каждом кандидате запускать один и тот же детерминированный тест.
  3. Делить диапазон пополам через git bisect.
  4. После нахождения читать diff и проверять причинность.

Тест должен иметь стабильный код возврата. Если данные, окружение или внешний API изменились, bisect может указать не на причину, а на случайную корреляцию.

Разобранный сценарий

Исследователь меняет нормализацию и получает лучшую метрику.

  1. git status показывает только ожидаемые исходники и тест.
  2. git diff подтверждает формулу и новый тест на нулевую дисперсию.
  3. Конфигурация запуска тоже версионируется.
  4. Большой чекпойнт сохраняется во внешнее хранилище; рядом записываются его SHA-256 и хеш коммита.
  5. git diff --staged проверяется до коммита.
  6. Из чистого клона создаётся окружение и повторяется команда.

Результат считается воспроизводимым не потому, что «есть Git», а потому, что каждый вход назван неизменяемым идентификатором и процесс автоматизирован.

Типичные ошибки

  • Коммит результата без кода запуска. Артефакт нельзя объяснить или повторить.
  • Огромный коммит. Review не отделяет изменение поведения от шума.
  • Слепой git add .. В историю попадают секреты и локальные файлы.
  • Хеш коммита при dirty-состоянии. Хеш описывает не фактически исполненный код.
  • Хранение больших бинарников в Git. Репозиторий растёт навсегда.
  • Force-push общей ветки. Идентификаторы переписываются, ссылки ломаются.
  • «Одинаковый seed значит воспроизводимость». Версии библиотек, данные, аппаратные ядра и порядок операций также влияют на результат.

Краткое резюме

  • Git хранит снимки; индекс отделяет текущую работу от следующего коммита.
  • Diff до и после staging — обязательная проверка содержимого.
  • Ветки именуют линии истории; rebase переписывает коммиты, merge интегрирует их.
  • Большие данные хранят отдельно, но связывают с кодом контрольными суммами.
  • Результат воспроизводим только при зафиксированных коде, данных, среде и запуске.

Связанные материалы