Привет! Меня зовут Станислав, я — дата-сайентист из команды Поиска в hh.ru. У нас в компании дата-сайентисты занимаются главным образом работой над рекомендательными системами. Если у вас есть резюме на hh.ru, то скорее всего вы хотя бы раз просматривали список подходящих вам вакансий. То, насколько они действительно релевантны для вас, и является нашей зоной ответственности.

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

ML в поиске hh.ru

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

Сайт hh.ru по своей сути — это двусторонний маркетплейс, где с одной стороны работодатели, публикующие вакансии, а с другой — соискатели, разместившие резюме. Для каждой из сторон мы разработали собственную рекомендательную систему. Они успешно функционируют в контексте нашей поисковой архитектуры наряду с другими моделями машинного обучения, над которыми мы работаем.

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

DSSM расшифровывается как “deep semantic similarity model”. Semantic similarity — это семантическое сходство, сходство по смыслу. Не секрет, что один и тот же смысл можно выразить совершенно разными словами, поэтому, простое наличие общих слов в двух текстах не отражает, насколько эти тексты похожи по смыслу.

И тут мы можем воспользоваться моделью DSSM. Она была описана в 2013 году, в статье от Microsoft, где они предложили такую архитектуру нейронной сети, в которой для каждой сущности выделяется отдельная ветвь, заканчивающаяся семантическим представлением этой сущности — вектором эмбеддингом. Мы хотим, чтобы эти векторы обладали определенным свойством, например, если две сущности похожи друг на друга, то и соответствующие им векторы должны быть близки в некотором векторном пространстве. А если эти сущности никак не связаны, то и векторы должны быть направлены в разные стороны.

С тех пор эта модель получила широкое применение в индустрии. В открытых источниках можно посмотреть примеры подобных архитектур у Mail.ru, Avito, Яндекса и многих других компаний. Они отличаются как по архитектуре, так и по способу обучения этих моделей.

В hh.ru мы также разработали свою версию модели DSSM, чтобы моделировать семантическое сходство между нашими вакансиями и резюме. На вход этой модели мы передаем все имеющиеся признаки документа. Например, категориальные — регионы вакансии, или числовые признаки — заработная плата, указанная в вакансии.

Но, пожалуй, центральное место в этой архитектуре занимает обработка текстовой информации — описание документа, например, описание вакансии или опыта в резюме. Этот текст мы передаем на вход слою RNN, который на выходе выдает по одному вектору на каждый входной токен, далее агрегируем эти вектора с помощью простого линейного attention-слоя, затем конкатенируем все полученные признаки и прогоняем еще через пару слоев нейронной сети. Таким образом на выходе получаем тот самый желанный вектор эмбеддинг или семантическое представление вакансии. Аналогичная архитектура у нас также есть и для резюме, она будет отличаться просто набором входных признаков, а на выходе мы тоже получаем вектор представления для резюме.

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

Причем тут суммаризация

На вход слою RNN мы даем не весь текст вакансии или резюме, а только первые 300 токенов. Это связано с вопросами производительности — большее количество нам сложнее обрабатывать в продакшене. Мы попробовали поэкспериментировать с разными эвристиками: брать не первые, а, например, последние 300 токенов, набирать их из середины описания документов или включать в них обязательные блоки. Но это не привело к улучшению метрик, и мы остановились на самом простом варианте — первых 300 токенах.

Рассмотрим пример — наша вакансия Java-разработчика. Она имеет достаточно большое и длинное описание, которое даже на скрин полностью не поместилось:

Первый абзац этого описания содержит информацию про рекламные продукты hh.ru, что в общем-то не сильно релевантная информация для самого Java-разработчика. Его скорее заинтересуют блоки с требованиями и технологиями, с которыми он будет работать. Серым выделено то, что не поместилось в первые 300 токенов, и мы видим, что первый абзац занял очень много места, а технологии не влезли вовсе. Отсюда у нас возникла идея построить такую модель, которая будет автоматически выделять самые важные части из описания документа. Тогда мы получим более полное и правильное семантическое представление этого документа.

Так у нас родилась задача суммаризации — построения краткого содержания текста.

Подходы к решению задачи суммаризации

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

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

Следующее деление подходов к решению задачи суммаризации делится на supervised- и unsupervised-learning. Простейший пример подхода unsupervised-learning — это алгоритм TextRank, он работает следующим образом: мы берем текст, разбиваем его на предложения, векторизуем каждое, а затем строим граф, где вершинами станут наши предложения, а ребра мы взвесим расстояниями между соответствующими предложениями.

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

Суть этого и похожих unsupervised-learning алгоритмов сводится к тому, чтобы выделить в тексте некоторую повторяемость, избавиться от нее и оставить просто некоторый каркас. Но, к сожалению, нам этот подход не подходит — такой unsupervised-алгоритм никак не сможет понять, что первый абзац про рекламные продукты хоть и является уникальным и нигде больше не повторяется в тексте нашей вакансии, но при этом абсолютно не релевантен непосредственно для профессии Java-разработчика. Да и вообще его хорошо бы полностью удалить.

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

Одна проблема — построить такой сэмпл вручную практически нереально. Чтобы человеку вручную разметить одну вакансию, потребуется очень много времени. Да и в принципе не до конца понятно, что именно ему нужно размечать, потому что задача стоит не слишком конкретно — отобрать такие предложения, выбрав из которых 300 токенов и передав их на вход нашей модели DSSM, мы получим более правильное и полноценное семантическое представление документа. Но как определить самые важные предложения? Тут у нас родилась следующая идея. Почему бы не спросить у самой модели DSSM, что ей важно, а что нет.  

Linear Attention

Вернемся к нашей архитектуре DSSM и подробнее разберем слой линейного аттеншна, который я упоминал ранее. На самом деле этот слой достаточно простой. Во-первых, на вход в слой RNN у нас поступает 300 токенов, а на выходе мы получаем по вектору на каждый входной токен. Грубо говоря, 300 векторов размерности 256.

Далее у нас есть простой линейный слой, который перемножает каждый этот вектор, а на выходе дает нам одно число — получается один вектор размерности 300. Теперь этот вектор пропускаем через softmax, который его нормирует, и в итоге получаем веса. Эти веса используются для того, чтобы посчитать взвешенную сумму выходов из RNN и таким образом получить результирующий вектор размерности 256.

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

Мы можем визуализировать эти веса на примере нашей вакансии Java-разработчика:

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

Первый абзац этой вакансии очень прозрачный, то есть модель на него смотрит меньше всего, и практически зануляет информацию по токенам из этого абзаца. При этом, ярко-красным у нас выделяется здесь Java, это значит, что модель действительно научилась и определила то, что эта вакансия Java-разработчика. Но, к сожалению, сам по себе текст “Java” нам не дает никакой новой, дополнительной информации, потому что уже присутствует в заголовке этой вакансии.

Посмотрим, как поведет себя модель, если мы передаем ей весь текст этой вакансии. Для этого нам нужно просто обучить расширенную модель, снять ограничение в 300 токенов и передавать гораздо больше текста. Что мы и сделали. Конечно, в продакшене такую модель мы использовать не можем, потому что там inference по одному документу будет занимать больше двух секунд, зато для офлайн-расчетов и анализа мы вполне можем подождать несколько дней, пока она будет обучаться.

Подсветка по всему документу будет выглядеть следующим образом:  

Тут мы видим, что модель практически полностью игнорирует первый абзац, что в общем-то логично, но теперь она заметила навыки Postgresql, Kafka, Hibernate, Spring etc. То есть сделала именно то, чего мы от нее и ожидали.

Частый трюк в машинном обучении — аппроксимация, замена чего-то сложного и большого на более простую модель, которая работает с погрешностью. Здесь мы тоже можем применить этот трюк и обучить некоторую простую модель, которая бы предсказывала разметку аттаншена нашей расширенной модели. Таким образом мы бы и решили задачу суммаризации, то есть бы выделяли те части вакансии, которые содержат самую релевантную информацию.

Сэмпл для обучения модели суммаризации

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

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

Поэтому, наш выбор — решать задачу бинарной классификации. Мы будем предсказывать таргет is_summary с ноликом либо единичкой. Решить эту задачу можно с помощью простой модели логистической регрессии, которая очень легко обучается и быстро работает в inference. И как бонус она интерпретируемая.

Обучив логистическую регрессию, мы видим то, что наши локальные метрики неплохо выросли, что говорит о том, что она действительно что-то выучила. Хороший знак.

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

Посмотрим на пример работы нашей модели стабилизации в виде такой логистической регрессии на примере все той же вакансии Java-разработчика:

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

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

А что в продакшене

Ну, одно дело локальные метрики и визуализация, и совсем другое — работа модели в продакшене. Чтобы показать, что мы сделали что-то действительно полезное, нам нужно запустить А/В-тест. Результаты тестирования показали, что у нас получился прирост по всем нашим ключевым метрикам после того, как мы внедрили нашу модель суммаризации в рекомендательные системы.

Интерпретация коэффициентов логистической регрессии

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

А наличие токенов с отрицательным коэффициентом будет сильно занижать это предложение и, вероятнее всего, оно окажется нерелевантным. Для вакансии это токены: “позволяет“, “миссия“, “придерживается” и так далее. В общем, те слова, которые сигнализируют о том, что пошло некоторое пространное описание, содержащее не особо важную информацию.

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

Summary

Итак, мы использовали модель суммаризации, чтобы выделить самые важные части из текста документа и передавать их на вход нашим алгоритмам матчинга. Это позволило получить более качественное семантическое представление документов. Автоматическое построение сэмпла на основе разметки attention-слоя избавила нас от поиска и подбора эвристик. При этом наличие такого сэмпла открывает возможности для дальнейшего развития идеи и построения более точной модели суммаризации.

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

Комментарии (4)


  1. VPryadchenko
    10.11.2022 09:16
    +1

    Привет, меня зовут Василий, я тоже дата-саентист, и на hh.ru на мое резюме мне как-то рекомендовалась вакансия: помощник бурильщика %) Я так-то без претензии, довольно забавно было)
    (кажется, года три назад)


  1. PavelCTI
    10.11.2022 11:18

    Есть несколько вопросов по статье:

    1. Как предобрабатывали текст(использовали ли лимматизацию, стемминг)

    2. Почему оставили в тексте пунктуацию

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

    PS: Механизм внимания(attention) в статье очень напоминает само-внимание(self attention)


    1. stasyarkin Автор
      10.11.2022 11:46

      1. Тект чистим от html тегов, разбиваем на предложения с помощью библиотеки razdel, кодируем с помощью BPE, который мы заранее предобучаем на нашем корпусе.

      2. Так как мы используем BPE в этом нет непосредственной необходимости, проще было ее оставить, плюс видно, что она дает дополнительную информации про структуры вакансии / резюме.

      3. Это интересная задача, но слишком сложная в данном контексте, возможно, в будущем к ней вернемся.

      4. self-attention и cross-attention это все же про трансформеры, в описанной модели трансформеров пока нет.


      1. PavelCTI
        10.11.2022 15:04

        А пробовали выделять сущности из вакансий и резюме и далее смотреть близость вакансии к резюме(или наоборот) исходя из выделенных сущностей? Интересно, такой подход должен работать, тк мы сразу можем сравнивать искомые сущности.