Appearance
自然语言推断(注意力版)
一、概述
之前我们学过自然语言推断任务和 SNLI 数据集,这个章节我们用注意力机制来解决自然语言推断问题,使用的是 “可分解注意力模型”。这个模型很厉害,它没有循环层或者卷积层,用更少的参数在 SNLI 数据集上实现了当时的最佳结果。它的核心思路是把一个文本里的词和另一个文本里的每个词对齐,然后比较和聚合这些信息,从而预测前提和假设之间的逻辑关系。
二、可分解注意力模型的结构
这个模型一共分三个步骤,就像流水线一样,一步步完成自然语言推断的任务:
1. 注意(Attending):让两个文本的词 “互相配对”
这一步的作用是把一个文本里的词和另一个文本里的每个词 “软对齐”,也就是用加权平均的方式,找到每个词在另一个文本里最相关的词。
比如我们有前提 “我确实需要睡眠” 和假设 “我累了”,通过注意力机制,我们可以把假设里的 “我” 和前提里的 “我” 对齐,把假设里的 “累” 和前提里的 “睡眠” 对齐;反过来,也可以把前提里的 “我” 和假设里的 “我” 对齐,把前提里的 “需要” 和 “睡眠” 和假设里的 “累” 对齐。
(1)注意力权重的计算
我们用A表示前提,B表示假设,A里有m个词,B里有n个词,每个词都是一个d维的向量。我们用一个多层感知机f来计算每个词对的注意力权重: 这里的f是一个简单的多层感知机,用分解的技巧,只需要计算m+n次,而不是m*n次,大大减少了计算量。
然后我们把注意力权重规范化,计算出假设里的词对前提里每个词的软对齐: 同样,也可以计算出前提里的词对假设里每个词的软对齐:
(2)代码实现(以 PyTorch 为例)
首先定义多层感知机mlp:
python
import torch
from torch import nn
from d2l import torch as d2l
def mlp(num_inputs, num_hiddens, flatten):
net = nn.Sequential()
net.add_module('dropout', nn.Dropout(0.2))
net.add_module('dense', nn.Linear(num_inputs, num_hiddens, bias=True))
net.add_module('relu', nn.ReLU())
if flatten:
net.add_module('flatten', nn.Flatten())
net.add_module('dropout2', nn.Dropout(0.2))
net.add_module('dense2', nn.Linear(num_hiddens, num_hiddens, bias=True))
return net然后定义Attend类来计算软对齐:
python
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的每个词元
beta = torch.bmm(nn.functional.softmax(e, dim=-1), B)
# alpha的形状:(批量大小,序列B的词元数,embed_size),
# 意味着序列A被软对齐到序列B的每个词元
alpha = torch.bmm(nn.functional.softmax(e.permute(0, 2, 1), dim=-1), A)
return beta, alpha2. 比较(Comparing):把对齐后的词 “对比一下”
这一步的作用是把一个文本里的词和它在另一个文本里对齐的词进行比较。比如前提里的 “需要” 和 “睡眠” 都和假设里的 “累” 对齐,我们就把 “累” 和 “需要睡眠” 放在一起比较。
我们把一个文本里的词和它对齐的词拼接起来,然后用另一个多层感知机g来处理:
代码实现
定义Compare类来完成比较步骤:
python
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):
# 把词和对齐的词拼接起来
A_with_beta = torch.cat([A, beta], dim=-1)
B_with_alpha = torch.cat([B, alpha], dim=-1)
# 比较后的结果
V_A = self.g(A_with_beta)
V_B = self.g(B_with_alpha)
return V_A, V_B3. 聚合(Aggregating):把比较的结果 “汇总一下”
这一步的作用是把比较后的结果汇总起来,从而预测前提和假设之间的逻辑关系。首先我们把两组比较后的向量分别求和: 然后把这两个求和结果拼接起来,用另一个多层感知机h来得到最终的分类结果:
代码实现
定义Aggregate类来完成聚合步骤:
python
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.dense = nn.Linear(num_hiddens, num_outputs, bias=True)
def forward(self, V_A, V_B):
# 对比较后的结果求和
v_A = V_A.sum(dim=1)
v_B = V_B.sum(dim=1)
# 拼接后输入到多层感知机
v = torch.cat([v_A, v_B], dim=-1)
h_v = self.h(v)
# 输出分类结果
return self.dense(h_v)4. 整合整个模型
把上面三个步骤整合起来,就得到了完整的可分解注意力模型:
python
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三、训练和评估模型
1. 读取数据集
我们用之前学过的 SNLI 数据集,首先加载数据集:
python
batch_size = 256
num_steps = 50
train_iter, test_iter, vocab = d2l.load_data_snli(batch_size, num_steps)2. 创建模型
我们用预训练的词向量来初始化词嵌入层,这样可以让模型更好地理解语义:
python
embed_size = 100
num_hiddens = 200
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 创建模型
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)
# 把模型放到设备上
net = net.to(device)3. 训练和评估模型
我们用 Adam 优化器和交叉熵损失函数来训练模型:
python
lr = 0.001
num_epochs = 4
# 定义优化器
optimizer = torch.optim.Adam(net.parameters(), lr=lr)
# 定义损失函数
loss = nn.CrossEntropyLoss(reduction='none')
# 训练模型
d2l.train_ch13(net, train_iter, test_iter, loss, optimizer, num_epochs, device)训练完成后,模型在测试集上的准确率大概能达到 80% 左右,说明模型能很好地预测前提和假设之间的逻辑关系。
4. 使用模型
我们可以用训练好的模型来预测两个文本之间的逻辑关系:
python
def predict_snli(net, vocab, premise, hypothesis):
# 把文本转换成词索引
premise = torch.tensor(vocab[premise.split()], device=device)
hypothesis = torch.tensor(vocab[hypothesis.split()], device=device)
# 增加批量维度
X = (premise.unsqueeze(0), hypothesis.unsqueeze(0))
# 预测
pred = net(X).argmax(dim=1)
# 返回预测结果
return '蕴涵' if pred == 0 else '矛盾' if pred == 1 else '中性'
# 测试一下
print(predict_snli(net, vocab, '我确实需要睡眠', '我累了')) # 输出:蕴涵
print(predict_snli(net, vocab, '我正在跑步', '我在睡觉')) # 输出:矛盾
print(predict_snli(net, vocab, '猫在睡觉', '猫是动物')) # 输出:中性四、小结
可分解注意力模型用三个步骤完成自然语言推断:注意、比较、聚合,用注意力机制实现了文本之间的软对齐。
这个模型没有循环层和卷积层,参数更少,计算效率更高,还能取得不错的效果。
我们可以用预训练的词向量来初始化模型的词嵌入层,让模型更好地理解语义,提升模型的性能。
这个模型不仅可以用于自然语言推断,还可以用于其他需要比较两个文本的任务,比如文本匹配、问答系统等。
(注:文档部分内容可能由 AI 生成) 源地址