?

Log in

No account? Create an account

Компьютерная лингвистика

Новостная лента www.solarix.ru

Previous Entry Share Flag Next Entry
Неудачная попытка использования XGBRanker и LGBMRanker для задачи определения перефразировки
kelijah
Под катом - особенности определения синонимичности фраз в языке, использование BERT, метрики для оценки моделей и проблема с задачей ранжирования.

Постановка

Одна из ключевых задач в чатботе заключается в том, чтобы для реплики пользователя найти наиболее подходящую эталонную фразу среди некоторого набора, например в вопросах FAQ. Простые необучаемые метрики типа расстояния Левенштейна-Дамерау или коэффициента Жаккара не работают с синонимами, и не способны учитывать близость смысла слов. С метриками поверх векторных представлений слов с этим получше. Но все они в той или иной степени страдают от особенностей синтаксического оформления фраз в естественном языке. Например, короткое словечко "не" во многих случаях (хотя см. пост 1 и 2) полностью меняет семантику фразы, но лишь незначительно изменяет посимвольную похожесть. Это делает результаты символьных метрик зачастую непригодными. Аналогичную эффект оказывают местоимения или наречия некоторых видов. Ведь понятно, что фразы "Я сплю" и "Ты спишь" относятся к разным субъектам и во многих ситуациях не должны считаться близкими, иначе чатбот будет страдать одной из форм расстройства личности, будет путать себя и других.

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

(1) - Ты решил задачу?
(2) - Я решаю задачу!

В ответе (2) подразумевается отрицание "Нет, но я решаю задачу", индикатором которого служит показатель глагольного времени.

Чтобы увидеть эти эффекты вживую, можно взять простой код other_relevance_calculators.py. Он позволяет в консоли вводить фразы и выводит десяток ближайших к введенной, используя одну из метрик. К примеру, для фразы "Я хожу в школу" получаются такие результаты. Коэффициент Жаккара с шинглами длиной 3:

:> я хожу в школу

0.590909 в школу я не хожу
0.517241 я каждый день хожу в школу
0.500000 я спешу в школу
0.478261 я побегу в школу
0.476190 я шел в школу
0.434783 я пошел в школу
0.434783 я пошла в школу
0.360000 я ненавижу школу
0.357143 я отправился в школу
0.333333 миша ходит в школу



Word mover distance:

:> я хожу в школу

0.811322 я шел в школу
0.791942 я пошел в школу
0.788527 я пошла в школу
0.780179 я отправился в школу
0.774977 в школу я не хожу
0.765454 я спешу в школу
0.763919 я побегу в школу
0.691840 я шел в библиотеку
0.671755 я вернусь в больницу
0.669050 я направляюсь в оранжерею




Отсюда возникает идея построить supervised модель, которая будет учитывать эти особенности языка. Для обучения модели нужен размеченный вручную датасет, содержащий как минимум пары синонимичных предложений. У меня сейчас такой датасет содержит примерно 100 тысяч таких пар. Также могут быть полезны примеры несинонимичных фраз, например "я сплю" и "ты спишь". Часть этого датасета лежит тут.

Классификация

В самом простом и прямом виде задача формулируется как бинарная классификация. Модель должна выдавать 1 для перефразировок и 0 для прочих пар.

В качестве baseline модели можно взять линейный классификатор LogisticRegression, соответствующий код лежит тут. Там реализован gridsearch для подбора параметров векторизации текстов и параметров самого классификатора - см. параметр запуска --run_mode gridsearch. В качестве признаков текста используется мешок шинглов или sentencepiece-токенов, см. классы ShingleVectorizer и SentencePieceVectorizer. При обучении финальной модели (см.  параметр запуска --run_mode train) код также выводит в текстовый файл веса самых значимых позитивных и негативных фич. К сожалению, эти веса достаточно трудно интерпретировать:

бе п(a&b)  = 6.093737538020524
с си(a&b)  = 5.390922356511366
ю см(a-b)  = 5.080459984724804
вь м(b-a)  = 5.01616775741977
мое (a&b)  = 5.0100646323114315
трав(a&b)  = 5.009792399786643
мое(a-b)   = 4.954400872157704
вь м(a-b)  = 4.893422992556576
, ту(a&b)  = 4.844557511941288


...


жи -(a-b)  = -5.117691807078531
ые и(a&b)  = -5.256227649952226
шь з(a&b)  = -5.298547739146427
 я ?(a-b)  = -5.409425560268217
у см(a-b)  = -5.608569897069139
ьна\n(a&b) = -5.77466930492533
жи -(b-a)  = -6.1251675048097525
 ми(b-a)   = -6.5043540802299065
 ми(a-b)   = -6.721502181709472
 я ?(a&b)  = -7.257981008937467


Суффикс (a&b) означает ситуацию, когда указанная подстрока встречается в обоих сравниваемых фразах. Суффиксы (a-b) и (b-a) означают, что подстрока встречается в одной и не встречается во второй фразе.

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

Код модели бинарного классификатора на базе BERT с fine tuning лежит тут. Обертка для BERT в tensorflow.hub ради удобства с Keras выделена в отдельный модуль. В параметрах класса BertLayer есть n_fine_tune_layers для задания количества дообучаемых словев. Я пробовал задавать от 1 до 3 слоев в fine tuning. Замечу, что модель на BERT требует очень много вычислительных ресурсов. Фактически обучение на ~300 тысячах позитивных и негативных пар идет примерно 2 часа при почти 100% утилизации GPU GTX 1080. Все вентиляторы на сервере раскручиваются на максимум, появляется шум, в общем, чувствуется, что 110 миллионов параметров в BERT это не шуточки.

Ранжировка

Возможна и альтернативная постановка задачи. Путь у нас есть пара синонимичных фраз "Я хочу спать" и "Мне бы вздремнуть". Добавляем к ним некоторое количество нерелевантных пар, например "Я хочу спать" & "Я хочу снять" и т.д. Пусть теперь модель учится выдавать для этого списка пар такие оценки, чтобы релевантная пара имела наивысшую оценку, а остальные - меньшие. Таким образом, нам нужна отранжировать список. Для такого рода задач есть специальные модели, в частности XGBRanker и LGBMRanker. У них очень похожий API метода fit, требующий передать матрицу признаков X, оценки ранга для каждой пары y и вектор групп.

Тут лежит код тренера.

Оценка качества: precision@1, mean reciprocal rank

Есть множество способов решения данной задачи, с разными "естественными" метриками. Например, бинарная классификация будет выдавать что-нибудь типа f1 метрики, и эта оценка будет "локальна" для пар. В некоторых случаях такая оценка синонимичности пары ничего не скажет с точки зрения использования в чатботе. Например, нам интересно, чтобы наиболее релевантный вопрос в FAQ был сопоставлен с максимальной оценкой, а остальные - ниже. Иначе говоря, если для релевантной пары будет выдана оценка 0.98, а конкурирующая нерелевантная пара даст ранг 0.99, то бот выберет неправильную запись. В условиях упомянутой нелинейности задачи всегда есть шанс, что какая-то нерелевантная пара вдруг выдаст локальный пик, испортив подбор записи в FAQ.

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

Во-первых, можно оценить, сколько групп (позитивная пара + негативные пары) после ранжирования дали в первой позиции именно позитивную пару. Эту метрику будем называть precision@1 (см. для справки метрики для поисковиков). Чем ближе precision@1 к 0, тем чаще релевантные пары расцениваются моделью как синонимичные.

Вторая оценка - mean reciprocal rank, то есть среднее значение обраной позиции релевантной пары после ранжирования. Чем она ближе к 1, тем лучше модель выталкивает релевантную пару вверх (где rank=1) при ранжировании.

Проблема с ранжировщиками

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

Например, линейный классификатор на LogisticRegression дает precision@1 около 0.944.

Модель на базе классификатора LightGBM дает precision@1 около 0.986.

Модель на базе XGBRanker выбивает precision@1 аж в 0.999.

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