8.4. Redes Neurais Recorrentes (RNNs)¶ Open the notebook in SageMaker Studio Lab
Em Section 8.3 introduzimos modelos de \(n\)-gramas, onde a probabilidade condicional da palavra \(x_t\) no passo de tempo \(t\) depende apenas das \(n-1\) palavras anteriores. Se quisermos incorporar o possível efeito de palavras anteriores ao passo de tempo \(t-(n-1)\) em \(x_t\), precisamos aumentar \(n\). No entanto, o número de parâmetros do modelo também aumentaria exponencialmente com ele, pois precisamos armazenar \(|\mathcal{V}|^n\) números para um conjunto de vocabulário \(\mathcal{V}\). Portanto, em vez de modelar \(P(x_t \mid x_{t-1}, \ldots, x_{t-n+1})\), é preferível usar um modelo de variável latente:
onde \(h_{t-1}\) é um estado oculto (também conhecido como uma variável oculta) que armazena as informações da sequência até o passo de tempo \(t-1\). Em geral, o estado oculto em qualquer etapa \(t\) pode ser calculado com base na entrada atual \(x_ {t}\) e no estado oculto anterior \(h_ {t-1}\):
Para uma função suficientemente poderosa \(f\) em (8.4.2), o modelo de variável latente não é uma aproximação. Afinal, \(h_t\) pode simplesmente armazenar todos os dados que observou até agora. No entanto, isso pode tornar a computação e o armazenamento caros.
Lembre-se de que discutimos camadas ocultas com unidades ocultas em Section 4. É digno de nota que camadas ocultas e estados ocultos referem-se a dois conceitos muito diferentes. Camadas ocultas são, conforme explicado, camadas que ficam ocultas da visualização no caminho da entrada à saída. Estados ocultos são tecnicamente falando entradas para tudo o que fazemos em uma determinada etapa, e elas só podem ser calculadas observando os dados em etapas de tempo anteriores.
Redes neurais recorrentes (RNNs) são redes neurais com estados ocultos. Antes de introduzir o modelo RNN, primeiro revisitamos o modelo MLP introduzido em Section 4.1.
8.4.1. Redes Neurais sem Estados Ocultos¶
Vamos dar uma olhada em um MLP com uma única camada oculta. Deixe a função de ativação da camada oculta ser \(\phi\). Dado um minibatch de exemplos \(\mathbf{X} \in \mathbb{R}^{n \times d}\) com tamanho de lote \(n\) e \(d\) entradas, a saída da camada oculta \(\mathbf{H} \in \mathbb{R}^{n \times h}\) é calculada como
Em (8.4.3), temos o parâmetro de peso \(\mathbf{W}_{xh} \in \mathbb{R}^{d \times h}\), o parâmetro de polarização \(\mathbf{b}_h \in \mathbb{R}^{1 \times h}\), e o número de unidades ocultas \(h\), para a camada oculta. Assim, a transmissão (ver Section 2.1.3) é aplicada durante a soma. Em seguida, a variável oculta \(\mathbf{H}\) é usada como entrada da camada de saída. A camada de saída é fornecida por
onde \(\mathbf{O} \in \mathbb{R}^{n \times q}\) é a variável de saída, \(\mathbf{W}_{hq} \in \mathbb{R}^{h \times q}\) é o parâmetro de peso, e \(\mathbf{b}_q \in \mathbb{R}^{1 \times q}\) é o parâmetro de polarização da camada de saída. Se for um problema de classificação, podemos usar \(\text{softmax}(\mathbf{O})\) para calcular a distribuição de probabilidade das categorias de saída.
Isso é inteiramente análogo ao problema de regressão que resolvemos anteriormente em Section 8.1, portanto omitimos detalhes. Basta dizer que podemos escolher pares de rótulo de recurso aleatoriamente e aprender os parâmetros de nossa rede por meio de diferenciação automática e gradiente descendente estocástico.
8.4.2. Redes Neurais Recorrentes com Estados Ocultos¶
As coisas são totalmente diferentes quando temos estados ocultos. Vejamos a estrutura com mais detalhes.
Suponha que temos um minibatch de entradas \(\mathbf{X}_t \in \mathbb{R}^{n \times d}\) no passo de tempo \(t\). Em outras palavras, para um minibatch de exemplos de sequência \(n\), cada linha de \(\mathbf{X}_t\) corresponde a um exemplo no passo de tempo \(t\) da sequência. Em seguida, denote por \(\mathbf{H}_t \in \mathbb{R}^{n \times h}\) a variável oculta do passo de tempo \(t\). Ao contrário do MLP, aqui salvamos a variável oculta \(\mathbf{H}_{t-1}\) da etapa de tempo anterior e introduzimos um novo parâmetro de peso \(\mathbf{W}_{hh} \in \mathbb{R}^{h \times h}\) para descrever como usar a variável oculta da etapa de tempo anterior na etapa de tempo atual. Especificamente, o cálculo da variável oculta da etapa de tempo atual é determinado pela entrada da etapa de tempo atual junto com a variável oculta da etapa de tempo anterior:
Comparado com (8.4.3), (8.4.5) adiciona mais um termo \(\mathbf{H}_{t-1} \mathbf{W}_{hh}\) e assim instancia (8.4.2). A partir da relação entre as variáveis ocultas \(\mathbf{H}_t\) e \(\mathbf{H}_{t-1}\) de etapas de tempo adjacentes, sabemos que essas variáveis capturaram e retiveram as informações históricas da sequência até sua etapa de tempo atual, assim como o estado ou a memória da etapa de tempo atual da rede neural. Portanto, essa variável oculta é chamada de estado oculto. Visto que o estado oculto usa a mesma definição da etapa de tempo anterior na etapa de tempo atual, o cálculo de (8.4.5) é recorrente. Consequentemente, redes neurais com estados ocultos com base em cálculos recorrentes são nomeados redes neurais recorrentes. Camadas que fazem o cálculo de (8.4.5) em RNNs são chamadas de camadas recorrentes.
Existem muitas maneiras diferentes de construir RNNs. RNNs com um estado oculto definido por (8.4.5) são muito comuns. Para a etapa de tempo \(t\), a saída da camada de saída é semelhante à computação no MLP:
Parâmetros do RNN incluem os pesos \(\mathbf{W}_{xh} \in \mathbb{R}^{d \times h}, \mathbf{W}_{hh} \in \mathbb{R}^{h \times h}\), e o bias \(\mathbf{b}_h \in \mathbb{R}^{1 \times h}\) da camada oculta, junto com os pesos \(\mathbf{W}_{hq} \in \mathbb{R}^{h \times q}\) e o bias \(\mathbf{b}_q \in \mathbb{R}^{1 \times q}\) da camada de saída. Vale a pena mencionar que mesmo em diferentes etapas de tempo, Os RNNs sempre usam esses parâmetros do modelo. Portanto, o custo de parametrização de um RNN não cresce à medida que o número de etapas de tempo aumenta.
Fig. 8.4.1 ilustra a lógica computacional de uma RNN em três etapas de tempo adjacentes. A qualquer momento, passo \(t\), o cálculo do estado oculto pode ser tratado como: i) concatenar a entrada \(\mathbf{X}_t\) na etapa de tempo atual \(t\) e o estado oculto \(\mathbf{H}_{t-1}\) na etapa de tempo anterior \(t-1\); ii) alimentar o resultado da concatenação em uma camada totalmente conectada com a função de ativação \(\phi\). A saída dessa camada totalmente conectada é o estado oculto \(\mathbf{H}_t\) do intervalo de tempo atual \(t\). Nesse caso, os parâmetros do modelo são a concatenação de \(\mathbf{W}_{xh}\) e \(\mathbf{W}_{hh}\), e um bias de \(\mathbf{b}_h\), tudo de (8.4.5). O estado oculto do passo de tempo atual \(t\), \(\mathbf{H}_t\), participará do cálculo do estado oculto \(\mathbf{H}_{t+1}\) do próximo passo de tempo \(t+1\). Além disso, \(\mathbf{H}_t\) também será alimentado na camada de saída totalmente conectada para calcular a saída \(\mathbf{O}_t\) do passo de tempo atual \(t\).
Fig. 8.4.1 Uma RNN com um estado oculto.¶
Acabamos de mencionar que o cálculo de
\(\mathbf{X}_t \mathbf{W}_{xh} + \mathbf{H}_{t-1} \mathbf{W}_{hh}\)
para o estado oculto é equivalente a multiplicação de matriz de
concatenação de \(\mathbf{X}_t\) and \(\mathbf{H}_{t-1}\) e
concatenação de \(\mathbf{W}_{xh}\) and \(\mathbf{W}_{hh}\).
Embora isso possa ser comprovado pela matemática, a seguir, apenas
usamos um trecho de código simples para mostrar isso. Começando por,
definir as matrizes X
,W_xh
, H
eW_hh
, cujas formas
são (3, 1), (1, 4), (3, 4) e (4, 4), respectivamente. Multiplicando
X
porW_xh
, e H
porW_hh
, respectivamente e, em
seguida, adicionando essas duas multiplicações, obtemos uma matriz de
forma (3, 4).
from mxnet import np, npx
from d2l import mxnet as d2l
npx.set_np()
X, W_xh = np.random.normal(0, 1, (3, 1)), np.random.normal(0, 1, (1, 4))
H, W_hh = np.random.normal(0, 1, (3, 4)), np.random.normal(0, 1, (4, 4))
np.dot(X, W_xh) + np.dot(H, W_hh)
array([[-0.21952868, 4.256434 , 4.5812645 , -5.344988 ],
[ 3.4478583 , -3.0177274 , -1.6777471 , 7.535347 ],
[ 2.239007 , 1.4199957 , 4.744728 , -8.421293 ]])
import torch
from d2l import torch as d2l
X, W_xh = torch.normal(0, 1, (3, 1)), torch.normal(0, 1, (1, 4))
H, W_hh = torch.normal(0, 1, (3, 4)), torch.normal(0, 1, (4, 4))
torch.matmul(X, W_xh) + torch.matmul(H, W_hh)
tensor([[ 0.9656, -0.2889, 1.2013, 0.3412],
[ 2.0865, -2.5401, 1.2020, 3.0863],
[-1.5326, 0.3890, -4.6161, -0.2931]])
import tensorflow as tf
from d2l import tensorflow as d2l
X, W_xh = tf.random.normal((3, 1), 0, 1), tf.random.normal((1, 4), 0, 1)
H, W_hh = tf.random.normal((3, 4), 0, 1), tf.random.normal((4, 4), 0, 1)
tf.matmul(X, W_xh) + tf.matmul(H, W_hh)
<tf.Tensor: shape=(3, 4), dtype=float32, numpy=
array([[ 0.34333366, -0.74025244, -3.3487055 , 0.5427597 ],
[-0.09303939, -1.6851621 , -3.1726928 , 6.49689 ],
[ 2.0532422 , 3.125818 , 3.4887466 , 0.23183292]],
dtype=float32)>
Agora vamos concatenar as matrizes X
eH
ao longo das colunas
(eixo 1), e as matrizes W_xh
eW_hh
ao longo das linhas (eixo
0). Essas duas concatenações resulta em matrizes de forma (3, 5) e da
forma (5, 4), respectivamente. Multiplicando essas duas matrizes
concatenadas, obtemos a mesma matriz de saída de forma (3, 4) como
acima.
np.dot(np.concatenate((X, H), 1), np.concatenate((W_xh, W_hh), 0))
array([[-0.2195287, 4.256434 , 4.5812645, -5.344988 ],
[ 3.4478583, -3.0177271, -1.677747 , 7.535347 ],
[ 2.2390068, 1.4199957, 4.744728 , -8.421294 ]])
torch.matmul(torch.cat((X, H), 1), torch.cat((W_xh, W_hh), 0))
tensor([[ 0.9656, -0.2889, 1.2013, 0.3412],
[ 2.0865, -2.5401, 1.2020, 3.0863],
[-1.5326, 0.3890, -4.6161, -0.2931]])
tf.matmul(tf.concat((X, H), 1), tf.concat((W_xh, W_hh), 0))
<tf.Tensor: shape=(3, 4), dtype=float32, numpy=
array([[ 0.34333366, -0.7402524 , -3.3487055 , 0.5427597 ],
[-0.09303934, -1.6851622 , -3.1726928 , 6.4968896 ],
[ 2.0532424 , 3.125818 , 3.4887466 , 0.23183289]],
dtype=float32)>
8.4.3. Modelos de Linguagem em Nível de Caracteres Baseados em RNN¶
Lembre-se que para a modelagem de linguagem em Section 8.3, pretendemos prever o próximo token com base em os tokens atuais e passados, assim, mudamos a sequência original em um token como os rótulos. Bengio et al. propuseram primeiro usar uma rede neural para modelagem de linguagem [Bengio et al., 2003]. A seguir, ilustramos como os RNNs podem ser usadas para construir um modelo de linguagem. Deixe o tamanho do minibatch ser um e a sequência do texto ser “máquina”. Para simplificar o treinamento nas seções subsequentes, nós tokenizamos o texto em caracteres em vez de palavras e considere um modelo de linguagem em nível de caractere. Fig. 8.4.2 demonstra como prever o próximo caractere com base nos caracteres atuais e anteriores através de um RNN para modelagem de linguagem em nível de caractere.
Fig. 8.4.2 Um modelo de linguagem de nível de caractere baseado no RNN. As sequências de entrada e rótulo são “machin” e “achine”, respectivamente.¶
Durante o processo de treinamento, executamos uma operação softmax na saída da camada de saída para cada etapa de tempo e, em seguida, usamos a perda de entropia cruzada para calcular o erro entre a saída do modelo e o rótulo. Devido ao cálculo recorrente do estado oculto na camada oculta, a saída da etapa de tempo 3 em Fig. 8.4.2, \(\mathbf{O}_3\), é determinada pela sequência de texto “m”, “a” e “c”. Como o próximo caractere da sequência nos dados de treinamento é “h”, a perda de tempo da etapa 3 dependerá da distribuição de probabilidade do próximo caractere gerado com base na sequência de características “m”, “a”, “c” e o rótulo “h” desta etapa de tempo.
Na prática, cada token é representado por um vetor \(d\)-dimensional e usamos um tamanho de batch \(n>1\). Portanto, a entrada $ mathbf X_t $ no passo de tempo $ t $ será uma matriz \(\mathbf X_t\), que é idêntica ao que discutimos em Section 8.4.2.
8.4.4. Perplexidade¶
Por último, vamos discutir sobre como medir a qualidade do modelo de linguagem, que será usado para avaliar nossos modelos baseados em RNN nas seções subsequentes. Uma maneira é verificar o quão surpreendente é o texto. Um bom modelo de linguagem é capaz de prever com tokens de alta precisão que veremos a seguir. Considere as seguintes continuações da frase “Está chovendo”, conforme proposto por diferentes modelos de linguagem:
“Está chovendo lá fora”
“Está chovendo bananeira”
“Está chovendo piouw; kcj pwepoiut”
Em termos de qualidade, o exemplo 1 é claramente o melhor. As palavras são sensatas e logicamente coerentes. Embora possa não refletir com muita precisão qual palavra segue semanticamente (“em São Francisco” e “no inverno” seriam extensões perfeitamente razoáveis), o modelo é capaz de capturar qual tipo de palavra se segue. O exemplo 2 é consideravelmente pior ao produzir uma extensão sem sentido. No entanto, pelo menos o modelo aprendeu como soletrar palavras e algum grau de correlação entre as palavras. Por último, o exemplo 3 indica um modelo mal treinado que não ajusta os dados adequadamente.
Podemos medir a qualidade do modelo calculando a probabilidade da sequência. Infelizmente, esse é um número difícil de entender e difícil de comparar. Afinal, as sequências mais curtas têm muito mais probabilidade de ocorrer do que as mais longas, portanto, avaliando o modelo na magnum opus de Tolstoy Guerra e paz produzirá inevitavelmente uma probabilidade muito menor do que, digamos, na novela de Saint-Exupéry O Pequeno Príncipe. O que falta equivale a uma média.
A teoria da informação é útil aqui. Definimos entropia, surpresa e entropia cruzada quando introduzimos a regressão softmax (Section 3.4.7) e mais sobre a teoria da informação é discutido no apêndice online sobre teoria da informação. Se quisermos compactar o texto, podemos perguntar sobre prever o próximo token dado o conjunto atual de tokens. Um modelo de linguagem melhor deve nos permitir prever o próximo token com mais precisão. Assim, deve permitir-nos gastar menos bits na compressão da sequência. Então, podemos medi-lo pela perda de entropia cruzada média sobre todos os \(n\) tokens de uma sequência:
onde \(P\) é dado por um modelo de linguagem e \(x_t\) é o token real observado no passo de tempo \(t\) da sequência. Isso torna o desempenho em documentos de comprimentos diferentes comparáveis. Por razões históricas, os cientistas do processamento de linguagem natural preferem usar uma quantidade chamada perplexidade. Em poucas palavras, é a exponencial de (8.4.7):
A perplexidade pode ser melhor entendida como a média harmônica do número de escolhas reais que temos ao decidir qual ficha escolher a seguir. Vejamos alguns casos:
No melhor cenário, o modelo sempre estima perfeitamente a probabilidade do token de rótulo como 1. Nesse caso, a perplexidade do modelo é 1.
No pior cenário, o modelo sempre prevê a probabilidade do token de rótulo como 0. Nessa situação, a perplexidade é infinita positiva.
Na linha de base, o modelo prevê uma distribuição uniforme de todos os tokens disponíveis do vocabulário. Nesse caso, a perplexidade é igual ao número de tokens exclusivos do vocabulário. Na verdade, se armazenássemos a sequência sem nenhuma compressão, seria o melhor que poderíamos fazer para codificá-la. Conseqüentemente, isso fornece um limite superior não trivial que qualquer modelo útil deve superar.
Nas seções a seguir, implementaremos RNNs para modelos de linguagem em nível de personagem e usaremos perplexidade para avaliar tais modelos.
8.4.5. Resumo¶
Uma rede neural que usa computação recorrente para estados ocultos é chamada de rede neural recorrente (RNN).
O estado oculto de uma RNN pode capturar informações históricas da sequência até o intervalo de tempo atual.
O número de parâmetros do modelo RNN não aumenta com o aumento do número de etapas de tempo.
Podemos criar modelos de linguagem em nível de caractere usando um RNN.
Podemos usar a perplexidade para avaliar a qualidade dos modelos de linguagem.
8.4.6. Exercícios¶
Se usarmos uma RNN para prever o próximo caractere em uma sequência de texto, qual é a dimensão necessária para qualquer saída?
Por que as RNNs podem expressar a probabilidade condicional de um token em algum intervalo de tempo com base em todos os tokens anteriores na sequência de texto?
O que acontece com o gradiente se você retropropaga através de uma longa sequência?
Quais são alguns dos problemas associados ao modelo de linguagem descrito nesta seção?