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.
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.
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.
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.
df_limpo = (
self.df
.filter(pl.col("text").str.len_chars() > 100)
.with_columns([
pl.col("text")
.str.replace_all(r'\[\d+\]', ' ')
.str.replace_all(r'\{\{[^}]*\}\}', ' ')
.str.replace_all(r'\[\[(?:[^\]|]*\|)?([^\]]+)\]\]', r'$1')
.str.replace_all(r'\[\[[ˆ\[\]]*\]\]', r'$1')
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.str.replace_all(r&
.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) &
(pl.col("text").str.count_matches(r'[a-zA-ZÀ-ú]') * 100 / pl.col("text").str.len_chars() > 70) &
(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")
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.
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 &
bloco_len &
if total_len &
corpus_parts.append(bloco)
total_len &
textos_adicionados &
continue
restante &
disponivel_para_texto &
if disponivel_para_texto > 100:
parc &
ponto &
if ponto > 0:
bloco &
corpus_parts.append(bloco)
total_len &
textos_adicionados &
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):
if len(corpus) + len(final_tag) <= limite_caracteres:
corpus += ("\n" if not corpus.endswith("\n") else "") + final_tag
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.