RecSys · учебник
Тренажёр Виджеты Повторение О проекте Все главы ← Две башни ANN →

Часть II · Кандидатогенерация · глава 8 из 19

Кодирование объектов: обучаемые эмбеддинги против контентных

Свободная таблица эмбеддингов — самая мощная модель объекта, какая вообще возможна. Вся глава про то, что этой мощностью почти невозможно воспользоваться, и про единственный работающий выход: ограничить модель так, чтобы редкие объекты наследовали знание от частых. Ключевой аргумент — «хвост признаков короче и толще хвоста айтемов» — здесь не заявлен, а проверен.

Что унести из главы
  • ID-эмбеддинги — верхняя граница выразительности. Что бы ни выдал контентный энкодер, таблица способна выдать то же самое. Обратное неверно.
  • И именно поэтому они не работают на хвосте. У 97.7% айтемов свободных параметров больше, чем наблюдений о них — задача недоопределена.
  • Хвост признаков действительно короче. Наблюдений меньше десяти: у 74.2% айтемов и у 0.0% признаков. Медиана — 4.5 против 1275, разница в 283 раза.
  • Переобучение здесь резкое, а не плавное: качество растёт первую эпоху и обрывается в начале второй.

1. Обучаемые эмбеддинги

Что значит «привязаны к идентификатору»

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

$$ h(i) \;=\; E\bigl[\mathrm{id}(i)\bigr], \qquad E \in \mathbb{R}^{|\mathcal{I}| \times k} $$

Таблица \(E\) — это \(|\mathcal{I}|\) строк по \(k\) чисел, и строка \(j\) обучается только тогда, когда айтем \(j\) попал в батч. Никакой другой информации об объекте модель не получает: ни названия, ни картинки, ни категории. Единственное, что она знает, — в какой строке он лежит.

Проверка на понимание

Возьмём два товара с абсолютно одинаковым описанием, но разными id. У ID-эмбеддингов их векторы окажутся никак не связаны — они обучались независимо, и совпадение содержимого модели попросту недоступно.

Более того: перезалили товар с новым id — всё накопленное о нём обнулилось.

Противоположный подход — вектор вычисляется из признаков: \(h(i) = f_\theta(\mathrm{features}(i))\). Здесь id не участвует вовсе, и два одинаковых товара получат одинаковые векторы автоматически.

Почему это самая мощная модель объекта

Утверждение сильнее, чем «работает хорошо»

У каждого объекта \(k\) собственных, ни с чем не связанных параметров. Значит, любое расположение векторов достижимо: нет ни одного ограничения на то, где окажется айтем \(i\) относительно айтема \(j\). Взяв \(k = |\mathcal{I}|\), можно точно воспроизвести любую матрицу похожестей.

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

То есть inductive bias у ID-эмбеддингов нулевой. Это верхняя граница мощности — и одновременно источник всех проблем ниже: модель без ограничений нечем удержать от запоминания шума.

Практическое следствие, которое стоит помнить

На голове каталога ID-эмбеддинги обычно выигрывают у любого контентного представления: они выучивают то, чего в признаках нет вовсе. Два внешне одинаковых товара могут по-разному конвертить из-за продавца, цены, отзывов, фотографии — таблица это подхватит, энкодер по названию нет.

Поэтому в проде ID-признаки почти никогда не выбрасывают целиком. Итоговая конструкция почти всегда гибридная, и вопрос не «что вместо чего», а «в какой пропорции».

2. Почему мощностью нельзя воспользоваться

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

Сколько именно «мало»

Каталог 10 млн, миллиард взаимодействий, размерность 256, популярность по Ципфу с \(\alpha = 1\):

Наблюдений об айтемеДоля каталогаИх доля во взаимодействиях
меньше 1040.1%3.1%
меньше 10094.0%16.9%
меньше 256 (= размерности)97.7%22.5%

Числа воспроизводятся скриптом _tools/tail_table.py в этом репозитории.

Последняя строка — приговор: у 97.7% айтемов свободных параметров больше, чем наблюдений о них. Для такой строки задача недоопределена, и «обучение» превращается в подгонку под несколько точек.

Минус 1. Трансдуктивность

Модель применима только к объектам обучающей выборки. Появился новый айтем — в таблице для него либо нет строки, либо есть строка из случайной инициализации, которой ни разу не коснулся градиент. Взять значение неоткуда: вычислить не из чего, признаков модель не принимает.

И случайный вектор в обученном пространстве — не «нейтральный»: он окажется случайно близок к какому-то кластеру и получит осмысленно выглядящие, но бессмысленные скоры.

Проблема шире, чем «новый товар»: новые пользователи — то же самое с другой стороны матрицы; смена идентификатора обнуляет накопленное; редкие категории формально не холодные, но с двумя-тремя наблюдениями ведут себя так же.

Минус 2. Меморизация и тяжёлый хвост

Что такое меморизация формально

Запоминание отдельных примеров вместо вывода общей закономерности. Измеряется через влияние примера: обучим две модели — с ним и без него — и сравним ошибку на этом примере.

  • разница маленькая → модель обобщилась: в данных есть похожие, и знание пришло от них;
  • разница большая → похожих нет, и модель его просто запомнила.

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

Вывод, который стоит запомнить дословно

В тяжёлом хвосте находятся айтемы, у которых мало примеров. Мы можем их запомнить, но сложно их порекомендовать.

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

А отличить адекватный редкий пример от шума модели трудно: снаружи они выглядят одинаково — одно наблюдение без похожих. Свободные эмбеддинги при этом сильно увеличивают способность к меморизации. Сильная меморизация плюс шумные данные — гарантированное переобучение.

Минус 3. Дрейф

Почему таблица не умеет «сейчас»

Строка \(E[j]\) обновляется каждый раз, когда айтем попадает в батч, и градиенты со всех эпизодов складываются в один вектор. При обучении на перемешанных данных взаимодействия годовой давности и вчерашние вносят одинаковый вклад — получается усреднённое представление объекта за всё время.

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

Катастрофическое забывание

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

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

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

Минус 4. Память

Арифметика, которая закрывает вопрос

Откуда берутся десятки миллиардов эмбеддингов: это не только айтемы. В промышленной модели обучаемые векторы заводят на всё категориальное — id айтема, пользователя, автора, рекламного блока, страну, модель устройства, — а также на кросс-признаки вроде пары «пользователь × категория». Число строк перемножается, а не складывается.

$$ 2\cdot10^{10} \times 256 \times 4\ \text{байта} \;\approx\; \mathbf{20{,}5\ \text{ТБ}} $$

С моментами Adam (два тензора того же размера) — 61 ТБ. При размерности 4096, как модно в LLM, — 328 ТБ весов и почти петабайт вместе с оптимизатором.

Пересчитывается скриптом _tools/tail_table.py.

Чем это оборачивается инженерно
  • На GPU таблица не помещается — ни на одну, ни на узел. Значит либо шардирование по устройствам, либо хранение в RAM и на parameter servers с подкачкой нужных строк на батч.
  • Обучение упирается в сеть, а не в вычисления. Модель поверх эмбеддингов может быть крошечной; узкое место — перекладывание строк между узлами. Это переворачивает привычную оптимизацию: считать быстрее бессмысленно, надо меньше пересылать.
  • Основную часть терабайт занимает хвост, который мы всё равно только запоминаем. Платим памятью за строки, не приносящие обобщения.

Минус 5. Переобучение и one-epoch phenomenon

Что именно наблюдают

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

Практический вывод, который из этого следует в проде: лучший результат обычно даёт обучение ровно в одну эпоху. Многие промышленные пайплайны так и устроены — один проход по потоку.

Три фактора и гипотеза о причине

Эффект связывают с архитектурой (embedding + MLP), быстро сходящимся оптимизатором и разреженностью признаков. Последнее главное: чем мельче гранулярность признака — а id айтема мельче некуда, — тем сильнее эффект.

Механизм: обозначим представление примера после слоя эмбеддингов как \(\mathrm{EMB}(x)\). Совместное распределение \(\bigl(\mathrm{EMB}(x), y\bigr)\) разное для примеров, которые модель уже видела, и для новых: у виденных эмбеддинг успел подстроиться под их метку. На второй эпохе все обучающие примеры становятся виденными, MLP быстро адаптируется именно к этому распределению — и теряет применимость к новым.

То есть модель переобучается не на примеры в обычном смысле, а на артефакт собственной таблицы эмбеддингов.

Почему нельзя просто «уменьшить разреженность»

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

Логика знакомая: редкие признаки одновременно источник переобучения и источник того самого сигнала, ради которого ID-эмбеддинги и заводили. Выбросив хвост, мы убираем переобучение вместе с пользой.

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

3. Inductive bias

Ограничения на модель, увеличивающие обобщающую способность. Уменьшаем мощность, растим смещение и уменьшаем дисперсию, снижаем меморизацию — а значит и переобучение на шум.

Простейший пример: мешок слов

Пусть айтемы — товары с названиями. Токенизируем название, берём обучаемые эмбеддинги токенов, усредняем:

$$ h(i) = \frac{1}{|T_i|}\sum_{t \in T_i} e_t $$

Какие предположения мы этим зашили? Первое: у товаров с похожими названиями похожие векторы. Второе: порядок слов не влияет.

Второе предположение неверно — «чехол для айфона» и «айфон для чехла» не одно и то же. Но это и есть плата за обобщение: ограничение неточно, зато редкий товар получает вектор из частых токенов.

Проверка интуиции

Три модели: (1) свободные эмбеддинги, (2) энкодер над одним признаком, (3) энкодер над двумя. Где inductive bias больше?

Ответ: 1 < 3 < 2. У свободных эмбеддингов bias наименьший — они ничем не ограничены. А модель с двумя признаками имеет больше степеней свободы, чем с одним, поэтому её bias меньше, чем у второй.

Полезное правило: больше признаков — меньше inductive bias, потому что модель может опереться на большее число различающих сигналов и снова начать «узнавать» конкретные объекты.

4. Контентное кодирование: проверяем главный аргумент

Аргумент в пользу контента формулируют так: хвост признаков короче и толще хвоста айтемов. Звучит как метафора, но это проверяемое утверждение.

Два хвоста рядом

Каталог 100 000 по Ципфу с \(\alpha = 1.2\), десять миллионов взаимодействий. Каждый айтем описан шестью признаками из словаря в 5 000 — то есть признаки переиспользуются между айтемами.

Наблюдений меньшеДоля айтемовДоля признаков
1074.2%0.0%
10096.2%0.1%
256 (= размерности)98.3%3.7%

Числа воспроизводятся скриптом _tools/content_tail.py в этом репозитории.

Медиана наблюдений: у айтема 4.5, у признака 1275 — в 283 раза больше.

И самая наглядная строчка: у самого редкого айтема каталога ожидается 1.96 наблюдения, а его шесть признаков видели 479, 6 088, 2 528 025, 536 660, 3 446 и 6 088 раз. Про сам айтем не известно ничего, а про то, из чего он состоит, — всё.

Плюс экономия параметров

Свободная таблица — 100 000 строк, из них у 98.3% наблюдений меньше размерности. Контентная — 5 000 строк, таких только 3.7%. Параметров в двадцать раз меньше, и каждый обучен на несравнимо большем объёме.

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

Чем за это платят
  • Потолок на голове. Два товара с одинаковым описанием получат одинаковые векторы, даже если данных достаточно, чтобы знать: конвертят они по-разному.
  • Качество признаков становится критичным. Плохая токенизация или бедная таксономия ограничивают модель сильнее, чем любая архитектура.
  • Дороже на инференсе. Вектор надо вычислить, а не достать из таблицы. Для айтемов лечится предподсчётом, для пользовательских признаков — не всегда.

Поэтому в проде почти всегда гибрид: контентная часть даёт обобщение и холодный старт, ID-часть добирает то, чего в признаках нет. Пропорция подбирается экспериментом.

5. Хеширование: компромисс между тем и другим

Промежуточный приём, который заслуживает отдельного разбора, потому что встречается везде.

Идея и цена

Вместо таблицы «id → строка» берём \(h(\mathrm{id}) \bmod H\), где \(H\) фиксировано. Размер таблицы перестаёт зависеть от кардинальности признака.

Цена — коллизии: несколько id делят одну строку и становятся для модели неразличимы. Вероятность того, что конкретная пара столкнётся, равна \(1/H\); при нескольких независимых хеш-функциях полная неразличимость требует совпадения по всем, и вероятность становится произведением.

И второй, менее очевидный выигрыш: новый объект представим немедленно. Не нужно ждать, пока его id попадёт в словарь и словарь пересоберут — хеш считается сразу. Для быстро обновляющегося каталога это не оптимизация, а необходимое условие.

Что здесь надо увидеть
  1. Уменьшайте размер таблицы: доля айтемов, делящих строку хоть с кем-то, растёт быстрее, чем кажется — это парадокс дней рождения.
  2. Добавьте вторую хеш-функцию: вероятность полной неразличимости падает как произведение, а память растёт линейно. Обмен выгодный.
  3. Смотрите, кого коллизии задевают: популярные айтемы страдают меньше, потому что у них достаточно данных, чтобы «перетянуть» общую строку на себя. Расплачивается за коллизии снова хвост.

Что сказать на собесе: «Хеширование меняет память на коллизии и снимает зависимость от кардинальности. Несколько хеш-функций делают полную неразличимость произведением вероятностей. И оно даёт представимость нового объекта без пересборки словаря».

Вопросы с собеседований

Почему ID-эмбеддинги называют самой мощной моделью объекта?

У каждого объекта \(k\) собственных, ни с чем не связанных параметров, поэтому достижимо любое расположение векторов; при \(k = |\mathcal{I}|\) воспроизводится любая матрица похожестей.

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

То есть inductive bias у них нулевой. Это верхняя граница выразительности и одновременно причина всех проблем: модель нечем удержать от запоминания шума.

Почему свободные эмбеддинги плохо работают на хвосте?

Потому что строка таблицы обучается только на примерах с этим айтемом, и связать её со строками похожих нечем. Арифметика: при каталоге 10 млн, миллиарде взаимодействий и размерности 256 у 97.7% айтемов свободных параметров больше, чем наблюдений о них.

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

Что такое one-epoch phenomenon и почему нельзя просто уменьшить разреженность?

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

Гипотеза о причине: распределение \((\mathrm{EMB}(x), y)\) различается для виденных и новых примеров, потому что у виденных эмбеддинг подстроился под метку. На второй эпохе MLP адаптируется именно к распределению виденных и теряет применимость к новым. То есть переобучение идёт на артефакт собственной таблицы.

Разреженность снизить можно — отфильтровать редкие значения или захешировать в меньше бакетов, — и эффект действительно ослабевает. Но это часто ухудшает итоговое качество: редкие признаки одновременно источник переобучения и источник сигнала, ради которого их заводили.

Почему контентное кодирование вытягивает хвост?

Потому что хвост признаков короче и толще хвоста айтемов, и это проверяемо. На каталоге 100 тысяч с шестью признаками из словаря в 5 тысяч: наблюдений меньше десяти у 74.2% айтемов и у 0.0% признаков. Медиана — 4.5 у айтема против 1275 у признака, разница в 283 раза.

Механизм в переиспользовании: редкий айтем собран из частых признаков. У самого редкого айтема каталога ожидается две единицы наблюдений, а его признаки видели от 479 до 2.5 миллиона раз. Про сам айтем не известно ничего, про то, из чего он состоит, — всё.

Плюс экономия: 5 тысяч строк вместо 100 тысяч, и у 3.7% из них наблюдений меньше размерности вместо 98.3%.

Чем платят за переход на контентные признаки?

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

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

Поэтому в проде гибрид: контент даёт обобщение и холодный старт, ID добирает то, чего в признаках нет. Вопрос не «что вместо чего», а в какой пропорции.

Что даёт hashing trick, кроме экономии памяти?

Главное — представимость нового объекта немедленно. Не надо ждать, пока id попадёт в словарь и словарь пересоберут: хеш считается сразу. Для быстро обновляющегося каталога это не оптимизация, а условие работоспособности.

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

И важная деталь: коллизии бьют по хвосту. У популярного айтема достаточно данных, чтобы перетянуть общую строку на себя; у редкого — нет.

Шпаргалка одним экраном

ID-эмбеддинг

\(h(i) = E[\mathrm{id}(i)]\). Нулевой inductive bias, верхняя граница мощности, ноль обобщения между объектами.

Арифметика хвоста

У 97.7% айтемов параметров больше, чем наблюдений. Задача недоопределена.

Пять минусов

Трансдуктивность, меморизация, дрейф с забыванием, память в терабайтах, обрыв на второй эпохе.

Два хвоста

Меньше 10 наблюдений: у 74.2% айтемов и 0.0% признаков. Медианы 4.5 против 1275.

Плата за контент

Потолок на голове, зависимость от качества признаков, дороже инференс. Отсюда гибрид.

Хеширование

Память за коллизии плюс мгновенная представимость нового. Коллизии бьют по хвосту.

Первоисточники