Pedro's dev blog

Sobre RNNs

RNN.jpeg
Published on
/6 mins read/---

RNNs: Processando Sequências com Memória

As Redes Neurais Recorrentes (Recurrent Neural Networks - RNNs) são uma classe de redes neurais projetadas especificamente para lidar com dados sequenciais, onde a ordem dos elementos é importante. Exemplos incluem texto (sequência de palavras ou caracteres), séries temporais (sequência de medições ao longo do tempo), áudio (sequência de amostras sonoras), etc.

A característica definidora de uma RNN é sua conexão recorrente ou loop: a saída de um passo de tempo é realimentada como entrada para o próximo passo. Isso permite que a rede mantenha uma "memória" ou estado oculto (hidden state) que resume as informações das partes anteriores da sequência.

Por Que RNNs para Sequências?

Redes neurais feedforward padrão (como as totalmente conectadas ou CNNs) processam cada entrada independentemente. Elas não têm uma maneira inerente de lembrar entradas anteriores ao processar a atual. Para tarefas como prever a próxima palavra em uma frase, entender o sentimento de um texto ou traduzir uma língua, o contexto anterior é crucial.

RNNs resolvem isso mantendo um estado interno que é atualizado a cada novo elemento da sequência.

O Loop Recorrente e o Estado Oculto

A unidade básica de uma RNN simples recebe dois inputs em cada passo de tempo t:

  1. A entrada atual da sequência: X_t
  2. O estado oculto do passo anterior: H_{t-1} (começa com um H_0 inicial, geralmente zeros).

Ela então calcula:

  1. O novo estado oculto: H_t
  2. A saída para o passo atual (opcional, dependendo da tarefa): O_t
graph TD
    subgraph RNN Unit at Time Step t
        direction LR
        X_t((Input X_t)) --> RNN_Cell;
        H_t_1[Hidden H_t-1] --> RNN_Cell{RNN Cell};
        RNN_Cell --> H_t[Hidden H_t];
        RNN_Cell --> O_t((Output O_t));
    end
    H_t --> Next_H[To H_t+1];
 
    style H_t_1 fill:#f9f
    style H_t fill:#f9f

Podemos "desdobrar" (unroll) esse loop ao longo da sequência para visualizar o fluxo de informação:

graph LR
    X_1((Input X_1)) --> RNN_1(RNN Unit);
    Start_H[H_0] --> RNN_1;
    RNN_1 --> H_1[H_1];
    RNN_1 --> O_1((Output O_1));
 
    X_2((Input X_2)) --> RNN_2(RNN Unit);
    H_1 --> RNN_2;
    RNN_2 --> H_2[H_2];
    RNN_2 --> O_2((Output O_2));
 
    X_n((Input X_n)) --> RNN_n(RNN Unit);
    H_2 --> RNN_n;
    RNN_n --> H_n[H_n];
    RNN_n --> O_n((Output O_n));
 
    style Start_H fill:#f9f
    style H_1 fill:#f9f
    style H_2 fill:#f9f
    style H_n fill:#f9f

O Estado Oculto (H_t): É o coração da memória da RNN. Ele é calculado tipicamente usando uma função de ativação (como tanh ou ReLU) sobre uma combinação linear da entrada atual (X_t) e do estado oculto anterior (H_{t-1}). Matematicamente (simplificado):

H_t = activation(W_hh * H_{t-1} + W_xh * X_t + b_h)

Onde W_hh, W_xh e b_h são os pesos e bias compartilhados em todos os passos de tempo.

A Saída (O_t): Pode ser calculada a partir do estado oculto atual, geralmente através de outra transformação linear:

O_t = activation_out(W_hy * H_t + b_y)

Exemplo: Modelo de Linguagem em Nível de Caractere

Vamos treinar uma RNN para prever o próximo caractere em uma sequência. Suponha que queremos que ela aprenda a palavra "HELLO".

  1. Entrada: A sequência "H", "E", "L", "L", "O". Cada caractere é representado por um vetor (ex: one-hot encoding).
  2. Passo 1 (Input "H"): A RNN recebe X_1 (vetor para "H") e H_0 (zeros). Calcula H_1 e prevê a probabilidade do próximo caractere (queremos que preveja "E").
  3. Passo 2 (Input "E"): A RNN recebe X_2 ("E") e H_1 (que contém info sobre "H"). Calcula H_2 e prevê o próximo caractere (queremos "L").
  4. Passo 3 (Input "L"): A RNN recebe X_3 ("L") e H_2 (info sobre "HE"). Calcula H_3 e prevê (queremos "L").
  5. Passo 4 (Input "L"): A RNN recebe X_4 ("L") e H_3 (info sobre "HEL"). Calcula H_4 e prevê (queremos "O").
  6. Passo 5 (Input "O"): A RNN recebe X_5 ("O") e H_4 (info sobre "HELL"). Calcula H_5 e prevê (talvez um caractere de fim de sequência).

Durante o treinamento, comparamos as previsões da RNN com os caracteres reais e usamos backpropagation (especificamente, Backpropagation Through Time - BPTT) para ajustar os pesos e minimizar o erro.

Tipos de Arquiteturas RNN

Dependendo da tarefa, as RNNs podem ter diferentes estruturas de entrada/saída:

ArquiteturaExemplo de uso
One-to-OneClassificação simples (não recorrente)
One-to-ManyImage captioning
Many-to-OneAnálise de sentimento
Many-to-Many (sync)Classificação por frame em vídeo
Many-to-Many (async)Tradução automática (Seq2Seq)

O Grande Desafio: Vanishing/Exploding Gradients

Apesar de sua elegância, RNNs simples sofrem com um problema sério durante o treinamento:

  • Vanishing Gradients (Desaparecimento do Gradiente): À medida que o erro é propagado para trás no tempo (BPTT), os gradientes podem se tornar exponencialmente pequenos. Isso significa que os pesos das conexões que afetam passos de tempo muito anteriores não são atualizados significativamente, tornando difícil para a rede aprender dependências de longo alcance. A rede "esquece" informações do início da sequência.

  • Exploding Gradients (Explosão do Gradiente): O oposto também pode ocorrer, onde os gradientes se tornam exponencialmente grandes, desestabilizando o treinamento. (Isso é geralmente mais fácil de lidar com técnicas como gradient clipping).

O problema do desaparecimento do gradiente foi a principal motivação para o desenvolvimento de arquiteturas RNN mais sofisticadas como LSTMs (Long Short-Term Memory) e GRUs (Gated Recurrent Units), que usam mecanismos de "portões" (gates) para controlar melhor o fluxo de informação e gradiente ao longo do tempo.

Vantagens das RNNs (Simples)

  • Modelagem de Sequências: São naturalmente adequadas para dados onde a ordem importa.
  • Memória (Teórica): Podem, em teoria, capturar informações de passos anteriores através do estado oculto.
  • Compartilhamento de Parâmetros no Tempo: Os mesmos pesos são usados para processar cada elemento da sequência, tornando-as eficientes em termos de parâmetros.

Desvantagens das RNNs (Simples)

  • Vanishing/Exploding Gradients: Dificultam o treinamento e o aprendizado de dependências de longo alcance.
  • Dificuldade com Longas Sequências: Devido ao problema dos gradientes, a "memória" efetiva é muitas vezes curta.
  • Processamento Sequencial: O cálculo para o passo t depende do resultado do passo t-1, o que limita a paralelização dentro de uma mesma sequência (diferente dos Transformers).

Conclusão

As RNNs simples foram um passo fundamental na modelagem de dados sequenciais, introduzindo a ideia de conexões recorrentes e estados ocultos para manter a memória. Embora tenham limitações significativas, principalmente relacionadas aos gradientes e dependências de longo alcance, elas estabeleceram a base para arquiteturas mais avançadas como LSTMs e GRUs, que dominaram o campo por muitos anos antes da chegada dos Transformers. Compreender as RNNs é essencial para entender a evolução das redes neurais para tarefas sequenciais.

Next post →Sobre LSTMs