Часть III · Ранжирование · глава 11 из 19
Признаки
Лосс выбран — осталось решить, что подавать на вход. В рекомендациях это не рутина: здесь признаковое пространство устроено так, что оно само диктует выбор архитектуры. Разбираем, почему градиентный бустинг проигрывает именно здесь, как устроен эмбеддинг-слой изнутри и что делают с вещественными признаками, прежде чем показать их сети.
- Дерево не умеет складывать координаты. Чтобы воспроизвести одно скалярное произведение в 16 измерениях, осевому дереву нужно 4.29 · 109 листьев; линейному слою — 16 умножений.
- Эмбеддинг — это one-hot, умноженный на матрицу. Никакой отдельной магии нет, а target encoding оказывается частным случаем при \(d = 1\).
- Кодирование может уничтожить информацию безвозвратно. Два айтема с одинаковым средним CTR и разной аудиторией после target encoding неразличимы — минус 50% кликов, и никакой лосс их не вернёт.
- Построчный оптимизатор экономит 20.4 ГБ на одной таблице, а sparse-градиенты трогают 0.041% параметров. Инженерия эмбеддинг-слоя — половина работы.
1. Почему здесь проигрывает бустинг
Вопрос звучит на собеседованиях постоянно и в лоб: «зачем вам нейросеть, возьмите CatBoost». Ответ есть, и он состоит из трёх конкретных пунктов, а не из общих слов про глубокое обучение.
Причина 1: высокая кардинальность
Как дерево кодирует категорию? Есть два способа, и оба ломаются.
- One-hot. Для признака с миллионом значений его просто нельзя построить.
- Target encoding. Строится, но теряет семантику: два разных айтема с одинаковым средним CTR становятся неразличимы, хотя нравятся совершенно разным людям.
Два айтема и два одинаковых по размеру сегмента аудитории:
| Айтем | CTR в сегменте A | CTR в сегменте B | Общий CTR |
|---|---|---|---|
| X | 0.20 | 0.00 | 0.100 |
| Y | 0.00 | 0.20 | 0.100 |
После target encoding оба айтема — одно и то же число 0.100. Модель физически не может их различить.
Показывая каждому сегменту подходящий айтем, получаем CTR 0.200. Не различая айтемы — 0.100. Это потеря 50% кликов, и никакой лосс её не вернёт: информация уничтожена на этапе кодирования признака, до всякого обучения.
Числа воспроизводятся скриптом _tools/features_demo.py в этом репозитории.
В нейросети та же категория кодируется естественно — через эмбеддинг, и двух измерений хватает, чтобы развести X и Y.
Причина 2: неструктурированные данные
Тексты, изображения, аудио, история пользователя. По ним можно нагенерировать признаки для бустинга, но работает это хуже. Можно построить нейросетевые модели и извлечь из них признаки — например, скалярное произведение из двухбашенной модели. И тут выясняется вещь поинтереснее.
Причина 3: эмбеддинг на входе в дерево
Координаты эмбеддинга по отдельности не значат ничего: семантика заложена в вектор целиком. А каждый сплит в дереве анализирует ровно одну координату.
Чтобы различить \(k\) уровней по каждой из \(d\) координат, осевому дереву нужна решётка из \(k^d\) ячеек:
| \(d\) | листьев при \(k=2\) | листьев при \(k=4\) | умножений у линейного слоя |
|---|---|---|---|
| 4 | 16 | 256 | 4 |
| 8 | 256 | 6.55e+04 | 8 |
| 16 | 6.55e+04 | 4.29e+09 | 16 |
| 32 | 4.29e+09 | 1.84e+19 | 32 |
При \(d=16\) и четырёх уровнях на координату это 4.29 · 109 листьев — больше, чем сэмплов в любом датасете, и глубина 32 уровня. Линейный слой делает то же самое за 16 умножений и 15 сложений.
Числа воспроизводятся скриптом _tools/features_demo.py.
А ведь 16 — скромная размерность: реальные эмбеддинги 64–256-мерные. Это не «бустинг чуть хуже», это принципиальная несовместимость представления с моделью.
Честный ответ на собеседовании обязан содержать и эту половину. Главный минус нейросетей — высокий порог входа: нужно уметь обучать сети, работать с GPU, вести распределённое обучение и внедрять всё это в рантайм.
Чек-лист «глубокое обучение оправдано»:
- много данных;
- важны высококардинальные признаки;
- важны неструктурированные данные;
- много сигналов сразу — лайки, досмотры, покупки: в одну модель бустинга их складывать мучительно, а отдельные модели дороги;
- нужно быстро дообучаться под дрейф: бустинг приходится перестраивать целиком, сеть можно дообучать инкрементально.
Если не выполняется ни один пункт — CatBoost на агрегатах будет и дешевле, и лучше. Это не оговорка из вежливости: значительная часть рекомендательных задач именно такова.
Общий фон здесь — bitter lesson Саттона: выигрывают методы, которые масштабируются вычислениями, а не человеческой изобретательностью. NLP и зрение этот переход уже прошли; рекомендации — последняя строчка, где он ещё идёт.
2. Категориальные признаки
Категориальный признак задаётся множеством значений, из которых на каждом сэмпле реализуется одно. Кардинальность в рекомендациях разнится от 2 (утро/вечер) до миллиардов (идентификатор айтема).
Вывод, который стоит уметь показать на доске за двадцать секунд. Представим признак one-hot вектором \(e_i \in \{0,1\}^n\) и применим линейный слой без свободного члена, то есть умножим на матрицу \(W \in \mathbb{R}^{n\times d}\):
$$ e_i W = \bigl(W_{ij}\bigr)_{j=1}^{d} $$Это просто \(i\)-я строка матрицы. Отсюда два вывода:
- \(W\) — матрица обучаемых эмбеддингов, где каждому значению признака сопоставлен свой вектор;
- \(e_i W\) — операция
embedding lookup: вместо умножения на разреженный вектор достаём нужную строку. Никакой отдельной «магии эмбеддингов» нет — есть оптимизация умножения на one-hot.
И приятное следствие, которое любят спрашивать: при \(d = 1\) модель может выучить средний CTR значения, то есть target encoding — частный случай эмбеддинга размерности один. Всё, что даёт эмбеддинг сверх него, — это дополнительные измерения, в которых помещается «кому именно нравится».
Какой размер эмбеддинга брать
Одинаковый размер для всех признаков неоптимален. Он должен зависеть от кардинальности — чтобы описать признак из двух значений, широкий вектор не нужен — и от информативности, то есть от полезности признака для задачи.
Оценка снизу, информационная. Пусть эмбеддинг состоит из \(d\) элементов по \(s\) бит. Всего кодируется \(2^{ds}\) значений, значит чтобы различить \(n\) значений, достаточно
$$ d\,s = \log_2 n $$Практическая эвристика Google: \(d = 6\sqrt[4]{n}\).
| Кардинальность | \(\log_2 n\), бит | \(6\sqrt[4]{n}\) | Память при float32 |
|---|---|---|---|
| 1e+03 | 10.0 | 34 | 0.00 ГБ |
| 1e+05 | 16.6 | 107 | 0.04 ГБ |
| 1e+07 | 23.3 | 337 | 13.50 ГБ |
| 1e+09 | 29.9 | 1067 | 4267.87 ГБ |
Числа воспроизводятся скриптом _tools/features_demo.py.
Разрыв огромный и он содержательный. При \(n = 10^7\) информационная оценка — 23 бита, меньше одного числа float32: чтобы просто различить значения, хватило бы \(d = 1\). Вся остальная ёмкость идёт под семантику, а не под идентификацию.
Строка про \(n = 10^9\) в таблице выше — это 4.3 терабайта на один признак. В проде типичные размерности 32–128, а не 337 и тем более не 1067.
Эвристика полезна как ориентир порядка величины и как напоминание, что размер должен расти с кардинальностью. Более честный путь — dimension optimization: подбирать размеры эмбеддингов прямо в процессе обучения, а не задавать константой.
3. Инженерия эмбеддинг-слоя
Здесь начинается то, что отличает разговор человека, который такое делал, от пересказа статьи. Три приёма, о которых спрашивают.
| Приём | Проблема | Решение |
|---|---|---|
| Batched lookup | отдельный лукап на каждый признак — это много маленьких операций на GPU, а у каждой есть накладные расходы на запуск ядра | объединить эмбеддинги всех признаков в одну матрицу и делать единый лукап со сдвигом, при котором каждый признак попадает в свою область |
| Sparse gradients | на батче встречается лишь малая часть строк, у остальных градиент ровно нулевой | sparse-оптимизатор обновляет только строки с ненулевым градиентом; при распределённом обучении нулевые градиенты не пересылаются вовсе |
| Rowwise optimizer | Adam хранит два момента на каждый параметр — это утроение памяти | хранить две статистики не на параметр, а на строку: память падает почти до размера самой таблицы |
Признак кардинальности 10 млн с эмбеддингом 256:
| сама таблица во float32 | 10.2 ГБ |
| таблица + два момента Adam | 30.7 ГБ (в 3 раза больше) |
| таблица + построчные статистики | 10.32 ГБ — на 0.8% больше таблицы |
| экономия | 20.4 ГБ, или в 3.0 раза |
А про разрежённость: батч на 4096 сэмплов трогает не больше 4096 строк из 10 млн — это 0.0410% таблицы. Пересылать и обновлять надо ровно эту долю параметров, всё остальное — нули.
Числа воспроизводятся скриптом _tools/features_demo.py.
Параметры со sparse-градиентом обновляются сильно реже плотных: строка редкого значения увидит градиент несколько раз за эпоху, а веса MLP — на каждом батче.
Поэтому для эмбеддингов обычно увеличивают learning rate относительно остальной сети. Это не тюнинг ради тюнинга, а компенсация разной частоты обновлений — и частая причина того, что «эмбеддинги почему-то не учатся».
Отдельная большая тема — что делать, когда таблицы не помещаются на GPU: шардирование (по координатам или по значениям признака), выгрузка на CPU с асинхронным обновлением и хеширование. Хеш-трюк и мультихэш разобраны в главе 8 вместе с арифметикой коллизий, здесь не повторяем.
4. Вещественные признаки
Большая часть признаков в рекомендациях — вещественные: счётчики, статистики по множествам, временные статистики вроде «время с последнего покупки в категории», характеристики пользователя и айтема.
Три причины, и все три специфичны именно для нейросетей:
- Масштаб. Сети чувствительны к масштабу: признаки с большими значениями дают большой вклад в градиент и доминируют при обучении.
- Выбросы есть всегда, и они либо ломают обучение, либо сильно ухудшают качество.
- Пропуски сеть не умеет обрабатывать без отдельного решения.
Обратите внимание: у деревьев ни одной из этих проблем нет — они инвариантны к монотонным преобразованиям и умеют обрабатывать пропуски штатно. Вся эта возня возникает как плата за переход к сетям, и на собеседовании это хорошо назвать вслух.
Пять преобразований
1. Логарифм. \(F(x) = \log(x+1)\) — уменьшает скошенность. В реальных данных часто встречаются логнормальные распределения: разница между 1 и 2 важна, а между 100001 и 100002 — уже нет. Логарифм ровно это и выражает.
2. Сигмоида. \(F(x) = \sigma(\gamma x + \beta)\) — зажимает признак в ограниченном диапазоне вместе с выбросами. Важно нормализовать значения перед сигмоидой, иначе производная на краях близка к нулю и градиенты затухают. Можно применить несколько сигмоид с разными обучаемыми \(\gamma, \beta\).
3. Функция распределения. \(F(x) = P(X \le x)\) — сопоставляет значению его квантиль и приводит признак к равномерному на \([0,1]\). Убирает и скошенность, и выбросы одним движением. Считать эмпирическую CDF по всем данным дорого, поэтому используют квантильную аппроксимацию по опорным точкам.
4. Периодические функции.
$$ F(x) = \bigl[\sin(2\pi c_1 x),\, \cos(2\pi c_1 x),\, \sin(2\pi c_2 x),\, \cos(2\pi c_2 x), \dots\bigr] $$Для временных признаков прежде всего. Коэффициенты \(c_i\) могут быть фиксированными (частоты «час», «день», «неделя») или обучаемыми. Механизм тот же, что у позиционных эмбеддингов в трансформере.
5. Квантизация. \(F(x) = \sum_i \mathbb{1}[a_i < x \le b_i]\cdot i\) — превращает вещественный признак в категориальный по квантильным бинам. Недостаток принципиальный: теряется отношение порядка — и между бинами, и внутри бина.
В сыром виде 23 и 0 оказываются максимально далёкими значениями, хотя это соседние часы. Синус и косинус одной частоты кладут время на окружность:
| Пара часов | \(|h_1 - h_2|\) сырое | расстояние на окружности |
|---|---|---|
| 23 и 0 | 23 | 0.2611 |
| 23 и 12 | 11 | 1.9829 |
| 0 и 1 | 1 | 0.2611 |
| 11 и 13 | 2 | 0.5176 |
Расстояние между 23 и 0 становится ровно таким же, как между 0 и 1 — 0.2611, минимальным из возможных. А самой далёкой парой закономерно оказывается 23 и 12, то есть противоположные точки суток.
Числа воспроизводятся скриптом _tools/features_demo.py.
Несколько частот дают несколько окружностей разного масштаба, то есть модель видит и час, и день недели, и сезон одновременно.
Кусочно-линейное кодирование
Квантизация ловит нелинейность, но выдаёт ступеньки: внутри бина значения неразличимы, на границе прогноз прыгает. Идея PLE — кодировать не только номер бина, но и положение внутри него.
Значение \(x\) кодируется вектором длины \(T\) по числу бинов: слева от текущего бина единицы, справа нули, и ровно одна компонента дробная — доля пройденного бина.
$$ \mathrm{PLE}(x)_t = \begin{cases} 1, & x \ge b_t \\[2pt] \dfrac{x - b_{t-1}}{b_t - b_{t-1}}, & b_{t-1} \le x < b_t \\[2pt] 0, & \text{иначе} \end{cases} $$Тогда \(\mathrm{Linear}(\mathrm{PLE}(x)) = v_0 + \sum_t e_t v_t\) даёт непрерывную кусочно-линейную функцию: нелинейность как у one-hot, но без разрывов и без потери разрешения внутри бина.
Приближаем \(f(x) = x^2\) на \([0,1]\) линейной моделью поверх кодирования:
| Бинов | MSE, one-hot по бинам | MSE, кусочно-линейное | Во сколько раз лучше |
|---|---|---|---|
| 2 | 2.649e-02 | 2.081e-03 | 12.7 |
| 4 | 6.893e-03 | 1.301e-04 | 53.0 |
| 8 | 1.741e-03 | 8.130e-06 | 214.1 |
| 16 | 4.358e-04 | 5.081e-07 | 857.6 |
Числа воспроизводятся скриптом _tools/features_demo.py.
Обратите внимание не только на разрыв, но и на то, как он растёт с числом бинов: у кусочно-постоянного кодирования ошибка падает как \(T^{-2}\), у кусочно-линейного — как \(T^{-4}\). Добавлять бины кусочно-линейному кодированию вчетверо выгоднее.
- Сравните три кривые. Сырой признак в линейной модели даёт только прямую. One-hot по бинам ловит нелинейность, но выдаёт ступеньки.
- Двигайте \(x\) внутри одного бина: у one-hot выход стоит на месте, у PLE плавно едет. Это главное отличие и главный аргумент.
- Увеличивайте число бинов: кривая приближает всё более сложную зависимость — ценой \(T\) параметров на признак.
Что сказать на собесе: «PLE — компромисс между сырым признаком и биннингом: нелинейность как у one-hot, но без разрывов и без потери разрешения внутри бина».
Практический приём из работы Airbnb про глубокое обучение в поиске: после преобразований смотреть на гладкость распределения признака.
Гладкое, близкое к нормальному распределение — признак того, что преобразование выбрано удачно. Горбы, тяжёлые хвосты и всплески на конкретных значениях — сигнал, что с признаком что-то не так: скорее всего, там прячется заглушка вместо пропуска, обрезка сверху или склейка разных сущностей в одну колонку.
Это дешёвая проверка, которая ловит целый класс ошибок в данных до обучения.
5. Выходы других моделей как признаки
Третий тип входов — эмбеддинги, пришедшие извне: контентные векторы, выходы двухбашенной модели, результаты предобучения. С ними делают три вещи.
- \(l_2\)-нормализация — тот же аргумент, что в главе 7: убираем влияние нормы, которая коррелирует с популярностью.
- PCA для понижения размерности.
- Векторная квантизация: превращаем эмбеддинг в категориальный признак, сопоставляя векторам элементы кодбука — например, обучаем k-means и берём номер центроида. Развитие этой идеи — semantic IDs.
Главный признак ранжирующей модели YouTube — идентификатор видео. Раньше для него использовали хеш-трюк; заменили на semantic IDs, обучив RQ-VAE поверх контентных эмбеддингов видео. Результат — заметный прирост на холодном срезе.
Самая интересная деталь, и именно её стоит уметь объяснить: прямое использование контентного эмбеддинга работает хуже, чем дискретизованный semantic ID.
Почему так? Плотный контентный вектор — «мягкий» признак: по нему модель обобщает, но не может запомнить специфику конкретного видео. Дискретный код даёт и то и другое сразу: общий префикс отвечает за обобщение, полный код — за меморизацию. Дискретизация здесь не потеря информации, а разделение двух режимов, которые в плотном векторе смешаны.
Обратная сторона того же аргумента — проблема хеширования: получаемые идентификаторы не несут семантики, и для нового айтема мультихэш выдаёт неизвестную модели комбинацию. Полный упор в меморизацию и ноль обобщения.
Вопросы с собеседований
Почему в рекомендациях не хватает градиентного бустинга?
Три конкретные причины. Высокая кардинальность: one-hot на миллион значений не построить, а target encoding теряет семантику — два айтема с одинаковым средним CTR и разной аудиторией склеиваются в одно число, и это стоит 50% кликов в простом примере с двумя сегментами.
Неструктурированные данные: тексты, картинки, история — по ним можно нагенерировать признаки, но работает хуже.
И главное: эмбеддинги на входе в дерево работают плохо принципиально. Координаты по отдельности ничего не значат, а сплит смотрит на одну координату; чтобы различить 4 уровня по каждому из 16 измерений, нужно 4.29 млрд листьев, тогда как линейный слой обходится 16 умножениями.
Плюс два практических аргумента: много сигналов сразу и дрейф — бустинг надо перестраивать целиком, сеть можно дообучать.
А когда бустинг наоборот лучше?
Когда не выполняется ни один пункт чек-листа: много данных, важные высококардинальные признаки, неструктурированные данные, много сигналов, потребность в быстром дообучении. Если ничего из этого нет, CatBoost на агрегатах будет и дешевле, и лучше.
Главный минус сетей — порог входа: GPU, распределённое обучение, инференс в рантайме. Это реальная стоимость, и делать вид, что её нет, на собеседовании не стоит.
Покажите, что эмбеддинг — это линейный слой.
Представим категорию one-hot вектором \(e_i \in \{0,1\}^n\) и умножим на матрицу \(W \in \mathbb{R}^{n \times d}\) без свободного члена. Результат \(e_i W\) — это \(i\)-я строка \(W\). Значит \(W\) и есть таблица эмбеддингов, а лукап — просто оптимизация умножения на разреженный вектор.
Полезное следствие: при \(d = 1\) модель выучивает средний CTR значения, то есть target encoding — частный случай эмбеддинга размерности один. Всё, что даёт эмбеддинг сверх этого, — измерения, в которых помещается «кому именно нравится».
Как выбрать размер эмбеддинга?
Он должен зависеть от кардинальности и от информативности признака; одинаковый размер для всех неоптимален.
Информационная оценка: \(d\cdot s = \log_2 n\) бит. Эвристика Google: \(d = 6\sqrt[4]{n}\). Разрыв между ними огромный — при \(n=10^7\) это 23 бита против d = 337 — и он содержательный: почти вся ёмкость идёт под семантику, а не под идентификацию.
Буквально эвристику не берут: при \(n=10^9\) она даёт 1067 и 4.3 терабайта на признак. В проде типичные размерности 32–128, а честный путь — подбирать размерности прямо в обучении.
Какие оптимизации эмбеддинг-слоя вы знаете?
Batched lookup: объединить таблицы всех признаков в одну матрицу и делать единый лукап со сдвигом — иначе получается много мелких операций на GPU с накладными расходами на запуск ядра.
Sparse gradients: батч на 4096 сэмплов трогает не больше 4096 строк из 10 млн, то есть 0.041% таблицы; остальные градиенты — нули, их не надо ни обновлять, ни пересылать между картами. Важная тонкость: для таких параметров стоит увеличивать learning rate, потому что обновляются они сильно реже плотных весов.
Rowwise optimizer: хранить статистики Adam не на параметр, а на строку. Таблица 10 млн × 256 занимает 10.2 ГБ, с моментами Adam — 30.7 ГБ, а с построчными статистиками 10.32 ГБ, то есть на 0.8% больше таблицы.
Плюс шардирование, выгрузка на CPU и хеш-трюк, когда таблицы не помещаются на GPU.
Зачем преобразовывать вещественные признаки и как?
Затем, что сети чувствительны к масштабу (большие значения доминируют в градиенте), ломаются на выбросах и не умеют обрабатывать пропуски. У деревьев ни одной из этих проблем нет — это плата за переход к сетям.
Пять преобразований: логарифм против скошенности; сигмоида, зажимающая выбросы (с нормализацией до неё, иначе затухают градиенты); функция распределения, приводящая признак к равномерному; периодические функции для времени; квантизация по квантилям.
У квантизации принципиальный недостаток — теряется отношение порядка и между бинами, и внутри бина. Его чинит кусочно-линейное кодирование.
Почему час нельзя подавать числом от 0 до 23?
Потому что в сыром виде 23 и 0 — максимально далёкая пара из возможных, хотя это соседние часы. Модель вынуждена тратить ёмкость на то, чтобы выучить склейку на границе суток.
Пара синус-косинус одной частоты кладёт время на окружность: расстояние между 23 и 0 становится 0.2611 — ровно таким же, как между 0 и 1, и минимальным из возможных. Самой далёкой парой оказывается 23 и 12, то есть противоположные точки суток, как и должно быть.
Несколько частот дают несколько окружностей разного масштаба — час, день недели, сезон одновременно. Механизм тот же, что у позиционных эмбеддингов в трансформере.
Что такое PLE и чем оно лучше биннинга?
Кусочно-линейное кодирование: вектор длины T, где слева от текущего бина единицы, справа нули, и ровно одна компонента дробная — доля пройденного бина. Линейный слой поверх такого вектора даёт непрерывную кусочно-линейную функцию.
Отличие от one-hot по бинам: внутри бина выход не константа, а линейно едет. На приближении \(x^2\) при 16 бинах MSE отличается в 857 раз. И важнее разрыва то, как он растёт: у кусочно-постоянного кодирования ошибка падает как \(T^{-2}\), у кусочно-линейного — как \(T^{-4}\).
Почему дискретизованный semantic ID работает лучше плотного контентного вектора?
Потому что он разделяет два режима, которые в плотном векторе смешаны. Плотный контентный вектор — «мягкий» признак: по нему модель обобщает, но не может запомнить специфику конкретного объекта. Дискретный иерархический код даёт и то и другое: общий префикс отвечает за обобщение на похожие объекты, полный код — за меморизацию конкретного.
Это ровно тот случай, когда дискретизация не теряет информацию, а структурирует её. Обратная сторона того же аргумента — хеш-трюк: его идентификаторы семантики не несут, поэтому для нового объекта получается неизвестная модели комбинация, полный упор в меморизацию и ноль обобщения.
Шпаргалка одним экраном
Против бустинга
Кардинальность, неструктурированное, эмбеддинги. При d=16 и k=4 дереву нужно 4.29e9 листьев.
За бустинг
Если не выполнен ни один пункт чек-листа — CatBoost на агрегатах дешевле и лучше.
Эмбеддинг
one-hot × W = строка W. Target encoding — тот же эмбеддинг при d = 1.
Размерность
Снизу \(\log_2 n\) бит, эвристика \(6\sqrt[4]{n}\). Разрыв — ёмкость под семантику, не под ID.
Инженерия слоя
Batched lookup, sparse-градиенты (0.041% строк), построчный Adam: 30.7 → 10.32 ГБ.
Вещественные
log1p, сигмоида, CDF, sin/cos, квантизация. Масштаб, выбросы, пропуски — проблемы сетей, не деревьев.
Время
23 и 0 в сыром виде — самая далёкая пара. На окружности 0.2611, как 0 и 1.
PLE
Нелинейность без ступенек. Ошибка падает как \(T^{-4}\) вместо \(T^{-2}\).
Первоисточники
- P. Covington, J. Adams, E. Sargin. Deep Neural Networks for YouTube Recommendations, RecSys 2016 — откуда пошла эвристика размерности и многое другое.
- Y. Gorishniy, I. Rubachev, A. Babenko. On Embeddings for Numerical Features in Tabular Deep Learning, NeurIPS 2022 — кусочно-линейное кодирование.
- B. Coleman, W.-C. Kang et al. Unified Embedding: Battle-Tested Feature Representations for Web-Scale ML Systems, NeurIPS 2023.
- M. Haldar et al. Applying Deep Learning to Airbnb Search, KDD 2019 — гладкость распределений как диагностика признака.
- A. Singh, T. Vu, N. Mehta et al. Better Generalization with Semantic IDs: A Case Study in Ranking for Recommendations, 2023 — разбор из раздела 5.
- Z. Liu et al. Monolith: Real Time Recommendation System With Collisionless Embedding Table, 2022 — про дообучение под дрейф.
- R. Sutton. The Bitter Lesson, 2019.
- Числа главы:
_tools/features_demo.pyв этом репозитории.