Содержание курса 28 / 37

28 / 37 · Задачи NLP

Семантическая близость и пары предложений

Задачи, на вход которым подаются два текста: семантическая близость текстов, поиск перефразировок и логический вывод на естественном языке; почему усреднённые векторы слов не справляются с парами, как близость оценивается ранговой корреляцией и два способа использовать трансформер: кросс-энкодер, который читает оба текста вместе, и би-энкодер (Sentence-BERT), который кодирует их по отдельности, с точностью и стоимостью каждого.

На этой странице 13

Классификация текстов даёт одну метку одному тексту. Многие вопросы касаются сразу двух текстов: повторяют ли друг друга два вопроса на Quora, следует ли одно предложение из другого, насколько близки по смыслу запрос и фрагмент документа? Модели теперь нужна функция от пары, и то, как она читает пару, определяет и её точность, и её стоимость. Эта заметка разбирает задачи на парах, базовый подход, который выглядит разумно и не работает, способ измерять близость и две архитектуры трансформера, из которых построены все последующие системы, включая семантический поиск.

Обозначения. Пара текстов (a,b)(a, b) с меткой yy: классом или оценкой близости. Энкодер ff переводит текст в векторы токенов; усреднённые векторы предложений — u=f(a)u = f(a) и v=f(b)v = f(b). cos⁡(u,v)=u⋅v/(∥u∥∥v∥)\cos(u, v) = u \cdot v / (\lVert u \rVert \lVert v \rVert).

Три задачи на парах#

  • Семантическая близость текстов (STS). Оценка от 0 (не связаны) до 5 (равнозначны), усреднённая по нескольким разметчикам. В бенчмарке STS 8 628 пар из подписей к изображениям, новостей и форумов: 5 749 для обучения, 1 500 для валидации и 1 379 для теста.
  • Поиск перефразировок и дубликатов. Бинарная метка: говорят ли два текста одно и то же? Пары вопросов Quora (QQP в бенчмарке GLUE) спрашивают, дублируют ли два вопроса друг друга, — это то, что нужно сайту вопросов и ответов, чтобы объединять ветки.
  • Логический вывод на естественном языке (NLI). Три класса для посылки и гипотезы: гипотеза следует (entailment), противоречит (contradiction) или ни то ни другое (neutral). В SNLI 570 000 размеченных пар, в MultiNLI — 430 000 из большего числа жанров.

Порядок текстов важен для NLI и для поиска (короткий запрос против длинного фрагмента) и не важен для STS и перефразировок. Все три задачи оцениваются на отложенных парах: долей правильных ответов или F1F_1 для классификационных задач, ранговой корреляцией с эталонными оценками для STS.

Базовый подход: усреднить векторы слов и сравнить косинусом#

Заметка о векторных представлениях уже превращала вопрос в один вектор усреднением векторов его слов. Если сделать так для обоих текстов и взять косинус, получится мера близости без всякого обучения. С 32-мерными векторами CBOW, обученными на вопросах Quora в заметке о GloVe (без стоп-слов, как при обучении), пять игрушечных пар с оценками близости, выставленными вручную по шкале STS, выглядят так:

ПараРучная оценкаКосинус
How do I learn Python? / What is the best way to learn Python?50.891
How can I make money online? / How can I earn money from the internet?40.938
Who is the best football player? / Who is the best cricket player?20.969
How do I lose weight? / How do I gain weight?10.964
Why is the sky blue? / How can I make money online?00.023

Несвязанная пара отделяется чисто; всё остальное упорядочено неправильно. Lose weight и gain weight — противоположные просьбы, а после удаления стоп-слов это lose weight ? и gain weight ?: два из трёх токенов общие, а сами lose и gain встречаются в одних и тех же контекстах, поэтому их векторы близки. У football player и cricket player общие тема и структура. Среднее сохраняет тему и отбрасывает то, что различает тексты: какое слово отличается, его роль, отрицание, направление.

То же видно и в большом масштабе. На бенчмарке STS усреднённые векторы GloVe дают корреляцию Спирмена 58.02 (по традиции приводится ×100\times 100), а предобученный BERT, использованный так же, справляется хуже: 46.35 для усреднённых векторов токенов и 16.50 для вектора [CLS] (Reimers and Gurevych, 2019). Маскированную языковую модель не учат делать расстояния осмысленными.

Измерение близости: ранговая корреляция#

Оценки близости модели и людей живут на разных шкалах (косинусы между 0.6 и 1, эталон между 0 и 5), а для поиска и удаления дубликатов важен только порядок. Поэтому STS оценивается ранговой корреляцией Спирмена: каждый список заменяется рангами его значений, и считается корреляция рангов. Без совпадающих значений

ρ=1−6∑idi2n(n2−1),\rho = 1 - \frac{6 \sum_i d_i^2}{n (n^2 - 1)},

где did_i — разность двух рангов пары ii.

Разобранный пример. Пять пар с эталонными оценками (4.8,3.6,2.5,1.2,0.2)(4.8, 3.6, 2.5, 1.2, 0.2) и оценками модели (0.91,0.62,0.70,0.35,0.10)(0.91, 0.62, 0.70, 0.35, 0.10). Эталонные ранги — (5,4,3,2,1)(5, 4, 3, 2, 1), ранги модели — (5,3,4,2,1)(5, 3, 4, 2, 1): модель меняет местами вторую и третью пары. Разности рангов — (0,1,−1,0,0)(0, 1, -1, 0, 0), ∑di2=2\sum d_i^2 = 2, и ρ=1−6⋅2/(5⋅24)=0.9\rho = 1 - 6 \cdot 2 / (5 \cdot 24) = 0.9. (Корреляция Пирсона исходных значений равна 0.948; она вознаграждала бы ещё и правильные промежутки между значениями, а у косинуса нет причин их соблюдать.) Для пяти пар вопросов выше ρ=0.10\rho = 0.10.

Кросс-энкодер: читать пару вместе#

Предобученный энкодер может прочитать оба текста в одном входе, [CLS] a [SEP] b [SEP], где эмбеддинги сегментов отмечают, к какому тексту относится каждый токен (вход BERT). В каждом слое каждый токен aa смотрит на каждый токен bb: lose можно сравнить с gain напрямую. Выходной слой на позиции [CLS] выдаёт класс (NLI, перефразировки) или оценку (STS, обучение регрессией), и вся модель дообучается на размеченных парах. Это кросс-энкодер.

Это точный вариант. BERT-base, дообученный на обучающих парах бенчмарка STS, достигает на тесте корреляции Спирмена 84.30, а если сначала дообучить его на NLI — 88.33 (Reimers and Gurevych, 2019). Цена в том, что для отдельного текста ничего не вычисляется: каждая пара — новый прямой проход. Найти самую близкую пару среди n=10,000n = 10{,}000 предложений — это n(n−1)/2=49,995,000n(n-1)/2 = 49{,}995{,}000 проходов, около 65 часов на одном GPU V100 в измерении той же статьи.

Би-энкодер: закодировать каждый текст один раз#

Би-энкодер кодирует aa и bb по отдельности одним и тем же энкодером и сравнивает два вектора. Стандартный рецепт — Sentence-BERT (Reimers and Gurevych, 2019):

  1. Закодировать. aa и bb проходят через один и тот же BERT, каждый отдельно; векторы токенов каждого текста усредняются в uu и vv.
  2. Обучить на NLI. Классификатор softmax на конкатенации (u,v,∣u−v∣)(u, v, |u - v|) с Wt∈R3×3dW_t \in \mathbb R^{3 \times 3d} предсказывает следование, противоречие или нейтральность; градиент обучает энкодер через обе копии.
  3. Использовать через косинус. После обучения классификатор выбрасывается, и мерой близости служит cos⁡(u,v)\cos(u, v). Для STS энкодер можно дообучить функцией потерь регрессии — квадратом разности между cos⁡(u,v)\cos(u, v) и эталонной оценкой, пересчитанной в диапазон косинуса: косинус не превышает 1, поэтому оценка yy по шкале 0–5 переводится в y/5∈[0,1]y/5 \in [0, 1], как в коде обучения Sentence-BERT. Эталонная оценка 3.6 становится 0.72, и косинус 0.62 обходится в (0.62−0.72)2=0.01(0.62 - 0.72)^2 = 0.01.

Конкатенация важнее способа усреднения. При обучении на NLI и измерении на валидационной части STS одно (u,v)(u, v) даёт 66.04, одно ∣u−v∣|u - v| — 69.78, а (u,v,∣u−v∣)(u, v, |u - v|) — 80.78, тогда как усреднение, [CLS] и максимум дают 80.78, 79.80 и 79.07. Поэлементная разность говорит классификатору, а через него и энкодеру, в каких измерениях два предложения расходятся.

Рисунок

Две архитектуры для пары предложений

Кросс-энкодер и би-энкодерСлева один энкодер читает [CLS] a [SEP] b [SEP], линии внимания соединяют токены двух предложений, голова на [CLS] выдаёт оценку. Справа два одинаковых энкодера читают a и b отдельно, усреднение даёт векторы u и v, которые сравниваются косинусом.(а) кросс-энкодер: пара читается вместе[CLS]a[SEP]b[SEP]энкодерголова на [CLS]оценка49 995 000 проходов на 10 000 предложенийоколо 65 часов(б) би-энкодер: каждое предложение отдельноaэнкодерmeanubэнкодерmeanvобщие весаcosобучение на NLI: softmax(W [u; v; |u − v|])10 000 проходов, затем косинусыоколо 5 секундвходсостояниевеса и гейтывыходКросс-энкодер и би-энкодерСлева один энкодер читает [CLS] a [SEP] b [SEP], линии внимания соединяют токены двух предложений, голова на [CLS] выдаёт оценку. Справа два одинаковых энкодера читают a и b отдельно, усреднение даёт векторы u и v, которые сравниваются косинусом.(а) кросс-энкодер: пара читается вместе[CLS]a[SEP]b[SEP]энкодерголова на [CLS]оценка49 995 000 проходов на 10 000 предложенийоколо 65 часов(б) би-энкодер: каждое предложение отдельноaэнкодерmeanubэнкодерmeanvобщие весаcosобучение на NLI: softmax(W [u; v; |u − v|])10 000 проходов, затем косинусыоколо 5 секундвходсостояниевеса и гейтывыход
(а) Кросс-энкодер получает оба предложения в одном входе, и в каждом слое каждый токен одного предложения может смотреть на каждый токен другого; голова на позиции [CLS] выдаёт класс или оценку. Это точнее, но каждая пара требует отдельного прохода: для 10 000 предложений — 49 995 000 проходов, около 65 часов в измерении Reimers и Gurevych (2019). (б) Би-энкодер (Sentence-BERT) прогоняет каждое предложение отдельно через один и тот же энкодер и усредняет векторы токенов в u и v; при обучении на NLI классификатор смотрит на (u, v, |u − v|), а при использовании пары сравниваются косинусом. 10 000 предложений — это 10 000 проходов, около 5 секунд, а дальше только скалярные произведения.

Результат — пригодный вектор предложения. Без всякого обучения на STS Sentence-BERT-base, обученный на NLI, достигает 77.03 на бенчмарке STS — против 58.02 у усреднённого GloVe и 16.50 у [CLS] BERT. Обученный на NLI, а затем на STS, он достигает 85.35, на три пункта ниже 88.33 у кросс-энкодера. Взамен 10 000 предложений требуют 10 000 проходов, около 5 секунд, а их сравнение — это матрица скалярных произведений.

Что выбрать#

Две архитектуры меняют точность на стоимость, и системы их сочетают. Чтобы внимательно сравнить несколько пар (убрать дубликаты в коротком списке, проверить, следует ли утверждение из ответа), берите кросс-энкодер. Чтобы искать, кластеризовать или чистить от дубликатов много текстов, закодируйте каждый текст один раз би-энкодером, сохраните векторы и сравнивайте их косинусом, а лучших кандидатов переранжируйте кросс-энкодером: это схема «найти и переранжировать» из заметки о семантическом поиске. Современные модели эмбеддингов — это би-энкодеры, обученные контрастивно на гораздо большем числе пар, чем NLI, как описано в разделе о модели эмбеддингов той заметки.

Что изменилось#

Усреднённые векторы словКросс-энкодерБи-энкодер (Sentence-BERT)
Входкаждый текст отдельнопара в одной последовательностикаждый текст отдельно
Взаимодействие текстовнетв каждом слое вниманиятолько через cos⁡(u,v)\cos(u, v)
Обучениенетдообучение на размеченных парахклассификация NLI, затем при желании STS
Бенчмарк STS, Спирмен58.02 (GloVe)88.3385.35
10 000 предложений10 000 усреднений49 995 000 проходов10 000 проходов

Ограничения#

  • Метки — это градуированные мнения. Оценки STS усредняют суждения нескольких разметчиков о размытом понятии; метки перефразировок зависят от инструкций датасета (дублируют ли друг друга вопросы, если один добавляет деталь?).
  • Би-энкодеры сжимают. Один вектор на текст не может отразить все способы, которыми текст может относиться к другому; отрицание и маленькие, но решающие различия (lose или gain) остаются самыми трудными случаями и после обучения.
  • Сдвиг домена. Модель, обученная на подписях NLI и новостях, может плохо упорядочивать пары из медицинских или юридических текстов; близость выучивается, а не универсальна.
  • Симметричные и асимметричные задачи. STS симметрична, а релевантность фрагмента вопросу — нет, и модели, обученные для одного, неидеально переносятся на другое.

Что нужно запомнить#

  • Задачи на парах: STS (градуированная оценка, Спирмен), перефразировки (бинарная), NLI (три класса).
  • Усреднение векторов слов сохраняет тему и теряет различие; оно считает lose weight и gain weight почти дубликатами.
  • ρ\rho Спирмена сравнивает упорядочения, потому что осмыслен только порядок близостей: ρ=1−6∑di2/(n(n2−1))\rho = 1 - 6\sum d_i^2 / (n(n^2-1)).
  • Кросс-энкодер читает [CLS] a [SEP] b [SEP] и точнее всего, ценой одного прохода на пару.
  • Би-энкодер (Sentence-BERT) кодирует каждый текст один раз, обучается на NLI с (u,v,∣u−v∣)(u, v, |u - v|), сравнивает косинусом и отстаёт на STS на три пункта при ничтожной доле стоимости. Находить би-энкодером, переранжировать кросс-энкодером.

Ноутбук#

week01_word_embeddings.ipynb строит базовый подход этой заметки (номера ячеек считают все ячейки, включая текстовые, начиная с 1). В сохранённом ноутбуке у этих ячеек нет выводов, поэтому чисел из него здесь нет.

  • Ячейка 81 определяет get_phrase_embedding: нижний регистр, токенизация, слова, известные модели, и среднее их векторов, с нулевым вектором, если известных нет.
  • Ячейки 82–85 кодируют около тысячи вопросов и рисуют их в двух измерениях с t-SNE.
  • Ячейки 86–91 кодируют первые 1 000 вопросов и определяют find_nearest, которая упорядочивает их по косинусу с запросом; ячейки 89–91 запускают её на трёх запросах.
  • Ячейки 92–93 используют те же средние как признаки для логистической регрессии на AG News.

Числа этой заметки взяты из раздела с кодом и из статьи о Sentence-BERT.

Код#

Базовый подход с усреднением и ρ\rho Спирмена, с векторами CBOW из заметки о FastText:

import re, numpy as np
from scipy.stats import spearmanr
from sklearn.feature_extraction.text import ENGLISH_STOP_WORDS

wv = w2v.wv                                       # Word2Vec(sents, vector_size=32, window=5, min_count=5, workers=1, seed=0)
tok = re.compile(r"\w+|[^\w\s]+")

def embed(text):
    words = [t for t in tok.findall(text.lower()) if t not in ENGLISH_STOP_WORDS and t in wv.key_to_index]
    return np.mean([wv[w] for w in words], axis=0)

pairs = [
    ("How do I learn Python?", "What is the best way to learn Python?", 5),
    ("How can I make money online?", "How can I earn money from the internet?", 4),
    ("Who is the best football player?", "Who is the best cricket player?", 2),
    ("How do I lose weight?", "How do I gain weight?", 1),
    ("Why is the sky blue?", "How can I make money online?", 0),
]
cos = []
for a, b, score in pairs:
    u, v = embed(a), embed(b)
    cos.append(u @ v / np.linalg.norm(u) / np.linalg.norm(v))
print(np.round(cos, 3))                                    # [0.891 0.938 0.969 0.964 0.023]
print(spearmanr([p[2] for p in pairs], cos).correlation)   # 0.1
print(spearmanr([4.8, 3.6, 2.5, 1.2, 0.2], [0.91, 0.62, 0.70, 0.35, 0.10]).correlation)  # 0.9

Дальше#

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

Литература#