15.5. Doğal Dil Çıkarımı: Dikkati Kullanma
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in SageMaker Studio Lab

Section 15.4 içinde doğal dil çıkarım görevini ve SNLI veri kümesini tanıttık. Karmaşık ve derin mimarilere dayanan birçok model göz önüne alındığında, Parikh ve ark. doğal dil çıkarımını dikkat mekanizmaları ile ele almayı önerdi ve bunu “ayrıştırılabilir dikkat modeli” olarak adlandırdı (Parikh et al., 2016). Bu, yinelemeli veya evrişimli katmanları olmayan bir modelle sonuçlanır ve SNLI veri kümesinde o anda çok daha az parametre ile en iyi sonucu elde eder. Bu bölümde, Fig. 15.5.1 içinde tasvir edildiği gibi doğal dil çıkarımı için bu dikkat tabanlı yöntemi (MLP’lerle) açıklayacağız ve uygulayacağız.

../_images/nlp-map-nli-attention.svg

Fig. 15.5.1 Bu bölüm, önceden eğitilmiş GloVe’i, doğal dil çıkarımı için dikkat ve MLP’lere dayalı bir mimariye besler.

15.5.1. Model

Öncüllerdeki ve hipotezlerdeki belirteçlerin sırasını korumaktan daha basit olarak, sadece bir metin dizisindeki belirteçleri diğerindeki her belirteçle hizalayabiliriz ve tersi de geçerlidir, daha sonra öncüller ve hipotezler arasındaki mantıksal ilişkileri tahmin etmek için bu bilgileri karşılaştırabilir ve birleştirebiliriz. Makine çevirisinde kaynak ve hedef cümleler arasında belirteçlerin hizalanmasına benzer şekilde, öncül ve hipotezler arasındaki belirteçlerin hizalanması dikkat mekanizmaları ile düzgün bir şekilde gerçekleştirilebilir.

../_images/nli-attention.svg

Fig. 15.5.2 Dikkat mekanizmalarını kullanan doğal dil çıkarımı.

Fig. 15.5.2 dikkat mekanizmalarını kullanan doğal dil çıkarım yöntemini tasvir eder. Üst düzeyde, ortak eğitilmiş üç adımdan oluşur: Dikkat etmek, karşılaştırmak ve biriktirmek. Onları aşağıda adım adım göstereceğiz.

from d2l import mxnet as d2l
from mxnet import gluon, init, np, npx
from mxnet.gluon import nn

npx.set_np()
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l

15.5.1.1. Dikkat Etmek

İlk adım, bir metin dizisindeki belirteçleri diğer dizideki her belirteçle hizalamaktır. Öncülün “benim uykuya ihtiyacım var” ve hipotezin “ben yorgunum” olduğunu varsayalım. Anlamsal benzerlik nedeniyle, hipotezdeki “ben” ile öncül içindeki “ben“‘i hizalamak ve hipotezdeki “yorgun“‘u öncül içindeki “uyku” ile hizalamak isteyebiliriz. Benzer şekilde, öncüldeki “ben”i hipotezdeki “ben” ile hizalamak ve öncüldeki “uyku” ve “ihtiyaç“‘ı hipotezdeki “yorgun” ile aynı hizaya getirmek isteyebiliriz. Bu tür bir hizalamanın, ideal olarak büyük ağırlıkların hizalanacak belirteçlerle ilişkilendirildiği, ağırlıklı ortalama kullanılarak yumuşak olduğunu unutmayın. Gösterim kolaylığı için, Fig. 15.5.2 böyle bir hizalamayı sert bir şekilde gösterir.

Şimdi dikkat mekanizmalarını kullanarak yumuşak hizalamayı daha ayrıntılı olarak tanımlıyoruz. \(\mathbf{A} = (\mathbf{a}_1, \ldots, \mathbf{a}_m)\) ve \(\mathbf{B} = (\mathbf{b}_1, \ldots, \mathbf{b}_n)\) ile, sırasıyla \(\mathbf{a}_i, \mathbf{b}_j \in \mathbb{R}^{d}\) (\(i = 1, \ldots, m, j = 1, \ldots, n\)) \(d\) boyutlu bir sözcük vektörü olan belirteç sayısı \(m\) ve \(n\) olan öncülü ve hipotezi belirtin. Yumuşak hizalama için \(e_{ij} \in \mathbb{R}\) dikkat ağırlıklarını aşağıdaki gibi hesaplıyoruz

(15.5.1)\[e_{ij} = f(\mathbf{a}_i)^\top f(\mathbf{b}_j),\]

burada \(f\) işlevi, aşağıdaki mlp işlevinde tanımlanan bir MLP’dir. \(f\)’in çıktı boyutu, mlp’nin num_hiddens argümanı tarafından belirlenir.

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
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)

(15.5.1) denkleminde \(f\)’in \(\mathbf{a}_i\) ve \(\mathbf{b}_j\) girdilerini girdi olarak bir çift almak yerine ayrı ayrı aldığı vurgulanmalıdır. Bu ayrıştırma püf noktası, \(mn\) kere uygulama (ikinci dereceden karmaşıklık) yerine \(f\)’in yalnızca \(m + n\) kere uygulamasına (doğrusal karmaşıklık) yol açar.

(15.5.1) içindeki dikkat ağırlıklarını normalleştirerek, varsayımdaki tüm belirteç vektörlerinin ağırlıklı ortalamasını hesaplıyoruz ve bu hipotezin temsilini elde etmek için \(i\) ile endeksli belirteç ile yumuşak bir şekilde hizalanan hipotezin temsilini elde ediyoruz:

(15.5.2)\[\boldsymbol{\beta}_i = \sum_{j=1}^{n}\frac{\exp(e_{ij})}{ \sum_{k=1}^{n} \exp(e_{ik})} \mathbf{b}_j.\]

Benzer şekilde, hipotezde \(j\) ile endekslenen her belirteç için öncül belirteçlerinin yumuşak hizalamasını hesaplarız:

(15.5.3)\[\boldsymbol{\alpha}_j = \sum_{i=1}^{m}\frac{\exp(e_{ij})}{ \sum_{k=1}^{m} \exp(e_{kj})} \mathbf{a}_i.\]

Aşağıda, hipotezlerin (beta) girdi öncülleri A ile yumuşak hizalamasını ve öncüllerin (alpha) girdi hipotezleri B ile yumuşak hizalamasını hesaplamak için Attend sınıfını tanımlıyoruz.

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`'nin şekli: (`batch_size`, A/B dizisindeki belirteç sayısı,
        # `embed_size`)
        # `f_A`/`f_B`'nin şekli: (`batch_size`, A/B dizisindeki belirteç
        # sayısı, `num_hiddens`)
        f_A = self.f(A)
        f_B = self.f(B)
        # `e`'nin şekli: (`batch_size`, A dizisindeki belirteç sayısı,
        # B dizisindeki belirteç sayısı)
        e = npx.batch_dot(f_A, f_B, transpose_b=True)
        # `beta`'nın şekli: (`batch_size`, A dizisindeki belirteç sayısı,
        # `embed_size`), burada B dizisi, A dizisindeki her bir belirteç ile
        # (`beta`'nın 1. ekseni) yumuşak bir şekilde hizalanır.
        beta = npx.batch_dot(npx.softmax(e), B)
        # `alpha`'nın şekli: (`batch_size`, dizi B'deki belirteç sayısı,
        # `embed_size`), burada A dizisi, B dizisindeki her bir belirteç ile
        # (`alpha`'nın 1. ekseni) yumuşak bir şekilde hizalanır
        alpha = npx.batch_dot(npx.softmax(e.transpose(0, 2, 1)), A)
        return beta, alpha
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`'nin şekli: (`batch_size`, A/B dizisindeki belirteç sayısı,
        # `embed_size`)
        # `f_A`/`f_B`'nin şekli: (`batch_size`, A/B dizisindeki belirteç
        # sayısı, `num_hiddens`)
        f_A = self.f(A)
        f_B = self.f(B)
        # `e`'nin şekli: (`batch_size`, A dizisindeki belirteç sayısı,
        # B dizisindeki belirteç sayısı)
        e = torch.bmm(f_A, f_B.permute(0, 2, 1))
        # `beta`'nın şekli: (`batch_size`, A dizisindeki belirteç sayısı,
        # `embed_size`), burada B dizisi, A dizisindeki her bir belirteç ile
        # (`beta`'nın 1. ekseni) yumuşak bir şekilde hizalanır.
        beta = torch.bmm(F.softmax(e, dim=-1), B)
        # `alpha`'nın şekli: (`batch_size`, dizi B'deki belirteç sayısı,
        # `embed_size`), burada A dizisi, B dizisindeki her bir belirteç ile
        # (`alpha`'nın 1. ekseni) yumuşak bir şekilde hizalanır
        alpha = torch.bmm(F.softmax(e.permute(0, 2, 1), dim=-1), A)
        return beta, alpha

15.5.1.2. Karşılaştırmak

Bir sonraki adımda, bir dizideki bir belirteci, bu belirteçle yumuşak bir şekilde hizalanan diğer diziyle karşılaştırırız. Yumuşak hizalamada, bir dizideki tüm belirteçlerin, muhtemelen farklı dikkat ağırlıklarına sahip olsa da, diğer dizideki bir belirteçle karşılaştırılacağını unutmayın. Kolay gösterim için, Fig. 15.5.2 belirteçleri sert bir şekilde hizalanmış belirteçlerle eşleştirir. Örneğin, dikkat etme adımının öncüldeki “ihtiyaç” ve “uyku“‘nun her ikisinin de hipotezdeki “yorgun” ile aynı hizada olduğunu belirlediğini varsayalım, “yorgun–uykuya ihtiyacım var” çifti karşılaştırılacaktır.

Karşılaştırma adımında, bir diziden belirteçlerin (operatör \([\cdot, \cdot]\)) ve diğer diziden hizalanmış belirteçlerin bitiştirilmesini \(g\) (bir MLP) işlevine besleriz:

(15.5.4)\[\begin{split}\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{split}\]

(15.5.4) içinde, \(\mathbf{v}_{A,i}\), öncüldeki \(i\) belirteci ve \(i\) belirteci ile yumuşak bir şekilde hizalanan tüm hipotez belirteçleri arasındaki karşılaştırmadır; \(\mathbf{v}_{B,j}\) ise hipotezdeki \(j\) belirteci ile \(j\) belirteci ile yumuşak bir şekilde hizalanmış tüm öncül belirteçler arasındaki karşılaştırmadır. Aşağıdaki Compare sınıfı, böyle bir karşılaştırma adımı tanımlar.

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
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

15.5.1.3. Biriktirmek

İki karşılaştırma vektörü kümesi \(\mathbf{v}_{A,i}\) (\(i = 1, \ldots, m\)) ve \(\mathbf{v}_{B,j}\) (\(j = 1, \ldots, n\)) ile, son adımda mantıksal ilişkiyi çıkarmak için bu tür bilgileri biriktireceğiz. Her iki kümeyi toplayarak başlıyoruz:

(15.5.5)\[\mathbf{v}_A = \sum_{i=1}^{m} \mathbf{v}_{A,i}, \quad \mathbf{v}_B = \sum_{j=1}^{n}\mathbf{v}_{B,j}.\]

Daha sonra mantıksal ilişkinin sınıflandırma sonucunu elde etmek için \(h\) (bir MLP) işlevine her iki özetleme sonuçlarının bitiştirilmesini besleriz:

(15.5.6)\[\hat{\mathbf{y}} = h([\mathbf{v}_A, \mathbf{v}_B]).\]

Biriktirme adımı aşağıdaki Aggregate sınıfında tanımlanır.

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):
        # Her iki karşılaştırma vektörü kümesini toplayın
        V_A = V_A.sum(axis=1)
        V_B = V_B.sum(axis=1)
        # Her iki özetleme sonucunun bitiştirmesini bir MLP'ye besleyin
        Y_hat = self.h(np.concatenate([V_A, V_B], axis=1))
        return Y_hat
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):
       # Her iki karşılaştırma vektörü kümesini toplayın
        V_A = V_A.sum(dim=1)
        V_B = V_B.sum(dim=1)
        # Her iki özetleme sonucunun bitiştirmesini bir MLP'ye besleyin
        Y_hat = self.linear(self.h(torch.cat([V_A, V_B], dim=1)))
        return Y_hat

15.5.1.4. Her Şeyi Bir Araya Koyma

Dikkat etme, karşılaştırma ve biriktirme adımlarını bir araya getirerek, bu üç adımı birlikte eğitmek için ayrıştırılabilir dikkat modelini tanımlarız.

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 olası çıktı vardır: gerekçe, çelişki ve tarafsızlık
        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
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 olası çıktı vardır: gerekçe, çelişki ve tarafsızlık
        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. Model Eğitimi ve Değerlendirilmesi

Şimdi SNLI veri kümesinde tanımlanmış ayrıştırılabilir dikkat modelini eğiteceğiz ve değerlendireceğiz. Veri kümesini okuyarak başlıyoruz.

15.5.2.1. Veri Kümesini Okuma

SNLI veri kümesini Section 15.4 içinde tanımlanan işlevi kullanarak indirip okuyoruz. Toplu iş boyutu ve dizi uzunluğu sırasıyla \(256\) ve \(50\) olarak ayarlanır.

batch_size, num_steps = 256, 50
train_iter, test_iter, vocab = d2l.load_data_snli(batch_size, num_steps)
read 549367 examples
read 9824 examples
batch_size, num_steps = 256, 50
train_iter, test_iter, vocab = d2l.load_data_snli(batch_size, num_steps)
read 549367 examples
read 9824 examples

15.5.2.2. Modeli Oluşturma

Girdi belirteçlerini temsil etmek için önceden eğitilmiş 100 boyutlu GloVe gömmeyi kullanıyoruz. Böylece, \(\mathbf{a}_i\) ve \(\mathbf{b}_j\) vektörlerinin (15.5.1) içindeki boyutunu önceden 100 olarak tanımlarız. (15.5.1) içindeki \(f\) ve (15.5.4) içindeki \(g\) fonksiyonlarının çıktı boyutu 200 olarak ayarlanmıştır. Ardından bir model örneği oluşturur, parametrelerini ilkler ve girdi belirteçlerinin vektörlerini ilklemek için GloVe gömmesini yükleriz.

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)
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);

15.5.2.3. Model Eğitimi ve Değerlendirilmesi

Section 12.5 içindeki split_batch işlevinin aksine, metin dizileri (veya imgeler) gibi tek girdileri alan split_batch işlevinin aksine, minigruplarda öncüller ve hipotezler gibi birden fazla girdiyi almak için split_batch_multi_inputs işlevi tanımlıyoruz.

#@save
def split_batch_multi_inputs(X, y, devices):
    """Split multi-input `X` and `y` into multiple devices."""
    """Çoklu girdili `X` and `y`'yi birden çok cihaza ayırın."""
    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))

Şimdi modeli SNLI veri kümesinde eğitebilir ve değerlendirebiliriz.

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)
loss 0.520, train acc 0.794, test acc 0.817
10229.4 examples/sec on [gpu(0), gpu(1)]
../_images/output_natural-language-inference-attention_b907c4_81_1.svg
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)
loss 0.495, train acc 0.806, test acc 0.821
18014.7 examples/sec on [device(type='cuda', index=0), device(type='cuda', index=1)]
../_images/output_natural-language-inference-attention_b907c4_84_1.svg

15.5.2.4. Modeli Kullanma

Son olarak, bir çift öncül ve hipotez arasındaki mantıksal ilişkiyi ortaya çıkaracak tahmin işlevini tanımlayın.

#@save
def predict_snli(net, vocab, premise, hypothesis):
    """Öncül ve hipotez arasındaki mantıksal ilişkiyi tahmin edin."""
    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'
#@save
def predict_snli(net, vocab, premise, hypothesis):
    """Öncül ve hipotez arasındaki mantıksal ilişkiyi tahmin edin."""
    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'

Eğitilmiş modeli, örnek bir cümle çifti için doğal dil çıkarım sonucunu elde etmek için kullanabiliriz.

predict_snli(net, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.'])
'contradiction'
predict_snli(net, vocab, ['he', 'is', 'good', '.'], ['he', 'is', 'bad', '.'])
'contradiction'

15.5.3. Özet

  • Ayrıştırılabilir dikkat modeli, öncüller ve hipotezler arasındaki mantıksal ilişkileri tahmin etmek için üç adımdan oluşur: Dikkat etmek, karşılaştırmak ve biriktirmek.

  • Dikkat mekanizmalarıyla, bir metin dizisinde belirteçleri diğerindeki her belirteçle hizalayabiliriz ve tam tersi de geçerlidir. Bu hizalama, ideal olarak büyük ağırlıkların hizalanacak belirteçlerle ilişkilendirildiği ağırlıklı ortalama kullanıldığından yumuşaktır.

  • Ayrıştırma püf noktası, dikkat ağırlıklarını hesaplarken ikinci dereceden karmaşıklıktan daha fazla arzu edilen doğrusal bir karmaşıklığa yol açar.

  • Doğal dil çıkarımı gibi aşağı akış doğal dil işleme görevi için girdi temsili olarak önceden eğitilmiş sözcük vektörlerini kullanabiliriz.

15.5.4. Alıştırmalar

  1. Modeli diğer hiper parametre kombinasyonları ile eğitin. Test kümesinde daha iyi doğruluk elde edebilir misiniz?

  2. Doğal dil çıkarımı için ayrıştırılabilir dikkat modelinin en büyük sakıncaları nelerdir?

  3. Herhangi bir cümle çifti için anlamsal benzerlik düzeyini (örneğin, 0 ile 1 arasında sürekli bir değer) elde etmek istediğimizi varsayalım. Veri kümesini nasıl toplayıp etiketleyeceğiz? Dikkat mekanizmaları ile bir model tasarlayabilir misiniz?