Técnicas de Processamento de Linguagem Natural com Redes Neurais

  1. Redes Neurais de Alimentação Direta para PLN

1.1 Conceitos Básicos

As redes neurais de alimentação direta (FFNNs) são componentes fundamentais no processamento de linguagem natural (NLP), aplicadas em tarefas como classificação de texto, rotulação de sequências e tradução automática. O princípio central é o fluxo unidirecional de informações, desde uma camada de entrada, passando por camadas ocultas, até uma camada de saída. A introdução de não-linearidade, crucial para aprender padrões complexos, é realizada por funções de ativação como ReLU ou Sigmoid.

Um Perceptron de Múltiplas Camadas (MLP), uma forma simples de FFNN, empilha múltiplos neurônios em camadas para resolver problemas que não são linearmente separáveis. Para o NLP, representar texto numericamente é o primeiro passo, frequentemente usando uma codificação simplificada como one-hot, onde a presença de um token é indicada por 1.

1.2 Implementação Prática: Classificação de Sobrenomes

O exemplo a seguir demonstra a classificação de nacionalidades com base em sobrenomes usando um MLP de duas camadas. A implementação inclui componentes para gerenciamento de vocabulário, vetorização de dados e um processo de treinamento estruturado.

Gerenciamento de Vocabulário


class GerenciadorVocabulario:
    """Classe para processar texto e extrair vocabulário para mapeamento."""
    
    def __init__(self, mapa_token_para_idx=None, adicionar_desconhecido=True, token_desconhecido="<UNK>"):
        self._mapa_token_para_idx = mapa_token_para_idx if mapa_token_para_idx is not None else {}
        self._mapa_idx_para_token = {idx: token for token, idx in self._mapa_token_para_idx.items()}
        self._adicionar_desconhecido = adicionar_desconhecido
        self._token_desconhecido = token_desconhecido
        self.indice_desconhecido = -1
        if adicionar_desconhecido:
            self.indice_desconhecido = self._adicionar_token(token_desconhecido)

    def _adicionar_token(self, token):
        if token not in self._mapa_token_para_idx:
            novo_indice = len(self._mapa_token_para_idx)
            self._mapa_token_para_idx[token] = novo_indice
            self._mapa_idx_para_token[novo_indice] = token
        return self._mapa_token_para_idx[token]

    def obter_indice(self, token):
        if self.indice_desconhecido >= 0:
            return self._mapa_token_para_idx.get(token, self.indice_desconhecido)
        else:
            return self._mapa_token_para_idx[token]

    def obter_token(self, indice):
        return self._mapa_idx_para_token[indice]

Vetorizador de Dados


class VetorizadorSobrenome:
    def __init__(self, vocab_sobrenome, vocab_nacionalidade):
        self.vocab_sobrenome = vocab_sobrenome
        self.vocab_nacionalidade = vocab_nacionalidade

    def vetorizar(self, sobrenome):
        representacao = np.zeros(len(self.vocab_sobrenome), dtype=np.float32)
        for caractere in sobrenome:
            representacao[self.vocab_sobrenome.obter_indice(caractere)] = 1
        return representacao

Arquitetura do Classificador


class ClassificadorSobrenome(nn.Module):
    def __init__(self, dim_entrada, dim_oculta, dim_saida):
        super().__init__()
        self.camada_linear1 = nn.Linear(dim_entrada, dim_oculta)
        self.camada_linear2 = nn.Linear(dim_oculta, dim_saida)

    def forward(self, tensor_entrada, aplicar_softmax=False):
        ativacao_oculta = F.relu(self.camada_linear1(tensor_entrada))
        predicao = self.camada_linear2(ativacao_oculta)
        if aplicar_softmax:
            predicao = F.softmax(predicao, dim=1)
        return predicao

Processo de Treinamento e Avaliação

O treinamento segue o padrão de loop épocas, com lotes de dados passados pela rede. A perda é calculada com a entropia cruzada, e os pesos são atualizados via otimização. Uma técnica de parada antecipada monitora a perda no conjunto de validação para evitar sobreajuste. Após o treino, o modelo é avaliado no conjunto de teste.


def calcular_acuracia(previsoes, alvos):
    _, indices_previstos = previsoes.max(dim=1)
    corretos = torch.eq(indices_previstos, alvos).sum().item()
    return corretos / len(indices_previstos)

Ao final, funções auxiliares permitem fazer previsões interativas, mostrando a nacionalidade mais provável (ou as top-k) para um sobrenome fornecido.

  1. Tradução Automática com Mecanismo de Atenção

2.1 Arquitetura Encoder-Decoder

A tradução de sequências para sequências é comumente abordada com uma arquitetura Encoder-Decoder. O codificador processa a sequência de entrada em uma representação oculta (contexto), e o decodificador gera a sequência de saída a partir desse contexto. O mecanismo de atenção aprimora esse processo, permitindo que o decodificador "foque" em partes específicas da entrada em cada etapa da geração.

2.2 Implementação

A implementação abaixo utiliza redes GRU tanto no codificador quanto no decodificador. O mecanismo de atenção calcula uma variável de contexto ponderando as saídas do codificador.


def mecanismo_atencao(contexto_codificador, estado_decodificador):
    # Estado do decodificador é expandido para corresponder à sequência do codificador
    estado_expandido = estado_decodificador.unsqueeze(0).expand_as(contexto_codificador)
    # Concatenação para calcular a pontuação de energia
    entrada_concatenada = torch.cat((contexto_codificador, estado_expandido), dim=2)
    pontuacoes_energia = self.camada_atencao(entrada_concatenada)
    pesos_atencao = F.softmax(pontuacoes_energia, dim=0)
    # Vetor de contexto como soma ponderada
    vetor_contexto = (pesos_atencao * contexto_codificador).sum(dim=0)
    return vetor_contexto

2.3 Avaliação com BLEU

A qualidade da tradução é avaliada com a métrica BLEU, que mede a precisão de n-gramas entre a tradução gerada e uma referência humana.


def calcular_bleu(tokens_previstos, tokens_referencia, k):
    # Implementação simplificada do cálculo de pontuação BLEU
    pontuacao = 0.0
    # ... cálculo detalhado de precisões de n-gramas e penalidade de brevidade ...
    return pontuacao
  1. Tradução Automática Japonês-Chinês com Transformerr

3.1 Pré-processamento e Tokenização

Para lidar com a morfologia complexa do japonês, utiliza-se o tokenizador SentencePiece. Ele segmenta o texto em sub-palavras (subwords), tratando de forma eficaz palavras raras ou desconhecidas. O vocabulário é construído a partir dos dados de treino.


def construir_vocabulario(frases, tokenizador):
    contagem = collections.Counter()
    for frase in frases:
        contagem.update(tokenizador.encode(frase, out_type=str))
    return Vocab(contagem, specials=['<unk>', '<pad>', '<bos>', '<eos>'])

3.2 Modelo Transformer

O modelo Transformer emprega auto-atenção multicamada em vez de recorrência, permitindo maior paralelização. Ele compõe-se de um encoder (para processar a sequência de entrada) e um decoder (para gerar a sequência de saída), cada um com múltiplas camadas.


class ModeloSeq2SeqTransformer(nn.Module):
    def __init__(self, num_camadas_encoder, num_camadas_decoder, tamanho_embedding,
                 tamanho_vocab_origem, tamanho_vocab_destino, dim_feedforward=512, dropout=0.1):
        super().__init__()
        camada_encoder = TransformerEncoderLayer(d_model=tamanho_embedding, nhead=8, dim_feedforward=dim_feedforward)
        self.encoder = TransformerEncoder(camada_encoder, num_layers=num_camadas_encoder)
        
        camada_decoder = TransformerDecoderLayer(d_model=tamanho_embedding, nhead=8, dim_feedforward=dim_feedforward)
        self.decoder = TransformerDecoder(camada_decoder, num_layers=num_camadas_decoder)
        
        self.embedding_origem = nn.Embedding(tamanho_vocab_origem, tamanho_embedding)
        self.embedding_destino = nn.Embedding(tamanho_vocab_destino, tamanho_embedding)
        self.posicao_encoding = PositionalEncoding(tamanho_embedding, dropout)
        self.camada_saida = nn.Linear(tamanho_embedding, tamanho_vocab_destino)

    def forward(self, src, tgt, src_mask, tgt_mask, src_padding_mask, tgt_padding_mask):
        emb_src = self.posicao_encoding(self.embedding_origem(src))
        memoria = self.encoder(emb_src, src_mask, src_padding_mask)
        
        emb_tgt = self.posicao_encoding(self.embedding_destino(tgt))
        saida_decoder = self.decoder(emb_tgt, memoria, tgt_mask, None, tgt_padding_mask, src_padding_mask)
        
        return self.camada_saida(saida_decoder)

3.3 Treinamento e Inferência

O treinamento utiliza a perda de entropia cruzada, ignorando os tokens de preenchimento (pad). A inferência para uma nova frase é feita usando decodificação gulosa (greedy decoding), onde a próxima palavra é escolhida iterativamente com maior probabilidade.


def decodificacao_gulosa(modelo, fonte, mascara_fonte, comprimento_maximo, simbolo_inicial):
    memoria = modelo.encoder(modelo.posicao_encoding(modelo.embedding_origem(fonte)), mascara_fonte)
    saida = torch.ones(1, 1).fill_(simbolo_inicial).type(torch.long)
    
    for _ in range(comprimento_maximo - 1):
        mascara_saida = gerar_mascara_subsequente(saida.size(0))
        previsao_decoder = modelo.decoder(
            modelo.posicao_encoding(modelo.embedding_destino(saida)),
            memoria, mascara_saida)
        probabilidade = modelo.camada_saida(previsao_decoder[:, -1])
        _, proxima_palavra = torch.max(probabilidade, dim=1)
        saida = torch.cat([saida, proxima_palavra.unsqueeze(0)])
        if proxima_palavra.item() == EOS_IDX:
            break
    return saida

Tags: Processamento de Linguagem Natural Redes Neurais Pytorch Encoder-Decoder atenção

Publicado em 6-29 01:19