From 97c483787ab789b9071fdb2d576ee71bc0aa739b Mon Sep 17 00:00:00 2001 From: bug00n Date: Fri, 11 Sep 2026 21:41:00 +0300 Subject: [PATCH] stage_5_back --- CODE_WALKTHROUGH.md | 122 ++++++++++ PROJECT_GUIDE.md | 72 ++++++ README.md | 9 + STAGE5.md | 217 +++++++++++------- .../test_stage5_uncertainty_policy.py | 9 + source/ml/__init__.py | 38 +++ 6 files changed, 379 insertions(+), 88 deletions(-) diff --git a/CODE_WALKTHROUGH.md b/CODE_WALKTHROUGH.md index d07a9fd..4f5fe6d 100644 --- a/CODE_WALKTHROUGH.md +++ b/CODE_WALKTHROUGH.md @@ -1892,3 +1892,125 @@ S_mix = sum(w_i * S_i) - stock shortfall; - запрет невалидных долей; - context-only статус газовых тегов. + +## 23. Что добавил Stage 5 + +Stage 5 добавляет слой неопределенности и применимости вокруг исторического ML-прогноза. +Он не меняет внешний CLI и не включает реальные управляющие воздействия. + +Общий поток: + +```text +prepared dataset +-> supervised features +-> point model artifact +-> fit_stage5_uncertainty() +-> calibrated upper predictor +-> save_stage5_model() +-> predict_quality() +-> check_applicability() +-> predict() + predict_upper() +-> check_constraints() +``` + +### `source/ml/uncertainty.py` + +`fit_upper_calibrator()` получает реальные validation targets и несколько вариантов raw +upper-прогнозов. Первая половина validation выбирает модель по pinball loss, вторая +половина считает non-negative shift: + +```text +shift = max(0, q95(y - upper_raw)) +``` + +`evaluate_upper_bounds()` проверяет held-out качество upper-границы: coverage и среднюю +ширину `upper - point`. + +`check_applicability()` не прогнозирует серу. Он только отвечает, можно ли применять +модель к текущей строке признаков: + +- missing/NaN -> `FEATURES_UNAVAILABLE`; +- выход за bounds -> `OUT_OF_DOMAIN`; +- все признаки внутри bounds -> available. + +`fit_stage5_uncertainty()` проверяет, что point artifact и dataset используют один и тот же +feature order и одни временные split boundaries. Потом обучает quantile-регрессоры, +калибрует upper, считает test metrics и robustness cases. + +`save_stage5_model()` сохраняет artifact так, чтобы metadata явно говорила: + +```text +supports_forecast = true +supports_uncertainty = true +supports_actions = false +``` + +### `source/ml/artifacts.py` + +`ModelBundle.predict_upper()` нужен для serving. Он проверяет: + +- artifact действительно заявил `supports_uncertainty`; +- feature order совпадает с metadata; +- predictor вернул одно конечное значение на строку; +- upper не ниже point. + +`ModelBundle.check_applicability()` берет `feature_bounds` из metadata и вызывает +`source.ml.uncertainty.check_applicability()`. + +### `source/agents/quality.py` + +В `history` mode quality-agent теперь ведет себя так: + +1. Проверяет совместимость target signal/unit/horizon. +2. Если artifact поддерживает uncertainty, вызывает applicability gate. +3. Если gate недоступен или не пройден, возвращает unavailable issue. +4. Считает point forecast. +5. Если доступен uncertainty, считает upper forecast. +6. Возвращает `MetricEstimate` с `interval_kind=EMPIRICAL` и `interval_level=0.95`. + +Главная идея: если upper нужен, но его нет, backend не должен делать вид, что качество +прошло constraint. + +### `source/ml/policy.py` + +Policy helpers работают отдельно от модели качества. Они отвечают на вопрос: + +```text +достаточно ли кандидат лучше hold, чтобы разрешить рекомендацию +``` + +`assess_change_policy()` смотрит на первый отличающийся active criterion. Если отличие +только в `change_size`, это не улучшение. Если есть существенное улучшение, проверяется +cooldown. Cooldown действует только при feasible hold. + +`tune_policy()` выбирает параметры на validation replay: сначала минимизирует пропущенные +обязательные изменения, потом лишние действия, потом общее число действий. + +### `source/ml/__init__.py` + +Фасад лениво экспортирует Stage 5 helper API: + +- `fit_stage5_uncertainty`; +- `fit_upper_calibrator`; +- `check_applicability`; +- `evaluate_upper_bounds`; +- `assess_change_policy`; +- `tune_policy`; +- `PolicyParameters`. + +Это удобно для внешнего кода и не заставляет импортировать тяжелые ML-зависимости, пока +конкретная функция не запрошена. + +### `global_tests/test_stage5_uncertainty_policy.py` + +Тесты Stage 5 проверяют: + +- раздельный selection/calibration split; +- held-out coverage и width; +- missing/OOD не превращаются в pass; +- robustness cases сохраняют unavailable; +- materiality threshold и first differing criterion; +- cooldown только при feasible hold; +- tuning policy на validation replay; +- интеграцию quality-agent с upper bound; +- доступность Stage 5 helper API через lazy `source.ml` facade. diff --git a/PROJECT_GUIDE.md b/PROJECT_GUIDE.md index c28e0a7..5ee9591 100644 --- a/PROJECT_GUIDE.md +++ b/PROJECT_GUIDE.md @@ -1673,3 +1673,75 @@ Stage 4 связывает прогноз гидроочистки с модел В коде Stage 4 живёт в `source/ml/blending.py`, а regression-тесты - в `global_tests/test_stage4_blending.py`. + +--- + +## 36. Что добавляет Stage 5 + +Stage 5 делает ML-прогноз честнее. До этого backend мог иметь только point forecast: + +```text +ожидаемая сера = 9.2 mg/kg +``` + +Но одного числа мало. Нужно понимать: + +- насколько верхняя граница риска выше point-прогноза; +- похож ли текущий режим на train-данные; +- можно ли использовать модель именно сейчас; +- стоит ли вообще менять рекомендацию, если улучшение слишком маленькое. + +Поэтому Stage 5 добавляет три слоя. + +### Uncertainty + +`source/ml/uncertainty.py` строит empirical upper estimate уровня `0.95`. + +Простыми словами: + +```text +point = модельное ожидание +upper = осторожная верхняя оценка +``` + +Если constraint требует серу `<= 10`, backend должен смотреть на `upper`, а не только +на point. Если upper нет, это не pass. Это `UNCERTAINTY_UNAVAILABLE`. + +### Applicability + +`ModelBundle.check_applicability()` проверяет, не вышли ли входные признаки за +train-only bounds. + +Результаты: + +- `FEATURES_UNAVAILABLE` - нужного признака нет или он нечисловой; +- `OUT_OF_DOMAIN` - признак есть, но режим не похож на train-данные; +- available - можно использовать forecast. + +Это важно: модель не должна уверенно отвечать там, где она не обучалась. + +### Policy Guardrails + +`source/ml/policy.py` решает не качество рецепта, а вопрос: + +```text +достаточно ли отличие кандидата от hold, чтобы вообще давать новую рекомендацию +``` + +Правила: + +- маленькое улучшение не создает рекомендацию; +- изменение только `change_size` не считается полезным; +- cooldown подавляет повторные рекомендации только когда hold безопасен; +- если hold нарушает hard constraint, cooldown не имеет права скрыть нарушение. + +### Где это в коде + +- `source/ml/uncertainty.py` - calibration, upper bound, applicability, robustness; +- `source/ml/policy.py` - materiality/cooldown policy; +- `source/ml/artifacts.py` - `supports_uncertainty`, `predict_upper`, `check_applicability`; +- `source/agents/quality.py` - осторожное применение uncertainty в history mode; +- `global_tests/test_stage5_uncertainty_policy.py` - regression tests. + +Stage 5 всё ещё не включает реальное управление установкой. Газ, setpoint-ы и action +model остаются вне разрешенных действий backend. diff --git a/README.md b/README.md index 77104dc..71c4bea 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - [Stage 2](STAGE2.md) — как оригинальные материалы превращаются в prepared dataset и `ProcessState`. - [Stage 3](STAGE3.md) — hard constraints, причины отбраковки кандидатов, materiality и cooldown. - [Stage 4](STAGE4.md) — hybrid chain, model blending и газовые теги как context-only сигналы. +- [Stage 5](STAGE5.md) — uncertainty, applicability, robustness и policy guardrails без action model. - [Code walkthrough](CODE_WALKTHROUGH.md) — папки, файлы и хронология вызовов почти построчно. - [ML system design](DESIGN.md#8-ml-неопределённость-и-модель-последствий) — обучение, метрики, анализ ошибок и жизненный цикл модели; общие контракты и данные описаны в том же документе. - [Материалы задания](materials/README.md) — ТЗ, схемы и исходные данные. @@ -31,6 +32,8 @@ в журнале; - stage 4: модельная связка гидроочистка -> блендинг, массовый баланс рецептур и gas context для `ht:F9`, `ht:F22`, `ht:Q21` без включения реального управления газом. +- stage 5: empirical upper estimate для прогноза серы, applicability/OOD gate, robustness + reporting и materiality/cooldown policy helpers без включения action model. Полноценной ML-модели, промышленного управления реальными уставками и Streamlit UI пока нет. Текущий backend уже умеет готовить данные и запускать безопасный demo-каркас с @@ -41,6 +44,11 @@ Stage 4 показывает связанную цепочку и модельн диапазонов и модели эффекта. Полная товарная спецификация также не заявлена: `T95`, цетановое число и весь паспорт продукта пока `not_assessed`. +Stage 5 добавляет осторожность вокруг ML-прогноза: если artifact поддерживает uncertainty, +quality-agent сначала проверяет область применимости признаков, потом использует point и +upper sulfur. Missing/OOD/отсутствующий upper не превращаются в pass. Это не action model: +backend по-прежнему не рекомендует реальные setpoint-изменения и не управляет газом. + ## Проверка Нужны Python 3.11, Git LFS и `tar` с поддержкой RAR. @@ -50,6 +58,7 @@ python -m venv .venv python -m pip install -r requirements.txt python -m source.main validate-stage0 python -m pytest +python -m pytest global_tests/test_stage5_uncertainty_policy.py python -m ruff check . python -m ruff format --check . python -m mypy source diff --git a/STAGE5.md b/STAGE5.md index 44c11a5..0d5b31d 100644 --- a/STAGE5.md +++ b/STAGE5.md @@ -1,89 +1,130 @@ -# Stage 5: Неопределённость, область применимости и устойчивость - -## Результат - -Исторический прогноз серы дополнен эмпирической верхней оценкой уровня `0.95`. -Quality-agent использует её для ограничения `sulfur <= 10 mg/kg`; если upper или -область применимости недоступны, результат становится `unknown/unavailable`, а не -`pass`. - -## Обучение без утечки - -1. Point-модель и фиксированные временные границы берутся из строгого Stage-2 - artifact. -2. HGB quantile-кандидаты обучаются только на train с `loss="quantile"`, - `quantile=0.95` и отключённым внутренним случайным early stopping. -3. Первая половина validation выбирает конфигурацию по pinball loss. -4. Вторая половина validation вычисляет - `shift=max(0, q95(y - upper_raw))`. -5. Test используется один раз только для coverage и средней ширины. - -Artifact сохраняет calibration split, shift, train-only feature bounds, -`supports_uncertainty=true`, `supports_actions=false` и поведение OOD. - -## Проверка на полном наборе - -Источник: prepared dataset `aacc7c1ab3d9`, PAK sulfur, 189 649 supervised строк, -54 признака, горизонт 60 минут. - -| Показатель | Значение | -| --- | ---: | -| Выбранная upper-модель | `hgb_quantile_1` | -| Selection / calibration | 26 277 / 26 277 строк | -| Начало calibration | `2025-07-02T08:30:00Z` | -| Calibration shift | 0.0 mg/kg | -| Test | 31 819 строк | -| Test coverage | 0.96342 | -| Средняя ширина upper − point | 1.94352 mg/kg | -| Доля test внутри feature domain | 0.63362 | -| Coverage внутри feature domain | 0.95987 | -| Средняя ширина внутри domain | 1.96250 mg/kg | -| Coverage при +0.5 mg/kg measurement error | 0.84327 | -| Coverage на последней четверти test | 0.97800 | - -Локальный проверенный artifact: `artifacts/models/sulfur-upper-aacc7c1ab3d9`. -Он не коммитится: модели и полные prepared data исключены из Git. - -## Admission, OOD и robustness - -- `ModelBundle.predict_upper` проверяет feature order, конечность и `upper >= point`. -- Train-only q0.001/q0.999 по каждому признаку задают консервативный marginal - applicability gate. Пропуск даёт `FEATURES_UNAVAILABLE`, выход — `OUT_OF_DOMAIN`. -- В отчёте отдельно видны nominal test, ошибка измерения `+0.5 mg/kg`, последняя - четверть test как простой regime-shift slice и факт 4-часовой задержки ЛИМС. -- Резкое падение coverage до 0.84327 при систематической ошибке +0.5 показывает, - что 0.95 — эмпирическая характеристика истории, не гарантия безопасности. - -## Materiality и cooldown - -- Сравнивается первый различающийся активный критерий. -- Risk использует абсолютный порог 0.02; throughput и cost — относительные 2%. -- Различие только в `change_size` не считается улучшением. -- Cooldown 60 минут подавляет повторное изменение только при feasible hold. -- Если hold нарушает обязательное ограничение, cooldown и экономический порог не - блокируют поиск безопасного действия. -- `tune_policy` выбирает параметры только по validation replay, сначала минимизируя - пропущенные обязательные изменения, затем лишние и общее число действий. - -## Что остаётся ограничением - -- Upper coverage зависит от исторического распределения и ухудшается при bias или - новом режиме; для production нужен drift monitor и периодическая recalibration. -- Marginal feature bounds не заменяют полноценную многомерную OOD-модель. -- Текущий строгий gate пропускает только 63.36% test-строк: это безопасное - воздержание, но слишком высокая доля отказов для production. Нужна отдельная - калибровка applicability threshold на validation без ослабления missing-data gate. -- Реальные setpoint-рекомендации всё ещё отключены: Stage 5 не создаёт причинную - action-модель и не исправляет неизвестные единицы/пределы `P8/T11/F19`. -- T95 и цетановое число не имеют валидированных blend/effect моделей, поэтому - полное соответствие товарного дизеля не заявляется. - -## Проверка - -```powershell -.\.venv\Scripts\python.exe -m pytest -q -.\.venv\Scripts\ruff.exe check source global_tests -.\.venv\Scripts\ruff.exe format --check source global_tests -.\.venv\Scripts\mypy.exe source -.\.venv\Scripts\python.exe -m source.main validate-stage0 +# Stage 5: uncertainty, applicability, policy guardrails + +## Зачем нужен этап + +До Stage 5 backend мог работать с точечным прогнозом серы: модель говорит "ожидаю 9.2 mg/kg", а дальше constraints сравнивают это с лимитом. Проблема в том, что точечный прогноз сам по себе слишком смелый: он не говорит, насколько прогноз надежен, похож ли текущий режим на обучающие данные и можно ли вообще использовать модель сейчас. + +Stage 5 добавляет честный слой осторожности: + +- empirical upper estimate уровня `0.95`; +- проверку области применимости признаков; +- robustness-отчет по стрессовым случаям; +- materiality/cooldown policy для решения, стоит ли менять рекомендацию. + +Это всё еще backend-only. Этап не включает action model, не разрешает управление газом и не делает промышленную оптимизацию уставок. + +## Что реализовано + +### Uncertainty + +Файл `source/ml/uncertainty.py` содержит отдельный слой для верхней оценки прогноза: + +- `fit_upper_calibrator()` выбирает upper-модель на первой половине validation и калибрует additive shift на второй половине validation; +- `evaluate_upper_bounds()` считает held-out coverage и среднюю ширину `upper - point`; +- `check_applicability()` отклоняет missing, invalid и out-of-domain features; +- `fit_stage5_uncertainty()` строит uncertainty-capable predictor поверх уже существующей point-модели; +- `save_stage5_model()` сохраняет artifact с `supports_uncertainty=true` и `supports_actions=false`. + +Важно: test-часть используется только для финальной оценки качества. Она не участвует в подборе upper-модели или policy. + +### Model Artifact + +`source/ml/artifacts.py` теперь умеет обслуживать uncertainty-capable artifact: + +- `ModelCapabilities.supports_uncertainty`; +- `ModelBundle.predict_upper(features)`; +- `ModelBundle.check_applicability(features)`. + +`predict_upper()` проверяет порядок признаков, конечность значений и гарантирует, что `upper >= point`. Если artifact не заявляет uncertainty support, метод падает явно, а не возвращает фиктивную границу. + +### Quality-Agent + +`source/agents/quality.py` в `history` mode использует Stage 5 осторожно: + +- если модель поддерживает uncertainty, сначала вызывается applicability gate; +- missing features дают `FEATURES_UNAVAILABLE`; +- выход за train-only bounds дает `OUT_OF_DOMAIN`; +- если upper недоступен, результат становится unavailable/degraded issue, а не pass; +- constraint по сере использует upper, когда scenario требует верхнюю границу. + +### Policy + +`source/ml/policy.py` содержит чистые helper-функции без DTO и без CLI: + +- `PolicyParameters` задает пороги risk/throughput/cost и cooldown; +- `assess_change_policy()` решает, является ли отличие кандидата от hold существенным; +- `tune_policy()` выбирает policy по validation replay. + +Правила: + +- risk threshold абсолютный, по умолчанию `0.02`; +- throughput/cost thresholds относительные, по умолчанию `2%`; +- изменение только `change_size` не считается полезным улучшением; +- cooldown подавляет повторную рекомендацию только если baseline/hold feasible; +- если hold нарушает hard constraint, cooldown не скрывает проблему. + +### Lazy Facade + +`source/ml/__init__.py` экспортирует Stage 5 helper API лениво. Это нужно, чтобы пользователь мог импортировать `source.ml.fit_stage5_uncertainty` или `source.ml.assess_change_policy`, но обычный импорт `source.ml` не тянул тяжелые зависимости раньше времени. + +## Поток работы + +Логика Stage 5 выглядит так: + +```text +prepared dataset +-> supervised features +-> point model artifact +-> fit_stage5_uncertainty() +-> calibrated upper predictor +-> save_stage5_model() +-> quality.predict_quality() +-> ModelBundle.check_applicability() +-> ModelBundle.predict() + predict_upper() +-> check_constraints() +``` + +Policy flow отдельно: + +```text +hold candidate + best feasible candidate +-> active ranking criteria +-> assess_change_policy() +-> allow recommend / keep hold / cooldown +``` + +На текущем этапе эти helper-функции уже покрыты unit tests. Отдельной публичной CLI-команды Stage 5 нет: внешний контракт проекта пока остается прежним. + +## Что не реализовано + +- Нет causal/action model. +- Нет реального управления газом. +- Нет разрешенных промышленных setpoint-рекомендаций. +- Нет гарантии safety coverage: `0.95` является эмпирической исторической оценкой, а не промышленной гарантией. +- Applicability bounds являются marginal q0.001/q0.999 по train-признакам, а не полноценной многомерной OOD-моделью. +- `T95`, цетановое число и полный паспорт товарного дизеля остаются `not_assessed`. + +## Как проверять + +```bash +python -m pytest global_tests/test_stage5_uncertainty_policy.py +python -m pytest global_tests/test_stage4_blending.py +python -m pytest +python -m ruff check . +python -m mypy source +python -m source.main validate-stage0 ``` + +Demo-команды должны сохранить прежние статусы: + +```bash +python -m source.main run-model-demo blend_normal +python -m source.main run-model-demo blend_risk +python -m source.main run-model-demo blend_missing +``` + +Ожидаемо: + +- `blend_normal` -> `hold`; +- `blend_risk` -> `recommend`; +- `blend_missing` -> `abstain`. diff --git a/global_tests/test_stage5_uncertainty_policy.py b/global_tests/test_stage5_uncertainty_policy.py index 76b4666..0094245 100644 --- a/global_tests/test_stage5_uncertainty_policy.py +++ b/global_tests/test_stage5_uncertainty_policy.py @@ -298,6 +298,15 @@ def test_stage5_fit_uses_temporal_splits_and_reports_held_out_robustness() -> No } +def test_stage5_helpers_are_available_from_lazy_ml_facade() -> None: + import source.ml as ml + + assert ml.fit_upper_calibrator is fit_upper_calibrator + assert ml.check_applicability is check_applicability + assert ml.assess_change_policy is assess_change_policy + assert ml.PolicyParameters is PolicyParameters + + def test_quality_agent_uses_upper_bound_and_rejects_ood() -> None: state = ProcessState.model_validate_json( Path("global_tests/fixtures/contracts/process_state.json").read_text(encoding="utf-8") diff --git a/source/ml/__init__.py b/source/ml/__init__.py index f455bbf..493ab99 100644 --- a/source/ml/__init__.py +++ b/source/ml/__init__.py @@ -7,29 +7,48 @@ _EXPORT_MODULES = { "EXPERT_VAK_CORRECTIONS": "source.ml.formulas", "GAS_CONTEXT_SIGNAL_IDS": "source.ml.blending", + "DEFAULT_POLICY": "source.ml.policy", + "ApplicabilityResult": "source.ml.uncertainty", "BlendOption": "source.ml.blending", "BlendResult": "source.ml.blending", + "CalibratedPointUpperRegressor": "source.ml.uncertainty", "FeatureFrame": "source.ml.features", "GasContextSignal": "source.ml.blending", "HybridComponentForecast": "source.ml.blending", "ModelBundle": "source.ml.artifacts", + "PolicyDecision": "source.ml.policy", + "PolicyParameters": "source.ml.policy", + "PolicyReplayCase": "source.ml.policy", + "RobustnessCaseResult": "source.ml.uncertainty", "SHEET_LA": "source.ml.formulas", "SHEET_VAK": "source.ml.formulas", + "Stage5FitResult": "source.ml.uncertainty", "SupervisedDataset": "source.ml.features", "TrainingResult": "source.ml.train", + "UpperBoundCalibration": "source.ml.uncertainty", + "UpperBoundMetrics": "source.ml.uncertainty", "apply_expert_vak_corrections": "source.ml.formulas", "apply_hydrotreater_forecast": "source.ml.blending", + "assess_change_policy": "source.ml.policy", "build_features": "source.ml.features", "build_supervised_dataset": "source.ml.features", "calculate_mass_blend": "source.ml.blending", + "check_applicability": "source.ml.uncertainty", "collect_gas_context": "source.ml.blending", "enumerate_two_component_recipes": "source.ml.blending", "evaluate_model": "source.ml.evaluate", + "evaluate_robustness_cases": "source.ml.uncertainty", + "evaluate_upper_bounds": "source.ml.uncertainty", + "fit_stage5_uncertainty": "source.ml.uncertainty", + "fit_upper_calibrator": "source.ml.uncertainty", "load_lab_parameters": "source.ml.formulas", "load_model": "source.ml.artifacts", "load_vak_formulas": "source.ml.formulas", + "pinball_loss": "source.ml.uncertainty", "rank_feasible_blends": "source.ml.blending", + "save_stage5_model": "source.ml.uncertainty", "sulfur_constraint_status": "source.ml.blending", + "tune_policy": "source.ml.policy", "train_model": "source.ml.train", } @@ -47,28 +66,47 @@ def __getattr__(name: str) -> object: __all__ = [ "EXPERT_VAK_CORRECTIONS", "GAS_CONTEXT_SIGNAL_IDS", + "DEFAULT_POLICY", + "ApplicabilityResult", "BlendOption", "BlendResult", + "CalibratedPointUpperRegressor", "FeatureFrame", "GasContextSignal", "HybridComponentForecast", "ModelBundle", + "PolicyDecision", + "PolicyParameters", + "PolicyReplayCase", + "RobustnessCaseResult", "SHEET_LA", "SHEET_VAK", + "Stage5FitResult", "SupervisedDataset", "TrainingResult", + "UpperBoundCalibration", + "UpperBoundMetrics", "apply_expert_vak_corrections", "apply_hydrotreater_forecast", + "assess_change_policy", "build_features", "build_supervised_dataset", "calculate_mass_blend", + "check_applicability", "collect_gas_context", "enumerate_two_component_recipes", "evaluate_model", + "evaluate_robustness_cases", + "evaluate_upper_bounds", + "fit_stage5_uncertainty", + "fit_upper_calibrator", "load_lab_parameters", "load_model", "load_vak_formulas", + "pinball_loss", "rank_feasible_blends", + "save_stage5_model", "sulfur_constraint_status", + "tune_policy", "train_model", ]