Вложенная кросс-валидация и доверительные интервалы

Вопрос или проблема

Надеюсь, кто-то сможет мне помочь разобраться с этим. Я наткнулся на множество различных ресурсов о вложенной кросс-валидации, но, похоже, я запутался в том, как выбрать модель и правильно построить доверительные интервалы для процесса обучения.

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

В настоящее время я разделил свои данные на обучающую и тестовую выборки (80/20) и применяю вложенную кросс-валидацию на обучающей выборке. Внешний и внутренний циклы имеют по 5 и 3 фолда соответственно. Мой подход к вложенной кросс-валидации заключается в том, чтобы убедиться, что я не оптимистично искажает результаты обучения, используя обычный стандартный подход к 5-фолдовой кросс-валидации при подборе/выборе модели, особенно учитывая мой маленький размер выборки, который, как я подозреваю, сильно чувствителен к случайным делениям моих данных (58 случаев гетерогенны между собой – это сложная задача предсказания).

Меня интересует, может ли кто-то прокомментировать, правильно ли я строю свою цепочку вложенной кросс-валидации и правильна ли моя стратегия бутстрепирования доверительных интервалов для оценки производительности моих моделей? Дополнительно, моя цепочка также выполняет выбор признаков, что может привести к тому, что каждая из полученных «лучших» моделей использует разные признаки. Все еще ли уместно усреднять результаты и бутстрепировать доверительные интервалы?

В настоящее время мой код выглядит примерно так:

Подготовка вспомогательных функций

cv_tune = StratifiedKFold(n_splits=5, shuffle=True, random_state=1839)

scorer = {'AUC': 'roc_auc', 
          'Precision': make_scorer(precision_score, zero_division = 0), 
          'Recall': 'recall',
          'Accuracy': 'accuracy',
          'log-loss': 'neg_log_loss',
          'F1': make_scorer(f1_score, average="binary")}

# это позволяет установить начальное значение
def mutual_info_seed(X, y):
    return mutual_info_classif(X, y, random_state=0)
#SelectKBest(score_func=mutual_info_seed)

# вызов smote для увеличения выборки
smt = SMOTE(random_state=42)

# Создать трансформаторы для каждого типа признаков
numeric_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

# Объединить трансформаторы в ColumnTransformer
step_impute_scale = ('scaler', ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, predictors_to_scale)
    ],
    remainder="passthrough"  # это оставляет другие столбцы без изменений
))

# инициировать импертуратор для пропущенных значений
simple_imputer = SimpleImputer(strategy='median')

def bootstrap_ci(scores, n_bootstrap=1000, ci=95):
    """Вычислить бутстрепные доверительные интервалы."""
    bootstrapped_scores = []
    n = len(scores)
    for _ in range(n_bootstrap):
        resample = np.random.choice(scores, size=n, replace=True)
        bootstrapped_scores.append(np.mean(resample))
    lower = np.percentile(bootstrapped_scores, (100 - ci) / 2)
    upper = np.percentile(bootstrapped_scores, 100 - (100 - ci) / 2)
    return np.mean(scores), lower, upper

Для запуска модели

# создаем rf
rf = RandomForestClassifier(random_state=1725)

# настроить конвейер для предобработки данных, выбора признаков и smote
pipeline = Pipeline(steps=[
    ('transform_columns', ColumnTransformer([('imputer', simple_imputer, predictors_to_scale)],
                                                               remainder="passthrough")),
    ('variance_selection', VarianceThreshold()),
    ('selectk', SelectKBest(score_func = mutual_info_seed)),
    ('smote', smt),
    ('classifier', rf)
])

# Определить распределение параметров
param_distributions = {
    'classifier__n_estimators': randint(100, 1001),        # Случайный выбор n_estimators от 100 до 1000
    'classifier__max_depth': randint(2, 10),              # Случайный выбор max_depth от 2 до 9
    'classifier__min_samples_split': randint(2, 6),       # Случайный выбор min_samples_split от 2 до 5
    'classifier__min_samples_leaf': randint(2, 6),        # Случайный выбор min_samples_leaf от 2 до 5
    'classifier__criterion': ['gini', 'entropy'],         # Случайный выбор между gini и entropy
    'smote__k_neighbors': randint(1, 10),                  # Случайный выбор k_neighbors для SMOTE от 1 до 5
    'selectk__k': randint(5, 15),                          # Случайный выбор k для выбора признаков от 5 до 15
    'variance_selection__threshold': uniform(loc=0, scale=0.3),
}

# Внешняя кросс-валидация
outer_cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=1839)

# Для хранения оценок для каждой метрики
outer_scores = {metric: [] for metric in scorer.keys()}

# Выполнить вложенную кросс-валидация
for train_idx, test_idx in outer_cv.split(X_mod, y):
    X_train, X_test = X_mod.iloc[train_idx], X_mod.iloc[test_idx]
    y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
    
    search = RandomizedSearchCV(pipeline, param_distributions, scoring=scorer, cv=3, n_iter = 250, n_jobs=-1, error_score="raise", 
                           refit="F1", random_state=0)
    search.fit(X_train, y_train)

    # Оценка на внешнем тестовом наборе для всех метрик
    y_pred = search.predict(X_test)
    y_prob = search.predict_proba(X_test)[:, 1] if hasattr(search, "predict_proba") else None

    for metric, scorer_fn in scorer.items():
        if metric == "AUC" and y_prob is not None:
            score = roc_auc_score(y_test, y_prob)
        elif metric == "log-loss" and y_prob is not None:
            score = -log_loss(y_test, y_prob)
        elif callable(scorer_fn):
            score = scorer_fn._score_func(y_test, y_pred)
        else:
            score = accuracy_score(y_test, y_pred)  # Резервный вариант по умолчанию
        outer_scores[metric].append(score)

# Вычислить среднее и бутстрепные доверительные интервалы для каждой метрики
results_summary = {}
for metric, scores in outer_scores.items():
    mean_score, ci_lower, ci_upper = bootstrap_ci(scores)
    results_summary[metric] = {
        'mean': mean_score,
        '95% CI': (ci_lower, ci_upper)
    }

Ответ или решение

Введение в Вложенную Кросс-Валидацию и Доверительные Интервалы

Настоящая статья поможет разобраться с концепцией вложенной кросс-валидации (nested cross-validation, NCV) и оценкой доверительных интервалов при обучении бинарного классификатора на малых наборах данных. Мы рассмотрим применение метода на небольшом наборе данных, в частности, на примере с 220 образцами, среди которых 58 имеют интересующий результат.

Конструкция Вложенной Кросс-Валидации

Вложения кросс-валидации необходимы для предотвращения оптимистичной оценки производительности модели, особенно при наличии ограниченного объёма данных. В вашем случае правильная установка NCV позволит разделить процесс настройки гиперпараметров и оценки производительности модели.

Подход:

  1. Внешний цикл (Outer Loop): Объект данных разбивается на n фолдов (например, 5), где в каждом случае один фолд используется для тестирования, а остальные для обучения модели. Это позволяет получить более реалистичную оценку производительности.

  2. Внутренний цикл (Inner Loop): Внутри каждого фолда внешнего цикла производится детальная настройка модели с использованием другого m фолда (например, 3). В случае, когда используется настройка гиперпараметров, важно учитывать, что надежность получаемых оценок зависит от корректного применения этой процедуры.

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

Конструирование Доверительных Интервалов

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

  1. Достаточное количество бутстрэпп-интераций: Вы выбрали 1000 итераций, что является хорошей практикой для получения стабильных оценок.

  2. Внимание на выборки: При бутстрэппинге важно правильно перебирать модели, чтобы удостовериться в достоверности выборок. Если каждый из лучших вариантов модели использует разные признаки, это может повлиять на однородность результатов. Постарайтесь объединить результаты моделей и методично оценить их, даже если они использовали разные наборы признаков.

Среднее значение и Бутстрэп для Разных Метрик

Сводка результатов в конце вашего кода, где вы собираете оценки шесть различных метрик, выглядит оптимально. Счётчики сравнения для различных метрик помогают в понимании распределения производительности модели. Например:

  • ROC AUC
  • Precision
  • Recall
  • Accuracy
  • Log-loss
  • F1 Score

Эти метрики позволяют собрать полную картину производительности вашей модели и избежать искажений, связанных с определенной метрикой.

Заключение

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

Продолжайте следить за проводимыми метриками и регулярно обновляйте ваш пайплайн, основываясь на новых данных и методах оптимизации. Ваши усилия в этой области помогут достичь более точных и обоснованных результатов в предсказаниях. Удачи в вашей работе!

Оцените материал
Добавить комментарий

Капча загружается...