Сходимость оптимизатора и философия best-NLL
NextStat использует L-BFGS-B и по умолчанию нацелен на лучший минимум NLL (best-NLL). Расхождения с pyhf по параметрам наилучшего фита на больших моделях (>100 параметров) ожидаемы и задокументированы: это поведение оптимизатора, а не баг модели.
Позиция: best-NLL по умолчанию
- NextStat намеренно не ограничивает оптимизатор ради совпадения с конкретным внешним инструментом.
- Если L-BFGS-B находит более глубокий минимум, чем SLSQP в pyhf, это корректный результат.
- Паритет функции цели проверен: NextStat и pyhf вычисляют одну и ту же NLL в одной и той же точке параметров (обычно ~1e-9 ... 1e-13).
- Расхождения дает оптимизатор, а не статистическая модель.
Типичный масштаб расхождений
| Модель | Параметры | ΔNLL (NS − pyhf) | Причина |
|---|---|---|---|
| simple_workspace | 2 | 0.0 | Оба сходятся |
| complex_workspace | 9 | 0.0 | Оба сходятся |
| tchannel | 184 | −0.01 ... −0.08 | преждевременная остановка SLSQP в pyhf |
| tHu | ~200 | −0.08 | преждевременная остановка SLSQP в pyhf |
| tttt | 249 | −0.01 | преждевременная остановка SLSQP в pyhf |
Отрицательная ΔNLL означает, что NextStat нашел лучший (меньший) минимум.
Уровни паритета
Уровень 1: паритет функции цели (P0, обязательно)
NLL(params) совпадает между NextStat и pyhf в одной и той же точке параметров. Допуски: rtol=1e-6, atol=1e-8. Проверено golden-тестами на всех fixture-workspace.
Уровень 2: паритет фита (P1, условно)
Параметры наилучшего фита совпадают в пределах допусков: atol=2e-4 по параметрам, atol=5e-4 по неопределенностям. На малых моделях (<50 параметров) обычно полное совпадение; на больших возможны расхождения из-за разных оптимизаторов. Это не дефект, если NS NLL ≤ pyhf NLL.
Уровень 3: совместимость оптимизатора (отвергнуто)
Намеренно ухудшать оптимизатор ради совпадения с SLSQP мы отвергаем: это искусственное ограничение без научной ценности.
Как проверить
# Для пользователей
import nextstat, json
ws = json.load(open("workspace.json"))
model = nextstat.from_pyhf(json.dumps(ws))
result = nextstat.fit(model)
print(f"NLL: {result.nll}") # чем меньше, тем лучше# Для разработчиков (проверки паритета)
make pyhf-audit-nll # Паритет функции цели (должно всегда проходить)
make pyhf-audit-fit # Паритет фита (на больших моделях возможны расхождения)
# Диагностика cross-eval
python tests/diagnose_optimizer.py workspace.jsonWarm-start для воспроизводимости pyhf
Если в конкретном сценарии нужно совпасть с pyhf (например, чтобы воспроизвести опубликованный результат):
import pyhf, nextstat, json
# 1. Фит в pyhf
ws = json.load(open("workspace.json"))
model = pyhf.Workspace(ws).model()
pyhf_pars, _ = pyhf.infer.mle.fit(
model.config.suggested_init(), model, return_uncertainties=True
)
# 2. Warm-start NextStat из точки pyhf
ns_model = nextstat.from_pyhf(json.dumps(ws))
result = nextstat.fit(ns_model, init_pars=pyhf_pars.tolist())
# result.nll <= pyhf NLL (гарантировано)L-BFGS-B vs SLSQP
| Аспект | L-BFGS-B (NextStat) | SLSQP (pyhf/scipy) |
|---|---|---|
| Гессиан | Квази-Ньютон (m=10 историй) | Rank-1 update |
| Границы | Нативные box-ограничения | Нативные box-ограничения |
| Сходимость | ||proj_grad|| < ftol | порог по ||grad|| |
| Сложность | O(m·n) на итерацию | O(n²) на итерацию |
| Большие модели (>100p) | Устойчиво | Часто останавливается преждевременно |
Подтверждение по профильному скану
| Фикстура | NS vs pyhf |dq(μ)| | NS vs ROOT |dq(μ)| | Фит ROOT |
|---|---|---|---|
| xmlimport | 1e-7 | 0.051 | Сошлось |
| multichannel | 4e-7 | 3.4e-8 | Сошлось |
| coupled_histosys | 5e-6 | 22.5 | ОШИБКА (status=-1) |
