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