Архитектура LLM с латентным пространством: эксперимент создания модели | AiManual
AiManual Logo Ai / Manual.
15 Янв 2026 Гайд

Прочь от токенов: как я строил LLM с латентным пространством и почему это не безумие

Практический гайд по созданию языковой модели с архитектурой энкодер-ядро-декодер. Обучение субмоделей, экономия VRAM и эксперимент с USER2-bge-m3.

Все устали от токенов. От их лимитов, от того, как они разрывают смысл на куски, от того, как контекстное окно упёрлось в физические пределы. Что, если есть другой путь? Не надстраивать очередной слой внимания, а переосмыслить саму архитектуру.

Я решил провести эксперимент: построить языковую модель, которая работает не с токенами, а с векторами в латентном пространстве. Не «кошка → [токен_1234]», а «кошка → [0.12, -0.45, 0.78, ...]». Звучит как академическая забава? Возможно. Но именно такие забавы иногда переворачивают всё.

Почему латентное пространство? Потому что токены — это костыль

Токенизация — это компромисс, принятый на заре NLP из-за вычислительной сложности. Мы превращаем смысл в дискретные символы, а потом пытаемся заново собрать из них смысл. Это как писать картину, разбивая её на пиксели, а потом пытаться понять сюжет по одному пикселю за раз.

Главная проблема токенов — они теряют семантическую близость. Слова «автомобиль» и «машина» могут быть далекими токенами, хотя означают почти одно и то же. Модель должна заново учить эту связь с нуля.

Латентное пространство решает это сразу. Векторное представление сохраняет семантику: похожие понятия оказываются рядом. Модель оперирует не символами, а смыслами. И это меняет всё.

Архитектура: энкодер, ядро, декодер. Три головы лучше одной

Классическая LLM — это монолит. Моя архитектура — модульный конструктор. Она состоит из трёх независимых, но связанных частей.

Модуль Задача Пример модели Выход
Энкодер Перевести текст в латентный вектор USER2-bge-m3, Sentence Transformer Вектор размерности 1024
Ядро (Латентная модель) Обработать последовательность векторов, сгенерировать следующий вектор Небольшая Transformer-like сеть Предсказанный вектор
Декодер Перевести вектор обратно в текст Небольшая авторегрессионная модель Последовательность токенов/символов

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

💡
Ключевое преимущество: разделение ответственности. Энкдер отлично понимает смысл, декодер отлично генерирует текст. Ядро учится только логике и стилю повествования между смысловыми векторами. Это резко сокращает объём обучаемых параметров.

Практика: как собрать эту штуку и не сжечь видеокарту

Теория — это хорошо, но код — это убедительнее. Вот пошаговый план эксперимента.

1 Выбираем и замораживаем энкодер

Не нужно изобретать велосипед. Мы берём готовую, отличную модель для эмбеддингов. Мой выбор пал на USER2-bge-m3. Почему? У неё отличное понимание инструкций и она создаёт плотные, информативные векторы.

from sentence_transformers import SentenceTransformer

# Загружаем и замораживаем энкодер
encoder = SentenceTransformer('USER2/bge-m3', trust_remote_code=True)
encoder.eval()
for param in encoder.parameters():
    param.requires_grad = False

# Превращаем текст в вектор
text = "Кошка сидит на ковре."
latent_vector = encoder.encode(text, normalize_embeddings=True)
print(f"Размерность вектора: {latent_vector.shape}")  # (1024,)

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

2 Проектируем латентное ядро

Здесь можно поэкспериментировать. Я начал с простой архитектуры на основе Transformer-декодера, но сильно уменьшенной. Вместо 4096-мерных эмбеддингов токенов — 1024-мерные входные векторы.

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

class LatentCore(nn.Module):
    def __init__(self, latent_dim=1024, nhead=8, num_layers=4, max_seq_len=512):
        super().__init__()
        self.latent_dim = latent_dim
        self.pos_encoder = nn.Embedding(max_seq_len, latent_dim)
        
        # Небольшой трансформер для работы с векторами
        encoder_layer = nn.TransformerEncoderLayer(
            d_model=latent_dim,
            nhead=nhead,
            dim_feedforward=latent_dim * 4,
            batch_first=True,
            dropout=0.1
        )
        self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
        
        # Проекция для предсказания следующего вектора
        self.output_proj = nn.Linear(latent_dim, latent_dim)
        
    def forward(self, latent_seq):
        # latent_seq: [batch_size, seq_len, latent_dim]
        batch_size, seq_len, _ = latent_seq.shape
        positions = torch.arange(seq_len, device=latent_seq.device).unsqueeze(0)
        pos_emb = self.pos_encoder(positions)  # [1, seq_len, latent_dim]
        
        x = latent_seq + pos_emb
        x = self.transformer(x)
        next_vector_pred = self.output_proj(x[:, -1, :])  # Берём последний вектор
        return F.normalize(next_vector_pred, dim=-1)  # Важно!

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

3 Обучение ядра: данные и лосс

Для обучения нужен датасет, где каждое предложение — это вектор. Мы используем простой самоучительный objective: предсказать следующий вектор в последовательности.

# Псевдокод цикла обучения
model = LatentCore().cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)

for batch in dataloader:
    # batch: список предложений
    with torch.no_grad():
        # Кодируем все предложения в векторы разом
        all_vectors = encoder.encode(batch, normalize_embeddings=True)
        all_vectors = torch.tensor(all_vectors).cuda()
    
    # Формируем последовательности: берем N векторов как контекст
    seq_len = 8
    for i in range(len(all_vectors) - seq_len):
        context = all_vectors[i:i+seq_len].unsqueeze(0)  # [1, seq_len, 1024]
        target = all_vectors[i+seq_len]                   # [1024]
        
        pred = model(context)  # [1, 1024]
        
        # Используем косинусное расстояние как лосс
        loss = 1 - F.cosine_similarity(pred, target.unsqueeze(0)).mean()
        
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()

Экономия VRAM здесь драматическая. Энкодер заморожен и работает в режиме torch.no_grad(). Обучается только ядро — модель на 10-50 миллионов параметров вместо миллиардов. Вы можете обучать это даже на одной RTX 4090.

4 Самый сложный этап: декодер

Перевести вектор обратно в связный текст — нетривиально. Здесь я экспериментировал с двумя путями.

  • Путь 1: Небольшая авторегрессионная модель. Берём TinyLlama или аналогичную микро-модель. Обучаем её принимать латентный вектор как префикс (через проекционный слой) и генерировать текст. Это ресурсоёмко, но качественно.
  • Путь 2: Retrieval-based декодер. Храним базу пар «вектор-текст» от энкодера. Когда ядро выдаёт предсказанный вектор, ищем в базе K ближайших соседей и используем их тексты как кандидаты для продолжения. Грязно, но работает удивительно хорошо для некоторых задач и требует нулевого обучения.

Для первого эксперимента я выбрал второй путь — чтобы быстро увидеть результат.

Что получилось, а что — провалилось

После двух недель экспериментов, вот холодные результаты.

Что работает отлично:

  • Семантическая согласованность. Модель прекрасно улавливает тематику. Если контекст про программирование, следующее предсказанное «предложение-вектор» будет про код, а не про кулинарию.
  • Экономия ресурсов. Обучение ядра на датасете в 1ГБ текста заняло 6 часов на RTX 3090. Попробуйте обучить с нуля 7B-модель за такое время.
  • Гибкость. Вы можете легко заменить энкодер на более мощный или декодер на более качественный, не переучивая всю систему. Это модульный подход в действии.

Где модель спотыкается:

  • Грамматика и синтаксис. Retrieval-based декодер выдаёт грамматически правильный текст (он же берёт его из реальных предложений), но связность между предложениями иногда хромает. Это проблема декодера, а не латентного ядра.
  • Длинная последовательность. Предсказание вектора на основе 8 предыдущих векторов работает. Но цепочка из 50 шагов накапливает ошибку, и смысл может уплыть. Нужны более сложные механизмы внимания в ядре.
  • «Скучность» вывода. Модель склонна выдавать усреднённые, безопасные предсказания. Тот же феномен консервативности, но на уровне смысловых векторов.

Ошибки, которые я совершил, чтобы вам не повторять

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

Ошибка 2: Слишком маленькая последовательность. Подавать ядру 2-3 вектора недостаточно. Оно не улавливает повествовательную структуру. Минимум 5-8 векторов для коротких текстов, для длинных — больше.

Ошибка 3: Попытка обучить всё сразу. Я попытался одновременно дообучать энкодер и ядро. Это разрушило уже хорошие эмбеддинги энкодера и привело к коллапсу. Замораживайте энкодер. Точка.

Зачем всё это? Сценарии, где такая архитектура блестит

Эта модель — не замена GPT-4. Это специализированный инструмент для конкретных задач.

  1. Семантическое сжатие диалогов. Представьте чат-бота, который работает не с историей сообщений, а с историей смысловых векторов. Контекстное окно вырастает в разы без увеличения вычислительных затрат. Это прямой ответ на проблему длинных промптов.
  2. Быстрое прототипирование стилей. Хотите модель, которая пишет в стиле вашего технического блога? Обучите ядро на векторах ваших статей. Декодер можно взять готовый. Вы получите работающий прототип за часы, а не за недели.
  3. Энергоэффективный предиктор. На edge-устройствах, где важен каждый ватт, предсказание следующего вектора требует на порядок меньше операций, чем генерация следующего токена полной LLM.
  4. Исследовательский стенд. Хотите поэкспериментировать с новыми механизмами внимания или архитектурами? Не нужно обучать 7-миллиардную модель с нуля. Постройте и обучите ядро за день, протестируйте идею. Если сработало — масштабируйте.

Этот подход перекликается с идеей разделения задач, как в семантическом пайплайне для LLM, но на более глубоком, архитектурном уровне.

Что дальше? Пять экспериментов для смелых

Моя реализация — лишь proof of concept. Вот что можно попробовать, чтобы выжать из архитектуры максимум.

  • Мультимодальное ядро. Что если энкодеры будут для текста, изображений и аудио, а ядро — одно? Оно научится предсказывать следующий смысловой вектор в мультимодальном диалоге.
  • Ядро с внешней памятью. Добавить механизм, похожий на RAG без галлюцинаций, но на уровне векторов. Ядро могло бы «запоминать» ключевые смыслы в отдельном хранилище и обращаться к ним.
  • Адаптивный декодер. Вместо одного декодера — их множество, каждый для своей задачи (технический текст, диалог, код). Ядро могло бы выбирать, какому декодеру передать вектор.
  • Обучение с подкреплением в латентном пространстве. Вместо того чтобы скормить LLM 10 000 примеров для RLHF, можно проводить RL прямо на ядре, работающем с векторами. Это должно быть на порядки быстрее.
  • Сжатие контекста. Ядро можно научить не просто предсказывать следующий вектор, а сжимать длинную последовательность векторов в один «суммарный» вектор. Мечта для работы с длинными документами.

Архитектура с латентным пространством — это не панацея. Это другой способ думать о генерации языка. Менее ресурсоёмкий, более модульный, открытый для экспериментов. Она не заменит трансформеры завтра, но может занять свою нишу там, где важны скорость, эффективность и контроль.

Самое интересное в этом эксперименте — не конкретная модель, а сам подход. Разделение монолитной LLM на специализированные, обучаемые независимо модули. Это как если бы вместо того, чтобы каждый раз строить новый автомобиль с нуля, мы могли бы менять в нём двигатель, не трогая корпус и салон. Дерзко. Непрактично? Возможно. Но именно такие идеи двигают область вперёд, пока все остальные настраивают гиперпараметры очередного LoRA-адаптера.