【自然语言处理】应用05:自然语言推断:使用注意力

【作者主页】Francek Chen

【专栏介绍】⌈ ⌈ ⌈PyTorch深度学习 ⌋ ⌋ ⌋ 深度学习 (DL, Deep Learning) 特指基于深层神经网络模型和方法的机器学习。它是在统计机器学习、人工神经网络等算法模型基础上,结合当代大数据和大算力的发展而发展出来的。深度学习最重要的技术特征是具有自动提取特征的能力。神经网络算法、算力和数据是开展深度学习的三要素。深度学习在计算机视觉、自然语言处理、多模态数据分析、科学探索等领域都取得了很多成果。本专栏介绍基于PyTorch的深度学习算法实现。

【GitCode】专栏资源保存在我的GitCode仓库:https://gitcode.com/Morse_Chen/PyTorch_deep_learning

文章目录


我们在自然语言推断与数据集中介绍了自然语言推断任务和SNLI数据集。鉴于许多模型都是基于复杂而深度的架构,Parikh等人提出用注意力机制解决自然语言推断问题,并称之为"可分解注意力模型"。这使得模型没有循环层或卷积层,在SNLI数据集上以更少的参数实现了当时的最佳结果。本节将描述并实现这种基于注意力的自然语言推断方法(使用MLP),如图1中所示。


图1 将预训练GloVe送入基于注意力和MLP的自然语言推断架构

一、模型

与保留前提和假设中词元的顺序相比,我们可以将一个文本序列中的词元与另一个文本序列中的每个词元对齐,然后比较和聚合这些信息,以预测前提和假设之间的逻辑关系。与机器翻译中源句和目标句之间的词元对齐类似,前提和假设之间的词元对齐可以通过注意力机制灵活地完成。


图2 利用注意力机制进行自然语言推断

图2描述了使用注意力机制的自然语言推断方法。从高层次上讲,它由三个联合训练的步骤组成:对齐、比较和汇总。我们将在下面一步一步地对它们进行说明。

python 复制代码
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

(一)注意(Attending)

第一步是将一个文本序列中的词元与另一个序列中的每个词元对齐。假设前提是"我确实需要睡眠",假设是"我累了"。由于语义上的相似性,我们不妨将假设中的"我"与前提中的"我"对齐,将假设中的"累"与前提中的"睡眠"对齐。同样,我们可能希望将前提中的"我"与假设中的"我"对齐,将前提中的"需要"和"睡眠"与假设中的"累"对齐。请注意,这种对齐是使用加权平均的"软"对齐,其中理想情况下较大的权重与要对齐的词元相关联。为了便于演示,图2以"硬"对齐的方式显示了这种对齐方式。

现在,我们更详细地描述使用注意力机制的软对齐。用 A = ( a 1 , ... , a m ) \mathbf{A} = (\mathbf{a}_1, \ldots, \mathbf{a}_m) A=(a1,...,am)和 B = ( b 1 , ... , b n ) \mathbf{B} = (\mathbf{b}_1, \ldots, \mathbf{b}_n) B=(b1,...,bn)表示前提和假设,其词元数量分别为 m m m和 n n n,其中 a i , b j ∈ R d \mathbf{a}i, \mathbf{b}j \in \mathbb{R}^{d} ai,bj∈Rd( i = 1 , ... , m , j = 1 , ... , n i = 1, \ldots, m, j = 1, \ldots, n i=1,...,m,j=1,...,n)是 d d d维的词向量。对于软对齐,我们将注意力权重 e i j ∈ R e{ij} \in \mathbb{R} eij∈R计算为:
e i j = f ( a i ) ⊤ f ( b j ) (1) e
{ij} = f(\mathbf{a}_i)^\top f(\mathbf{b}_j) \tag{1} eij=f(ai)⊤f(bj)(1) 其中,函数 f f f是在下面的mlp函数中定义的多层感知机。输出维度 f f f由mlpnum_hiddens参数指定。

python 复制代码
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)

值得注意的是,在式(1)中, f f f分别输入 a i \mathbf{a}_i ai和 b j \mathbf{b}_j bj,而不是将它们一对放在一起作为输入。这种分解 技巧导致 f f f只有 m + n m + n m+n个次计算(线性复杂度),而不是 m n mn mn次计算(二次复杂度)

对式(1)中的注意力权重进行规范化,我们计算假设中所有词元向量的加权平均值,以获得假设的表示,该假设与前提中索引 i i i的词元进行软对齐:
β i = ∑ j = 1 n exp ⁡ ( e i j ) ∑ k = 1 n exp ⁡ ( e i k ) b j (2) \boldsymbol{\beta}i = \sum{j=1}^{n}\frac{\exp(e_{ij})}{ \sum_{k=1}^{n} \exp(e_{ik})} \mathbf{b}j \tag{2} βi=j=1∑n∑k=1nexp(eik)exp(eij)bj(2) 同样,我们计算假设中索引为 j j j的每个词元与前提词元的软对齐:
α j = ∑ i = 1 m exp ⁡ ( e i j ) ∑ k = 1 m exp ⁡ ( e k j ) a i (3) \boldsymbol{\alpha}j = \sum{i=1}^{m}\frac{\exp(e
{ij})}{ \sum_{k=1}^{m} \exp(e_{kj})} \mathbf{a}_i \tag{3} αj=i=1∑m∑k=1mexp(ekj)exp(eij)ai(3)

下面,我们定义Attend类来计算假设(beta)与输入前提A的软对齐以及前提(alpha)与输入假设B的软对齐。

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的第1个维度)
        beta = torch.bmm(F.softmax(e, dim=-1), B)
        # beta的形状:(批量大小,序列B的词元数,embed_size),
        # 意味着序列A被软对齐到序列B的每个词元(alpha的第1个维度)
        alpha = torch.bmm(F.softmax(e.permute(0, 2, 1), dim=-1), A)
        return beta, alpha

(二)比较

在下一步中,我们将一个序列中的词元与与该词元软对齐的另一个序列进行比较。请注意,在软对齐中,一个序列中的所有词元(尽管可能具有不同的注意力权重)将与另一个序列中的词元进行比较。为便于演示,图2对词元以 的方式对齐。例如,上述的注意(attending)步骤确定前提中的"need"和"sleep"都与假设中的"tired"对齐,则将对"疲倦-需要睡眠"进行比较。

在比较步骤中,我们将来自一个序列的词元的连结(运算符 [ ⋅ , ⋅ ] [\cdot, \cdot] [⋅,⋅])和来自另一序列的对齐的词元送入函数 g g g(一个多层感知机):
v A , i = g ( [ a i , β i ] ) , i = 1 , ... , m v B , j = g ( [ b j , α j ] ) , j = 1 , ... , n (4) \begin{aligned}\mathbf{v}_{A,i} = g([\mathbf{a}_i, \boldsymbol{\beta}i]), i = 1, \ldots, m \\ \mathbf{v}{B,j} = g([\mathbf{b}_j, \boldsymbol{\alpha}_j]), j = 1, \ldots, n \end{aligned}\tag{4} vA,i=g([ai,βi]),i=1,...,mvB,j=g([bj,αj]),j=1,...,n(4)

在式(4)中, v A , i \mathbf{v}{A,i} vA,i是指,所有假设中的词元与前提中词元 i i i软对齐,再与词元 i i i的比较;而 v B , j \mathbf{v}{B,j} vB,j是指,所有前提中的词元与假设中词元 i i i软对齐,再与词元 i i i的比较。下面的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):
        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

(三)聚合

现在我们有两组比较向量 v A , i \mathbf{v}{A,i} vA,i( i = 1 , ... , m i = 1, \ldots, m i=1,...,m)和 v B , j \mathbf{v}{B,j} vB,j( j = 1 , ... , n j = 1, \ldots, n j=1,...,n)。在最后一步中,我们将聚合这些信息以推断逻辑关系。我们首先求和这两组比较向量:
v A = ∑ i = 1 m v A , i , v B = ∑ j = 1 n v B , j (5) \mathbf{v}A = \sum{i=1}^{m} \mathbf{v}{A,i}, \quad \mathbf{v}B = \sum{j=1}^{n}\mathbf{v}{B,j} \tag{5} vA=i=1∑mvA,i,vB=j=1∑nvB,j(5)

接下来,我们将两个求和结果的连结提供给函数 h h h(一个多层感知机),以获得逻辑关系的分类结果:
y ^ = h ( [ v A , v B ] ) (6) \hat{\mathbf{y}} = h([\mathbf{v}_A, \mathbf{v}_B]) \tag{6} y^=h([vA,vB])(6)

聚合步骤在以下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.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

(四)整合代码

通过将注意步骤、比较步骤和聚合步骤组合在一起,我们定义了可分解注意力模型来联合训练这三个步骤。

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

二、训练和评估模型

现在,我们将在SNLI数据集上对定义好的可分解注意力模型进行训练和评估。我们从读取数据集开始。

(一)读取数据集

我们使用自然语言推断与数据集中定义的函数下载并读取SNLI数据集。批量大小和序列长度分别设置为 256 256 256和 50 50 50。

python 复制代码
batch_size, num_steps = 256, 50
train_iter, test_iter, vocab = d2l.load_data_snli(batch_size, num_steps)

(二)创建模型

我们使用预训练好的100维GloVe嵌入来表示输入词元。我们将向量 a i \mathbf{a}_i ai和 b j \mathbf{b}_j bj在式(1)中的维数预定义为100。式(1)中的函数 f f f和式(4)中的函数 g g g的输出维度被设置为200.然后我们创建一个模型实例,初始化它的参数,并加载GloVe嵌入来初始化输入词元的向量。

python 复制代码
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);

(三)训练和评估模型

多GPU训练中接受单一输入(如文本序列或图像)的split_batch函数不同,我们定义了一个split_batch_multi_inputs函数以小批量接受多个输入,如前提和假设。

现在我们可以在SNLI数据集上训练和评估模型。

python 复制代码
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)


(四)使用模型

最后,定义预测函数,输出一对前提和假设之间的逻辑关系。

python 复制代码
#@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'

我们可以使用训练好的模型来获得对示例句子的自然语言推断结果。

python 复制代码
predict_snli(net, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.'])

小结

  • 可分解注意模型包括三个步骤来预测前提和假设之间的逻辑关系:注意、比较和聚合。
  • 通过注意力机制,我们可以将一个文本序列中的词元与另一个文本序列中的每个词元对齐,反之亦然。这种对齐是使用加权平均的软对齐,其中理想情况下较大的权重与要对齐的词元相关联。
  • 在计算注意力权重时,分解技巧会带来比二次复杂度更理想的线性复杂度。
  • 我们可以使用预训练好的词向量作为下游自然语言处理任务(如自然语言推断)的输入表示。
相关推荐
一条破秋裤12 小时前
【文献-1/6】一种高效的非参数特征校准方法用于少样本植物病害分类
人工智能·分类·数据挖掘
kisshuan1239612 小时前
使用YOLO11-C3k2-VSSD模型实现脐橙病害智能检测与分类,从数据准备到模型训练的完整指南
人工智能·分类·数据挖掘
ws20190712 小时前
湾区引擎轰鸣:AUTO TECH China 2026广州汽车零部件展何以撬动全球汽车供应链?
大数据·人工智能·科技·汽车
BitaHub202412 小时前
文献分享 | Meta CLIP 2 来了:打破多语言瓶颈的全球化视觉-语言模型
人工智能·语言模型·自然语言处理
2501_9418059312 小时前
在苏黎世智慧医疗场景中构建影像实时分析与高并发诊断辅助平台的工程设计实践经验分享
人工智能
电商API_1800790524712 小时前
获取淘宝商品视频API教程:从授权到落地实战
大数据·数据库·人工智能·数据分析·音视频
wyz19112 小时前
第19章 数据治理的发展趋势
大数据·人工智能·数据治理·数据要素·数据资源
Baihai_IDP12 小时前
AI 算力是一种需要被定价、对冲和交易的风险资产?
人工智能·面试·llm
Hcoco_me12 小时前
大模型面试题45:从小白视角递进讲解DeepSeek V3的MLA机制
人工智能·深度学习·lstm·transformer·word2vec
Hcoco_me12 小时前
大模型面试题43:从小白视角递进讲解大模型训练的梯度累加策略
人工智能·深度学习·学习·自然语言处理·transformer