.. _sec_word2vec_pretraining: Pré-treinamento do word2vec =========================== Nesta seção, treinaremos um modelo skip-gram definido em :numref:`sec_word2vec`. Primeiro, importe os pacotes e módulos necessários para o experimento e carregue o conjunto de dados PTB. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python from mxnet import autograd, gluon, np, npx from mxnet.gluon import nn from d2l import mxnet as d2l npx.set_np() batch_size, max_window_size, num_noise_words = 512, 5, 5 data_iter, vocab = d2l.load_data_ptb(batch_size, max_window_size, num_noise_words) .. raw:: html
.. raw:: html
.. code:: python import torch from torch import nn from d2l import torch as d2l batch_size, max_window_size, num_noise_words = 512, 5, 5 data_iter, vocab = d2l.load_data_ptb(batch_size, max_window_size, num_noise_words) .. raw:: html
.. raw:: html
O Modelo Skip-Gram ------------------ Implementaremos o modelo skip-gram usando camadas de incorporação e multiplicação de minibatch. Esses métodos também são frequentemente usados para implementar outros aplicativos de processamento de linguagem natural. Camada de incorporação ~~~~~~~~~~~~~~~~~~~~~~ Conforme descrito em :numref:`sec_seq2seq`, A camada na qual a palavra obtida é incorporada é chamada de camada de incorporação, que pode ser obtida criando uma instância ``nn.Embedding`` em APIs de alto nível. O peso da camada de incorporação é uma matriz cujo número de linhas é o tamanho do dicionário (``input_dim``) e cujo número de colunas é a dimensão de cada vetor de palavra (``output_dim``). Definimos o tamanho do dicionário como :math:`20` e a dimensão do vetor da palavra como :math:`4`. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python embed = nn.Embedding(input_dim=20, output_dim=4) embed.initialize() embed.weight .. parsed-literal:: :class: output Parameter embedding0_weight (shape=(20, 4), dtype=float32) .. raw:: html
.. raw:: html
.. code:: python embed = nn.Embedding(num_embeddings=20, embedding_dim=4) print(f'Parameter embedding_weight ({embed.weight.shape}, ' 'dtype={embed.weight.dtype})') .. parsed-literal:: :class: output Parameter embedding_weight (torch.Size([20, 4]), dtype={embed.weight.dtype}) .. raw:: html
.. raw:: html
A entrada da camada de incorporação é o índice da palavra. Quando inserimos o índice :math:`i` de uma palavra, a camada de incorporação retorna a linha :math:`i^\mathrm{th}` da matriz de peso como seu vetor de palavra. Abaixo, inserimos um índice de forma (:math:`2`, :math:`3`) na camada de incorporação. Como a dimensão do vetor de palavras é 4, obtemos um vetor de palavras de forma (:math:`2`, :math:`3`, :math:`4`). .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python x = np.array([[1, 2, 3], [4, 5, 6]]) embed(x) .. parsed-literal:: :class: output array([[[ 0.01438687, 0.05011239, 0.00628365, 0.04861524], [-0.01068833, 0.01729892, 0.02042518, -0.01618656], [-0.00873779, -0.02834515, 0.05484822, -0.06206018]], [[ 0.06491279, -0.03182812, -0.01631819, -0.00312688], [ 0.0408415 , 0.04370362, 0.00404529, -0.0028032 ], [ 0.00952624, -0.01501013, 0.05958354, 0.04705103]]]) .. raw:: html
.. raw:: html
.. code:: python x = torch.tensor([[1, 2, 3], [4, 5, 6]]) embed(x) .. parsed-literal:: :class: output tensor([[[-1.1053, 0.1184, 0.0823, -0.2267], [ 2.4417, -1.0708, 0.1795, -0.3554], [ 0.3089, -0.4537, 1.1768, 1.3481]], [[-1.8293, -1.1143, -0.4053, 1.0142], [-0.4587, -0.8997, 1.1724, -0.9287], [-0.0265, 0.8163, 0.7636, -2.3677]]], grad_fn=) .. raw:: html
.. raw:: html
Cálculo de avanço do modelo Skip-Gram ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ No cálculo progressivo, a entrada do modelo skip-gram contém o índice central da palavra-alvo ``center`` e o contexto concatenado e o índice da palavra de ruído ``contexts_and_negatives``. Em que, a variável ``center`` tem a forma (tamanho do lote, 1), enquanto a variável ``contexts_and_negatives`` tem a forma (tamanho do lote, ``max_len``). Essas duas variáveis são primeiro transformadas de índices de palavras em vetores de palavras pela camada de incorporação de palavras e, em seguida, a saída da forma (tamanho do lote, 1, ``max_len``) é obtida pela multiplicação de minibatch. Cada elemento na saída é o produto interno do vetor de palavra de destino central e do vetor de palavra de contexto ou vetor de palavra de ruído. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python def skip_gram(center, contexts_and_negatives, embed_v, embed_u): v = embed_v(center) u = embed_u(contexts_and_negatives) pred = npx.batch_dot(v, u.swapaxes(1, 2)) return pred .. raw:: html
.. raw:: html
.. code:: python def skip_gram(center, contexts_and_negatives, embed_v, embed_u): v = embed_v(center) u = embed_u(contexts_and_negatives) pred = torch.bmm(v, u.permute(0, 2, 1)) return pred .. raw:: html
.. raw:: html
Verifique se a forma de saída deve ser (tamanho do lote, 1, ``max_len``). .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python skip_gram(np.ones((2, 1)), np.ones((2, 4)), embed, embed).shape .. parsed-literal:: :class: output (2, 1, 4) .. raw:: html
.. raw:: html
.. code:: python skip_gram(torch.ones((2, 1), dtype=torch.long), torch.ones((2, 4), dtype=torch.long), embed, embed).shape .. parsed-literal:: :class: output torch.Size([2, 1, 4]) .. raw:: html
.. raw:: html
Treinamento ----------- Antes de treinar o modelo de incorporação de palavras, precisamos definir a função de perda do modelo. Função de perda de entropia cruzada binária ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ De acordo com a definição da função de perda na amostragem negativa, podemos usar diretamente a função de perda de entropia cruzada binária de APIs de alto nível. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python loss = gluon.loss.SigmoidBCELoss() .. raw:: html
.. raw:: html
.. code:: python class SigmoidBCELoss(nn.Module): "BCEWithLogitLoss with masking on call." def __init__(self): super().__init__() def forward(self, inputs, target, mask=None): out = nn.functional.binary_cross_entropy_with_logits( inputs, target, weight=mask, reduction="none") return out.mean(dim=1) loss = SigmoidBCELoss() .. raw:: html
.. raw:: html
Vale ressaltar que podemos usar a variável máscara para especificar o valor predito parcial e o rótulo que participam do cálculo da função de perda no minibatch: quando a máscara for 1, o valor predito e o rótulo da posição correspondente participarão do cálculo de a função de perda; Quando a máscara é 0, eles não participam. Como mencionamos anteriormente, as variáveis de máscara podem ser usadas para evitar o efeito do preenchimento nos cálculos da função de perda. Dados dois exemplos idênticos, máscaras diferentes levam a valores de perda diferentes. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python pred = np.array([[.5]*4]*2) label = np.array([[1., 0., 1., 0.]]*2) mask = np.array([[1, 1, 1, 1], [1, 1, 0, 0]]) loss(pred, label, mask) .. parsed-literal:: :class: output array([0.724077 , 0.3620385]) .. raw:: html
.. raw:: html
.. code:: python pred = torch.tensor([[.5]*4]*2) label = torch.tensor([[1., 0., 1., 0.]]*2) mask = torch.tensor([[1, 1, 1, 1], [1, 1, 0, 0]]) loss(pred, label, mask) .. parsed-literal:: :class: output tensor([0.7241, 0.3620]) .. raw:: html
.. raw:: html
Podemos normalizar a perda em cada exemplo devido a vários comprimentos em cada exemplo. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python loss(pred, label, mask) / mask.sum(axis=1) * mask.shape[1] .. parsed-literal:: :class: output array([0.724077, 0.724077]) .. raw:: html
.. raw:: html
.. code:: python loss(pred, label, mask) / mask.sum(axis=1) * mask.shape[1] .. parsed-literal:: :class: output tensor([0.7241, 0.7241]) .. raw:: html
.. raw:: html
Inicializando os parâmetros do modelo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Construímos as camadas de incorporação das palavras central e de contexto, respectivamente, e definimos a dimensão do vetor da palavra hiperparâmetro ``embed_size`` para 100. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python embed_size = 100 net = nn.Sequential() net.add(nn.Embedding(input_dim=len(vocab), output_dim=embed_size), nn.Embedding(input_dim=len(vocab), output_dim=embed_size)) .. raw:: html
.. raw:: html
.. code:: python embed_size = 100 net = nn.Sequential(nn.Embedding(num_embeddings=len(vocab), embedding_dim=embed_size), nn.Embedding(num_embeddings=len(vocab), embedding_dim=embed_size)) .. raw:: html
.. raw:: html
Treinamento ~~~~~~~~~~~ A função de treinamento é definida abaixo. Devido à existência de preenchimento, o cálculo da função de perda é ligeiramente diferente em comparação com as funções de treinamento anteriores. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python def train(net, data_iter, lr, num_epochs, device=d2l.try_gpu()): net.initialize(ctx=device, force_reinit=True) trainer = gluon.Trainer(net.collect_params(), 'adam', {'learning_rate': lr}) animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, num_epochs]) metric = d2l.Accumulator(2) # Sum of losses, no. of tokens for epoch in range(num_epochs): timer, num_batches = d2l.Timer(), len(data_iter) for i, batch in enumerate(data_iter): center, context_negative, mask, label = [ data.as_in_ctx(device) for data in batch] with autograd.record(): pred = skip_gram(center, context_negative, net[0], net[1]) l = (loss(pred.reshape(label.shape), label, mask) / mask.sum(axis=1) * mask.shape[1]) l.backward() trainer.step(batch_size) metric.add(l.sum(), l.size) if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: animator.add(epoch + (i + 1) / num_batches, (metric[0] / metric[1],)) print(f'loss {metric[0] / metric[1]:.3f}, ' f'{metric[1] / timer.stop():.1f} tokens/sec on {str(device)}') .. raw:: html
.. raw:: html
.. code:: python def train(net, data_iter, lr, num_epochs, device=d2l.try_gpu()): def init_weights(m): if type(m) == nn.Embedding: nn.init.xavier_uniform_(m.weight) net.apply(init_weights) net = net.to(device) optimizer = torch.optim.Adam(net.parameters(), lr=lr) animator = d2l.Animator(xlabel='epoch', ylabel='loss', xlim=[1, num_epochs]) metric = d2l.Accumulator(2) # Sum of losses, no. of tokens for epoch in range(num_epochs): timer, num_batches = d2l.Timer(), len(data_iter) for i, batch in enumerate(data_iter): optimizer.zero_grad() center, context_negative, mask, label = [ data.to(device) for data in batch] pred = skip_gram(center, context_negative, net[0], net[1]) l = (loss(pred.reshape(label.shape).float(), label.float(), mask) / mask.sum(axis=1) * mask.shape[1]) l.sum().backward() optimizer.step() metric.add(l.sum(), l.numel()) if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1: animator.add(epoch + (i + 1) / num_batches, (metric[0] / metric[1],)) print(f'loss {metric[0] / metric[1]:.3f}, ' f'{metric[1] / timer.stop():.1f} tokens/sec on {str(device)}') .. raw:: html
.. raw:: html
Now, we can train a skip-gram model using negative sampling. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python lr, num_epochs = 0.01, 5 train(net, data_iter, lr, num_epochs) .. parsed-literal:: :class: output loss 0.373, 95065.6 tokens/sec on gpu(0) .. figure:: output_word2vec-pretraining_d81279_93_1.svg .. raw:: html
.. raw:: html
.. code:: python lr, num_epochs = 0.01, 5 train(net, data_iter, lr, num_epochs) .. parsed-literal:: :class: output loss 0.373, 368911.4 tokens/sec on cuda:0 .. figure:: output_word2vec-pretraining_d81279_96_1.svg .. raw:: html
.. raw:: html
Aplicando o modelo de incorporação de palavras ---------------------------------------------- Depois de treinar o modelo de incorporação de palavras, podemos representar a similaridade no significado entre as palavras com base na similaridade do cosseno de dois vetores de palavras. Como podemos ver, ao usar o modelo de incorporação de palavras treinadas, as palavras com significado mais próximo da palavra “chip” estão principalmente relacionadas a chips. .. raw:: html
mxnetpytorch
.. raw:: html
.. code:: python def get_similar_tokens(query_token, k, embed): W = embed.weight.data() x = W[vocab[query_token]] # Compute the cosine similarity. Add 1e-9 for numerical stability cos = np.dot(W, x) / np.sqrt(np.sum(W * W, axis=1) * np.sum(x * x) + 1e-9) topk = npx.topk(cos, k=k+1, ret_typ='indices').asnumpy().astype('int32') for i in topk[1:]: # Remove the input words print(f'cosine sim={float(cos[i]):.3f}: {vocab.idx_to_token[i]}') get_similar_tokens('chip', 3, net[0]) .. parsed-literal:: :class: output cosine sim=0.547: microprocessor cosine sim=0.534: intel cosine sim=0.425: chips .. raw:: html
.. raw:: html
.. code:: python def get_similar_tokens(query_token, k, embed): W = embed.weight.data x = W[vocab[query_token]] # Compute the cosine similarity. Add 1e-9 for numerical stability cos = torch.mv(W, x) / torch.sqrt(torch.sum(W * W, dim=1) * torch.sum(x * x) + 1e-9) topk = torch.topk(cos, k=k+1)[1].cpu().numpy().astype('int32') for i in topk[1:]: # Remove the input words print(f'cosine sim={float(cos[i]):.3f}: {vocab.idx_to_token[i]}') get_similar_tokens('chip', 3, net[0]) .. parsed-literal:: :class: output cosine sim=0.513: microprocessor cosine sim=0.493: intel cosine sim=0.451: drives .. raw:: html
.. raw:: html
Sumário ------- - Podemos pré-treinar um modelo de grama de salto por meio de amostragem negativa. Exercícios ---------- 1. Defina ``sparse_grad = True`` ao criar uma instância de ``nn.Embedding``. Acelera o treinamento? Consulte a documentação do MXNet para aprender o significado desse argumento. 2. Tente encontrar sinônimos para outras palavras. 3. Ajuste os hiperparâmetros e observe e analise os resultados experimentais. 4. Quando o conjunto de dados é grande, costumamos amostrar as palavras de contexto e as palavras de ruído para a palavra de destino central no minibatch atual apenas ao atualizar os parâmetros do modelo. Em outras palavras, a mesma palavra de destino central pode ter palavras de contexto ou palavras de ruído diferentes em épocas diferentes. Quais são os benefícios desse tipo de treinamento? Tente implementar este método de treinamento. .. raw:: html
mxnetpytorch
.. raw:: html
`Discussão `__ .. raw:: html
.. raw:: html
`Discussão `__ .. raw:: html
.. raw:: html
.. raw:: html