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

Часть III · Ранжирование · глава 12 из 19

Взаимодействия признаков

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

Что унести из главы
  • Явные кросс-признаки не выживают дважды. При суммарной кардинальности 3 млн это 18 ТБ весов — и, что важнее, вес невиданной комбинации не обучен вовсе.
  • Факторизация чинит обе проблемы одним движением. 384 МБ вместо 18 ТБ, в 47 тысяч раз меньше, плюс обобщение по транзитивности.
  • MLP не выучивает произведение сам — это главный сюжет главы. Универсальность аппроксимации говорит о существовании весов, а не о том, что их найдёт градиентный спуск на разреженных данных.
  • По умолчанию берут DCN-v2. Всё, что левее в цепочке, — история; всё, что правее, — исследовательский фронт.

1. Цепочка из шести шагов

каждый шаг — ответ на конкретную поломку предыдущего линейная пар нет вовсе ? кросс-признаки O(n²), не обобщает FM только 2-я степень ранние сети MLP не находит пары вписать умножение в архитектуру DCN-v2 продовый стандарт трансформеры дорого, не всегда лучше ← история фронт →
Практический итог: по умолчанию DCN-v2. Знать надо всю цепочку, применять — её середину.

2. Линейная модель и кросс-признаки

Почему линейная модель не видит сочетаний

Линейная модель на конкатенации one-hot представлений равносильна тому, что каждому значению признака сопоставили обучаемый скаляр, а потом сложили скаляры реализовавшихся значений:

$$ \hat y = w_0 + \sum_{i} w_i x_i $$

Взаимодействий здесь нет вовсе — вклад «Россия» одинаков независимо от жанра. Добавим все кросс-признаки второй степени, то есть комбинации вида «language=ru, genre=rock»:

$$ \hat y = w_0 + \sum_i w_i x_i + \sum_{i < j} w_{ij}\, x_i x_j $$

Формально задача решена. Практически — сломана в двух местах.

Поломка первая: размер

Параметров \(O(n^2)\), где \(n\) — суммарная кардинальность всех признаков:

Суммарная кардинальностьВесовПамять во float32
1e+045.00e+07200.0 МБ
1e+055.00e+0920.0 ГБ
1e+065.00e+112.0 ТБ
3e+064.50e+1218.0 ТБ

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

Три миллиона суммарной кардинальности — это скромно по меркам рекомендаций. Восемнадцать терабайт весов ради взаимодействий второй степени.

Поломка вторая, и она серьёзнее: обобщения нет

Если комбинация не встречалась в обучении, соответствующий \(w_{ij}\) просто не обучен. Про невиданные сочетания модель не может сказать вообще ничего.

Насколько это критично? Возьмём 30 полей по 100 000 значений и датасет на миллиард сэмплов:

  • пар полей: 435;
  • возможных комбинаций значений: 4.35e+12;
  • наблюдений пар во всём датасете: 4.35e+11.

Даже в идеальном случае, когда все наблюдения различны, хотя бы раз встретится не более 10% комбинаций. А распределение степенное (глава 1), так что реальная доля на порядки меньше.

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

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

3. Factorization Machines

Факторизуем матрицу весов

Решение: каждому признаку дадим вектор, а вес пары получим скалярным произведением.

$$ w_{ij} \approx \langle v_i, v_j\rangle \qquad\Longrightarrow\qquad \hat y = w_0 + \sum_i w_i x_i + \sum_{i < j} \langle v_i, v_j\rangle\, x_i x_j $$

Одно движение чинит обе поломки сразу — и это стоит проговаривать именно так, а не как два отдельных достоинства.

  • Память \(O(nd)\) вместо \(O(n^2)\). Матрица \(V\) — это просто эмбеддинги признаков, ровно те же, что в предыдущей главе.
  • Обобщение на невиданные комбинации. Чтобы оценить вес пары \((i,j)\), не нужно было видеть именно эту пару: достаточно, что \(v_i\) обучился на других парах с участием \(i\), а \(v_j\) — на парах с \(j\).

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

Во сколько раз дешевле и почему это работает

При суммарной кардинальности 3 млн: явно 4.50e+12 весов (18.0 ТБ), факторизационная машина с \(d = 32\) — 9.60e+07 (384.0 МБ). Разница в 47 тысяч раз.

Но интереснее посмотреть на то же самое со стороны информации. Сколько независимых чисел нужно определить:

Признаков \(n\)Всего парПараметров FM при \(d=16\)Доля
1004 9501 60032.32%
1000499 50016 0003.20%
1000049 995 000160 0000.32%

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

При тысяче признаков информации от 3.2% пар достаточно, чтобы восстановить остальные. Вот это и есть обобщение — не метафора, а счётный факт про число степеней свободы.

Field-aware FM идёт дальше: у признака не один вектор, а по вектору на каждое поле, с которым он взаимодействует. Историческая справка, которую иногда спрашивают: FFM выиграла Kaggle Criteo CTR Prediction Challenge.

4. Ранние нейросетевые архитектуры

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

МодельИдеяЧто нового
Concat + MLP конкатенируем эмбеддинги категориальных и нормализованные вещественные, сверху MLP простейшая ранжирующая сеть; взаимодействия только неявные
Wide & Deep линейная модель с вручную отобранными кросс-признаками плюс сеть; предсказание — сумма явное разделение ролей: wide отвечает за меморизацию и взаимодействия до второго порядка, deep — за обобщение и высокие порядки
DeepFM заменили ручные кросс-признаки на факторизационную машину убрали ручной feature engineering; эмбеддинги переиспользуются и в FM, и в сети
DLRM нижний MLP превращает вещественные признаки в единый эмбеддинг, верхний работает над конкатенацией попарных скалярных произведений явные попарные взаимодействия без \(O(n^2)\) параметров

Детали Wide & Deep, которые полезно помнить как образец инженерных решений своего времени: эмбеддинги размера 32 для категориальных, CDF-преобразование для вещественных, конкатенация в вектор размерности 1200, три слоя с ReLU.

Общее ограничение DeepFM и DLRM

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

А в промышленных данных кардинальности варьируются от десятков до миллионов, и единая размерность заведомо неоптимальна: мы только что видели, что размер должен расти с кардинальностью. Это ограничение и снимет DCN-v2.

5. Почему MLP не выучивает произведение сам

Ключевой вопрос главы, и ответ у него неочевидный настолько, что его стоит выучить дословно.

Универсальность — это не про обучаемость

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

Произведение \(x_i x_j\) для MLP — неудобная функция: он строит её кусочно-линейными сплайнами, и на это уходит много нейронов и много данных. Rendle и соавторы показали это прямо: выученная MLP-похожесть проигрывает обычному скалярному произведению.

А в рекомендациях положение хуже, чем в среднем по машинному обучению: признаки разреженные, конкретная комбинация встречается редко, и «выучить произведение по данным» просто не на чем — мы посчитали выше, что не наберётся и 10% комбинаций.

Отсюда вся дальнейшая линия: не надеяться на MLP, а вписать умножение в архитектуру.

6. DCN-v2: умножение как часть слоя

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

Кросс-слой
$$ x_{l+1} = x_0 \odot \bigl(W_l x_l + b_l\bigr) + x_l $$

Разберём по частям, потому что в этой строчке всё существенное.

  • \(W_l x_l\) — линейная комбинация всей конкатенации, то есть внутри неё можно получить скалярное произведение любых векторов признаков. Значит любые пары, независимо от их размерностей — ограничение DeepFM и DLRM снято.
  • \(x_0 \odot (\cdot)\) — покоординатное умножение на исходный вход. Это и есть вписанное в архитектуру произведение: каждый слой поднимает степень на единицу.
  • \(+\, x_l\) — остаточная связь, о которой отдельный разговор в следующем разделе.

\(L\) кросс-слоёв моделируют все комбинации степени до \(L+1\).

Откуда берутся diminishing returns
Слоёв \(L\)СтепеньРазличных мономов при 100 признаках
124.950e+03
231.617e+05
343.921e+06
457.529e+07

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

Число комбинаций растёт быстрее, чем данные способны их покрыть: на четвёртой степени это уже 3.92 млн мономов при всего сотне признаков. Отсюда и наблюдаемое на практике правило — больше 2–3 слоёв прироста не дают.

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

Низкоранговый кросс-слой

Кросс-слой вычислительно тяжёл: матрица \(W_l\) имеет размер \(d \times d\), где \(d\) — размерность всей конкатенации. При \(d = 1024\) и трёх слоях это 3.15e+06 параметров.

Лечение — факторизация \(W_l \approx U_l V_l^{\top}\) с узкими матрицами:

Ранг \(r\)ПараметровДешевле вДоля от полной
169.83e+0432.03.1%
321.97e+0516.06.2%
643.93e+058.012.5%
1287.86e+054.025.0%

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

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

Три практических вывода, о которых спрашивают
  1. Stacked обычно лучше parallel. То есть cross network сначала, MLP над его выходом, а не две ветки в сумму. Перед MLP широкий вектор из cross network сужается.
  2. Cross network не даёт прироста поверх абстрактных векторных представлений. Его нужно применять именно над конкатенацией векторов признаков. Деталь неочевидная и важная: если подать на вход уже «перемешанное» представление — скажем, выход трансформера, — умножать оказывается нечего, координаты уже не соответствуют признакам.
  3. Разделение труда, а не конкуренция. Cross network явно моделирует взаимодействия низкого порядка, MLP — неявно и высокого. Они дополняют друг друга, и потому оба и стоят в архитектуре.

7. Трансформеры над признаками

DCN-v2 моделирует взаимодействия одинаково для всех пар: одна матрица \(W_l\) на слой. Следующий вопрос напрашивается — а нельзя ли решать, какие пары важны, динамически, как это делает внимание? Ответ пока «можно, но не очевидно, что лучше».

МодельМеханизмВердикт
AutoIntmulti-head attention над эмбеддингами признаков со skip-связью, сверху MLPработает хуже DCN-v2
Hiformerгетерогенное внимание: раздельные \(Q, K, V\) для каждого признака и свой FFN на признак; плюс композитные признакирешает реальную проблему, но слои требуют очень много памяти
Field-aware Transformerдля каждой пары признаков свой обучаемый коэффициент внутри вниманияперенос идеи FFM в трансформер
RankMixerвнимание заменено простым перемешиванием токенов плюс FFN на токендешевле внимания, линия развивается
Почему обычный трансформер не подходит для признаков

Хороший вопрос на собеседовании, и ответ короткий. Трансформер обрабатывает все токены одинаково. Для текста это правильно — токены однородны по природе.

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

Отсюда и устройство Hiformer: раздельные проекции и свой FFN на каждый признак. Проблема настоящая, а цена решения — память.

8. Почему нельзя просто добавить слоёв

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

Насколько именно затухают

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

СлоёвОбычный MLP: \(0.8^L\)Путь по skip-связямВерхняя оценка \((1+0.8)^L\)
44.10e-011.001.05e+01
81.68e-011.001.10e+02
162.81e-021.001.21e+04
327.92e-041.001.47e+08

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

При 32 слоях градиент падает в 1262 раза — нижние слои практически не обучаются.

В остаточной сети якобиан блока равен \(I + F'\), и в раскрытии произведения есть слагаемое из одних единиц — путь целиком по skip-связям. Поэтому градиент снизу ограничен единицей и не затухает вовсе. Зато сверху растёт, и ровно поэтому skip connection всегда идёт в паре с нормализацией.

Наблюдение, которое стоит рассказать

При разработке первой двухбашенной трансформерной модели для рекомендаций был замечен такой эффект: если башней айтема сделать MLP, сохраняется около половины прироста метрик; если заменить MLP на трансформер над одним токеном — около 80%.

А что такое трансформер над одним токеном? Внимание над последовательностью длины 1 вырождается в тождественное преобразование. Значит от трансформера остаются только skip connections и нормализация вокруг FFN — и весь дополнительный выигрыш дали именно они.

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

Две конструкции: сумма против конкатенации

ResNet: \(y = F(x) + x\). Градиент течёт напрямую через сумму; лишний слой можно «занулить» — при \(F \equiv 0\) он становится тождественным, и глубина перестаёт вредить. Проблема: сумма требует совпадения размерностей, поэтому слои получаются тяжёлыми.

DenseNet: вместо суммы конкатенация, \(x \to [x; F_1(x)] \to [x; F_1(x); F_2(\cdot)] \to \dots\) Размерности совпадать не обязаны, значит \(F\) может быть дешёвым сужающим слоем; каждый новый слой добавляет детализацию, а исходный вход остаётся доступен на всех уровнях.

Что это даёт по параметрам

Вход размерности 512, четыре блока:

СлоиПараметров
ResNet512 × 512 каждый1.05e+06
DenseNet, рост 64512×64, 576×64, 640×64, 704×641.56e+05

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

В 6.7 раза меньше параметров, и на выходе размерность 768 вместо 512. Плата — растущая ширина: каждый блок добавляет к входу следующего, и на глубоких сетях это само становится проблемой.

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

Расскажите цепочку моделей взаимодействий признаков.

Шесть шагов, каждый чинит поломку предыдущего. Линейная модель — взаимодействий нет вовсе. Кросс-признаки второй степени — есть, но \(O(n^2)\) параметров и никакого обобщения на невиданные комбинации. FM — факторизуем матрицу весов, память \(O(nd)\) и обобщение по транзитивности, но только вторая степень. Ранние сети (Wide & Deep, DeepFM, DLRM) — ставим MLP сверху, и выясняется, что пары он сам не находит. DCN-v2 — вписываем умножение прямо в слой. Трансформеры над признаками — пытаемся выбирать важные пары динамически.

Практический итог: по умолчанию берут DCN-v2. Всё левее — история, всё правее — исследовательский фронт.

Что не так с явными кросс-признаками?

Две вещи, и вторая серьёзнее. Размер: параметров \(O(n^2)\) по суммарной кардинальности; при n = 3 млн это 4.5 триллиона весов, 18 ТБ во float32 — и это ради взаимодействий всего второй степени.

Обобщение: вес невиданной комбинации не обучен вовсе, а невиданных подавляющее большинство. При 30 полях по 100 тысяч значений и миллиарде сэмплов даже в идеальном случае встретится не более 10% комбинаций, а с учётом степенного распределения — на порядки меньше. Это проблема представления, а не масштаба: железом её не решить.

Как FM решает обе проблемы?

Факторизацией матрицы весов: \(w_{ij} \approx \langle v_i, v_j \rangle\). Каждому признаку — вектор, вес пары — скалярное произведение.

Память падает с \(O(n^2)\) до \(O(nd)\): при n = 3 млн и d = 32 это 384 МБ вместо 18 ТБ, в 47 тысяч раз меньше.

Обобщение возникает по транзитивности: чтобы оценить вес пары (i, j), достаточно, чтобы \(v_i\) обучился на других парах с i, а \(v_j\) — на парах с j. Счётно это выглядит так: при 1000 признаков всего 499 500 пар, а параметров FM при d = 16 всего 16 000, то есть 3.2% — информации от такой доли пар хватает, чтобы восстановить остальные.

Почему нельзя просто поставить MLP и надеяться, что он выучит произведения?

Потому что универсальность аппроксимации — утверждение о существовании весов, а не о том, что градиентный спуск найдёт их на конечных данных.

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

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

Как устроен кросс-слой DCN-v2?

\(x_{l+1} = x_0 \odot (W_l x_l + b_l) + x_l\). Три части. \(W_l x_l\) — линейная комбинация всей конкатенации, поэтому внутри можно получить скалярное произведение любых векторов признаков независимо от их размерностей: ограничение DeepFM и DLRM на единую размерность снято. Покоординатное умножение на \(x_0\) — вписанное в архитектуру произведение, каждый слой поднимает степень на единицу. Плюс остаточная связь.

L слоёв дают комбинации степени до L+1, но больше 2–3 слоёв прироста не дают: число мономов растёт быстрее, чем данные их покрывают — на четвёртой степени при сотне признаков это уже 3.9 млн комбинаций.

Слой тяжёлый: \(W_l\) размера d × d по всей конкатенации. Отсюда низкоранговый вариант \(W_l \approx U_l V_l^\top\) — при d = 1024 и r = 64 в 8 раз дешевле — и дальше смесь низкоранговых разложений.

Как комбинировать cross network с MLP?

Stacked обычно работает лучше parallel: сначала cross network, потом MLP над его выходом, с сужением широкого вектора перед MLP.

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

И разделение труда: cross явно моделирует низкие порядки, MLP неявно — высокие. Они дополняют друг друга, а не конкурируют.

Почему обычный трансформер плохо подходит для признаков?

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

Отсюда Hiformer с раздельными Q, K, V и своим FFN на признак — проблема настоящая, но слои требуют очень много памяти. AutoInt, применяющий обычное внимание, работает хуже DCN-v2.

Почему добавление слоёв в MLP ухудшает качество и что с этим делают?

Градиенты затухают: множители якобианов перемножаются. Если каждый слой умножает градиент на 0.8, то при 32 слоях остаётся 7.9e-04 — падение в 1262 раза, нижние слои практически не обучаются.

ResNet: \(y = F(x) + x\). Якобиан блока это \(I + F'\), и в произведении есть слагаемое из одних единиц — путь целиком по skip-связям, поэтому снизу градиент ограничен единицей. Заодно лишний слой можно занулить: при \(F \equiv 0\) он тождественный. Сверху произведение растёт, поэтому skip всегда идёт с нормализацией. Минус — сумма требует совпадения размерностей, слои тяжёлые.

DenseNet: вместо суммы конкатенация, размерности совпадать не обязаны, F может быть дешёвым сужающим слоем. При входе 512 и четырёх блоках с ростом 64 это 1.56e+05 параметров против 1.05e+06 у ResNet — в 6.7 раза меньше. Плата — растущая ширина.

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

Цепочка

линейная → кросс → FM → ранние сети → DCN-v2 → трансформеры. По умолчанию DCN-v2.

Кросс-признаки

\(O(n^2)\): 18 ТБ при n=3e6. И вес невиданной пары не обучен — а таких больше 90%.

FM

\(w_{ij}=\langle v_i,v_j\rangle\). 384 МБ вместо 18 ТБ; 3.2% пар определяют все остальные.

Главный тезис

Универсальность ≠ обучаемость. Произведение MLP строит сплайнами, а данных на это нет.

Кросс-слой

\(x_0 \odot (W_l x_l + b_l) + x_l\). L слоёв → степень L+1. Разные размерности разрешены.

Низкий ранг

\(W_l \approx U_lV_l^\top\): при d=1024, r=64 дешевле в 8 раз. Дальше — смесь разложений.

Глубина

\(0.8^{32}\) = 7.9e-04. Skip даёт путь с множителем 1 — снизу ограничен, сверху растёт, отсюда нормализация.

ResNet / DenseNet

Сумма требует равных размерностей; конкатенация — нет. 1.56e+05 против 1.05e+06 параметров.

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