Воспроизводимый эксперимент: seed, 30 прогонов и непараметрический критерий
Все предыдущие статьи трека рассказывали, чем искать. Эта — про то, как отличить «мы улучшили алгоритм» от «нам повезло с запуском». Без неё трек остаётся набором приёмов: любой из них можно применить, но нельзя проверить.
Повод конкретный. В предыдущей главе стенд выдал таблицу, где у трёх режимов из четырёх написано «различия не показано». Такая таблица честнее любой другой — но только если известно, как она получена.
Почему результаты не воспроизводятся
Четыре причины, и ни одна из них не про злой умысел.
Стохастичность считают деталью реализации. Генетический алгоритм — случайный процесс. Один прогон — одно наблюдение из распределения, а не «результат метода». Публикуется при этом обычно лучший прогон, потому что он и запомнился.
Seed фиксируют, но этого мало. Об этом ниже — это главный технический сюжет главы.
Бюджет сравнения не зафиксирован. Метод A получил 50 поколений популяции 100, метод B — 50 поколений популяции 20. Формально «одинаковые условия», фактически бюджет отличается впятеро.
Условия хранятся в голове. Версия кода, параметры, набор экземпляров, порядок обработки — всё это через полгода не восстанавливается даже автором. Диссертация, на которую опирается глава 14, формулирует ту же проблему на уровне предметной области: «ручной выбор становится слабо воспроизводимым, а сравнение альтернатив затрудняется из-за отсутствия единого протокола оценки».
Seed недостаточно: именованные подпотоки
Стандартный совет «зафиксируйте seed» верен и недостаточен. Один генератор на весь прогон привязывает результат к порядку обращений к нему.
rng = Random(42)
population = [random_individual(rng) for _ in range(pop_size)]
# ... где-то ниже добавили строку диагностики:
if debug:
sample = rng.choice(population) # съел одно число
# ... и вся дальнейшая последовательность мутаций сдвинулась
Эффект коварный: оба прогона выглядят правдоподобно, оба «с seed 42», числа разные. Ошибка проявляется как «у меня не повторяется твой результат» через полгода после того, как строка была добавлена.
Лечение — выводить поток из имени сущности, а не из номера обращения. В
исполняемом примере это
rng.mjs:
const streams = seedStreams('прогон-1');
streams.stream('селекция').next();
streams.stream('кроссовер').next();
streams.stream('мутация').next();
Поток 'мутация' получает состояние, выведенное из хеша пары (seed, имя). Отсюда три свойства,
каждое из которых закреплено тестом:
- порядок обращений не влияет на содержимое потоков — строки выше можно переставить;
- добавление нового потока не сдвигает существующие — можно завести
'логирование'; - повторное обращение по имени продолжает поток, а не начинает заново — иначе мутация выдавала бы одно и то же значение всё время.
Уровни каскада получают собственные пространства имён: пилот конфигурации compact физически не
может съесть случайные числа финального запуска. Это не педантизм — именно на стыке уровней такие
ошибки и живут, потому что там меняется порядок вызовов при каждой правке.
Проверять это надо тестом, а не глазами:
✔ ПОРЯДОК обращения к потокам не влияет на их содержимое
✔ добавление нового потока не сдвигает существующие
✔ два прогона с одним seed совпадают до последнего знака
Почему не меньше 30 прогонов
Правило «не менее 30 прогонов на конфигурацию» — не суеверие и не магия числа 30. Причин две.
Оценка распределения. Вы сравниваете не два числа, а два распределения. По пяти наблюдениям нельзя разумно оценить ни медиану, ни разброс, ни долю случаев, где метод проваливается.
Мощность критерия. Непараметрический критерий на выборках по 10 наблюдений различает только очень грубые эффекты. Разница «на 3 % лучше», ради которой обычно и затевается работа, на таком объёме принципиально необнаружима — и «различий не найдено» будет означать «мы не смотрели».
В диссертации это оформлено как элемент дизайна: «на каждое сочетание класса и размерности
приходится 30 независимых экземпляров», сравнение каждого режима основано на 270 запусках,
основная таблица содержит 1620 записей для шести режимов. Правило сформулировано и в более ранней
редакции — «не менее 30 независимых прогонов на ячейку дизайна, одни и те же seed для
сопоставимых режимов».
Отдельно: 30 — это нижняя граница для одной ячейки дизайна, а не для всей работы. Если у вас три класса сценариев и три размерности, ячеек девять, и 30 прогонов нужны в каждой.
Что мерить
Метрика — это то, что попадёт в таблицу и что потом будут цитировать. Минимальный набор для поисковой задачи выглядит так.
| Что | Зачем | Чего не говорит |
|---|---|---|
| Медиана целевой функции | устойчива к выбросам, в отличие от среднего | ничего о разбросе |
| Межквартильный размах | показывает надёжность метода | ничего о худшем случае |
| Доля допустимых решений | ограничения либо соблюдены, либо нет | ничего о качестве допустимых |
| Число оценок целевой функции | аппаратно-независимая стоимость | ничего о времени в секундах |
| Причина остановки | различает «сошлось» и «кончился бюджет» | ничего о качестве |
Про бюджет автор формулирует прямо: «основным аппаратно-независимым показателем экономии является число оценок целевой функции». Секунды остаются в отчёте как стендовая характеристика — их можно приводить, но на них нельзя строить вывод, потому что они описывают машину.
Про допустимость стоит сказать отдельно, потому что это самая частая потеря информации при скаляризации. Вот фрагмент фактического вывода стенда на размерности M:
| Режим | Медиана J_p | Оценок (медиана) | Допустимых |
| ----------- | ----------: | ---------------: | ---------: |
| ga_single | 0.0664 | 1200 | 70 % |
| ga1_ga2 | 0.0636 | 1184 | 50 % |
По свёртке $J_p$ каскад лучше. По доле планов, которые вообще можно отдать команде, — хуже на двадцать процентных пунктов. Публиковать первое без второго технически не ложь, но практически дезинформация.
«Среднее стало лучше» — не вывод
Распределение целевой функции по прогонам асимметрично, обрезано снизу и содержит выбросы. Нормальности нет, и t-критерий здесь неуместен. Работает ранговый подход: критерий Манна — Уитни для факта различия и величина эффекта $\hat{A}{12}$ для его размера. $\hat{A}{12} = 0{,}61$ читается буквально: случайный прогон A окажется лучше случайного прогона B в 61 % случаев.
И третий обязательный элемент — поправка на множественность. Сравнивая четыре режима с опорным,
вы проводите четыре теста; при $\alpha = 0{,}05$ вероятность увидеть хотя бы одно «различие»
на чистом шуме близка к 19 %. Диссертация использует поправку Холма: «нулевая гипотеза проверяется
парным рандомизационным sign-flip тестом; семейство p-value корректируется методом Холма при
уровне значимости 0,05». В учебном примере критерий другой (Манн — Уитни, реализован в
stats.mjs), поправка та же.
Как это выглядит на реальном выводе стенда:
| Режим | Δ медианы J_p | Â₁₂ | Эффект | p | p (Холм) | Вывод |
| ------------- | ------------: | ----: | --------- | ------: | -------: | -------------------- |
| greedy | +0.0000 | 0.479 | ничтожный | 0.7844 | 1.0000 | различия не показано |
| random_repair | +0.0484 | 0.114 | большой | <0.0001 | <0.0001 | различие есть |
| ga1_ga2 | -0.0024 | 0.611 | малый | 0.1412 | 0.4237 | различия не показано |
| ga0_ga1_ga2 | +0.0000 | 0.533 | ничтожный | 0.6627 | 1.0000 | различия не показано |
Строка ga1_ga2 — учебный случай. Медиана лучше, $\hat{A}_{12} = 0{,}611$ выглядит убедительно,
сырое $p = 0{,}14$ уже нет, а после поправки $0{,}42$. Правильная формулировка: «различие не
показано на 30 экземплярах». Не «различия нет» — отсутствие доказательства и доказательство
отсутствия здесь разные вещи, и увеличение выборки вполне может изменить вывод.
Формулировки в диссертации выдержаны в том же тоне: «гипотеза подтверждена частично и с установленной областью применимости: GA0 сокращает число вычислений на S и M, но эффект уменьшается с ростом задачи и сопровождается потерей качества; универсальное превосходство каскада, GA1 или графового слоя экспериментом не заявляется».
Протокол публикации
Минимум, по которому чужой человек повторит ваш результат:
Для каждого сценария, конфигурации и результата сохраняются входные данные, seed, параметры, версии алгоритмов, хеши и причина остановки. <…> Такой протокол позволяет повторить вычисления без смешения обучающей и тестовой частей и проследить каждое число раздела 3.4 до соответствующего CSV-файла.
В учебном примере это одна запись на прогон (protocol.mjs):
{
"scenario": "S#20260803",
"seed": "demo/ga0_ga1_ga2/S#20260803",
"config": "compact",
"evaluations": 576,
"budget": 1200,
"jp": 0.054602761046135685,
"feasible": true,
"readiness": 0.6845277512837074,
"reason": "ready",
"instanceHash": "21584fa675fb9c4e"
}
instanceHash ловит незаметную подмену входных данных, reason различает раннюю остановку и
исчерпание бюджета, budget фиксирует условия сравнения. Проверка воспроизводимости — это
отдельный прогон по сохранённым записям; в диссертации он выполнен на 270 запусках и дал
«совпадение хешей сценариев и результатов 1,000».
И последнее правило протокола, самое неудобное: критерий и число прогонов выбираются до эксперимента. Выбор теста после того, как вы посмотрели на данные, превращает $p$-значение в декорацию.
Отдельный вопрос: а верна ли сама оценка?
Всё сказанное защищает от случайности в поиске. Остаётся вопрос уровнем ниже: правильно ли считается сам фитнес? Ошибка в формуле $J_p$ или в правиле допустимости воспроизводится идеально и вместе с seed — воспроизводимость её не ловит. В подсистеме ASK упомянутой диссертации для ответов системы предусмотрен слой формальных спецификаций CH/FTS — «соответствие Карри–Ховарда, witness для аудита ответов», причём автор отдельно оговаривает, что этот слой «не входящий в ядро сравнения режимов GA0→GA1→GA2». Идея переносится на фитнес-функцию: правило, записанное как исполняемая модель с примерами, проверяется само по себе, а не только по результатам поиска.
категория «Планирование итерации»
объект План
«нарушения компетенций» является числом
«нормированная переработка» является числом
утилита «Проверить допустимость плана»
принимает План
возвращает число
начинает с 0
правило «Компетенции нарушены»
если «нарушения компетенций» больше 0
то добавить 1
правило «Переработка выше порога»
если «нормированная переработка» больше 0.05
то добавить 1
пример «План допустим»
дано «нарушения компетенций» равно 0
дано «нормированная переработка» равна 0.02
ожидается результат равен 0
Подробнее о том, как такие модели пишутся и исполняются, — в курсе про FTS.
Типичные ошибки
- Один прогон вместо тридцати. Самая частая и самая дорогая.
- Seed зафиксирован, подпотоков нет. Результат зависит от порядка вычислений, и это выясняется через полгода.
- Сравнение по времени вместо оценок. Публикуется характеристика машины.
- Разный бюджет у сравниваемых методов. Часто маскируется одинаковым числом поколений при разных популяциях.
- Среднее вместо медианы. Один провалившийся прогон утаскивает среднее и прячет типичное поведение.
- $p$-значение без величины эффекта. На больших выборках значимой становится любая мелочь.
- Множественные сравнения без поправки. Четыре теста — и «находка» появляется сама собой.
- «Различий нет» вместо «различие не показано». Разные утверждения, и второе — то, что вы на самом деле проверили.
- Выбор критерия после просмотра данных.
- Настройка параметров на тех же экземплярах, на которых отчитываетесь — см. главу 13.
Мини-итог
- Один прогон стохастического алгоритма — наблюдение, а не результат.
- Фиксации seed мало: подпотоки должны выводиться из имён, иначе результат зависит от порядка обращений к генератору. Это проверяется тестом, а не соглашением в команде.
- Не меньше 30 прогонов на ячейку дизайна: меньше не хватает ни на оценку распределения, ни на мощность критерия.
- Бюджет измеряется в оценках целевой функции; секунды — стендовая характеристика.
- Публикуются медиана, межквартильный размах, доля допустимых решений, число оценок и причина остановки. Скаляризация без доли допустимых решений вводит в заблуждение.
- Вывод делается по непараметрическому критерию с величиной эффекта и поправкой на множественность. «Различие не показано» — нормальный результат, который надо уметь писать.
- Протокол прогона (сценарий, seed, параметры, бюджет, хеши, причина остановки) — это то, что превращает таблицу в проверяемое утверждение.
Источники
- Arcuri A., Briand L. «A Hitchhiker’s Guide to Statistical Tests for Assessing Randomized Algorithms in Software Engineering», STVR 2014 — базовый протокол: 30 прогонов, Манн — Уитни, $\hat{A}_{12}$.
- Vargha A., Delaney H. D. «A Critique and Improvement of the CL Common Language Effect Size Statistics», JEBS 2000 — происхождение $\hat{A}_{12}$ и границы «малый / средний / большой».
- Holm S. «A Simple Sequentially Rejective Multiple Test Procedure», Scandinavian Journal of Statistics, 1979.
- Arcuri A., Fraser G. «Parameter tuning or default values?», EMSE 2013 — переобучение мета-уровня на наборе экземпляров.
- Зимнуров М. Ф. Иерархическая многокритериальная оптимизация плана назначения в СППР: модель, каскад GA0→GA1→GA2 и воспроизводимый эксперимент в курсе системного анализа // Образование и наука: современный вектор развития: материалы V Национальной научно-практической конференции. — Керчь, 2026. — С. 48–54.
- Зимнуров М. Ф., Астраханцева И. А. Методология создания многосвязных структур данных с применением LLM в рабочих проектах // Современные наукоемкие технологии. Региональное приложение. — 2025. — № 1 (81). — С. 76–83. — DOI 10.6060/snt.20258101.0009.
- Исполняемые примеры к главам 14–15:
examples/sbse/.
Что дальше
Методическая часть трека на этом закрыта: от постановки задачи как оптимизационной до протокола, по которому результат можно проверить. Дальше — три главы, где протокол из этой статьи уже применяется к конкретным замерам: сначала фитнесом становится компилятор, потом над поиском надстраиваются метауровни.
- Исполняемая спецификация как фитнес-функция — что происходит с поиском, когда судьёй становится код возврата, и почему переобучение под безупречный оракул всё равно случается;
- Иерархический каскад: три уровня поиска, делящих один бюджет — парный замер настройки против фиксированных параметров, когда бюджет у уровней общий;
- Антагонистический режим: два подагента и явное управление областью поиска — компромисс «исследование против использования» как решение явного арбитра.
Общая карта — в дорожной карте.
Практический шаг на завтра: возьмите свой последний эксперимент с поиском и попробуйте повторить его число в отчёте. Если для этого не хватает записанных условий — начинать надо не со следующего алгоритма, а с протокола.