Прим. Wunder Fund: короткая статья о том, как эмбеддинги могут помочь при работе с категориальными признаками и сетками. А если вы и так умеете в сетки — то мы скоро открываем набор рисерчеров и будем рады с вами пообщаться, stay tuned.
Создание эмбеддингов признаков (feature embeddings) — это один из важнейших этапов подготовки табличных данных, используемых для обучения нейросетевых моделей. Об этом подходе к подготовке данных, к сожалению, редко говорят в сферах, не связанных с обработкой естественных языков. И, как следствие, его почти полностью обходят стороной при работе со структурированными наборами данных. Но то, что его, при работе с такими данными, не применяют, ведёт к значительному ухудшению точности моделей. Это стало причиной появления заблуждения, которое заключается в том, что алгоритмы градиентного бустинга, вроде того, что реализован в библиотеке XGBoost, это всегда — наилучший выбор для решения задач, предусматривающих работу со структурированными наборами данных. Нейросетевые методы моделирования, улучшенные за счёт эмбеддингов, часто дают лучшие результаты, чем методы, основанные на градиентном бустинге. Более того — обе группы методов показывают серьёзные улучшения при использовании эмбеддингов, извлечённых из существующих моделей.
Эта статья направлена на поиск ответов на следующие вопросы:
Что такое эмбеддинги признаков?
Как они используются при работе со структурированными данными?
Если использование эмбеддингов — это столь мощная методика — почему она недостаточно широко распространена?
Как создавать эмбеддинги?
Как использовать существующие эмбеддинги для улучшения других моделей?
Эмбеддинги признаков
Нейросетевые модели испытывают сложности при необходимости работы с разреженными категориальными признаками. Эмбеддинги — это возможность уменьшения размерности таких признаков ради повышения производительности модели. Прежде чем говорить о структурированных наборах данных, полезно будет разобраться с тем, как обычно используются эмбеддинги. В сфере обработки естественных языков (Natural Language Processing, NLP) обычно работают со словарями, состоящими из тысяч слов. Эти словари обычно вводят в модель с использованием методики быстрого кодирования (One-Hot Encoding), что, в математическом смысле, равносильно наличию отдельного столбца для каждого из слов. Когда слово передаётся в модель, в соответствующем столбце оказывается единица, в то время как во всех остальных выводятся нули. Это ведёт к появлению очень сильно разреженных наборов данных. Решение этой проблемы заключается в создании эмбеддинга.
По сути дела, речь идёт о том, что эмбеддинг, на основе обучающего текста, группирует слова со сходным значением и возвращает их местоположение. Например — значение эмбеддинга слова «fun» может быть подобно значениям эмбеддингов слов «humor» и «dancing», или словосочетания «machine learning». На практике нейронные сети демонстрируют значительное улучшение производительности при применении подобных репрезентативных свойств.
Эмбеддинги и структурированные данные
Структурированные наборы данных тоже часто содержат разреженные категориальные признаки. В вышеприведённой таблице, содержащей данные о продажах, имеются столбцы с почтовым индексом и идентификатором магазина. Так как в этих столбцах могут присутствовать сотни или тысячи уникальных значений, использование этих столбцов приведёт к появлению тех же проблем с производительностью, о которых мы говорили ранее в применении к NLP-моделям. Почему бы тут, по вышеописанной схеме, не воспользоваться эмбеддингами?
Проблема заключается в том, что сейчас мы имеем дело более чем с одним признаком. В данном случае это два отдельных разреженных категориальных столбца (zip_code
и store_id
), а так же другие ценные признаки, вроде общего объёма продаж конкретному клиенту. Мы просто не можем заложить все эти признаки в эмбеддинг. Но мы, однако, можем подготовить эмбеддинги в первом слое модели и добавить в модель, вместе с эмбеддингами, и обычные признаки. Это не только приведёт к преобразованию почтового индекса и идентификатора магазина в ценные признаки, но и позволит не «размывать» другие ценные признаки тысячами столбцов.
Почему эмбеддингам не уделяют достаточно внимания?
В крупнейших компаниях, занимающихся машинным обучением (Machine Learning, ML), эта техника работы с данными, бесспорно, применяется. Проблема заключается в том, что подавляющее большинство дата-сайентистов, находящихся за пределами таких компаний, никогда даже и не слышали о таком способе использования эмбеддингов. Почему это так? Хотя я и не сказал бы, что эти методы чрезвычайно сложно реализовать, они сложнее того, чему учат на типичных онлайн-курсах. Большинство начинающих ML-практиков просто не научили тому, как использовать эмбеддинги вместе с другими некатегориальными признаками. В результате признаки вроде почтовых индексов и идентификаторов магазинов просто выбрасывают из модели. Но это — важные данные!
Некоторые значения признака можно включить в модель, воспользовавшись другими техниками, вроде метода среднего кодирования (Mean Encoding), но подобное даёт лишь небольшие улучшения. Это привело к тенденции, когда от нейросетевых моделей полностью отказываются, выбирая методы, задействующие алгоритмы градиентного бустинга, которые способны лучше работать с категориальными признаками. Но, как уже было сказано, эмбеддинги могут улучшить оба метода моделирования. Сейчас мы разовьём эту тему.
Создание эмбеддингов
Самое сложное при работе с эмбеддингами — разобраться с наборами данных TensorFlow. И хотя устроены они совсем не так понятно, как датафреймы Pandas, научиться работать с ними весьма полезно. В частности — полезно для того, кто хотя бы задумывается о том, чтобы его модели работали бы с огромными наборами данных, и для того, кто стремится к созданию более сложных нейросетевых моделей, чем те, которые он уже разрабатывает.
В этом примере мы воспользуемся гипотетической таблицей продаж, фрагмент которой приведён выше. Наша цель заключается в прогнозировании целевых месячных продаж. Мы, ради краткости и простоты, опустим то, что имеет отношение к дата-инжинирингу, и начнём с предварительно подготовленных датафреймов Pandas. При работе с более крупными наборами данных, скорее всего, работа начнётся не с датафреймов, но это — тема для отдельной статьи.
Первый шаг заключается в преобразовании датафреймов Pandas в наборы данных TensorFlow:
trainset = tf.data.Dataset.from_tensor_slices((
dict(X_train),dict(y_train))).batch(32)
validationset = tf.data.Dataset.from_tensor_slices((
dict(X_val),dict(y_val))).batch(32)
Здесь стоит обратить внимание на то, что наборы данных TensorFlow и результаты дальнейших преобразований данных не хранятся в памяти так же, как хранятся датафреймы Pandas. Они, по сути, представляют собой конвейер. Через него, пакет за пакетом, походят данные, что позволяет модели эффективно обучаться на наборах данных, которые слишком велики для размещения их в памяти. Именно поэтому мы преобразуем в наборы данных словари датафреймов, а не реальные данные. Обратите внимание на то, что мы, кроме прочего, задаём сейчас, а не во время обучения модели, размер пакета данных, поступая не так, как обычно поступают при использовании API Keras.
Теперь создадим список уникальных значений для zip_code
и store_id
. Эти материалы мы используем позже, создавая и извлекая эмбеддинги.
zip_codes = X_train['zip_code'].unique()
store_ids = X_train['store_id'].unique()
Теперь, пользуясь столбцами признаков TensorFlow, можно определить конвейеры для обработки данных. Существуют разные способы решения этой задачи, выбор конкретного варианта зависит от типов признаков, хранящихся в таблице. Подробности об этом можно узнать в документации TensorFlow по столбцам признаков.
# передаём модели числовые признаки:
feature_columns = []
feature_columns.append(
tf.feature_column.numeric_column('gender')
feature_columns.append(
tf.feature_column.numeric_column('age)
feature_columns.append(
tf.feature_column.numeric_column('previous_sales')
# создаём категориальные столбцы с использованием ранее созданных списков:
zip_col = tf.feature_column.categorical_column_with_vocabulary_list(
'zip_code', zip_codes)
store_col = tf.feature_column.categorical_column_with_vocabulary_list(
'store_id', store_ids)
# создаём эмбеддинг из категориального столбца:
zip_emb = tf.feature_column.embedding_column(zip_col,dimension=6)
store_emb = tf.feature_column.embedding_column(store_col,dimension=4)
# добавляем эмбеддинги в список столбцов признаков:
tf.feature_columns.append(zip_emb)
tf.feature_columns.append(store_emb)
# создаём входной слой для модели:
feature_layer = tf.keras.layers.DenseFeatures(feature_columns)
Обратите внимание на то, что при создании эмбеддингов нам нужно указать число измерений. Это определяет то количество признаков, к которому будут сведены признаки из категориальных столбцов. Общее правило заключается в том, что результирующее количество признаков выбирается равным корню четвёртой степени из общего количества категорий (например — 1000 уникальных почтовых индексов сводятся к ~6 столбцам эмбеддинга). Но это — один из тех параметров, которые можно настраивать в собственной модели.
Теперь создадим простую модель:
model = tf.keras.models.Sequential()
model.add(feature_layer)
model.add(tf.keras.layers.Dense(units=512,activation=’relu’))
model.add(tf.keras.layers.Dropout(0.25))
# тут можно добавить нужные слои
Model.add(tf.keras.layers.Dense(units=1))
# компилируем и обучаем модель
model.compile(loss='mse', optimizer='adam', metrics=['accuracy'])
model.fit(trainset, validation_data=valset, epochs=20, verbose=2)
Поздравляю! Вы только что обучили модель, в которой используются эмбеддинги. А как извлечь эмбеддинги из модели для использования их в других моделях? Достаточно просто взять из модели веса:
zip_emb_weights = model.get_weights()[1]
store_emb_weights = model.get_weights()[0]
Обратите внимание на то, что порядок слоёв эмбеддингов может меняться, поэтому проверьте соответствие длины слоя весов количеству уникальных значений, объявленных выше. Это позволит вам убедиться в том, что вы взяли данные из правильного слоя. Теперь веса можно сохранить в датафрейме:
# создаём названия столбцов для эмбеддингов
zip_emb_cols = ['zip_emb1', 'zip_emb2', 'zip_emb3', …]
store_emb_cols = ['store_emb1', 'store_emb2', 'store_emb3', …]
# создаём датафрейм Pandas:
zip_emb_df = pd.DataFrame(columns=zip_emb_cols,
index=zip_codes,data=zip_emb_weights)
store_emb_df = pd.DataFrame(columns=store_emb_cols,
index=store_ids,data=store_emb_weights)
# и наконец - сохраняем датафреймы в формате CSV или в каком-то другом формате
zip_emb_df.to_csv('zip_code_embeddings.csv')
store_emb_df.to_csv('store_id_embeddings.csv')
Итоги
Теперь, когда эмбеддинги сохранены, их можно включить в состав исходного набора данных. Можно даже ввести их в другие наборы данных, используя те же категориальные признаки. Я пока не встречался с ситуациями, когда использование расширенных наборов данных не приводило бы к росту точности моделей. Попробуйте эмбеддинги — и я обещаю, что их применение станет частью вашей обычной работы в сфере машинного обучения.
О, а приходите к нам работать? ????
Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.
Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь.
Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.
Комментарии (5)
ratatosk
23.11.2021 00:31+1Возможно, будет интересно посмотреть нашу статью, про эмбеддинги последовательностей событий, где каждое событие состоит из категриальных и численных полей: https://arxiv.org/abs/2002.08232.
excoder
23.11.2021 10:26Вы используете обработку языка для финмоделей, или прямо данные стакана обрабатываете ML/DL?
MagicWolf
26.11.2021 10:34+1А откуда взялось правило про размерность эмбеддинга - "количество признаков выбирается равным корню четвёртой степени из общего количества категорий"?
n0isy
А подскажите пожалуйста, столкнулся с проблемой: у категорий уже есть "веса", которые я хотел бы использовать:
Допустим количество штук товаров, купленные людьми. Товаров много. One Hot это 1 или 0, а тут Quantity уже есть. Разный для разных товаров.
MagicWolf
А если использовать это "количество штук" как дополнительный числовой признак наподобие previous_sales выше?