Сначала зафиксируйте grain данных, допущения, метрику и риск leakage, затем обсуждайте модель, инструмент или инфраструктуру.
Вопросы и ответы
9 подробных ответов
01Как тестировать ML-код, если «правильный ответ» стохастический?
concept
Короткий ответ: Разделить детерминированное и стохастическое. Трансформы, лоссы и метрики покрываются обычными юнит-тестами на просчитываемых вручную кейсах; обучение проверяется смоук-тестами (оверфит крошечного батча, shape- и градиент-чеки) и поведенческими тестами с допуском.
Подробно:
| Слой | Что проверяет | Пример |
|---|---|---|
| Юнит-тесты | трансформы, лоссы, метрики на ручных кейсах | loss(y, y) == 0 |
| Shape/dtype/градиенты | размерности, типы, градиент доходит до всех параметров | после backward() ни один param.grad не None |
| Оверфит одного батча | пайплайн способен выучить 10 примеров до ~100% | не может → сломана «проводка», а не данные |
| Поведенческие | инвариантность и направленность | синоним не меняет класс; «ужасно» понижает sentiment |
| Golden-предикты | эталонные предсказания с допуском, гоняются в CI | ловят молчаливые регрессии после рефакторинга |
- Тестируем код, а не модель — детерминированные куски (фичи, лоссы) обязаны иметь точные ассерты, стохастические — допуски и инварианты.
- Смоук в CI — короткий прогон на 2–3 шага: пайплайн собирается, лосс конечен и падает.
⚠️ Частая ошибка: «мы смотрим на test accuracy» — это оценка модели, а не тестирование кода. Баг в даталоадере может стоить пару пунктов метрики и никогда не всплыть.
02Лосс на обучении не падает. Опишите порядок отладки.
middle
Короткий ответ: Идти от данных к архитектуре, а не наоборот. Сначала посмотреть батч и метки глазами, затем решающий эксперимент — оверфит одного батча. Только потом LR, ошибки «проводки» тренировочного цикла и инициализация.
Подробно:
1. Данные глазами: сырой батч, метки, соответствие X ↔ y
2. Оверфит ОДНОГО батча до ~100% — решающий эксперимент:
не может → сломана проводка; может → данные или LR
3. LR-свип: ×10 вверх и вниз — слишком большой LR
тоже выглядит как «не падает»
4. Проводка: забытый optimizer.zero_grad(),
неверная редукция лосса, метки не выровнены с логитами
(сдвиг, порядок классов), замороженные параметры,
забытый model.train()
5. Инициализация и нормализация входов
- Порядок важнее списка — интервьюер оценивает именно то, что данные вы проверяете раньше архитектуры: они ломаются на порядки чаще.
- Оверфит батча — одна проверка режет пространство гипотез пополам: неспособность выучить 10 примеров исключает «сложную задачу» как оправдание.
⚠️ Частая ошибка: первым делом менять архитектуру или добавлять слои — почти всегда виноваты данные или проводка цикла, а не ёмкость модели.
03Что вы валидируете во входных данных до обучения — кодом, прямо в пайплайне?
middle
Короткий ответ: Схему (колонки и dtype), границы значений, доли null, домены категорий, объём строк относительно прошлого запуска и распределение таргета. Проверки — это код в пайплайне, который громко падает ДО обучения, а не глаза аналитика после.
Подробно:
def validate(df, ref):
assert set(df.columns) == set(ref.columns) # схема
assert df["age"].between(18, 100).all() # границы значений
assert df["income"].isna().mean() < 0.02 # доля null
assert set(df["region"]) <= ref.region_domain # домен категорий
assert 0.5 < len(df) / ref.row_count < 2.0 # объём vs прошлый ран
assert abs(df["y"].mean() - ref.y_rate) < 0.05 # распределение таргета
- Fail loudly vs карантин — нарушение схемы или таргета валит пайплайн целиком; единичные битые строки можно отправлять в карантин с алертом, но порог доли — тоже проверка.
- Референс из прошлого запуска — половина проверок относительные: сравнение с зафиксированной статистикой предыдущего успешного рана.
- Инструменты — Great Expectations, pandera; но интервьюеру важна концепция «валидация как гейт», а не название библиотеки.
⚠️ Частая ошибка: проверять только схему. Типы совпали, а колонка на 40% превратилась в null — обучение «успешно» пройдёт на мусоре.
04Почему фиксация random seed не гарантирует воспроизводимость обучения?
middle
Короткий ответ: Сид контролирует только генераторы случайных чисел. Остаются недетерминированные CUDA-ядра, автотюнинг cuDNN, планирование воркеров даталоадера, неассоциативность float-арифметики и версии библиотек. Полный ответ: сиды + детерминированные флаги + запиненное окружение + версии данных/кода/конфига.
Подробно:
| Источник недетерминизма | Митигация |
|---|---|
| CUDA-ядра с atomics в редукциях — порядок сложений плавает | torch.use_deterministic_algorithms(True) |
| cuDNN autotune — выбирает алгоритм по замерам на лету | cudnn.benchmark=False, cudnn.deterministic=True |
| Воркеры даталоадера — порядок и сиды процессов | worker_init_fn с сидом от worker id, фиксированный generator |
| Float неассоциативен — другое число GPU → другой порядок сумм | фиксировать конфигурацию железа и world size |
| Версии библиотек/драйверов | lock-файл или Docker-образ + версии данных и конфига |
- Цена детерминизма — детерминированные ядра заметно медленнее; в проде часто фиксируют всё, кроме них, и принимают шум в пределах допуска.
- Воспроизводимость ≠ бит-в-бит — практический стандарт: та же метрика в пределах заявленного разброса при полном восстановлении lineage.
⚠️ Частая ошибка: «поставил seed=42, значит воспроизводимо» — на другом GPU или другой версии cuDNN цифры разойдутся.
05Что должен записывать трекер экспериментов, чтобы запуск был воспроизводимым и сравнимым?
junior
Короткий ответ: Всё, из чего собирается результат: git-коммит кода, версию/хеш данных, полный конфиг с гиперпараметрами, окружение, метрики по шагам и артефакты (чекпоинты). Логировать одни метрики — ловушка: победителя потом не воспроизвести.
Подробно:
- Код — точный commit hash плюс флаг «грязного» рабочего дерева: незакоммиченный патч делает коммит бесполезным.
- Данные — версия или хеш датасета (DVC, снапшот): «та же таблица» через месяц — уже другие данные.
- Конфиг целиком — все гиперпараметры, сплиты, версии фичей; не только те, что «крутили» в этом эксперименте.
- Окружение — lock-файл зависимостей или образ, версия CUDA/драйвера.
- Метрики по шагам — кривые train/val, а не одна финальная цифра: видно расхождение и раннюю остановку.
- Артефакты — чекпоинт, токенизатор, препроцессор — привязанные к запуску.
run = код (commit) + данные (hash) + конфиг + env
└─► метрики по шагам + артефакты (чекпоинт)
⚠️ Частая ошибка: сравнивать запуски по метрике, когда у них незаметно разные данные или сплит — сравнение бессмысленно без одинакового lineage.
06GPU-утилизация 30% во время обучения. Как найдёте узкое место?
middle
Короткий ответ: Это producer-consumer проблема: GPU почти всегда голодает из-за входного пайплайна. Сначала профилирование (torch.profiler, py-spy, «пила» в nvidia-smi), потом фиксы: num_workers, pin_memory, офлайн-препроцессинг, prefetch — и убрать синхронизации вроде .item() на каждом шаге.
Подробно:
1. Профилировать, не гадать: torch.profiler / py-spy;
«пила» в nvidia-smi utilization = GPU ждёт данные
2. Даталоадер: num_workers > 0, pin_memory=True,
.to(device, non_blocking=True)
3. Вынести тяжёлое из __getitem__: пре-токенизация,
пре-ресайз офлайн, кэш на быстром диске
4. Prefetch следующего батча; батч крупнее,
если позволяет память
5. Синхронизации в цикле: .item() / логирование
каждый шаг, лишние .cpu() — скрытые sync-точки
6. Хранилище: сетевой диск, миллионы мелких файлов →
шардированные форматы (webdataset, tfrecord)
- Фрейминг — конвейер «диск → CPU-декодирование → H2D-копия → GPU»: утилизация 30% значит, что самый медленный этап не GPU.
- Профайлер решает спор за минуты — timeline сразу показывает, кто держит шаг: DataLoader, копирование или вычисление.
⚠️ Частая ошибка: сразу просить GPU помощнее — ускорит только consumer, который и так простаивает 70% времени.
07Что на самом деле делают num_workers и pin_memory в PyTorch DataLoader?
junior
Короткий ответ: num_workers поднимает отдельные процессы (не потоки — из-за GIL), которые параллельно читают и декодируют батчи с префетчем. pin_memory кладёт готовый батч в page-locked память, что позволяет асинхронно копировать его на GPU через non_blocking=True.
Подробно:
loader = DataLoader(ds, batch_size=64,
num_workers=8, # 8 процессов декодируют и префетчат батчи
pin_memory=True) # батчи в page-locked (pinned) памяти
x = batch.to("cuda", non_blocking=True) # асинхронный H2D:
# копия перекрывается с вычислениями — только из pinned памяти
- Процессы, а не потоки — Python GIL не даёт декодировать параллельно в потоках; воркеры форкаются и общаются через очередь.
- Pinned память — ОС не может выгрузить страницу, поэтому DMA-копирование на GPU идёт без промежуточного буфера и асинхронно.
- Цена воркеров — каждый держит свою копию датасета в памяти: RAM ×N; ленивая инициализация тяжёлых объектов обязательна.
⚠️ Частая ошибка: «поставим num_workers=32, будет быстрее» — воркеров больше, чем ядер CPU, значит контекст-свитчи и деградация; подбирается замером, стартуя с числа ядер.
08Спроектируйте тренировочный пайплайн как DAG: какими свойствами должен обладать каждый шаг?
middle
Короткий ответ: Каждый шаг — идемпотентный, обменивается с соседями только явными версионированными артефактами, параметризован датой запуска (ради бэкфиллов) и переживает ретраи. Плюс два гейта: валидация данных ДО обучения и eval ДО промоушена в registry.
Подробно:
ingest ──► validate ──► features ──► train ──► eval ──► promote
│ гейт: fail │ │ гейт: метрики
▼ до трейна ▼ ▼ ≥ порога
стоп checkpoint registry
каждый шаг: идемпотентен · читает/пишет версионированные
артефакты · параметризован датой · с ретраями
- Идемпотентность — повторный запуск шага за ту же дату даёт тот же результат и не дублирует данные: пишем в путь с версией/датой, а не append.
- Явные артефакты — шаги общаются через хранилище (parquet, чекпоинт), а не через память процесса: любой шаг можно перезапустить отдельно.
- Параметризация датой —
run(date)вместо «сегодня»: бэкфилл за месяц — это просто 30 запусков. - Гейты — битые данные не доходят до обучения, слабая модель — до продовой registry; промоушен всегда через сравнение с текущим чемпионом.
⚠️ Частая ошибка: сыпать названиями операторов Airflow вместо свойств шагов — интервьюер спрашивает про инварианты, оркестратор вторичен.
09Модель отлично работает офлайн и плохо в проде. Назовите инженерные (не статистические) причины.
middle
Короткий ответ: Прежде чем говорить «дрифт», исключить расхождения кода и данных между трейном и сервингом: два кодовых пути фичей, point-in-time нарушения в трейн-джоине, разъехавшиеся версии препроцессинга, устаревшие онлайн-фичи, потери при сериализации.
Подробно:
| Причина | Как проверить |
|---|---|
| Training-serving skew — фича реализована дважды (SQL офлайн, сервисный код онлайн) | сверить офлайн- и онлайн-значения фичей на одних объектах |
| Point-in-time нарушение — трейн-джоин взял агрегаты «на сейчас», а не на момент события | пересобрать джоин as-of даты события; офлайн-метрика упадёт до продовой |
| Версия препроцессинга — токенизатор/нормализация обновились, модель — нет | хеш артефакта препроцессинга в трейне == в сервинге |
| Устаревшие онлайн-фичи — фича-стор отдаёт вчерашние значения | мониторинг freshness; доля дефолтных значений в ответах |
| Сериализация/точность — экспорт в ONNX/fp16, другой рантайм | сравнить логиты до/после экспорта на одном батче |
- Дельта с ответом дата-сайентиста — «дрифт» — статистическая гипотеза; сеньорный инженерный ответ сначала называет кодовые пути, где трейн и прод видят разные данные.
⚠️ Частая ошибка: сразу переобучать «на свежих данных» — если фичи в сервинге считаются иначе, переобучение ничего не чинит.
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.