Alura > Cursos de Inteligência Artificial > Cursos de IA para Dados > Conteúdos de IA para Dados > Primeiras aulas do curso Transformers: fundamentos e prática com PyTorch

Transformers: fundamentos e prática com PyTorch

Fundamentos de Transformers e Preparação do Dataset - Introdução ao curso de Redes Neurais Transforms com Pytorch

Apresentando o curso e o instrutor

Sejam bem-vindos ao curso de Redes Neurais Transformers com o PyTorch. Meu nome é Alura e serei o instrutor durante esta jornada, na qual aprenderemos a construir um Transformer do zero. Este é o coração dos modelos generativos mais utilizados no mercado, seja na geração de texto, imagem ou conteúdos de forma geral.

Audiodescrição: Alura é um instrutor fictício, representando a equipe de instrutores da plataforma. Ao fundo, há uma parede branca com uma luz rosa e azul refletida nela.

Explorando o funcionamento dos Transformers

Neste curso, aprenderemos como funcionam internamente as redes neurais Transformers, uma das bases mais poderosas das IAs modernas, especialmente os modelos generativos. Vamos entender os componentes principais, tudo na prática, utilizando o PyTorch.

Ao longo das aulas, aprenderemos a compreender a arquitetura interna dos Transformers, incluindo os mecanismos de atenção, self-attention (autoatenção), positional encoding (codificação posicional), as camadas de LayerNorm (normalização de camada) e as conexões residuais. Construiremos o Transformer encoder (codificador Transformer) e o decoder (decodificador), utilizando o PyTorch de forma passo a passo.

Preparando e utilizando dados reais

Prepararemos um dataset real, utilizando a base de dados do Hugging Face, que contém uma extração da Wikipedia em português. Organizaremos todos esses dados, tratando-os como dados reais, como se fosse um projeto no qual estaríamos trabalhando no mercado de trabalho.

Integrando tokenizadores e ajustando modelos

Aprenderemos a usar tokenizadores pré-treinados e integrar embeddings (incorporações) para alimentar nossos modelos. Treinaremos e ajustaremos os hiperparâmetros desses modelos, trazendo então uma estabilização e gerando um resultado factível para colocar em produção.

Avaliando resultados e expandindo o modelo

Por último, iremos avaliar os resultados que geramos, utilizando essas redes neurais que construiremos do zero, e então entender como expandir esse modelo para tarefas mais complexas, salvando todos os artefatos necessários para poder colocar em produção e fazer o deploy dessa nossa rede neural Transformers.

Construindo o projeto final

No projeto final, construiremos um Transformer funcional do zero, treinando com textos em português, uma base bem grande de dados, passando por todas as etapas de pré-processamento, tokenização, definição da arquitetura e treinamento. A partir de uma base sólida, construiremos e evoluiremos a rede neural para geração de texto.

Nos vemos no nosso curso.

Fundamentos de Transformers e Preparação do Dataset - Introdução teórica Redes Neurais Transformers

Introduzindo o conceito de Transformers

Os Transformers e transformações surgiram com um artigo chamado "Attention is All You Need". Basicamente, ele oferece um contraponto às tarefas de linguagem e sequências que eram dominadas por redes neurais RNNs e LSTMs. Essas redes analisavam token a token, como se fosse uma série temporal, considerando o estado atual e o estado anterior. Isso causava uma limitação de paralelização e, ao trabalhar com dependências muito longas, textos extensos ou séries temporais prolongadas, era necessário utilizar ferramentas para achatar a série temporal, como trabalhar com estatísticas para aprender os padrões.

Com o surgimento desse artigo, surgiu a possibilidade de relacionar a posição e a sequência de forma paralela via atenção, que estudaremos mais profundamente nas aulas futuras. A atenção torna o treinamento mais eficiente, permitindo verificar características da sequência de forma paralela e treinar a rede com base não apenas nos tokens, mas também do ponto de vista de multi-atenção. Assim, temos a atenção introduzida no "Attention is All You Need" e o multi-attention, que seria o multi-head, permitindo verificar a atenção sobre diferentes características da sequência.

Explicando a arquitetura dos Transformers

A arquitetura padrão clássica da rede neural Transformers segue essa formação. Temos o input, que passa por um embedding e um embedding posicional. O embedding numérico transforma a sequência em números e também traz sua posição. A partir do multi-head e adicionando normalizações no decoder e no encoder da rede, conseguimos passar para o decoder e, por meio de processos e interações, criamos as probabilidades e identificamos a próxima palavra ou caractere na sequência. A base da rede aprende e segue todos esses passos nas camadas, gerando probabilidades possíveis e interagindo sobre a sequência.

Visualizando o gráfico da rede que utilizaremos no curso, usaremos multi-head e atenção para verificar atenções em sequências diferentes, seguindo essa característica. Aqui está todo o código da rede neural e da estrutura que verificaremos. Seguimos o mesmo padrão da arquitetura tradicional, com o input e o positional encoder. No encoder, realizamos todo o processo de normalização, criação das multi-head e atenção, adicionamos a normalização e criamos uma lei de normalização e posicional das características. No decoder, realizamos os mesmos processos, mas acrescentamos a máscara de multi-atenção, que verifica o futuro. Em vez de olhar toda a sequência anterior, focamos nas probabilidades futuras, dando atenção ao que vem depois. Essa é a ideia principal ao trabalhar com máscaras multi-head e self-attention.

Explorando o funcionamento do self-attention

Seguindo toda a arquitetura, ao final, com a Softmax, obtemos as probabilidades das próximas palavras e como vamos seguir na sequência. Por exemplo, ainda não entraremos profundamente nos exemplos práticos, mas veremos mais à frente. Se dissermos uma frase como "eu amo pizza" ou "eu gosto de pizza", ambas seguem uma sequência que transmite basicamente o mesmo significado, indicando afeição por essa comida específica. A partir disso, poderíamos criar uma base de self-attention. Assim, ao dizer "eu amo", a próxima palavra provável seria "pizza", e o mesmo ocorre com "eu gosto". A ideia é criar uma rede que atente para determinadas palavras e suas sequências, identificando padrões nos dados com os quais trabalhamos. Neste curso, focaremos em dados textuais, mas a lógica pode ser aplicada a outros tipos de dados, inclusive não estruturados, como imagens.

O self-attention segue uma equação que define as queries, as keys e os values, calculando a similaridade entre as palavras e utilizando o softmax para normalizar e identificar as probabilidades, multiplicando pela média ponderada desses valores. A atenção utiliza as queries para determinar qual a próxima palavra, as keys para identificar as palavras disponíveis, e os values para trazer a informação da palavra. Essa é a lógica do self-attention. Não se preocupe, pois faremos isso na prática, sem ainda inserir na arquitetura ou na rede neural, utilizando um exemplo básico com matrizes para entender seu funcionamento.

Detalhando o multi-head attention e o positional encoding

O multi-head attention consiste em várias "cabeças" que analisam problemas distintos. Criamos várias atenções diferentes para identificar padrões, como o comprimento da palavra, sintaxe e estilo. Todas essas características são concatenadas e ponderadas para projetar os pesos das palavras, trazendo coerência à geração das redes neurais.

O positional encoding é uma assinatura rítmica que determina a posição específica de uma palavra dentro de uma sentença. Pode ser treinada com uma embedding posicional ou utilizando equações pré-treinadas, como o senoidal clássico, que não é treinável. Nesse caso, calculamos o seno e o cosseno para criar posições indeterminadas para conjuntos de palavras e sequências dentro das funções senoidais e cossênicas.

Preparando o ambiente de trabalho

Agora, precisamos configurar nosso ambiente de trabalho e trazer os dados para começar a trabalhar com redes neurais Transformers. Na próxima aula, vamos configurar e reunir todas as informações necessárias.

Fundamentos de Transformers e Preparação do Dataset - Carregando e Limpando o Dataset

Configurando o ambiente no Colab

Vamos iniciar a configuração do nosso ambiente. Precisaremos instalar algumas bibliotecas no Colab, além de incorporar outras que provavelmente já utilizamos, como Regex, PyTorch, Seaborn, Matplotlib, entre outras.

Primeiramente, conectaremos o Colab. Será necessário instalar as bibliotecas do Hugging Face, pois não vêm por padrão no Colab. Essas bibliotecas nos ajudarão a obter tanto o tokenizador quanto os datasets. Utilizaremos o comando !pip install com a opção -q para suprimir as mensagens de instalação.

!pip install transformers sentencepiece -q
!pip install datasets -q

Após a instalação, importaremos as bibliotecas que utilizaremos: re para Regex, essencial para trabalhar com texto; os para manipulação de arquivos textuais; unicodedata para informações de codificação textual; torch para treinar redes neurais; math para funções matemáticas; torch.nn para redes neurais; torch.nn.functional para funcionalidades adicionais; numpy, pandas, matplotlib, seaborn, pathlib para manipulação de caminhos, e tipografias como dicionário, lista, opcionais, tuplas e uniões.

# Importações essenciais
import re
import shutil
import unicodedata
import torch
import math
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Union
from dataclasses import dataclass
from collections import Counter
import polars as pl
from tqdm import tqdm
import json
import random

Além disso, utilizaremos Dataset para criar tipos diferentes de dados, Counter para criar contadores, polars para leitura rápida de datasets, tqdm para acompanhar o progresso do treinamento dos modelos, json e random para replicabilidade, e warnings para gerenciar avisos.

import warnings
from datasets import load_dataset

Importando bibliotecas e verificando GPU

Importaremos também load_dataset do módulo datasets para carregar nosso dataset do Hugging Face. Do módulo torch.optim.lr_scheduler, importaremos a função CosineAnnealingWarmRestarts para ajustar a taxa de aprendizado durante o treinamento da rede neural. Utilizaremos torch para manipulação de tensores, amp para precisão mista, autocast e GradScaler para ajustes na descida do gradiente.

# Importar tokenizer pré-treinado
from transformers import AutoTokenizer
from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts
from torch.cuda.amp import autocast, GradScaler

Verificaremos se a CUDA está disponível para o treinamento, pois os dados são pesados. No entanto, para demonstração, utilizaremos CPU.

# Verificar se temos GPU disponível
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Usando: {device}")
if torch.cuda.is_available():
    print(f"GPU: {torch.cuda.get_device_name(0)}")

Configuraremos a replicabilidade no PyTorch com torch.manual_seed(42) e np.random.seed(42). Se a CUDA estiver disponível, configuraremos torch.cuda.manual_seed(42).

torch.manual_seed(42)
np.random.seed(42)
random.seed(42)

if torch.cuda.is_available():
    torch.cuda.manual_seed(42)
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False

Outro ponto importante é a importação da biblioteca huggingface_hub para realizar o login no Hugging Face com um token específico. Isso permitirá treinar o modelo sem problemas.

from huggingface_hub import login
login(token='hf_...')

Criando a classe do dataset

Seguiremos para criar a classe do nosso dataset, que será baixado utilizando polars para uma rápida obtenção dos dados do Hugging Face. Concatenaremos esses dados para gerar nossa base de dados específica.

A classe será chamada WikipediaPortugueseLoader, representando a base de dados do Hugging Face, que contém o Wikipedia em português, extraído por Pablo Moreira. A base é extensa, por isso utilizaremos um limitador de caracteres.

class WikipediaPortugueseLoader:
    def __init__(self, dataset_path: str = 'hf://datasets/pablo-moreira/wikipedia-pt/latest/train-*.parquet'):
        print("Carregando dataset...")
        self.df = pl.read_parquet(dataset_path, columns=['text'])
        print(f"Carregado: {len(self.df):,} artigos")

Iniciaremos a classe com self.dataset_path, apontando para o caminho do dataset pablo-moreira/wikipedia-pt-latest, que possui duas subsets, sendo uma delas train. Utilizaremos polars.read_parquet para criar um dataframe a partir dos arquivos Parquet, focando na coluna de texto.

Dentro da classe, criaremos um módulo para carregar os dados, permitindo definir um limite de caracteres (padrão de 500 mil) e um separador opcional.

def carregar_tudo(self, limite_caracteres: int = 500_000, sep_token: str = "[SEP]",
                  inserir_sep_final: bool = True) -> str:
    print("Processando textos...")

Limpando e filtrando textos

O dataset será processado para criar uma variável dataset_limpo, aplicando filtros e substituições com Regex para tratar os artigos textuais, removendo caracteres indesejados que possam dificultar o treinamento da rede neural.

# Limpa e filtra textos de forma rigorosa (conforme seu pipeline atual)
df_limpo = (
    self.df
    .filter(pl.col("text").str.len_chars() > 100) # pegar texto com mais de 100 caracteres
    .with_columns([
        pl.col("text")
        # Remover elementos específicos da Wikipedia
        .str.replace_all(r'\[\d+\]', ' ')                  # Referências [1], [2]
        .str.replace_all(r'\{\{[^}]*\}\}', ' ')            # Templates {{...}}
        .str.replace_all(r'\[\[(?:[^\]|]*\|)?([^\]]+)\]\]', r'$1') # Links [[...]]
        .str.replace_all(r'\[\[[ˆ\[\]]*\]\]', r'$1')        # Links simples
        
        # URLs, emails, HTML
        .str.replace_all(r'https?://\S+|www\.\S+', ' ')     # URLs
        .str.replace_all(r'\S+@\w+\.\w+', ' ')              # emails
        .str.replace_all(r'<.*?>', ' ')                     # Tags HTML
        .str.replace_all(r'&\w+;', ' ')                     # Entidades HTML (&amp;, &nbsp;)
        .str.replace_all(r'==.*==+', ' ')                   # Headers Wiki (==Título==)

        # Caracteres de controle e problemáticos
        .str.replace_all(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', ' ')

        # Normalizar pontuação
        .str.replace_all(r'["“”]', '"')                     # Aspas diferentes
        .str.replace_all(r"['‘’]", "'")                     # Aspas simples
        .str.replace_all(r'[–—]', '-')                      # Travessões
        .str.replace_all(r'[\.]{3,}', '...')                # Reticências

        # Remover números longos sem contexto e símbolos matemáticos
        .str.replace_all(r'\b\d{5,}\b', ' ')                # Números muito longos
        .str.replace_all(r'[=≈≠≤≥∑∫∏√∞∇∂∈∉∩∪⊂⊃∧∨¬∀∃]', ' ')

        # Manter apenas caracteres válidos para português
        .str.replace_all(r'[^a-zA-ZÀ-ú0-9\s.,;:!?-()\'"\[\]%]', ' ')

        # Limpar múltipla pontuação
        .str.replace_all(r'(\.){2,}', '.')
        .str.replace_all(r'(,){2,}', ',')
        .str.replace_all(r'(:){2,}', ':')
        .str.replace_all(r'(;){2,}', ';')
        .str.replace_all(r'(!){2,}', '!')
        .str.replace_all(r'(\?){2,}', '?')

        # Espaços e formatação final
        .str.replace_all(r'\s+([.,;:!?])', r'$1')           # Remove espaço antes de pontuação
        .str.replace_all(r'\s+', ' ')                       # Múltiplos espaços
        .str.strip_chars()
    ])
)

Realizamos diversos testes para identificar os tipos de elementos que mais apareciam e aplicamos os filtros necessários. Removemos números muito longos e mantemos apenas caracteres válidos do português. Caso haja alfabetos diferentes que possam causar problemas, fazemos a substituição da pontuação, transformando vírgulas em pontos, por exemplo. Ajustamos espaçamentos finais, removendo espaços antes de pontuações duplicadas e múltiplos espaços.

.filter(
    (pl.col("text").str.len_chars() > 50) &
    (pl.col("text").str.len_chars() < 5000) & # Não muito longo
    # Pelo menos 70% de caracteres alfabéticos
    (pl.col("text").str.count_matches(r'[a-zA-ZÀ-ú]') * 100 / pl.col("text").str.len_chars() > 70) &
    # Deve ter pelo menos uma frase completa
    (pl.col("text").str.contains(r'[.!?]\s+[A-ZÀ-ú]'))
)

Concatenando textos processados

Com o dataset pronto após a limpeza, verificamos o tamanho de cada artigo e convertemos o texto em listas, concatenando-os para gerar um texto extenso. Este texto é então separado em diferentes sentenças para treinar nosso modelo, formando nossas sequências de blocos.

print(f"Após limpeza: {len(df_limpo):,} artigos")
# Converter para lista mantendo ordem original
textos = df_limpo.get_column("text").to_list()

Para a concatenação, seguimos duas estratégias. A primeira utiliza um separador entre cada artigo, aplicando strip no texto e adicionando espaços, respeitando os limites dos caracteres e cortando os textos nas fronteiras. Assim, pegamos cada texto, pulamos uma linha, colocamos separadores e respeitamos os blocos de texto ao montar nosso dataset.

# --- Concatenação com DELIMITADOR [SEP]
# Estratégia:
# # Para cada artigo, adicionamos: "[SEP]\n" + texto.strip() + "\n"
# # Isso cria fronteiras claras entre documentos.
# # Respeitamos 'limite_caracteres' e cortamos o último texto em fronteira de frase, se necessário.
corpus_parts = []
total_len = 0
textos_adicionados = 0

# Pré-calculamos o custo fixo do prefixo/sufixo de cada bloco
prefix = f"{sep_token}\n"
suffix = "\n"
prefix_len = len(prefix)
suffix_len = len(suffix)

Implementando estratégias de corte e concatenação

O corpus é uma lista que contém o comprimento total dos textos adicionais que somamos, pré-calculando os custos de cada texto. Iteramos sobre todos os textos, verificando se cada bloco cabe dentro do limite estabelecido. Se não couber, ele será limitado e adicionado à próxima sentença, subtraindo o número de caracteres e adicionando novamente. Isso é necessário devido ao tamanho da memória da GPU. Com uma memória maior, poderíamos carregar toda a base de dados e trabalhar de forma diferente, verificando sentenças semelhantes e criando clusters de palavras.

for texto in textos:
    t = (texto or "").strip()
    if not t:
        continue
    
    bloco = f"{prefix}{t}{suffix}"
    bloco_len = len(bloco)

    # Cabe inteiro?
    if total_len + bloco_len <= limite_caracteres:
        corpus_parts.append(bloco)
        total_len += bloco_len
        textos_adicionados += 1
        continue
    
    # Se não cabe inteiro, tentamos um corte "limpo" (até fim de frase) se houver espaço suficiente
    restante = limite_caracteres - total_len
    # Espaço disponível para o conteúdo (descontando prefixo/sufixo)
    disponivel_para_texto = restante - prefix_len - suffix_len

    if disponivel_para_texto > 100:
        # Cortamos o texto no que cabe e tentamos encontrar a última sentença completa
        parc = t[:max(0, disponivel_para_texto)]
        # Procura o último delimitador de frase que caiba
        ponto = max(parc.rfind('.'), parc.rfind('!'), parc.rfind('?'))
        if ponto > 0:
            bloco = f"{prefix}{parc[:ponto+1].rstrip()}{suffix}"
            corpus_parts.append(bloco)
            total_len += len(bloco)
            textos_adicionados += 1
    
    # Independente do corte, atingimos o limite
    break

Verificamos a disponibilidade dos textos e adicionamos as delimitações, carregando-os na lista de corpus. No final, fazemos um join de cada parte do corpus em uma string, adicionando separadores específicos no início e no final, conforme as estratégias mencionadas.

corpus = "".join(corpus_parts).strip()

# [SEP] final para marcar término do último documento
if inserir_sep_final:
    final_tag = f"{sep_token}\n"
    if not corpus.endswith(final_tag):
        # Se ainda houver espaço para adicionar o [SEP] final sem estourar o limite
        if len(corpus) + len(final_tag) <= limite_caracteres:
            corpus += ("\n" if not corpus.endswith("\n") else "") + final_tag
        # Senão, ignora silenciosamente (prioriza conteúdo)

print(f"Concluído: {len(corpus):,} caracteres de {textos_adicionados:,} textos (limite: {limite_caracteres:,})")
return corpus

Carregando e limpando o dataset

Criamos uma classe que faz o download e carrega o dataset em uma variável. Primeiramente, chamamos o loader, que será o nosso WikipediaPortugueseLoader, instanciamos e carregamos os textos, limitando a 5.000 caracteres para teste. O dataset é baixado e carregado na memória.

loader = WikipediaPortugueseLoader()
textos = loader.carregar_tudo(limite_caracteres=5_000)

Após a limpeza, verificamos o número de caracteres e as sentenças, resultando em 833 artigos que passaram pelos filtros. Com o limite de 5.000 caracteres, o texto é carregado. Ao verificar, o texto é extenso, com separadores e espaços no início. Ao realizar um split pelos separadores, obtemos cada linha de texto. Por exemplo, ao pegar um elemento, temos o primeiro texto, seguido pelo segundo, que é a primeira sentença dentro dos parâmetros verificados e limpos.

textos.split("[SEP]")
textos.split("[SEP]")[0]
textos.split("[SEP]")[1]

Preparando para a próxima etapa

Na próxima aula, transformaremos o texto em formato numérico, tokenizando os dados. Podemos treinar um tokenizador, mas utilizaremos um pré-treinado, que possui um dicionário extenso de informações, incluindo palavras, sentenças, expressões e símbolos. Quanto mais palavras no dicionário, melhor será para o tokenizador, permitindo a criação de sentenças complexas e um treinamento mais eficaz.

Na próxima aula, utilizaremos o Batch, treinado com a língua portuguesa, para transformar nossos textos em tokens.

Sobre o curso Transformers: fundamentos e prática com PyTorch

O curso Transformers: fundamentos e prática com PyTorch possui 136 minutos de vídeos, em um total de 40 atividades. Gostou? Conheça nossos outros cursos de IA para Dados em Inteligência Artificial, ou leia nossos artigos de Inteligência Artificial.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda IA para Dados acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas