English
preview
Архитектура машинного обучения для MetaTrader 5 (Часть 16): Вложенная кросс-валидация для несмещённой оценки

Архитектура машинного обучения для MetaTrader 5 (Часть 16): Вложенная кросс-валидация для несмещённой оценки

MetaTrader 5Интеграция |
65 0
Patrick Murimi Njoroge
Patrick Murimi Njoroge

Оглавление

  1. Разделение на три зоны
  2. Внутренний цикл: walk-forward поиск с правилом 1-SE
  3. Внешний цикл: оценка выбранной конфигурации
  4. OOF-калибровка во вложенной схеме
  5. Сборка после циклов: консенсус, калибровка и контрольные точки
  6. Шлюз финального тестирования
  7. Реализация: UnifiedValidationCalibrator
  8. Практические соображения
  9. Заключение
  10. Список литературы
  11. Приложенные файлы


Введение

Стандартный ML-конвейер обучает модель на одной части данных, настраивает гиперпараметры на второй и оценивает результат на третьей. В академическом машинном обучении такая процедура работает, поскольку все три части независимо получены из стационарного распределения. Финансовые данные нарушают оба предположения. Наблюдения не являются независимыми (метки triple-barrier перекрываются во времени), а распределение не является стационарным (режимы меняются). Каждая граница между частями данных является потенциальным источником утечки информации, и каждая утечка завышает оценку качества на величину, которую практик уже не сможет количественно определить постфактум.

Проблема усиливается в трех точках принятия решений конвейера. Поиск гиперпараметров выбирает конфигурацию, которая лучше всего показала себя на валидационных фолдах. Это форма переобучения на валидационном наборе. Калибратор строит отображение неоткалиброванных вероятностей в откалиброванные на основе прогнозов модели. Если эти прогнозы получены на наблюдениях, использованных для обучения, калибровочная кривая становится оптимистично смещенной. Финальная оценка может быть заново «открыта» после просмотра результата, превращая «тестовый» набор во второй набор для настройки. Каждый из этих факторов по отдельности смещает оценку вверх. В совокупности это приводит к значениям коэффициента Шарпа (Sharpe ratio) или точности классификации (accuracy), которые могут быть существенно более оптимистичными, чем истинное вневыборочное качество модели.

Вложенная схема V-in-V (validation-in-validation) блокирует все три пути утечки. Данные делятся на три временные зоны, каждая из которых защищает отдельную границу принятия решений. Внутри крупнейшей зоны две концентрические CV-петли отделяют выбор гиперпараметров от оценки качества. Между циклами располагается слой калибровки, использующий OOF-прогнозы, которые не использовались ни моделью, ни поиском гиперпараметров. В результате получается оценка качества, не искаженная ни одним из трех процессов принятия решений, которые она должна оценивать.

В этой статье рассматривается теория каждого слоя и объясняется необходимость каждого проектного решения. Также разбирается промышленная реализация в UnifiedValidationCalibrator. Код интегрируется с компонентами AFML-конвейера из предыдущих статей: PurgedWalkForwardCV из статьи Unified Validation Pipeline, CombinatorialPurgedCV из той же статьи и механизмами изотонической калибровки из Части 12.


1. Разделение на три зоны

Первое ключевое архитектурное решение — временное разбиение полного набора данных на три зоны. Такая архитектура восходит к Masters (1993), который утверждал, что разделения train/test на две части недостаточно, если исследователь может многократно дорабатывать модель: просмотр тестового результата с последующей корректировкой превращает тестовый набор во второй обучающий набор и делает оценку недействительной. Решение Masters — разбиение на три зоны, где финальная зона может быть открыта ровно один раз.

Полный конвейер UnifiedValidationCalibrator

Рисунок 1. Трехзонное разбиение данных V-in-V

  • Внешнее обучение (~60%): Все операции вложенной CV, включая поиск гиперпараметров, обучение модели, генерацию OOF-прогнозов и построение калибровочного отображения, выполняются внутри этой зоны. На этом этапе исследователь может свободно дорабатывать модель, не загрязняя последующие оценки.
  • Внутренняя валидация (~20%): Контрольная точка предварительного отбора. После завершения вложенного CV-цикла и сборки финальной модели ее прогнозы в этой зоне оцениваются. Исследователь анализирует результат, но не должен перенастраивать модель. Если оценка Брайера (Brier score) на внутренней валидации существенно хуже внешних OOF-оценок, значит что-то пошло не так (обычно переобучение на внешних обучающих фолдах или смена режима на границе). На этом этапе модель либо принимается, либо отклоняется.
  • Финальный тест (~20%): Открывается ровно один раз. Класс DataPartition обеспечивает это с помощью флага _final_opened, который вызывает RuntimeError при любом последующем вызове open_final_test(). После получения этого результата оценка считается зафиксированной. Любая последующая корректировка модели делает всю процедуру оценки недействительной.

Разбиение является временным: 60% внешнее обучение, 20% внутренняя валидация и 20% финальный тест (самые последние данные). Рандомизация нарушила бы временной порядок, позволяя модели обучаться на будущих данных и тестироваться на прошлых. Функция partition_data везде использует iloc, чтобы сохранить индексы DataFrame и выравнивание по datetime. Это критично, потому что PurgedWalkForwardCV проверяет, что Series t1 имеет тот же индекс, что и матрица признаков.

def partition_data(
    X: pd.DataFrame,
    y: pd.Series,
    t1: pd.Series,
    sample_weight: pd.Series,
    inner_val_pct: float = 0.20,
    final_test_pct: float = 0.20,
) -> DataPartition:
    n = len(X)
    outer_end = int(n * (1.0 - inner_val_pct - final_test_pct))
    val_end = int(n * (1.0 - final_test_pct))

    return DataPartition(
        X_outer=X.iloc[:outer_end],
        y_outer=y.iloc[:outer_end],
        sw_outer=sample_weight.iloc[:outer_end],
        t1_outer=t1.iloc[:outer_end],
        X_inner_val=X.iloc[outer_end:val_end],
        y_inner_val=y.iloc[outer_end:val_end],
        sw_inner_val=sample_weight.iloc[outer_end:val_end],
        X_final=X.iloc[val_end:],
        y_final=y.iloc[val_end:],
        sw_final=sample_weight.iloc[val_end:],
    )


2. Внутренний цикл: walk-forward поиск с правилом 1-SE

Внутри зоны внешнего обучения первая задача — выбор гиперпараметров. Наивный подход — выполнить grid search по комбинациям параметров с использованием k-fold CV и выбрать конфигурацию с наивысшей средней оценкой. Для финансовых данных у такого подхода есть две проблемы. Во-первых, k-fold CV нарушает временной порядок. Во-вторых, выбор конфигурации с абсолютной лучшей оценкой приводит к переобучению на валидационных фолдах; выбранной оказывается конфигурация, которая наиболее эффективно использовала конкретное временное расположение этих фолдов.

Внутренний цикл решает обе проблемы. Он использует PurgedWalkForwardCV с закрепленным началом и расширяющимся окном, сохраняет временной порядок и применяет purging и embargo для предотвращения утечки через метки. В качестве правила выбора реализовано правило Masters 1-SE (one standard error): среди всех конфигураций, средняя оценка которых находится в пределах одной стандартной ошибки от лучшего, выбирается самая простая.

Правило 1-SE Masters для выбора гиперпараметров

Рисунок 2. Правило 1-SE Masters для выбора гиперпараметров

  • Синяя линия: Средняя оценка лучшей конфигурации по внутренним CV-фолдам.
  • Красная пунктирная линия: Порог на одну стандартную ошибку ниже лучшего среднего значения. Все конфигурации выше этой линии статистически неотличимы от лучшей с учетом шума оценивания.
  • Зеленая область: Набор конфигураций в пределах 1 SE от лучшей. Бирюзовый маркер показывает выбранную конфигурацию: самую простую (первую в порядке ParameterGrid), которая попадает в эту область.

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

# 1-SE rule implementation (from inner_cv_search)
best_entry = max(all_scores, key=lambda x: x['mean_score'])
threshold = best_entry['mean_score'] - best_entry['std_score']
within_1se = [s for s in all_scores if s['mean_score'] >= threshold]

# ParameterGrid is ordered; within_1se[0] is the simplest
best_params = within_1se[0]['params']

Одно предупреждение: обозначение «самая простая» опирается на то, что ParameterGrid выдает конфигурации в согласованном порядке, где более простые конфигурации идут первыми. Это выполняется, если списки параметров упорядочены от простого к сложному (например, n_estimators: [50, 200, 500]). Если списки заданы в произвольном порядке, первая конфигурация внутри области не обязательно будет самой простой в каком-либо содержательном смысле. Более надежный подход — назначить каждой конфигурации явную оценку сложности и минимизировать ее внутри области 1-SE.


3. Внешний цикл: оценка выбранной конфигурации

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

Внешний цикл поддерживает две CV-стратегии, переключаемые параметром outer_cv_type. walk-forward (PurgedWalkForwardCV) формирует последовательность расширяющихся обучающих наборов с тестовыми наборами, упорядоченными во времени; обучающий набор каждого фолда включает все данные до некоторой точки отсечения, а тестовый набор — следующий временной блок. CPCV (CombinatorialPurgedCV) формирует φ[N, k] комбинаторных путей, обеспечивая более богатое распределение оценок качества ценой более высоких вычислительных затрат.

Критически важна одна деталь интеграции. CombinatorialPurgedCV.split() возвращает (train_idx, test_idx_list), где test_idx_list — это список массивов (по одному на тестовый фолд), а не плоский массив. walk-forward CV возвращает (train_idx, test_idx) как плоские массивы. Внешний цикл нормализует это, вызывая np.concatenate(test_idx_list) для CPCV. Если упустить это различие, возникнут ошибки индексации или, что еще хуже, произойдет незаметное неверное назначение фолдов.

Архитектура вложенной кросс-валидации

Рисунок 3. Архитектура вложенной кросс-валидации

  • Внешний цикл (синяя пунктирная рамка): Проходит по walk-forward- или CPCV-разбиениям зоны внешнего обучения. На каждой итерации формируется train/test-разделение.
  • Внутренний цикл (оранжевая пунктирная рамка): Для каждого внешнего фолда запускает walk-forward grid search по пространству гиперпараметров, применяет правило 1-SE, генерирует OOF-прогнозы для калибровки и обучает изотонический калибратор данного фолда.
  • Этапы после внутреннего цикла (внутри внешнего цикла, под оранжевым блоком): Классификатор повторно обучается на полном внешнем обучающем фолде с выбранными параметрами. Применяют калибратор данного фолда к прогнозам внешнего тестового набора. Оценивают как неоткалиброванные, так и откалиброванные вероятности.
  • После цикла (зеленый блок): Результаты агрегируются по фолдам для определения консенсусных параметров, финальный калибратор обучается на всех OOF-прогнозах, затем выполняется проверка на внутренней валидации.


4. OOF-калибровка во вложенной схеме

Калибровка во вложенной CV требует строгого контроля за тем, какие прогнозы доступны калибратору. Часть 12 показала, что калибровка на прогнозах, на которых модель была обучена, создает оптимистично смещенное преобразование. Здесь действует тот же принцип, но с дополнительным ограничением: калибратор также должен оставаться независимым от процедуры выбора гиперпараметров.

Решение — второй проход walk-forward CV внутри каждого внешнего фолда. После того как внутренний поиск выбирает лучшие гиперпараметры, _oof_for_fold() запускает свежий PurgedWalkForwardCV с этими параметрами. Он клонирует базовую модель (estimator) для каждого внутреннего разбиения, обучает его на внутреннем обучающем наборе и собирает прогнозы на внутреннем тестовом наборе. Результатом становится массив OOF-прогнозов полной длины для внешнего обучающего фолда. Каждый прогноз производится моделью, которая не обучалась на этом наблюдении.

def _oof_for_fold(self, X_tr, y_tr, sw_tr, t1_tr, params):
    inner_cv = PurgedWalkForwardCV(
        n_splits=self.n_inner_splits,
        t1=t1_tr,
        pct_embargo=self.pct_embargo,
        expanding_window=True,
        min_train_size=self.min_train_size,
    )
    inner_oof = pd.Series(np.nan, index=X_tr.index)

    for in_tr, in_val in inner_cv.split(X_tr, y_tr):
        clf_oof = clone(self.estimator)
        clf_oof.set_params(**params)
        try:
            clf_oof.fit(
                X_tr.iloc[in_tr], y_tr.iloc[in_tr],
                sample_weight=sw_tr.iloc[in_tr].values,
            )
        except TypeError:
            clf_oof.fit(X_tr.iloc[in_tr], y_tr.iloc[in_tr])

        inner_oof.iloc[in_val] = clf_oof.predict_proba(
            X_tr.iloc[in_val]
        )[:, 1]

    return inner_oof

Затем изотонический калибратор обучается на этих OOF-прогнозах относительно истинных меток, с теми же весами выборки, которые использовались для обучения модели. Калибратор данного фолда преобразует неоткалиброванные вероятности внешнего тестового набора. Это гарантирует, что откалиброванные внешние тестовые оценки качества отражают то, что калибратор выдавал бы на действительно вневыборочных данных, а не на данных, в генерации которых он участвовал.

Поток данных через один внешний фолд

Рисунок 4. Поток данных через один внешний фолд

  • Шаги 1–3 (левая колонка): Внутренний поиск выбирает гиперпараметры; генерируются внутренние OOF-прогнозы; изотонический калибратор обучается на этих прогнозах. Все три шага работают исключительно на внешнем обучающем фолде.
  • Шаг 4: Классификатор повторно обучается на полном внешнем обучающем фолде с выбранными параметрами. Эта модель обучалась на всех обучающих данных данного фолда, но не использовала внешние тестовые данные.
  • Шаги 5–6 (правая колонка): Повторно обученная модель прогнозирует внешний тестовый фолд; калибратор данного фолда преобразует эти прогнозы; записываются как оценки качества для неоткалиброванных и откалиброванных вероятностей.


5. Сборка после циклов: консенсус, калибровка и контрольные точки

После завершения всех внешних фолдов каждый фолд выбрал собственные лучшие гиперпараметры по правилу 1-SE. Эти варианты могут совпадать или различаться. Конвейер снимает расхождения большинством голосов: подсчитывает, какой набор параметров выбирался чаще всего по фолдам, и назначает его консенсусным.

# Consensus: most frequently selected params across folds
param_strs = [str(sorted(p.items())) for p in best_params_per_fold]
consensus_str = Counter(param_strs).most_common(1)[0][0]
consensus_params = best_params_per_fold[param_strs.index(consensus_str)]

Консенсусная модель обучается на полной зоне внешнего обучения. Параллельно финальный изотонический калибратор обучается на полном массиве OOF-прогнозов, собранных по всем внешним фолдам. Такой калибратор использует больше данных, чем любой отдельный калибратор на уровне фолда, поэтому его ступенчатая функция стабильнее и оценивается точнее.

Затем на этапе внутренней валидации консенсусная модель и финальный калибратор применяются к соответствующей зоне данных. Это последняя возможность обнаружить проблемы до открытия финального тестового набора. Если оценка Брайера на внутренней валидации существенно хуже средней внешней OOF-оценки Брайера, возможные объяснения включают переобучение на внешних обучающих фолдах, смену режима на границе 60/20 или недостаточный объем данных в зоне внутренней валидации для стабильного оценивания качества. Результат можно анализировать, но принцип остается тем же: модель не следует перенастраивать. Любая корректировка на основе этого результата превращает зону внутренней валидации во второй обучающий набор, сводя трехзонную архитектуру к двухзонной.

После контрольной точки финальная модель обучается заново на объединенных зонах внешнего обучения и внутренней валидации (80% данных), используя консенсусные параметры. Именно эта модель затем используется в продакшене или оценивается на финальном тестовом наборе.


6. Шлюз финального тестирования

Метод evaluate_final_test() предоставляет доступ к финальной тестовой зоне ровно один раз. Dataclass DataPartition обеспечивает это с помощью булевого флага:

def open_final_test(self):
    if self._final_opened:
        raise RuntimeError(
            "The Final Test Set has already been opened. "
            "Any further model adjustment invalidates the evaluation."
        )
    self._final_opened = True
    return self.X_final, self.y_final, self.sw_final

Это не рекомендация, а архитектурное ограничение. RuntimeError срабатывает безусловно при любом втором вызове, независимо от того, была ли цель «просто посмотреть» результат или внести «небольшую корректировку». Такое решение делает протокол оценки принудительно обеспечиваемым программно, а не зависящим от дисциплины пользователя. Masters (1993) утверждал, что человеческой дисциплины недостаточно, потому что искушение подсмотреть присутствует всегда; программный барьер полностью устраняет такую возможность.

Оценка вычисляет четыре метрики: неоткалиброванную и откалиброванную оценку Брайера, а также неоткалиброванную и откалиброванную логарифмическую функцию потерь (log loss). Метод также возвращает массивы неоткалиброванных и откалиброванных вероятностей для последующего анализа (reliability diagrams, распределения размера позиции, симуляции equity). Именно эти массивы вероятностей используются далее в продакшене. Они получены от модели, обученной на 80% данных, откалиброванной на OOF-прогнозах из первых 60% и оцененной на тестовом наборе, не использовавшемся ни одним компонентом конвейера.


7. Реализация: UnifiedValidationCalibrator

Полный конвейер реализован в одном sklearn-совместимом классе. Он реализует fit(), predict(), predict_proba() и evaluate_final_test(). Метод fit() запускает всю процедуру вложенной CV; после обучения predict_proba() применяет консенсусную модель и финальный калибратор к новым данным.

Класс наследуется от ClassifierMixin и BaseEstimator и соблюдает полный контракт конструктора sklearn: все параметры __init__ являются простыми скалярами или объектами базовых моделей (estimator), поэтому clone(), get_params(), set_params() и интеграция с Pipeline также работают без дополнительных изменений. Аргументы, несущие данные, — t1, sample_weight, close_prices и primary_sides — относятся к fit(), а не к __init__(). Передача pd.Series как параметра конструктора нарушила бы проверки равенства в clone(); именно этого паттерна дизайн избегает. Класс также валидирует outer_cv_type во время конструирования, поэтому опечатка вроде 'WalkForward' сразу вызывает ValueError, а не приводит к неявному переходу в режим walk-forward.

Тесты check_estimator(), которые вызывают fit(X, y) без временных метаданных, пропускаются через __sklearn_tags__. Аргумент t1 нельзя синтезировать только из X и y; он кодирует времена завершения меток, необходимые PurgedWalkForwardCV для корректного применения purging и embargo. Отсутствие этого аргумента — не упрощенный режим работы, а недопустимое состояние. Все остальные контракты sklearn соблюдены.

Полный конвейер UnifiedValidationCalibrator

Рисунок 5. Полный конвейер UnifiedValidationCalibrator

  • Верх: partition_data() делит полный набор данных на зоны 60/20/20.
  • Синяя пунктирная рамка: Внешний цикл итерируется по walk-forward или CPCV-разбиениям. Для каждого внешнего фолда внутренний поиск выбирает гиперпараметры, генерируются OOF-прогнозы и обучается калибратор данного фолда.
  • Зеленые блоки: Сборка после цикла — консенсусные параметры голосованием большинства, финальный калибратор на всех OOF-прогнозах.
  • Желтый блок: Контрольная точка внутренней валидации. Изучить, но не перенастраивать.
  • Красный блок: Финальная модель обучается заново на Outer + Inner Val; evaluate_final_test() открывает финальную тестовую зону ровно один раз.

Использование

from sklearn.ensemble import RandomForestClassifier
from afml.cross_validation.nested_cv import UnifiedValidationCalibrator

uvc = UnifiedValidationCalibrator(
    estimator=RandomForestClassifier(random_state=42),
    param_grid={
        'n_estimators': [100, 200, 500],
        'max_depth': [3, 5, 8],
    },
    outer_cv_type='walkforward',
    n_outer_splits=5,
    n_inner_splits=3,
    pct_embargo=0.01,
    min_train_size=0.1,
    scoring='neg_brier',
)

# Run the full nested CV pipeline
uvc.fit(X, y, t1=events['t1'], sample_weight=events['tW'])

# Inspect outer fold scores
print(uvc.outer_scores_summary())

# Open the final test set — EXACTLY ONCE
result = uvc.evaluate_final_test()
print(f"Final Brier (cal): {result['brier_cal']:.4f}")

# Inference on new data uses consensus model + final calibrator
probs = uvc.predict_proba(X_new)[:, 1]

Режим CPCV

Когда outer_cv_type='cpcv', внешний цикл использует CombinatorialPurgedCV вместо walk-forward. Это формирует φ[N, k] комбинаторных путей, каждый из которых дает независимую вневыборочную оценку качества. После завершения всех внешних фолдов конвейер запускает CPCVAnalyzer, чтобы вычислить распределение коэффициента Шарпа по путям и провести PBO-аудит. Режим CPCV требует двух дополнительных аргументов данных: close_prices (для расчета коэффициента Шарпа на основе переоценки по рынку (mark-to-market)) и primary_sides (для направленных сигналов мета-разметки (meta-labeling)). Оба передаются в fit(), а не в __init__(); это сохраняет конструктор свободным от массивов данных и поддерживает совместимость с clone().

В CPCV каждое наблюдение может появляться в нескольких тестовых путях. Если записывать прогнозы позиция за позицией, получится схема «побеждает последняя запись»: сохраняется только прогноз последнего пути. Правильный подход — накапливать все прогнозы по каждой позиции из всех тестовых путей и усреднять их перед обучением финального калибратора. Именно это и делает промышленная реализация через словарь-аккумулятор. Различие принципиально: подход last-write-wins делает обучающие данные калибратора зависимыми от произвольного порядка обхода путей CPCV.

uvc_cpcv = UnifiedValidationCalibrator(
    estimator=RandomForestClassifier(random_state=42),
    param_grid={'n_estimators': [100, 200], 'max_depth': [3, 5]},
    outer_cv_type='cpcv',
    cpcv_n_folds=6,
    cpcv_n_test_folds=2,
    scoring='neg_brier',
)
# close_prices and primary_sides go to fit(), not __init__()
uvc_cpcv.fit(
    X, y,
    t1=events['t1'],
    sample_weight=events['tW'],
    close_prices=close_series,
    primary_sides=side_predictions,
)

# Path Sharpe distribution is available after fit
print(uvc_cpcv.cpcv_distribution_metrics_)


8. Практические соображения

Вычислительная стоимость

Вложенная CV требует значительных вычислительных ресурсов. Для каждого внешнего фолда внутренний цикл запускает полный grid search с walk-forward CV, а затем второй проход для генерации OOF-прогнозов. При 5 внешних фолдах, 3 внутренних разбиениях и сетке из 9 комбинаций параметров общее число процедур обучения модели составляет 5 × (9 × 3 + 3) = 150. Для CPCV с N=6 и k=2 есть 15 внешних разбиений, что увеличивает итог до 15 × (9 × 3 + 3) = 450 обучений. На финансовых наборах данных с тысячами наблюдений и Random Forest из 200 деревьев каждое обучение занимает секунды; полный конвейер завершается за минуты. На наборах данных с сотнями тысяч наблюдений или с gradient boosted trees затраты могут достигать часов.

Внутренний OOF-проход (_oof_for_fold) увеличивает число процедур обучения модели сверх того, что требуется одному grid search. Эта цена неизбежна: калибратору нужны прогнозы, не затронутые ни обучением модели, ни поиском гиперпараметров; единственный способ получить такие прогнозы — обучать модель заново на каждом внутреннем фолде с выбранными параметрами и собирать прогнозы на тестовых фолдах.

Порядок сетки параметров

Поведение правила 1-SE «выбрать самое простое» зависит от того, что ParameterGrid выдает конфигурации в порядке, где более простые идут первыми. ParameterGrid итерируется по декартову произведению в порядке указанных значений. Чтобы гарантировать, что самая простая конфигурация окажется первой, упорядочивайте список значений каждого параметра от наименее сложного к наиболее сложному. Для моделей на деревьях это означает: меньшее число деревьев должно идти перед большим, меньшая глубина — перед большей, а более сильная регуляризация — перед более слабой.

Минимальные требования к данным

Здесь взаимодействуют три ограничения по размеру данных. Зона внешнего обучения (60% данных) должна быть достаточно большой, чтобы внешняя CV создавала информативные разбиения. Внутренние разбиения внутри каждого внешнего фолда должны содержать достаточно наблюдений, чтобы обучающие окна walk-forward содержали достаточно данных для построения адекватной модели. OOF-прогнозов должно быть как минимум несколько сотен, чтобы изотонический калибратор сформировал стабильную ступенчатую функцию. В качестве грубого ориентира полный набор данных должен содержать не менее 2 000 независимых наблюдений (после учета того, что label concurrency снижает эффективный размер выборки). Ниже этого порога рассмотрите Platt scaling вместо isotonic regression, уменьшение числа внутренних разбиений или расширение зоны внешнего обучения за счет зон внутренней валидации и финального теста.

Когда не стоит использовать вложенную CV

Вложенная CV — правильная процедура, когда модель будет использоваться в продакшене с теми гиперпараметрами и калибратором, которые она выбирает. Она не нужна, когда гиперпараметры фиксированы извне (например, унаследованы из опубликованной стратегии или предписаны политикой). В таком случае достаточно одноцикловой OOF-калибровки через CalibratorCV (Часть 12). Вложенная CV также не нужна, когда практик проводит исследовательскую работу и не нуждается в несмещенных оценках эффективности; дополнительные вычислительные затраты замедляют итерации без соответствующей пользы на этапе исследования.

Совместимость со sklearn

UnifiedValidationCalibrator спроектирован как верхнеуровневый компонент управления валидацией, а не как базовая модель (estimator) внутри другой sklearn-метамодели. Оборачивать его в GridSearchCV или CalibratedClassifierCV — концептуальная ошибка: это вложило бы одну CV-процедуру в другую без принципиального разделения того, что оценивает каждый слой.

В рамках этого ограничения класс удовлетворяет полному контракту конструктора sklearn. Это обеспечивается тремя правилами проектирования. Во-первых, __init__ содержит только скаляры и объекты базовых моделей (estimator); все массивы данных (t1, sample_weight, close_prices, primary_sides) принимаются только методом fit(). Во-вторых, outer_cv_type валидируется во время конструирования по множеству {'walkforward', 'cpcv'}, поэтому недопустимое значение сразу вызывает ValueError, а не приводит к тихому изменению поведения глубоко внутри fit(). В-третьих, check_estimator() требует порядок mixin-классов ClassifierMixin, BaseEstimator (более специализированный перед более общим). Следствие: clone(), get_params(), set_params() и интеграция с Pipeline также работают без дополнительных изменений. Тесты check_estimator(), вызывающие fit(X, y) без временных метаданных, пропускаются через __sklearn_tags__; причина документирована в строке документации класса (docstring).


Заключение

UnifiedValidationCalibrator объединяет три слоя защиты от утечки информации. Трехзонное разбиение не позволяет перенастраивать модель после просмотра тестовых результатов. Вложенная кросс-валидация отделяет выбор гиперпараметров от оценки качества. OOF-калибровка внутри внутреннего цикла гарантирует, что вероятностное преобразование обучается на прогнозах, не затронутых ни обучением модели, ни поиском гиперпараметров.

Ключевые выводы:

  • Трехзонное разбиение обеспечивается программно. DataPartition.open_final_test() вызывает RuntimeError при любом втором вызове. Это превращает дисциплину однократного открытия Masters (1993) из договоренности в архитектурное ограничение.
  • Правило 1-SE регуляризует выбор модели. Среди конфигураций в пределах одной стандартной ошибки от лучшей выбирается самая простая. Упорядочивайте списки значений в param_grid от наименее сложных к наиболее сложным, чтобы ParameterGrid помещал более простые конфигурации первыми.
  • OOF-калибровка во вложенной CV требует второго внутреннего прохода. Метод _oof_for_fold генерирует прогнозы, независимые как от поиска гиперпараметров, так и от обучающих данных модели. Это корректный и фактически единственный допустимый источник калибровочных данных во вложенной схеме.
  • Консенсусные параметры используют голосование большинства. Каждый внешний фолд выбирает собственную лучшую конфигурацию; наиболее часто выбранный набор становится консенсусным. Если ни одна конфигурация не получает большинства, конвейер все равно выбирает наиболее частую, но практик должен трактовать расхождения как сигнал чувствительности модели к обучающему окну.
  • Контрольная точка внутренней валидации предназначена для проверки, а не для корректировки. Если оценка на внутренней валидации существенно хуже внешних OOF-оценок, модель следует отклонить на этом этапе, а не перенастраивать ее. Корректировка по результату внутренней валидации превращает ее во второй обучающий набор.
  • Вложенная CV относится к Python, а не к MQL5. Это процедура выбора и валидации модели. Ее выход — обученная модель, калибратор и набор консенсусных гиперпараметров. MQL5 потребляет эти артефакты; ему не нужно воспроизводить процесс выбора.


Список литературы

  1. López de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. Главы 4, 7 и 12.
  2. Masters, T. (1993). Practical Neural Network Recipes in C++. Wiley. Глава о validation-in-validation и правиле 1-SE.
  3. Varma, S., & Simon, R. (2006). "Bias in error estimation when using cross-validation for model selection." BMC Bioinformatics, 7(1), 91.
  4. Cawley, G. C., & Talbot, N. L. C. (2010). "On over-fitting in model selection and subsequent selection bias in performance evaluation." JMLR, 11, 2079–2107.


Приложенные файлы

 

Файл

Описание

1. nested_cv.py DataPartition, partition_data, inner_cv_search и UnifiedValidationCalibrator. Реализует V-in-V трехзонное разбиение, внутренний walk-forward-поиск с фиксированной начальной точкой с правилом 1-SE, изотоническую OOF-калибровку и шлюз финального теста с однократным открытием.
2. cross_validation.py Необходимая зависимость. Предоставляет PurgedKFold и PurgedWalkForwardCV, которые nested_cv.py использует для внутреннего цикла поиска гиперпараметров, OOF-прохода для генерации прогнозов и внешних walk-forward-разбиений.
3. combinatorial.py Необходимая зависимость. Предоставляет CombinatorialPurgedCV, CPCVAnalyzer и optimal_folds_number, которые nested_cv.py использует при outer_cv_type='cpcv' для генерации распределения путей φ[N, k] и PBO-аудита.

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22040

Прикрепленные файлы |
afml.zip (59.08 KB)
Алгоритм оптимизации на основе коронавируса — Corona Virus Optimization (CVO) Алгоритм оптимизации на основе коронавируса — Corona Virus Optimization (CVO)
Описываем и реализуем CVO: заражение как генерация кандидатов, покоординатное нормальное возмущение, динамическая популяция. Алгоритм интегрирован в C_AO и проверен на стандартном бенчмарке. Разбор выявляет масштабную причину стагнации и даёт прикладное решение — переход к относительному шагу по ширине диапазона; код готов к использованию.
Моделирование рынка: Первые шаги на SQL в MQL5 (III) Моделирование рынка: Первые шаги на SQL в MQL5 (III)
В предыдущей статье мы рассмотрели пример реализации класса на MQL5 для обеспечения базовой поддержки. Его цель заключается именно в том, чтобы позволить хранить SQL-код в отдельном файле скрипта. Таким образом, нам не потребуется писать тот же SQL-код в виде строки внутри кода MQL5. Хотя данное решение функционально, в нём есть некоторые детали, которые мы можем и должны улучшить.
Моделирование рынка: Первые шаги на SQL в MQL5 (II) Моделирование рынка: Первые шаги на SQL в MQL5 (II)
Хотя многие считают, что мы можем без проблем встраивать SQL-код в другой код, обычно это не так. Причина заключается в том, что SQL-код включается в исполняемый файл в виде строки. И тот факт, что SQL-код внедряется в виде строки, хотя и не вызывает проблем в небольших фрагментах, в итоге это может создать нам немало головной боли.
Создаем объемные 3D бары на MQL5 Создаем объемные 3D бары на MQL5
Переносим 3D-бары из Python в нативный MQL5: вместо plotly и моста к терминалу — сцена на CCanvas3D и DirectX 11 прямо на графике. Цена, время и тиковый объём раскладываются по трём осям, геометрия собирается вручную из вершин и треугольников, а орбитальная камера на событиях мыши даёт интерактивный осмотр без внешних зависимостей.