Мультимодальные модели: речь, зрение и язык в одной сети
Весь трек до этого места двигался по модальностям раздельно: свёрточные сети работали с картинками, рекуррентные и трансформеры — с текстом, генеративные модели порождали изображения. Реальность так не устроена. Пользователь присылает фотографию счётчика и спрашивает голосом, сколько платить. Служба поддержки хочет расшифровку звонка вместе со скриншотом ошибки. Модерация должна понимать мем, где текст безобиден, а картинка — нет.
Мультимодальная модель — это не «три модели в контейнере». Это одна сеть, внутри которой разные модальности живут в общем пространстве представлений и потому могут влиять друг на друга: слово «полосатый» сдвигает интерпретацию пикселей, а интонация — интерпретацию слов. Эта статья про то, как такое пространство строится, чем платят за каждый его вариант и что ломается при выкатке.
1. Общий принцип: всё превращается в последовательность векторов
Главная идея, из которой следует остальное, обманчиво проста. Трансформеру безразлично, что стоит за входным вектором. Ему нужна последовательность элементов размерности $d$ — дальше работает внимание. Значит, задача сводится к одному вопросу: как из сырой модальности нарезать токены.
| Модальность | Чем режем | Сколько позиций получается |
|---|---|---|
| Текст | BPE-токенизатор | ≈ 0,75 токена на слово |
| Изображение | патчи 14×14 или 16×16 пикселей (ViT) | 336×336 → 576 патчей |
| Аудио | окна 25 мс с шагом 10 мс → мел-спектрограмма → свёртки stride 2 | 30 с → 1500 позиций |
| Видео | кадры (1–2 fps) × патчи, плюс временная ось | 10 с при 1 fps → 10 × 576 |
| Аудио для генерации | нейрокодек (RVQ), 8 кодовых книг | 50–75 токенов на секунду на книгу |
Из этой таблицы следует главный практический вывод статьи, который стоит усвоить раньше всех архитектурных деталей: картинка стоит как страница текста, а звук — как несколько страниц. Одна фотография в разрешении 336×336 занимает 576 позиций контекста, примерно как 430 слов. Десять минут аудио в лоб — это 30 000 позиций, что не влезает никуда и стоит квадратично. Все ухищрения дальше — пулинг патчей, Q-Former, тайлинг, нарезка по речевой активности — придуманы ради экономии этого бюджета.
def context_budget(images: int = 0, image_res: int = 336, patch: int = 14,
tiles: int = 1, audio_sec: float = 0.0,
words: int = 0) -> dict:
"""Во сколько позиций контекста обойдётся мультимодальный запрос."""
per_image = (image_res // patch) ** 2 * tiles # тайлинг умножает линейно
img_tokens = images * per_image
audio_tokens = int(audio_sec * 50) # Whisper: 1 позиция ≈ 20 мс
text_tokens = int(words * 1.33)
total = img_tokens + audio_tokens + text_tokens
return {"картинки": img_tokens, "звук": audio_tokens,
"текст": text_tokens, "всего": total}
print(context_budget(images=4, tiles=5, audio_sec=60, words=200))
# {'картинки': 11520, 'звук': 3000, 'текст': 266, 'всего': 14786}
Четыре скриншота в высоком разрешении и минута речи — почти 15 тысяч позиций. Это уже не «добавим картинку в промпт», это архитектурное решение с ценником.
2. Таксономия: три разные задачи под одним словом
Слово «мультимодальность» покрывает три несводимые друг к другу постановки.
модели)) Понимание Зрение и язык VQA, описание изображений Документы, скриншоты, OCR-free Речь в текст офлайн-расшифровка стриминг Видео и язык поиск момента суммаризация записи Генерация Текст в изображение диффузия (статья 07) Текст в речь нейровокодеры, кодек-LM Any-to-any общий словарь токенов Сопоставление Контрастное выравнивание CLIP, SigLIP Кросс-модальный поиск текст ищет картинку Zero-shot классификация классы задаются словами
Границы важны: сопоставление учит два энкодера класть похожие объекты рядом (это разобрано в статье об эмбеддингах), понимание прикручивает энкодер к языковой модели, чтобы та рассуждала о содержимом, а генерация требует декодера в целевой модальности. Модель, отлично ищущая картинки по тексту, может быть неспособна ответить «сколько на фото людей» — CLIP не считает объекты, он сравнивает.
3. Как соединяют модальности: четыре стратегии
энкодер] --> A6[вектор] A3 -.->|косинус| A6 end subgraph S2["2. Projector (LLaVA)"] B1[картинка] --> B2[замороженный ViT] --> B3[MLP-проектор] --> B4[576 «токенов»] B4 --> B5[LLM: обычный self-attention
по смеси токенов] B6[текст] --> B5 end subgraph S3["3. Cross-attention (Flamingo)"] C1[картинка] --> C2[энкодер] --> C3[K, V] C4[LLM, слои заморожены] --> C5[вставные gated
cross-attn слои] C3 --> C5 end subgraph S4["4. Единый словарь токенов"] D1[картинка] --> D2[VQ-кодек] --> D3[дискретные коды] D4[звук] --> D5[RVQ-кодек] --> D3 D3 --> D6[один трансформер,
одна задача: next token] end
| Стратегия | Плюсы | Минусы | Когда выбирать |
|---|---|---|---|
| Раздельные башни | эмбеддинги считаются заранее, поиск за миллисекунды | нет рассуждения, только близость | поиск, дедупликация, zero-shot метки |
| Projector | дёшево обучается (часы на 8 картах), переиспользует готовую LLM | картинка съедает контекст | 90 % прикладных VLM |
| Cross-attention | контекст не раздувается, LLM не деградирует на тексте | сложнее, нужны изменения в LLM | много изображений в одном запросе, видео |
| Единый словарь | одна модель для всего, генерация в любой модальности | дорого обучать, потери на квантизации | фронтир, any-to-any |
Рецепт projector победил по прагматике: он описан в LLaVA (Liu et al., 2023) и состоит буквально из двух-трёх строк — взять выходы предпоследнего слоя ViT, прогнать через MLP в размерность LLM и вставить на место специального токена-плейсхолдера.
import torch
import torch.nn as nn
class VisionProjector(nn.Module):
"""MLP-проектор: d_vision -> d_llm. Ровно это и есть «мультимодальность» в LLaVA-1.5."""
def __init__(self, d_vision: int = 1024, d_llm: int = 4096):
super().__init__()
self.net = nn.Sequential(nn.Linear(d_vision, d_llm),
nn.GELU(),
nn.Linear(d_llm, d_llm))
def forward(self, patch_feats: torch.Tensor) -> torch.Tensor:
# patch_feats: [B, 576, d_vision] -> [B, 576, d_llm]
return self.net(patch_feats)
def merge_multimodal(input_ids, text_embed_fn, image_feats, image_token_id):
"""Подменяем эмбеддинги плейсхолдеров на признаки патчей.
input_ids: [B, T], где image_token_id повторён 576 раз подряд.
image_feats: [B, 576, d_llm] — выход проектора.
Сложность: O(B*T*d) по времени и памяти — это просто копирование.
"""
embeds = text_embed_fn(input_ids) # [B, T, d_llm]
mask = (input_ids == image_token_id) # [B, T]
embeds[mask] = image_feats.reshape(-1, embeds.shape[-1]).to(embeds.dtype)
return embeds
Дальше в LLM ничего не меняется: обычное причинное внимание по смешанной последовательности. Модель «видит» ровно потому, что часть её входных векторов пришла не из таблицы эмбеддингов, а из зрительного энкодера.
4. Выравнивание пространств и modality gap
Чтобы подмена работала, пространства должны быть согласованы. Есть два способа их согласовать.
Контрастно. CLIP (Radford et al., 2021) обучает две башни на 400 млн пар «картинка — подпись» симметричным InfoNCE: правильная пара должна быть ближе, чем все остальные в батче. Детали лосса разобраны в статье про эмбеддинги; здесь важны два прикладных следствия. Во-первых, качество линейно зависит от размера батча — негативы берутся из него же, поэтому CLIP обучают с батчами в десятки тысяч, а это сразу отправляет нас к распределённому обучению. Во-вторых, SigLIP заменяет softmax-нормировку попарной сигмоидой и снимает зависимость от гигантского батча — сегодня это дефолт для энкодера в VLM.
Генеративно. Проектор обучается на задаче «опиши картинку»: градиент от языковой потери проходит через LLM в проектор и подстраивает его выход под то, что LLM умеет читать. Никакого контрастного лосса, никаких негативов.
Тонкость, о которую спотыкаются при отладке: даже у хорошо обученного CLIP изображения и тексты лежат в разных конусах общего пространства — это явление называется modality gap и подробно разобрано в Mind the Gap (Liang et al., 2022). Косинус между картинкой и её собственной подписью может быть 0,3, а между двумя случайными подписями — 0,6. Отсюда правило: сравнивать можно только внутри пары модальностей и только по ранжированию, абсолютные пороги косинуса переносить с текст-текста на текст-картинку нельзя.
5. Речь: три способа превратить звук в текст
Распознавание речи — самая инженерно зрелая мультимодальная задача, и её стоит разобрать отдельно, потому что она задаёт словарь для всего остального.
Проблема в основе: аудио — 1500 позиций, текст — 20 токенов, и выравнивания между ними нет. Три семейства решений отличаются именно тем, как они справляются с этим.
5.1 CTC: сумма по всем выравниваниям
Connectionist Temporal Classification (Graves et al., 2006) предлагает предсказывать по символу на каждый кадр, добавив пустой символ «бланк». Любая последовательность кадров, которая после схлопывания повторов и удаления бланков даёт целевой текст, считается допустимым выравниванием; вероятность текста — сумма по всем таким путям:
$$P(y \mid x) = \sum_{\pi \in \mathcal{B}^{-1}(y)} \prod_{t=1}^{T} P(\pi_t \mid x)$$
Сумма считается динамическим программированием за $O(T \cdot U)$, где $U$ — длина текста. Плата за простоту — условная независимость кадров: CTC не моделирует $P(y_t \mid y_{<t})$, поэтому охотно пишет «превет» и нуждается во внешней языковой модели при декодировании.
5.2 RNN-T: то же самое, но со стримингом
RNN-Transducer добавляет к акустическому энкодеру предсказывающую сеть по уже выданным символам и объединяющую сеть. Условная независимость снимается, при этом модель остаётся строго причинной по времени — её можно кормить звуком по 200 мс и получать текст на лету. Это стандарт голосовых ассистентов и телефонии.
5.3 Attention seq2seq: Whisper
Whisper (Radford et al., 2022) — обычный encoder-decoder трансформер: энкодер съедает 30-секундное окно лог-мел-спектрограммы, декодер авторегрессивно порождает текст, а задача (язык, перевод, таймстемпы) задаётся специальными токенами в начале. Обучен на 680 тысячах часов слабо размеченного веб-аудио — отсюда устойчивость к шуму и акцентам, ради которой его и берут.
| Подход | Стриминг | Внешняя LM | Слабое место | Типичное применение |
|---|---|---|---|---|
| CTC | да | обычно нужна | орфография, нет контекста | лёгкие on-device модели |
| RNN-T | да | опционально | сложное обучение, память | ассистенты, телефония |
| Attention (Whisper) | нет (окна по 30 с) | не нужна | галлюцинации на тишине | офлайн-расшифровка |
перекрытие 1-2 с] D -->|Нет| F[Батчим сегменты
одной длины] E --> F F --> G[ASR-модель] G --> H{Нужны точные тайминги?} H -->|Да| I[Forced alignment
отдельной CTC-моделью] H -->|Нет| J[Таймстемпы из декодера] I --> K[Диаризация: кто говорит] J --> K K --> L[Постобработка: пунктуация,
числа, термины домена] L --> M[Транскрипт с метаданными]
Практические грабли, каждая стоит пары дней:
- Галлюцинации на тишине. Whisper на длинном молчании уверенно выдаёт «Субтитры
сделал DimaTorzok» и подобные артефакты обучающих данных. Лечится VAD-предфильтром
и порогом по
no_speech_prob— не постобработкой текста. - Нарезка по времени, а не по паузам режет слова пополам и портит соседние сегменты. Резать надо по речевой активности с перекрытием; см. подход WhisperX.
- WER скрывает то, что важно. Word Error Rate считает все слова равными, а бизнесу важны имена, суммы и артикулы. Заведите отдельную метрику на сущностях.
- Домен решает больше архитектуры. Список терминов, имён и товаров, поданный через biasing или переранжирование гипотез, обычно даёт больший выигрыш, чем переход на модель вдвое крупнее.
def wer(reference: str, hypothesis: str) -> float:
"""Word Error Rate = расстояние Левенштейна по словам / число слов эталона.
Время O(n*m), память O(min(n,m)) при построчной свёртке.
"""
r, h = reference.split(), hypothesis.split()
prev = list(range(len(h) + 1))
for i, rw in enumerate(r, start=1):
cur = [i] + [0] * len(h)
for j, hw in enumerate(h, start=1):
cost = 0 if rw == hw else 1
cur[j] = min(prev[j] + 1, # удаление
cur[j - 1] + 1, # вставка
prev[j - 1] + cost) # замена
prev = cur
return prev[-1] / max(len(r), 1)
print(round(wer("перевести десять тысяч рублей", "перевести 10 000 рублей"), 2)) # 1.0
Обратите внимание на последнюю строку: формально WER = 1.0, то есть «полный провал», хотя смысл распознан идеально. Без нормализации чисел, регистра и пунктуации метрика измеряет не качество, а форматирование.
Обратная задача — синтез речи — устроена сегодня как языковое моделирование над токенами нейрокодека (EnCodec, VALL-E): текст превращается в последовательность акустических кодов, кодек-декодер восстанавливает волну. То есть и здесь всё сводится к «нарезать модальность на токены».
6. VLM: как собирают модель, которая видит
Стандартный рецепт (LLaVA-1.5, Qwen-VL и почти все открытые VLM) состоит из трёх блоков и двух стадий обучения.
Блоки. Зрительный энкодер (SigLIP или CLIP ViT-L, ~300 млн параметров) → проектор (MLP, десятки млн) → языковая модель (7–70 млрд). Соотношение размеров объясняет всю экономику: обучается почти всегда только проектор и, на второй стадии, LLM.
недооценённая часть латентности P->>V: 5 тайлов 336×336 (AnyRes) V->>V: self-attention по патчам V-->>API: 5 × 576 векторов API->>API: проектор → размерность LLM API->>L: [2880 «зрительных» токенов] + [текст] Note over L: префилл 2900 токенов —
дороже, чем весь ответ L-->>U: стриминг ответа
Стадия 1 — выравнивание. Замораживаем и энкодер, и LLM, учим только проектор на парах «картинка — подпись». Цель — научить проектор говорить на языке эмбеддингов LLM. Дёшево: сотни GPU-часов.
Стадия 2 — инструкционная настройка. Размораживаем LLM (или вешаем LoRA), учим на диалогах с изображениями: вопросы, рассуждения, OCR, таблицы. Именно здесь модель учится отвечать, а не описывать.
Разрешение — главный рычаг качества на документах и скриншотах. 336×336 недостаточно, чтобы прочитать текст на скриншоте, а поднять разрешение в лоб нельзя: число патчей растёт квадратично. Отсюда приёмы: тайлинг (AnyRes — картинка режется на несколько плиток плюс уменьшенная копия целиком), динамическое разрешение (Qwen2-VL), пулинг патчей и Q-Former из BLIP-2, сжимающий картинку до 32 обучаемых запросов вместо 576 патчей.
Что ломается при обучении VLM:
- Катастрофическое забывание текста. Разморозили LLM на картиночных данных — просели чисто текстовые метрики. Лечение: подмешивать 20–40 % текстовых данных и держать LR для LLM на порядок ниже, чем для проектора.
- Слепота к деталям. Модель уверенно отвечает по «общему виду» картинки, потому что обучающие подписи описывали общий вид. Нужны данные с координатами, счётом объектов, чтением мелкого текста.
- Позиционное кодирование. Патчи — двумерная сетка, а LLM ждёт одномерную ось. Наивная развёртка теряет геометрию; отсюда 2D-RoPE и его варианты.
7. Оценка: где мультимодальные метрики врут
| Что меряем | Метрика | Чем обманывает |
|---|---|---|
| Кросс-модальный поиск | Recall@K, mAP | зависит от размера базы; R@1 на 1000 и на 10 млн — разные числа |
| Речь | WER, CER | нормализация; равный вес всех слов |
| VQA | точность по exact match | штрафует синонимы и развёрнутые ответы |
| Описания | CIDEr, BLEU | коррелируют с человеком слабо |
| Галлюцинации | POPE | легко переобучиться на «нет» |
| Инструкции | LLM-as-judge | предвзятость к длине и к своему семейству моделей |
Два правила, которые экономят месяцы. Первое: публичные бенчмарки протекли в обучающие данные. Веб-скрейпы содержат сами тесты, и высокий балл может означать запоминание. Второе: собственный размеченный набор из 300–500 примеров вашего домена информативнее любого рейтинга — методика построения таких наборов разобрана в Оценке и бенчмарках.
Отдельный тест, который стоит завести всегда: negative control. Подайте вопрос про объект, которого на картинке нет. Модель, отвечающая «да, он красный», не понимает изображение — она угадывает по языковому приору. Это самый быстрый способ отличить рабочую VLM от красивой демонстрации.
8. Продакшн: где на самом деле уходит время и деньги
Мультимодальный сервис ломается не там, где текстовый.
Препроцессинг — не бесплатно. Декодирование JPEG 12 Мп, ресайз, нормализация, для аудио — ресемплинг и мел-спектрограмма: десятки миллисекунд CPU на запрос. На загруженном сервисе это становится узким местом раньше GPU. Выносите в отдельный пул воркеров, кэшируйте по хешу файла, для аудио считайте спектрограмму на GPU.
Префилл доминирует. Как показано в разделе про инференс LLM, стоимость складывается из префилла и декодирования. У VLM 2900 зрительных токенов префилла против 150 токенов ответа — то есть 90 % вычислений происходит до первого символа. Практические следствия: кэшируйте зрительные признаки для повторно используемых изображений, снижайте разрешение там, где не нужен OCR, и не считайте стоимость запроса «по длине ответа».
Батчинг сложнее. Изображения разного размера и аудио разной длины не батчатся наивно. Работающая схема — бакеты по длине: сортируем очередь по числу токенов, собираем батч из близких. Общая механика continuous batching описана в статье про инференс, сюда добавляется только группировка по размеру входа.
Безопасность расширяется. Изображение — это канал ввода, а значит канал инъекции: текст на картинке модель читает как инструкцию. Атака описана в Abusing Images and Sounds for Indirect Instruction Injection, защита — та же, что для текста: недоверие к содержимому входа, ограничение прав инструментов, проверка действий. Подробно — в статье про безопасность моделей и в ИИ-инженерии.
Деградация по модальностям. Если ASR-подсистема упала, сервис должен уметь работать в текстовом режиме, а не отдавать 500. Мультимодальный пайплайн — это цепочка из нескольких моделей, и её надёжность считается как произведение, а не как минимум.
9. Типичные ошибки
- Считать, что картинка «почти бесплатна» в промпте. Она стоит сотни или тысячи позиций контекста и львиную долю вычислений.
- Гнаться за разрешением вместо данных. Модель не читает мелкий текст чаще из-за того, что её этому не учили, а не из-за нехватки пикселей.
- Переносить пороги косинуса между модальностями. Modality gap делает такие пороги бессмысленными.
- Мерить ASR только по WER без нормализации. Числа, регистр и пунктуация дадут фиктивные ошибки и скроют настоящие.
- Резать аудио по таймеру. Нарезка должна идти по паузам, с перекрытием.
- Разморозить всё сразу. Проектор ещё не выровнен, а градиенты уже ломают LLM: получается модель, которая хуже исходной и на тексте, и на картинках.
- Забыть про negative control в оценке. Без него не отличить понимание от языкового приора.
- Обучать VLM с нуля. Почти всегда правильный ответ — взять готовый энкодер и готовую LLM и обучить проектор; это разница между сотнями GPU-часов и сотнями тысяч.
10. Мини-итог
- Мультимодальность сводится к одному приёму: нарезать любую модальность на последовательность векторов размерности $d$ и отдать трансформеру.
- Бюджет позиций — главный практический ограничитель: 576 токенов на картинку, 50 позиций на секунду аудио. Считайте его до выбора архитектуры.
- Четыре способа соединения: раздельные башни (поиск), проектор (рабочая лошадь VLM), cross-attention (много изображений), единый словарь токенов (фронтир).
- Выравнивание пространств бывает контрастным (CLIP/SigLIP) и генеративным (обучение проектора через языковую потерю); modality gap никуда не исчезает.
- В речи три подхода: CTC (просто, стриминг, нужна LM), RNN-T (стриминг + контекст), attention seq2seq (Whisper: качество офлайн, галлюцинации на тишине).
- VLM собирается за две стадии: выравнивание проектора, затем инструкционная настройка. Разрешение решается тайлингом, а не лобовым увеличением.
- В проде дороже всего препроцессинг и префилл; изображение — полноценный канал инъекции.
Источники
- Radford et al. Learning Transferable Visual Models From Natural Language Supervision (CLIP) (2021).
- Zhai et al. Sigmoid Loss for Language Image Pre-Training (SigLIP) (2023).
- Dosovitskiy et al. An Image is Worth 16x16 Words (ViT) (2020).
- Alayrac et al. Flamingo: a Visual Language Model for Few-Shot Learning (2022).
- Li et al. BLIP-2: Bootstrapping Language-Image Pre-training with Q-Former (2023).
- Liu et al. Visual Instruction Tuning (LLaVA) (2023) и Improved Baselines (LLaVA-1.5) (2023).
- Radford et al. Robust Speech Recognition via Large-Scale Weak Supervision (Whisper) (2022).
- Graves et al. Connectionist Temporal Classification (2006) и Sequence Transduction with RNN (2012).
- Gulati et al. Conformer: Convolution-augmented Transformer for Speech Recognition (2020).
- Baevski et al. wav2vec 2.0 (2020).
- Défossez et al. High Fidelity Neural Audio Compression (EnCodec) (2022).
- Liang et al. Mind the Gap: Understanding the Modality Gap (2022).
- Li et al. Evaluating Object Hallucination in LVLMs (POPE) (2023).
- Bagdasaryan et al. Abusing Images and Sounds for Indirect Instruction Injection (2023).
- Библиотеки и данные: transformers, open_clip, faster-whisper, WhisperX, LAION-5B.
Что дальше
На этом трек «Нейронные сети» завершён. Путь получился такой: от перцептрона и обратного распространения через свёртки, рекуррентность, трансформеры и LLM к масштабному обучению, деплою, безопасности и моделям, которые работают со всеми модальностями сразу. Если хочется освежить общую картину — вернитесь к карте трека.
Куда двигаться дальше:
- Фундамент. Калибровка, сдвиг распределения, метрики и срезы — это классический ML, доведённый до предела. Систематически он разобран в треке Машинное обучение.
- Алгоритмическая база. Чтобы уверенно оценивать сложность и писать эффективный инференс, полезно вернуться к Алгоритмам и Структурам данных.
- Прикладной слой. Как строить продукты поверх готовых моделей — промптинг, RAG, агенты, дообучение: трек ИИ-инженерия.
- Инженерия сервисов. ML-система на 90 % состоит из обычного бэкенда: очереди, таймауты, обсервабилити. См. Go и Архитектурные паттерны.
- Данные. Пайплайны, качество и происхождение данных — Data Engineering.
- Процесс. Как встроить evals и релизы моделей в работу команды — Управление продуктом и Управление проектами.
Полная карта портала с рекомендованным порядком изучения — в Роадмапе.