15.5 Логический вывод на естественном языке: внимание
В разделе 15.4 мы представили задачу логического вывода на естественном языке и набор данных SNLI. Поскольку многие модели основаны на сложных и глубоких архитектурах, Parikh и др. предложили решать задачу логического вывода на естественном языке с помощью механизмов внимания и назвали этот подход «моделью декомпозируемого внимания» (decomposable attention model) (Parikh et al., 2016). Так получилась модель без рекуррентных и свёрточных слоёв, которая достигла на наборе данных SNLI лучшего на тот момент результата с гораздо меньшим числом параметров. В этом разделе мы опишем и реализуем такой метод логического вывода на естественном языке на основе внимания (с использованием многослойных перцептронов), как показано на рис. 15.5.1.
Рис. 15.5.1. Подача предобученных GloVe в архитектуру логического вывода на естественном языке на основе внимания и многослойных перцептронов
15.5.1 Модель
Вместо того чтобы сохранять порядок токенов в посылке и гипотезе, можно выровнять токены одной текстовой последовательности с каждым токеном другой, а затем сравнить и агрегировать эту информацию, чтобы предсказать логическое отношение между посылкой и гипотезой. Подобно выравниванию токенов между исходными и целевыми предложениями в машинном переводе, выравнивание токенов между посылкой и гипотезой можно гибко выполнять с помощью механизмов внимания.
Рис. 15.5.2. Логический вывод на естественном языке с помощью механизмов внимания
На рис. 15.5.2 показан метод логического вывода на естественном языке с механизмами внимания. На высоком уровне он состоит из трёх совместно обучаемых шагов: внимания (attending), сравнения (comparing) и агрегации (aggregating). Ниже мы опишем их шаг за шагом.
MXNet:
from d2l import mxnet as d2l
from mxnet import gluon, init, np, npx
from mxnet.gluon import nn
npx.set_np()
PyTorch:
from d2l import torch as d2l
import torch
from torch import nn
from torch.nn import functional as F
PaddlePaddle:
from d2l import paddle as d2l
import warnings
warnings.filterwarnings("ignore")
import paddle
from paddle import nn
from paddle.nn import functional as F
15.5.1.1 Внимание (Attending)
Первый шаг — выровнять токены одной текстовой последовательности с каждым токеном другой. Допустим, посылка — «i do need sleep» («мне действительно нужно поспать»), а гипотеза — «i am tired» («я устал»). Из-за семантической близости можно выровнять «i» в гипотезе с «i» в посылке и «tired» в гипотезе с «sleep» в посылке. Аналогично можно выровнять «i» в посылке с «i» в гипотезе, а «need» и «sleep» в посылке — с «tired» в гипотезе. Обратите внимание, что такое выравнивание «мягкое» — оно использует взвешенное среднее, где в идеале большие веса связаны с токенами, которые нужно выровнять. Для простоты демонстрации на рис. 15.5.2 это выравнивание показано «жёстким» способом.
Теперь опишем мягкое выравнивание с механизмами внимания подробнее. Обозначим через и посылку и гипотезу, число токенов в которых равно и соответственно, где () — -мерные векторы слов. Для мягкого выравнивания мы вычисляем веса внимания как
где функция — многослойный перцептрон, определённый в следующей функции mlp. Выходная размерность задаётся аргументом num_hiddens функции mlp.
MXNet:
def mlp(num_hiddens, flatten):
net = nn.Sequential()
net.add(nn.Dropout(0.2))
net.add(nn.Dense(num_hiddens, activation='relu', flatten=flatten))
net.add(nn.Dropout(0.2))
net.add(nn.Dense(num_hiddens, activation='relu', flatten=flatten))
return net
PyTorch:
def mlp(num_inputs, num_hiddens, flatten):
net = []
net.append(nn.Dropout(0.2))
net.append(nn.Linear(num_inputs, num_hiddens))
net.append(nn.ReLU())
if flatten:
net.append(nn.Flatten(start_dim=1))
net.append(nn.Dropout(0.2))
net.append(nn.Linear(num_hiddens, num_hiddens))
net.append(nn.ReLU())
if flatten:
net.append(nn.Flatten(start_dim=1))
return nn.Sequential(*net)
PaddlePaddle:
def mlp(num_inputs, num_hiddens, flatten):
net = []
net.append(nn.Dropout(0.2))
net.append(nn.Linear(num_inputs, num_hiddens))
net.append(nn.ReLU())
if flatten:
net.append(nn.Flatten(start_axis=1))
net.append(nn.Dropout(0.2))
net.append(nn.Linear(num_hiddens, num_hiddens))
net.append(nn.ReLU())
if flatten:
net.append(nn.Flatten(start_axis=1))
return nn.Sequential(*net)
Следует отметить, что в (15.5.1) принимает на вход и по отдельности, а не пару из них вместе. Такой приём декомпозиции приводит к тому, что вычисляется всего раз (линейная сложность), а не раз (квадратичная сложность).
Нормируя веса внимания из (15.5.1), мы вычисляем взвешенное среднее всех векторов токенов гипотезы, чтобы получить представление гипотезы, мягко выровненной с токеном посылки с индексом :
Аналогично вычислим мягкое выравнивание токенов посылки для каждого токена гипотезы с индексом :
Ниже мы определяем класс Attend, который вычисляет мягкое выравнивание гипотезы (beta) с входной посылкой A и мягкое выравнивание посылки (alpha) с входной гипотезой B.
MXNet:
class Attend(nn.Block):
def __init__(self, num_hiddens, **kwargs):
super(Attend, self).__init__(**kwargs)
self.f = mlp(num_hiddens=num_hiddens, flatten=False)
def forward(self, A, B):
# Форма A/B: (размер минибатча, число токенов последовательности A/B,
# embed_size)
# Форма f_A/f_B: (размер минибатча, число токенов
# последовательности A/B, num_hiddens)
f_A = self.f(A)
f_B = self.f(B)
# Форма e: (размер минибатча, число токенов последовательности A,
# число токенов последовательности B)
e = npx.batch_dot(f_A, f_B, transpose_b=True)
# Форма beta: (размер минибатча, число токенов последовательности A,
# embed_size), то есть последовательность B мягко выравнивается
# с каждым токеном последовательности A (измерение 1 у beta)
beta = npx.batch_dot(npx.softmax(e), B)
# Форма alpha: (размер минибатча, число токенов последовательности B,
# embed_size), то есть последовательность A мягко выравнивается
# с каждым токеном последовательности B (измерение 1 у alpha)
alpha = npx.batch_dot(npx.softmax(e.transpose(0, 2, 1)), A)
return beta, alpha
PyTorch:
class Attend(nn.Module):
def __init__(self, num_inputs, num_hiddens, **kwargs):
super(Attend, self).__init__(**kwargs)
self.f = mlp(num_inputs, num_hiddens, flatten=False)
def forward(self, A, B):
# Форма A/B: (размер минибатча, число токенов последовательности A/B,
# embed_size)
# Форма f_A/f_B: (размер минибатча, число токенов
# последовательности A/B, num_hiddens)
f_A = self.f(A)
f_B = self.f(B)
# Форма e: (размер минибатча, число токенов последовательности A,
# число токенов последовательности B)
e = torch.bmm(f_A, f_B.permute(0, 2, 1))
# Форма beta: (размер минибатча, число токенов последовательности A,
# embed_size), то есть последовательность B мягко выравнивается
# с каждым токеном последовательности A (измерение 1 у beta)
beta = torch.bmm(F.softmax(e, dim=-1), B)
# Форма alpha: (размер минибатча, число токенов последовательности B,
# embed_size), то есть последовательность A мягко выравнивается
# с каждым токеном последовательности B (измерение 1 у alpha)
alpha = torch.bmm(F.softmax(e.permute(0, 2, 1), dim=-1), A)
return beta, alpha
PaddlePaddle:
class Attend(nn.Layer):
def __init__(self, num_inputs, num_hiddens, **kwargs):
super(Attend, self).__init__(**kwargs)
self.f = mlp(num_inputs, num_hiddens, flatten=False)
def forward(self, A, B):
# Форма A/B: (размер минибатча, число токенов последовательности A/B,
# embed_size)
# Форма f_A/f_B: (размер минибатча, число токенов
# последовательности A/B, num_hiddens)
f_A = self.f(A)
f_B = self.f(B)
# Форма e: (размер минибатча, число токенов последовательности A,
# число токенов последовательности B)
e = paddle.bmm(f_A, f_B.transpose([0, 2, 1]))
# Форма beta: (размер минибатча, число токенов последовательности A,
# embed_size), то есть последовательность B мягко выравнивается
# с каждым токеном последовательности A (измерение 1 у beta)
beta = paddle.bmm(F.softmax(e, axis=-1), B)
# Форма alpha: (размер минибатча, число токенов последовательности B,
# embed_size), то есть последовательность A мягко выравнивается
# с каждым токеном последовательности B (измерение 1 у alpha)
alpha = paddle.bmm(F.softmax(e.transpose([0, 2, 1]), axis=-1), A)
return beta, alpha
15.5.1.2 Сравнение (Comparing)
На следующем шаге мы сравниваем токен одной последовательности с той последовательностью, которая мягко с ним выровнена. Обратите внимание, что при мягком выравнивании все токены одной последовательности (хотя, возможно, с разными весами внимания) сравниваются с токеном другой последовательности. Для простоты демонстрации на рис. 15.5.2 токены выровнены жёстко. Например, если на шаге внимания (attending) определено, что «need» и «sleep» в посылке выровнены с «tired» в гипотезе, то будет сравниваться пара «tired — need sleep».
На шаге сравнения мы подаём конкатенацию (оператор ) токенов одной последовательности и выровненных токенов другой в функцию (многослойный перцептрон):
В (15.5.2) — это сравнение токена посылки со всеми токенами гипотезы, мягко выровненными с ним, а — сравнение токена гипотезы со всеми токенами посылки, мягко выровненными с ним. Шаг сравнения определяет следующий класс Compare.
MXNet:
class Compare(nn.Block):
def __init__(self, num_hiddens, **kwargs):
super(Compare, self).__init__(**kwargs)
self.g = mlp(num_hiddens=num_hiddens, flatten=False)
def forward(self, A, B, beta, alpha):
V_A = self.g(np.concatenate([A, beta], axis=2))
V_B = self.g(np.concatenate([B, alpha], axis=2))
return V_A, V_B
PyTorch:
class Compare(nn.Module):
def __init__(self, num_inputs, num_hiddens, **kwargs):
super(Compare, self).__init__(**kwargs)
self.g = mlp(num_inputs, num_hiddens, flatten=False)
def forward(self, A, B, beta, alpha):
V_A = self.g(torch.cat([A, beta], dim=2))
V_B = self.g(torch.cat([B, alpha], dim=2))
return V_A, V_B
PaddlePaddle:
class Compare(nn.Layer):
def __init__(self, num_inputs, num_hiddens, **kwargs):
super(Compare, self).__init__(**kwargs)
self.g = mlp(num_inputs, num_hiddens, flatten=False)
def forward(self, A, B, beta, alpha):
V_A = self.g(paddle.concat([A, beta], axis=2))
V_B = self.g(paddle.concat([B, alpha], axis=2))
return V_A, V_B
15.5.1.3 Агрегация (Aggregating)
Теперь у нас есть два набора векторов сравнения () и (). На последнем шаге мы агрегируем эту информацию, чтобы вывести логическое отношение. Сначала суммируем оба набора векторов сравнения:
Затем подаём конкатенацию двух сумм в функцию (многослойный перцептрон), чтобы получить результат классификации логического отношения:
Шаг агрегации определён в следующем классе Aggregate.
MXNet:
class Aggregate(nn.Block):
def __init__(self, num_hiddens, num_outputs, **kwargs):
super(Aggregate, self).__init__(**kwargs)
self.h = mlp(num_hiddens=num_hiddens, flatten=True)
self.h.add(nn.Dense(num_outputs))
def forward(self, V_A, V_B):
# Суммируем два набора векторов сравнения по отдельности
V_A = V_A.sum(axis=1)
V_B = V_B.sum(axis=1)
# Подаём конкатенацию двух сумм в многослойный перцептрон
Y_hat = self.h(np.concatenate([V_A, V_B], axis=1))
return Y_hat
PyTorch:
class Aggregate(nn.Module):
def __init__(self, num_inputs, num_hiddens, num_outputs, **kwargs):
super(Aggregate, self).__init__(**kwargs)
self.h = mlp(num_inputs, num_hiddens, flatten=True)
self.linear = nn.Linear(num_hiddens, num_outputs)
def forward(self, V_A, V_B):
# Суммируем два набора векторов сравнения по отдельности
V_A = V_A.sum(dim=1)
V_B = V_B.sum(dim=1)
# Подаём конкатенацию двух сумм в многослойный перцептрон
Y_hat = self.linear(self.h(torch.cat([V_A, V_B], dim=1)))
return Y_hat
PaddlePaddle:
class Aggregate(nn.Layer):
def __init__(self, num_inputs, num_hiddens, num_outputs, **kwargs):
super(Aggregate, self).__init__(**kwargs)
self.h = mlp(num_inputs, num_hiddens, flatten=True)
self.linear = nn.Linear(num_hiddens, num_outputs)
def forward(self, V_A, V_B):
# Суммируем два набора векторов сравнения по отдельности
V_A = V_A.sum(axis=1)
V_B = V_B.sum(axis=1)
# Подаём конкатенацию двух сумм в многослойный перцептрон
Y_hat = self.linear(self.h(paddle.concat([V_A, V_B], axis=1)))
return Y_hat
15.5.1.4 Сборка кода
Объединяя шаги внимания, сравнения и агрегации, мы определяем модель декомпозируемого внимания, совместно обучающую эти три шага.
MXNet:
class DecomposableAttention(nn.Block):
def __init__(self, vocab, embed_size, num_hiddens, **kwargs):
super(DecomposableAttention, self).__init__(**kwargs)
self.embedding = nn.Embedding(len(vocab), embed_size)
self.attend = Attend(num_hiddens)
self.compare = Compare(num_hiddens)
# Возможны 3 выхода: следование, противоречие и нейтральность
self.aggregate = Aggregate(num_hiddens, 3)
def forward(self, X):
premises, hypotheses = X
A = self.embedding(premises)
B = self.embedding(hypotheses)
beta, alpha = self.attend(A, B)
V_A, V_B = self.compare(A, B, beta, alpha)
Y_hat = self.aggregate(V_A, V_B)
return Y_hat
PyTorch:
class DecomposableAttention(nn.Module):
def __init__(self, vocab, embed_size, num_hiddens, num_inputs_attend=100,
num_inputs_compare=200, num_inputs_agg=400, **kwargs):
super(DecomposableAttention, self).__init__(**kwargs)
self.embedding = nn.Embedding(len(vocab), embed_size)
self.attend = Attend(num_inputs_attend, num_hiddens)
self.compare = Compare(num_inputs_compare, num_hiddens)
# Возможны 3 выхода: следование, противоречие и нейтральность
self.aggregate = Aggregate(num_inputs_agg, num_hiddens, num_outputs=3)
def forward(self, X):
premises, hypotheses = X
A = self.embedding(premises)
B = self.embedding(hypotheses)
beta, alpha = self.attend(A, B)
V_A, V_B = self.compare(A, B, beta, alpha)
Y_hat = self.aggregate(V_A, V_B)
return Y_hat
PaddlePaddle:
class DecomposableAttention(nn.Layer):
def __init__(self, vocab, embed_size, num_hiddens, num_inputs_attend=100,
num_inputs_compare=200, num_inputs_agg=400, **kwargs):
super(DecomposableAttention, self).__init__(**kwargs)
self.embedding = nn.Embedding(len(vocab), embed_size)
self.attend = Attend(num_inputs_attend, num_hiddens)
self.compare = Compare(num_inputs_compare, num_hiddens)
# Возможны 3 выхода: следование, противоречие и нейтральность
self.aggregate = Aggregate(num_inputs_agg, num_hiddens, num_outputs=3)
def forward(self, X):
premises, hypotheses = X
A = self.embedding(premises)
B = self.embedding(hypotheses)
beta, alpha = self.attend(A, B)
V_A, V_B = self.compare(A, B, beta, alpha)
Y_hat = self.aggregate(V_A, V_B)
return Y_hat
15.5.2 Обучение и оценка модели
Теперь мы обучим и оценим определённую модель декомпозируемого внимания на наборе данных SNLI. Начнём с чтения набора данных.
15.5.2.1 Чтение набора данных
Мы скачиваем и читаем набор данных SNLI с помощью функции, определённой в разделе 15.4. Размер минибатча и длина последовательности равны и соответственно.
MXNet, PyTorch:
batch_size, num_steps = 256, 50
train_iter, test_iter, vocab = d2l.load_data_snli(batch_size, num_steps)
PaddlePaddle:
def load_data_snli(batch_size, num_steps=50):
"""Скачивает набор данных SNLI и возвращает итераторы данных и словарь
Определено в разделе 15.4"""
data_dir = d2l.download_extract('SNLI')
train_data = d2l.read_snli(data_dir, True)
test_data = d2l.read_snli(data_dir, False)
train_set = d2l.SNLIDataset(train_data, num_steps)
test_set = d2l.SNLIDataset(test_data, num_steps, train_set.vocab)
train_iter = paddle.io.DataLoader(train_set,batch_size=batch_size,
shuffle=True,
num_workers=0,
return_list=True)
test_iter = paddle.io.DataLoader(test_set, batch_size=batch_size,
shuffle=False,
num_workers=0,
return_list=True)
return train_iter, test_iter, train_set.vocab
batch_size, num_steps = 256, 50
train_iter, test_iter, vocab = load_data_snli(batch_size, num_steps)
15.5.2.2 Создание модели
Для представления входных токенов мы используем предобученные 100-мерные вложения GloVe. Таким образом, размерность векторов и в (15.5.1) заранее задана равной 100. Выходная размерность функции в (15.5.1) и функции в (15.5.2) равна 200. Затем мы создаём экземпляр модели, инициализируем её параметры и загружаем вложения GloVe для инициализации векторов входных токенов.
MXNet:
embed_size, num_hiddens, devices = 100, 200, d2l.try_all_gpus()
net = DecomposableAttention(vocab, embed_size, num_hiddens)
net.initialize(init.Xavier(), ctx=devices)
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]
net.embedding.weight.set_data(embeds)
PyTorch:
embed_size, num_hiddens, devices = 100, 200, d2l.try_all_gpus()
net = DecomposableAttention(vocab, embed_size, num_hiddens)
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]
net.embedding.weight.data.copy_(embeds);
PaddlePaddle:
embed_size, num_hiddens, devices = 100, 200, d2l.try_all_gpus()
net = DecomposableAttention(vocab, embed_size, num_hiddens)
glove_embedding = d2l.TokenEmbedding('glove.6b.100d')
embeds = glove_embedding[vocab.idx_to_token]
net.embedding.weight.set_value(embeds);
15.5.2.3 Обучение и оценка модели
В отличие от функции split_batch из раздела 12.5, принимающей один вход (например, текстовую последовательность или изображение), мы определяем функцию split_batch_multi_inputs, которая принимает в минибатче несколько входов, например посылки и гипотезы.
MXNet:
#@save
def split_batch_multi_inputs(X, y, devices):
"""Разделяет многовходные 'X' и 'y' между несколькими устройствами"""
X = list(zip(*[gluon.utils.split_and_load(
feature, devices, even_split=False) for feature in X]))
return (X, gluon.utils.split_and_load(y, devices, even_split=False))
Теперь можно обучить и оценить модель на наборе данных SNLI.
MXNet:
lr, num_epochs = 0.001, 4
trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': lr})
loss = gluon.loss.SoftmaxCrossEntropyLoss()
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices, split_batch_multi_inputs)
PyTorch:
lr, num_epochs = 0.001, 4
trainer = torch.optim.Adam(net.parameters(), lr=lr)
loss = nn.CrossEntropyLoss(reduction="none")
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices)
PaddlePaddle:
lr, num_epochs = 0.001, 4
trainer = paddle.optimizer.Adam(learning_rate=lr, parameters=net.parameters())
loss = nn.CrossEntropyLoss(reduction="none")
d2l.train_ch13(net, train_iter, test_iter, loss, trainer, num_epochs,
devices[:1])
15.5.2.4 Использование модели
Наконец, определим функцию предсказания, которая выводит логическое отношение между парой посылки и гипотезы.
MXNet:
#@save
def predict_snli(net, vocab, premise, hypothesis):
"""Предсказывает логическое отношение между посылкой и гипотезой"""
premise = np.array(vocab[premise], ctx=d2l.try_gpu())
hypothesis = np.array(vocab[hypothesis], ctx=d2l.try_gpu())
label = np.argmax(net([premise.reshape((1, -1)),
hypothesis.reshape((1, -1))]), axis=1)
return 'entailment' if label == 0 else 'contradiction' if label == 1 \
else 'neutral'
PyTorch:
#@save
def predict_snli(net, vocab, premise, hypothesis):
"""Предсказывает логическое отношение между посылкой и гипотезой"""
net.eval()
premise = torch.tensor(vocab[premise], device=d2l.try_gpu())
hypothesis = torch.tensor(vocab[hypothesis], device=d2l.try_gpu())
label = torch.argmax(net([premise.reshape((1, -1)),
hypothesis.reshape((1, -1))]), dim=1)
return 'entailment' if label == 0 else 'contradiction' if label == 1 \
else 'neutral'
PaddlePaddle:
#@save
def predict_snli(net, vocab, premise, hypothesis):
"""Предсказывает логическое отношение между посылкой и гипотезой"""
net.eval()
premise = paddle.to_tensor(vocab[premise], place=d2l.try_gpu())
hypothesis = paddle.to_tensor(vocab[hypothesis], place=d2l.try_gpu())
label = paddle.argmax(net([premise.reshape((1, -1)),
hypothesis.reshape((1, -1))]), axis=1)
return 'entailment' if label == 0 else 'contradiction' if label == 1 \
else 'neutral'
С помощью обученной модели можно получить результат логического вывода на естественном языке для примера предложений.
predict_snli(net, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.'])
Резюме
- Модель декомпозируемого внимания состоит из трёх шагов для предсказания логического отношения между посылкой и гипотезой: внимания, сравнения и агрегации.
- С помощью механизмов внимания можно выровнять токены одной текстовой последовательности с каждым токеном другой и наоборот. Это выравнивание мягкое и использует взвешенное среднее, где в идеале большие веса связаны с токенами, которые нужно выровнять.
- При вычислении весов внимания приём декомпозиции даёт линейную, а не квадратичную сложность, что выгоднее.
- В качестве входного представления для прикладных задач обработки естественного языка, таких как логический вывод на естественном языке, можно использовать предобученные векторы слов.
Упражнения
- Обучите модель с другими сочетаниями гиперпараметров. Можно ли получить более высокую точность на тестовом наборе?
- Каковы главные недостатки модели декомпозируемого внимания для логического вывода на естественном языке?
- Допустим, нужно получить уровень семантического сходства для любой пары предложений (например, непрерывное значение от 0 до 1). Как собрать и разметить набор данных? Попробуйте спроектировать модель с механизмами внимания.
MXNet: Обсуждение
PyTorch: Обсуждение
PaddlePaddle: Обсуждение
Литература
- Parikh, A. P., Täckström, O., Das, D., & Uszkoreit, J. (2016). A decomposable attention model for natural language inference. arXiv preprint arXiv:1606.01933.