NLI (natural language inference) – это задача автоматического определения логической связи между текстами. Обычно она формулируется так: для двух утверждений A и B надо выяснить, следует ли B из A. Эта задача сложная, потому что она требует хорошо понимать смысл текстов. Эта задача полезная, потому что "понимательную" способность модели можно эксплуатировать для прикладных задач типа классификации текстов. Иногда такая классификация неплохо работает даже без обучающей выборки!

До сих пор в открытом доступе не было нейросетей, специализированных на задаче NLI для русского языка, но теперь я обучил целых три: tiny, twoway и threeway. Зачем эти модели нужны, как они обучались, и в чём между ними разница – под катом.

Модели NLI можно применять и для логического вывода, и для классификации текстов
Модели NLI можно применять и для логического вывода, и для классификации текстов

Задача NLI

На русский язык понятие natural language inference можно перевести как логический вывод (или умозаключения) на естественном языке. Обычно эта задача формулируется как классификация пары текстов на два класса (entailment/not_entailment) или на три класса (entailment/contradiction/neutral). Выглядят классы примерно так:

  • из "Петя – хакер" следует, что "Петя – айтишник", ибо нам известно, что все хакеры – айтишники. Это entailment.

  • из "Петя – хакер" не следует "Петя – кот", более того, второе утверждение противоречит первому, потому что хакерами вроде как бывают только люди, но не коты. Это contradiction.

  • из "Петя – козёл" не следует, что "Вася тоже хакер", но и противоречия между этими утверждениями нет. Это отношение зовётся neutral.

В задаче NLI левый текст обычно называется предпосылкой (premise), а правый – гипотезой (hypothesis), и я буду придерживаться такой же терминологии.

Чтобы правильно делать подобные суждения, модель должна уметь очень много. Она должна знать слова ("хакер", "кот") и связи между ними ("хакеры – айтишники", "айтишники – люди", "коты – не люди"). Должна уметь в логические операции (отрицания, и/или, если/то, каждый/некоторый и т.п.). Должна понимать природу сущностей ("Швеция – это страна"). Должна понимать, как связаны друг с другом отношения между сущностями (если существует "Король Швеции", то "Швеция – это королевство"). Более подробно про эти умения можно почитать на сайте RussianSuperGLUE, но уже и так понятно, что модель должна очень хорошо понимать смысл текстов.

За последние несколько лет появилось много нейросетей типа BERT, предобученных так, что базовое понимание языка у них уже неплохое, в том числе и ряд моделей для русского языка. При обучении двух из них (rubert-base-cased-sentence от DeepPavlov и sbert_large_nlu_ru от SberDevices) даже использовались датасеты NLI, переведённые на русский язык. Но обе они устроены так, что сначала обрабатывают каждый текст по отдельности, а потом сравнивают между собой уже абстрактные представления (точнее, sentence embeddings) этих текстов. Если же модель читает оба текста одновременно, при необходимости "подглядывая" механизмом внимания из одного текста в другой (это называется cross-encoder), она имеет больше шансов понять, как связаны друг с другом смыслы этих текстов. Именно такие модели я и обучил.

Применение

Задача NLI важна для компьютерных лингвистов, ибо она позволяет детально рассмотреть, какие языковые явления данная модель понимает хорошо, а на каких – "плывёт"; по этому принципу устроены диагностические датасеты SuperGLUE и RussianSuperGLUE. Кроме этого, модели NLI обладают прикладной ценностью по нескольким причинам.

Во-первых, NLI можно использовать для контроля качества генеративных моделей. Есть масса задач, где на основе текста X нужно сгенерировать близкий к нему по смыслу текст Y: суммаризация, упрощение текстов, перефразирование, перенос стиля на текстах, текстовые вопросно-ответные системы, и даже машинный перевод. Современные seq2seq нейросети типа T5 (которая в этом году появилась и для русского языка) в целом неплохо справляются с такими задачами, но время от времени лажают, упуская какую-то важную информацию из Х, или, наоборот, дописывая в текст Y что-то нафантазированное "от себя". С помощью модели NLI можно проверять, что из X следует Y (то есть в новом тексте нету "отсебятины", придуманной моделью), и что из Y следует X (т.е. вся информация, присутствовавшая в исходном тексте, в новом также отражена).

Во-вторых, с помощью моделей NLI можно находить нетривиальные парафразы и в целом определять смысловую близость текстов. Для русского языка уже существует ряд моделей и датасетов по перефразированию, но кажется, что можно сделать ещё больше и лучше. В статье Improving Paraphrase Detection with the Adversarial Paraphrasing Task предложили считать парафразами такую пару предложений, в которой каждое логически следует из другого – и это весьма логично. Поэтому модели NLI можно использовать и для сбора обучающего корпуса парафраз (и не-парафраз, если стоит задача их детекции), и для фильтрации моделей, генерирующих парафразы.

В-третьих, NLI можно переиспользовать для задачи классификации текстов с небольшим числом обучающих примеров или даже вообще без обучающей выборки. В статье Entailment as Few-Shot Learner модель, обученную на задаче NLI, дообучали буквально на 8 примерах на новые задачи классификации текстов, и в результате модель справлялась с ними весьма неплохо; в других работах этот подход демонстрировали вообще без дообучения (хотя с этим и обнаружены проблемы). Работает это так: для текста, который надо классифицировать, готовится несколько выводов соответствующих разным, и выбирается самый правдоподобный из них. Например, так можно решить задачу анализа тональности текста:

# !pip install transformers sentencepiece
from transformers import pipeline
p = pipeline(
  task='zero-shot-classification', 
  model='cointegrated/rubert-base-cased-nli-twoway'
)
p(
  sequences="Сервис приличный, кормили вкусно", 
  candidate_labels="Мне понравилось, Мне не понравилось", 
  hypothesis_template="{}."
)
# {'labels': ['Мне понравилось', 'Мне не понравилось'],
#  'scores': [0.9580550789833069, 0.0419449619948864],
#  'sequence': 'Сервис приличный, кормили вкусно'}

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

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

p(
  sequences="Я хочу поехать в Дагестан", 
  candidate_labels="спорт,путешествия,музыка,кино,книги,наука,политика", 
  hypothesis_template="Мои интересы - {}."
)
# {'sequence': 'Я хочу поехать в Дагестан', 
   'labels': ['путешествия', 'спорт', 'политика', 'наука', 'кино', 'музыка', 'книги'], 
   'scores': [0.948, 0.019, 0.007, 0.006, 0.006, 0.005, 0.005]}

Лично у меня zero-shot классификация на базе NLI не особо взлетела. Например, на задаче классификации 68 интентов zero-shot классификация на основе NLI с написанными вручную 68 гипотезами для каждого класса отработала хуже, чем метод ближайших соседей на эмбеддингах LaBSE с всего лишь тремя примерами на каждый класс. На задачах классификации тональности и токсичности подход Labse+KNN тоже сравнялся по качеству с NLI+zero-shot на нескольких десятках размеченных примеров.

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

Впрочем, несмотря на свою относительную бесполезность, zero-shot классификация – это всё равно очень прикольно.

Датасеты

Насколько мне известно, сегодня существует два датасета, посвящённых задаче NLI на русском языке: TERRa, собранная из русскоязычных публикаций и вручную размеченная, и XNLI, где английские размеченные тексты были переведены на русский и ряд других языков. Оба эти датасета не очень большие, зато на английском языке существуют буквально миллионы размеченных пар текстов. Поэтому в качестве обучающей выборки я использовал в основном корпусы, машинно переведённые с английского языка. Большинство из них было взято из репозитория Felipe Salvatore.

  • Add-one RTE: корпус, в котором в текст добавляется одно слово, которое может поменять или не поменять его смысл. Например, "вселенная" и "вся вселенная" идентичны по смыслу, а "вселенная" и "воображаемая вселенная" – разные.

  • ANLI: три корпуса, собранные вручную таким образом, чтобы с примерами из них плохо справлялись уже имеющиеся продвинутые NLI модели.

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

  • NLI-style FEVER: корпус, проверяющий, есть ли в данном источнике подтверждение данного факта.

  • HELP: автоматически созданный корпус, требующий работы со значениями слов и логикой.

  • IMPPRES: автоматически сгенерированный датасет, анализирующий допущения, неявно подразумеваемые в тексте.

  • IIE: корпус из статьи"Inference is everything", представляющий в форме NLI другие лингвистические задачи: определение семантических ролей, понимание контекстных парафраз, и расшифровка местоимений.

  • JOCI: корпус, фокусирующийся на применении "здравого смысла" (common sense).

  • MNLI: большой многожанровый корпус, собранный из разнообразных устных и письменных источников.

  • MoNLI: корпус, фокусирующийся на отношении "частное/общее".

  • MPE: корпус, где вывод нужно сделать на основе не одной, а множества предпосылок.

  • SCITAIL: корпус вопросов на научную тематику, собранный из экзаменов и интернета.

  • SICK: корпус, ориентированный на понимание того, как смысл фразы складывается из отдельных слов.

  • SNLI: огромный корпус подписей к картинкам, первый крупномасштабный датасет для задачи NLI.

  • TERRa: единственный корпус, который не пришлось переводить на русский.

Переведённые тексты я объединил в общий корпус. Для большинства корпусов я использовал готовую train/dev/test разбивку (хотя во многих из них test часть была скрыта), а остальные разбил сам. Для обучения двухклассовых моделей (entailment/not_entailment) я использовал все корпусы (1.6 миллиона обучающих примеров), а для трёхклассовой (entailment/contradiction/neutral) – только ANLINLI-style FEVER, IMPPRES.JOCIMNLIMPESICKSNLI (1.3 миллиона).

Модели

На собранном мною датасете я обучил три модели (доступен блокнот с обучением и оценкой). Модель cointegrated/rubert-base-cased-nli-threeway обучалась разделять все три класса, а модели cointegrated/rubert-base-cased-nli-twoway и cointegrated/rubert-tiny-bilingual-nli – только отличать entailment от остальных. В моделях threeway и twoway за основу взята нейросеть DeepPavlov/rubert-base-cased размером 700 мб, а в модели tiny – cointegrated/rubert-tiny размером 45 мб. Поэтому версия tiny получилась ожидаемо глупее своих более крупных братьев, но зато и на порядок быстрее. Кроме того, в версии tiny я в 30% случаев подменял русский текст обучающего примера на английский, поэтому она умеет работать с разными комбинациями предпосылки и гипотезы на русском и английском языках.

Собственно для NLI модели можно применять примерно так:

import torch
from transformers import AutoTokenizer, AutoModelForSequenceClassification

model_checkpoint = 'cointegrated/rubert-base-cased-nli-threeway'
tokenizer = AutoTokenizer.from_pretrained(model_checkpoint)
model = AutoModelForSequenceClassification.from_pretrained(model_checkpoint)
if torch.cuda.is_available():
    model.cuda()

text1 = 'Сократ - человек, а все люди смертны.'
text2 = 'Сократ никогда не умрёт.'
with torch.inference_mode():
    out = model(**tokenizer(text1, text2, return_tensors='pt').to(model.device))
    proba = torch.softmax(out.logits, -1).cpu().numpy()[0]
print(proba)
# [0.00952593 0.9332064  0.05726764]
print({v: proba[k] for k, v in model.config.id2label.items()})
# {'entailment': 0.009525929, 'contradiction': 0.9332064, 'neutral': 0.05726764} 

Чтобы оценить качество моделей, я рассчитал ROC AUC на dev выборке каждого из доступных датасетов. Для двухклассовых моделей я считал AUC класса entailment, для трёхклассовой – AUC каждого из классов. Кроме этого, я добавил в таблицу две мультиязычные NLI модели, понимающие в том числе и русский: xlm-roberta-large-xnli-anli и bart-large-mnli.

model

class

add_one_rte

anli_r1

anli_r2

anli_r3

copa

fever

help

iie

imppres

joci

mnli

monli

mpe

scitail

sick

snli

terra

total

n_observations

387

1000

1000

1200

200

20474

3355

31232

7661

939

19647

269

1000

2126

500

9831

307

101128

tiny

entailment

0.77

0.59

0.52

0.53

0.53

0.90

0.81

0.78

0.93

0.81

0.82

0.91

0.81

0.78

0.93

0.95

0.67

0.77

twoway

entailment

0.89

0.73

0.61

0.62

0.58

0.96

0.92

0.87

0.99

0.90

0.90

0.99

0.91

0.96

0.97

0.97

0.87

0.86

threeway

entailment

0.91

0.75

0.61

0.61

0.57

0.96

0.56

0.61

0.99

0.90

0.91

0.67

0.92

0.84

0.98

0.98

0.90

0.80

xlm-roberta-large-xnli-anli

entailment

0.88

0.79

0.63

0.66

0.57

0.93

0.56

0.62

0.77

0.80

0.90

0.70

0.83

0.84

0.91

0.93

0.93

0.78

bart-large-mnli

entailment

0.51

0.41

0.43

0.47

0.50

0.74

0.55

0.57

0.60

0.63

0.70

0.52

0.56

0.68

0.67

0.72

0.64

0.58

threeway

contradiction

0.71

0.64

0.61

0.97

1.00

0.77

0.92

0.89

0.99

0.98

0.85

threeway

neutral

0.79

0.70

0.62

0.91

0.99

0.68

0.86

0.79

0.96

0.96

0.83

Перформанс модели XLM весьма неплох, но надо сделать скидку на то, что это большая и довольно медленная модель класса large. На видеокарте, с которой я работал, XLM и BART обрабатывали за секунду чуть меньше 2 батчей по 32 пары текстов, RuBERT-base – 7 батчей в секунду, а RuBERT-tiny – аж 36 батчей в секунду.

В целом, для задач zero-shot classification и распознавания entailment я рекомендую выбирать между умной и медленной cointegrated/rubert-base-cased-nli-twoway и глупой и быстрой cointegrated/rubert-tiny-bilingual-nli. Если же вам важно различать разницу между классами neutral и contradiction, то берите модель cointegrated/rubert-base-cased-nli-threeway, которая тоже работает весьма неплохо.

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

А если вы уже успели попробовать применить русские модели для NLI, то пишите в комменты: на каких данных применяли, какой результат получился, какое общее впечатление? И не забывайте лайкать этот пост ????.

Мир вам, и да пребудет с вами сила умозаключений!

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


  1. MAXH0
    10.10.2021 16:03
    +3

    Давид, классный проект! Нам такого точно не хватает.

    Но русский язык он такой: >> Хакер Вася - кот. Он пасёт индивидуалок девочек-вебщиц. Поэтому он козел! << Я понимаю, что это про жаргон. Но просто ваши примеры в такой пасьянс у меня сложились в голове.


    1. cointegrated Автор
      10.10.2021 16:21
      +3

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


  1. Gorodecki
    10.10.2021 16:49

    Давид, спасибо за проделанный труд! Обязательно протестирую на какой-нибудь задачке????????