GMVAE для классификации с малыми данными | Практическое руководство 2026 | AiManual
AiManual Logo Ai / Manual.
17 Апр 2026 Гайд

GMVAE на практике: как обучать классификатор с минимальным количеством размеченных данных

Пошаговое руководство по использованию GMVAE для обучения классификатора с минимальной разметкой. Экономьте на дорогих данных с EMNIST и PyTorch.

Проблема: когда разметка данных стоит как Bentley

Вы собрали терабайты сырых данных. Фотографии, тексты, сигналы. Мечтаете обучить модель. Но есть нюанс - каждый размеченный пример вытягивает из бюджета сотни рублей. А нужно десятки тысяч. Классический supervised learning в 2026 году все еще упирается в эту стену. Вы либо платите фрилансерам до умопомрачения, либо пытаетесь автоматизировать разметку и получаете иллюзию смысла в данных.

Цифра на 17.04.2026: разметка одного изображения для медицинской диагностики стоит от 300 до 5000 рублей в зависимости от сложности. Для промышленного CV проектов бюджет на разметку регулярно превышает миллион.

Полу-обучение (semi-supervised learning) обещает решение. Но большинство методов либо слишком сложны для production, либо требуют тонкой настройки, которая сводит на всю экономию. Тут появляется GMVAE - Gaussian Mixture Variational Autoencoder. Не самый новый, но чертовски эффективный инструмент, который за последние два года получил второе дыхание благодаря улучшениям в стабилизации обучения.

GMVAE: ваш секретный ингредиент

Представьте обычный вариационный автоэнкодер (VAE). Он учится сжимать данные в латентное пространство, где каждая точка - это сжатое представление исходного примера. Теперь добавьте к этому гауссову смесь (Gaussian Mixture). Вместо одного облака точек в латентном пространстве у вас появляется несколько кластеров. Каждый кластер интуитивно стремится представлять отдельный класс данных.

Как GMVAE работает под капотом

Архитектура умнее, чем кажется. Энкодер преобразует входные данные x в параметры распределения в латентном пространстве z. Но здесь появляется скрытая категориальная переменная y, которая указывает, к какому компоненту смеси (читай: классу) относится пример. Декодер пытается восстановить x из z. Магия в функции потерь: она включает не только реконструкцию и KL-дивергенцию для z, но и регуляризацию для распределения по y.

💡
Ключевое отличие от обычного VAE: латентное пространство GMVAE структурировано заранее. Модель вынуждена организовывать данные в кластеры еще на этапе обучения без учителя. Когда вы добавляете горсть размеченных примеров, они просто "притягивают" соответствующие кластеры к правильным меткам.

Почему это лучше, чем просто обучить VAE, а потом сделать кластеризацию? Потому что обучение совместное. Кластеризация и реконструкция оптимизируются вместе, заставляя модель находить такие латентные представления, которые одновременно хороши для восстановления данных и для разделения на семантические группы. Это близко к идеям self-supervised learning, но с более явной структурой.

Пошаговый план: от данных до модели

Теория - это скучно. Давайте к коду. Возьмем EMNIST (Extended MNIST) - классический датасет рукописных букв и цифр. Представим, что у нас есть доступ ко всем изображениям, но метки есть только для 1% данных. Наша задача - добиться точности, сравнимой с обучением на 100% размеченных данных.

1Шаг 1: Готовим данные и среду

Используем PyTorch 2.3 (актуально на 17.04.2026) и torchvision. Первое решение - как разделить данные. Нельзя просто взять первые 1% примеров с метками - распределение классов может быть нарушено. Используем стратифицированную выборку.

import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, Subset
import numpy as np
from sklearn.model_selection import train_test_split

# Преобразования для EMNIST
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Lambda(lambda x: x.view(-1))  # Вытягиваем в вектор
])

# Загрузка полного датасета
full_dataset = datasets.EMNIST(root='./data', split='byclass', 
                               train=True, download=True, transform=transform)

# Стратифицированная выборка 1% данных с метками
targets = full_dataset.targets.numpy()
indices = np.arange(len(targets))
train_idx, labeled_idx = train_test_split(indices, test_size=0.01, 
                                          stratify=targets, random_state=42)

# Создаем размеченный и неразмеченный датасеты
unlabeled_dataset = Subset(full_dataset, train_idx)
labeled_dataset = Subset(full_dataset, labeled_idx)

# Для теста используем стандартный тестовый набор
test_dataset = datasets.EMNIST(root='./data', split='byclass', 
                               train=False, download=True, transform=transform)

print(f"Неразмеченных примеров: {len(unlabeled_dataset)}")
print(f"Размеченных примеров: {len(labeled_dataset)}")
print(f"Тестовых примеров: {len(test_dataset)}")

Ошибка новичка: брать размеченные данные без стратификации. Если в 1% выборке не окажется, скажем, цифры '5', модель никогда не научится ее распознавать. В production это гарантированный провал.

2Шаг 2: Строим архитектуру GMVAE

Здесь нужна аккуратность. Слишком простая сеть не уловит паттерны, слишком сложная переобучится на скудных размеченных данных. Я предлагаю архитектуру с латентной размерностью 32 и 62 компонентами смеси (по количеству классов в EMNIST byclass).

import torch.nn as nn
import torch.nn.functional as F

class GMVAE(nn.Module):
    def __init__(self, input_dim=784, latent_dim=32, n_components=62):
        super().__init__()
        self.latent_dim = latent_dim
        self.n_components = n_components
        
        # Энкодер
        self.encoder = nn.Sequential(
            nn.Linear(input_dim, 512),
            nn.ReLU(),
            nn.Linear(512, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
        )
        # Слой для параметров латентного распределения
        self.fc_mu = nn.Linear(128, latent_dim)
        self.fc_logvar = nn.Linear(128, latent_dim)
        # Слой для предсказания вероятностей компонентов смеси
        self.fc_pi = nn.Linear(128, n_components)
        
        # Декодер
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 128),
            nn.ReLU(),
            nn.Linear(128, 256),
            nn.ReLU(),
            nn.Linear(256, 512),
            nn.ReLU(),
            nn.Linear(512, input_dim),
            nn.Sigmoid()
        )
        
    def encode(self, x):
        h = self.encoder(x)
        mu = self.fc_mu(h)
        logvar = self.fc_logvar(h)
        pi_logits = self.fc_pi(h)
        return mu, logvar, pi_logits
    
    def reparameterize(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std
    
    def decode(self, z):
        return self.decoder(z)
    
    def forward(self, x):
        mu, logvar, pi_logits = self.encode(x)
        z = self.reparameterize(mu, logvar)
        x_recon = self.decode(z)
        return x_recon, mu, logvar, pi_logits, z

Обратите внимание на fc_pi. Этот слой выдает логиты для категориального распределения по компонентам смеси. Во время обучения без учителя модель учится присваивать примеры к компонентам на основе их семантического сходства.

3Шаг 3: Обучение с акцентом на неразмеченные данные

Самое интересное - функция потерь. Она состоит из трех частей: реконструкция (MSE или BCE), KL-дивергенция для латентной переменной z, и KL-дивергенция для категориальной переменной y. Важно правильно взвесить эти компоненты.

def gmvae_loss(x, x_recon, mu, logvar, pi_logits, beta=1.0, gamma=1.0):
    # Реконструкция
    recon_loss = F.binary_cross_entropy(x_recon, x, reduction='sum')
    
    # KL-дивергенция для z (аналогично VAE)
    kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
    
    # KL-дивергенция для pi (распределение по компонентам смеси)
    # Предполагаем априорное равномерное распределение
    pi_probs = F.softmax(pi_logits, dim=-1)
    uniform_prior = torch.ones_like(pi_probs) / pi_probs.size(-1)
    pi_kl_loss = F.kl_div(pi_probs.log(), uniform_prior, reduction='sum')
    
    total_loss = recon_loss + beta * kl_loss + gamma * pi_kl_loss
    return total_loss, recon_loss, kl_loss, pi_kl_loss

Параметры beta и gamma - это ваши ручки настройки. Начните с beta=1.0, gamma=0.1. Gamma контролирует "жесткость" кластеризации. Слишком высокое значение заставит модель равномерно распределять примеры по кластерам, слишком низкое - коллапсировать в несколько компонентов.

💡
Процесс обучения двухэтапный. Сначала вы обучаете GMVAE на ВСЕХ данных (размеченных и неразмеченных) с функцией потерь выше. Модель учится кластеризовать данные в латентном пространстве. Затем вы используете немного размеченных данных, чтобы сопоставить кластеры с реальными метками.

4Шаг 4: Дообучение классификатора

После предобучения GMVAE, у вас есть мощный энкодер, который преобразует изображения в структурированные латентные векторы. На этом этапе можно заморозить веса GMVAE и обучить простой классификатор (например, линейный слой) на размеченных данных. Но есть хитрее способ.

# Используем энкодер GMVAE как фичейзер
classifier = nn.Sequential(
    nn.Linear(32, 128),  # 32 - размерность латентного пространства
    nn.ReLU(),
    nn.Dropout(0.3),
    nn.Linear(128, 62)   # 62 класса в EMNIST byclass
)

# Замораживаем веса GMVAE
for param in gmvae.parameters():
    param.requires_grad = False

# Обучаем только классификатор на размеченных данных
optimizer = torch.optim.Adam(classifier.parameters(), lr=1e-3)
for epoch in range(50):
    for batch, (data, target) in enumerate(labeled_loader):
        with torch.no_grad():
            mu, _, _ = gmvae.encode(data)  # Используем только mu как фичи
        output = classifier(mu)
        loss = F.cross_entropy(output, target)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

Почему используем только mu, а не весь латентный вектор? Потому что mu - это детерминированное представление входных данных, в то время как полный латентный вектор z включает случайный шум (через reparameterization trick). Для классификации детерминированное представление обычно работает лучше.

Нюансы, которые взорвут ваш эксперимент

Реализовать GMVAE - полдела. Заставить его работать стабильно - искусство. Вот что редко пишут в туториалах:

  • Инициализация весов для fc_pi. Если инициализировать этот слой нулями, все компоненты смеси будут иметь равные вероятности в начале. Это хорошо. Но лучше добавить небольшой шум, чтобы избежать симметрии.
  • Температура для softmax в pi. Вместо обычного softmax используйте softmax с температурой: softmax(pi_logits / tau). Начинайте с tau=1.0 и постепенно уменьшайте до 0.5 во время обучения. Это "закаливает" распределение, делая присвоение к кластерам более уверенным.
  • Баланс между размеченными и неразмеченными батчами. Когда вы обучаете классификатор, смешивайте размеченные и неразмеченные данные в одном батче, но вычисляйте loss только для размеченных. Неразмеченные данные проходят через модель и влияют на градиенты через unsupervised loss, что действует как регуляризация. Это похоже на трюк из парадокса fine-tuning.

Предупреждение: Не пытайтесь использовать GMVAE, если ваши данные не кластеризуемы. Если изображения в датасете - это случайный шум или между классами нет визуального различия (например, классификация по эмоциям в тексте без контекста), метод не сработает. Он полагается на то, что неразмеченные данные имеют внутреннюю структуру.

Частые ошибки и как их избежать

ОшибкаСимптомРешение
Коллапс кластераОдин компонент смеси захватывает >90% данных, accuracy низкаяУвеличить gamma в loss, добавить регуляризацию на минимальный размер кластера
Переобучение на размеченные данныеОтличная точность на train, ужасная на testЗаморозить GMVAE при дообучении классификатора, использовать сильный dropout
Нестабильные градиентыLoss скачет, иногда становится NaNКлиппинг градиентов, уменьшить learning rate, использовать AdamW вместо Adam

FAQ по GMVAE

Сколько размеченных данных достаточно? Зависит от сложности задачи. Для EMNIST - 1% (около 600 примеров) дает ~70% accuracy. Для CIFAR-10 нужно уже 5-10%. Для медицинских изображений может потребоваться 20%, но это все равно в 5 раз меньше, чем при fully-supervised подходе.

GMVAE или контрастивный self-supervised learning? GMVAE дает явные кластеры, что удобно для интерпретации. Контрастивные методы (SimCLR, BYOL) часто показывают лучшие линейные оценки, но требуют больше вычислительных ресурсов и сложнее в отладке. Выбор зависит от ваших целей. Если нужно быстро прототипировать - GMVAE. Если гонитесь за state-of-the-art и есть GPU ферма - контрастивное обучение.

Как интегрировать GMVAE в production пайплайн? Обученный энкодер можно экспортировать в ONNX формат и использовать для извлечения признаков в реальном времени. Классификатор работает поверх этих признаков. Мониторьте "уверенность" модели через entropy распределения pi. Высокая энтропия (модель не уверена, к какому кластеру отнести пример) - сигнал для human-in-the-loop. Это экономит время на разметке данных для сложных случаев.

Что дальше? Неочевидный совет

GMVAE - не панацея. Это инструмент, который особенно хорошо работает, когда у вас есть много неразмеченных данных и вы примерно представляете количество классов. Но самый интересный тренд 2026 года - гибридные подходы. Представьте: GMVAE для начальной кластеризации, затем маленькая языковая модель (например, Gemma 3 4B) для генерации псевдометок на основе кластеров, и наконец - тонкая настройка классификатора на смеси реальных и псевдо-размеченных данных.

И последнее: никогда не доверяйте слепо кластерам, найденным моделью. Визуализируйте латентное пространство с помощью UMAP или t-SNE. Посмотрите, какие примеры попали в один кластер. Иногда модель находит закономерности, которые вы не ожидали (например, объединяет все изображения с зеленым фоном). Иногда это баг. Иногда - фича. Разница лишь в том, можно ли это монетизировать.

Подписаться на канал