Настроить self-training с псевдо-метками

ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Настроить self-training с псевдо-метками

1. Цель задачи

Разработать и реализовать пайплайн self-training (самообучения) для задачи классификации, в котором начальная модель обучается на небольшом размеченном наборе данных, затем используется для генерации псевдо-меток на большом неразмеченном наборе, после чего дообучается вместе с размеченными данными. Цикл повторяется несколько итераций. Ключевой результат: улучшение качества классификации (F1-score) не менее чем на 10% относительно baseline, обученного только на исходных размеченных данных.

2. Исходные данные

Что нужноОткуда взять
Набор изображений для классификации (CIFAR-10)torchvision.datasets.CIFAR10 (встроен в PyTorch)
Размеченная подвыборка (20% от всего набора)Случайная стратифицированная выборка из train set
Неразмеченная подвыборка (оставшиеся 80% от train set, метки убраны)Искусственно скрыть метки (удалить из DataFrame / заменить на -1)
Baseline модельResNet-18, предобученная на ImageNet (через torchvision.models)
Метрика качестваF1-score (weighted) на полном тестовом наборе CIFAR-10

Если нет реального инструмента — симулируем:

  1. Загрузить CIFAR-10 через torchvision.datasets.CIFAR10(..., train=True, download=True).
  2. Разделить train set на labeled (20%) и unlabeled (80%) с сохранением пропорций классов (stratify).
  3. Для unlabeled части заменить метки на -1 и использовать torch.utils.data.Dataset, который возвращает только изображение (метку не используем).
  4. Создать отдельный тестовый набор (стандартный test split CIFAR-10).

3. Технологический стек

КомпонентИнструментыНазначение
Фреймворк глубокого обученияPyTorch 2.x, torchvisionПостроение, обучение, инференс модели
Работа с даннымиnumpy, scikit-learn (train_test_split)Разделение данных, расчёт метрик
Метрикиsklearn.metrics.f1_score, classification_reportОценка качества
Визуализацияmatplotlib, seabornДинамика метрик по итерациям
Управление экспериментамиtensorboard (опционально) или print-логиОтслеживание loss, accuracy, F1
КодPython 3.10+, Jupyter Notebook или .py скриптыРеализация пайплайна

4. Этапы выполнения

Этап 1: Подготовка данных и baseline (1.5 часа)

Действия

  1. Загрузить CIFAR-10, применить нормализацию (mean=(0.4914, 0.4822, 0.4465), std=(0.2023, 0.1994, 0.2010)).
  2. Сформировать labeled_loader и unlabeled_loader (для unlabeled — без меток, только изображения). Для unlabeled можно создать класс UnlabeledDataset, возвращающий (image, 0) или использовать ignore_index.
  3. Создать test_loader (стандартный 10k изображений).
  4. Загрузить предобученную ResNet-18, заменить последний fully connected слой на nn.Linear(512, 10).
  5. Обучить baseline только на labeled_loader: 10 эпох, оптимизатор SGD lr=0.01, momentum=0.9, stepLR, CrossEntropyLoss.
  6. Замерить F1 на test_loader — это baseline.
# Пример фрагмента
model = torchvision.models.resnet18(pretrained=True)
model.fc = nn.Linear(512, 10)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)

Ожидаемый результат этапа

  • Получен baseline-скор (например F1≈0.55-0.65 на 20% labeled).
  • Код разделения данных и загрузчиков готов.

Этап 2: Реализация цикла self-training (2 часа)

Действия

  1. Написать функцию generate_pseudo_labels(model, unlabeled_loader, threshold=0.9):
    • model в режиме eval.
    • Для каждого батча изображений получить вероятности softmax.
    • Отобрать образцы, где максимальная вероятность > threshold.
    • Вернуть индексы образцов, псевдо-метки (argmax) и вероятности.
  2. Написать функцию update_dataset(original_unlabeled_indices, selected_indices, pseudo_labels): расширяет labeled набор, удаляя отобранные образцы из unlabeled.
  3. Реализовать основной цикл (3-5 итераций):
    • Генерация псевдо-меток.
    • Объединение labeled + новые псевдо-меченые данные.
    • Дообучение модели на расширенном наборе (2-3 эпохи на каждой итерации, чтобы не переобучиться).
    • Оценка на test set.
  4. Использовать threshold = 0.95 на первой итерации, затем можно снизить до 0.85.
# Псевдо-код цикла
for iteration in range(num_iterations):
    model.eval()
    selected = []
    with torch.no_grad():
        for images, _ in unlabeled_loader:
            outputs = model(images)
            probs = torch.softmax(outputs, dim=1)
            max_probs, preds = torch.max(probs, dim=1)
            mask = max_probs > threshold
            selected.append((images[mask], preds[mask]))
    # Добавить выбранные в labeled_loader
    # Дообучить модель

Ожидаемый результат этапа

  • Полный пайплайн self-training, который исполняется без ошибок.
  • Логирование F1 после каждой итерации.

Этап 3: Эксперименты с порогом и количеством итераций (1 час)

Действия

  1. Проверить 3 варианта порога: 0.95, 0.90, 0.85.
  2. Для каждого порога выполнить цикл из 5 итераций.
  3. Сохранить динамику F1 и размер накопленного labeled набора.
  4. Построить график: номер итерации vs F1-test для каждого порога.
  5. Выбрать лучший порог по конечному F1.

Ожидаемый результат этапа

  • Таблица с результатами (порог, F1 после каждой итерации, итоговый F1).
  • График.

Этап 4: Финальная оценка и анализ ошибок (1 час)

Действия

  1. На лучшей конфигурации обучить финальную модель на всех размеченных + псевдо-меченых данных (10 эпох с lr=0.001).
  2. Вычислить F1-score на тесте, сравнить с baseline.
  3. Построить confusion matrix (heatmap) для выявления классов, которые улучшились/ухудшились.
  4. Посчитать, сколько образцов реально добавлено (соотношение добавленных к общему числу unlabeled).

Ожидаемый результат этапа

Этап 5: Оформление отчёта (0.5 часа)

Действия

  1. Записать результаты эксперимента в формате: используемый датасет, baseline, порог, итерации, итоговый F1.
  2. Приложить код и графики.
  3. Указать выводы и возможные улучшения (например, изменение порога по итерациям, использвание consistency regularization).

Ожидаемый результат этапа

  • Файл report.md или report.ipynb с финальными метриками и графиками.

5. Критерии приемки (Definition of Done)

  • Baseline (только 20% размеченных) вычислен и зафиксирован.
  • Реализована функция generate_pseudo_labels с поддержкой порога уверенности.
  • Цикл self-training исполняется не менее 3 итераций.
  • F1 на тестовом наборе улучшился не менее чем на 10% относительно baseline.
  • Построен график зависимости F1 от итерации для как минимум двух порогов.
  • Confusion matrix подтверждает улучшение на большинстве классов.
  • Код пайплайна оформлен в виде Python-скрипта или ноутбука с комментариями.
  • Отчёт содержит baseline, итоговую метрику, выбранный порог и выводы.

6. Ожидаемый результат

  • Основной артефакт Jupyter Notebook или Python-скрипт с полной реализацией self-training, включая раздел с экспериментом.
  • Содержание ноутбука/скрипта
    • Загрузка и разделение CIFAR-10.
    • Baseline обучение.
    • Функции генерации псевдо-меток и обновления датасета.
    • Цикл self-training с разными порогами.
    • Финальная оценка (F1, confusion matrix).
  • Дополнительные результаты
    • Графики динамики метрик.
    • Таблица сравнения порогов.
    • Файл отчёта .md с текстовыми выводами.

7. Возможные сложности и их решение

СложностьРешение
Псевдо-метки низкого качества (много шума)Установить высокий порог (≥0.95) на первых итерациях, постепенно снижать. Добавить фильтрацию по энтропии.
Модель запоминает ошибки (confirmation bias)Использовать отдельную модель для генерации меток (teacher-student). Либо дообучать с очень низким lr и малым числом эпох.
Дисбаланс классов после добавления псевдо-метокПрименять weighted loss или class-balanced sampling в DataLoader.
Переобучение на маленьком labeled набореИспользовать аугментацию данных (RandomCrop, HorizontalFlip).
Память GPU не хватает для большого unlabeledИспользовать DataLoader с batch_size ≤ 256, накапливать градиенты.

8. Бюджет времени (оценка)

ЭтапВремя
Подготовка данных и baseline1.5 часа
Реализация цикла self-training2 часа
Эксперименты с порогом и итерациями1 час
Финальная оценка и анализ1 час
Оформление отчёта0.5 часа
Итого6 часов

Примечание для первого раза: возможны задержки на отладку DataLoader с неразмеченными данными, а также на подбор оптимального числа эпох на итерацию. Рекомендуется выделить дополнительный час на отладку.

9. Связанные вопросы из базы знаний

ВопросТема
45Как разделить данные на labeled/unlabeled с сохранением стратификации?
112Какие метрики использовать при дисбалансе классов в semi-supervised обучении?
203Реализация порога уверенности для псевдо-меток
278Confirmation bias в self-training и способы борьбы
345Аугментация данных для CIFAR-10 в PyTorch
401Как построить confusion matrix с помощью sklearn?
512Динамика обучения: почему self-training может ухудшить качество?
623Teacher-student vs self-training: сравнение подходов
714Weighted CrossEntropyLoss для несбалансированных данных
899Оптимизация hyperparams для дообучения на расширенном датасете

10. Чек-лист самопроверки

  • Я убедился, что baseline модель обучена корректно и метрики зафиксированы до начала self-training.
  • Я правильно организовал unlabeled DataLoader — он не использует метки, но хранит исходные индексы.
  • В цикле self-training после добавления псевдо-меток я не использую эти же данные повторно как unlabeled (удаляю из unlabeled набора).
  • Я проверил, что порог уверенности применяется к вероятностям softmax (а не к logits).
  • Я построил хотя бы один график динамики F1 по итерациям и убедился, что результат не хуже baseline.