import torch import transformers import numpy as np import torch.nn.functional as F from accelerate import Accelerator from torch.utils.data import Dataset from transformers import AutoConfig, AutoModelForCausalLM, GPT2Tokenizer from transformers import GPT2Tokenizer # Tamanho do vocabulário vocab_size = 13 # Comprimento da sequência sequence_length = 4 # Comprimento do resultado result_length = 2 # Comprimento do contexto context_length = sequence_length + result_length # Parâmetros de configuração do modelo GPT-2 config = AutoConfig.from_pretrained("gpt2", vocab_size = vocab_size, n_ctx = context_length, n_head = 4, n_layer = 2) # Carrega o modelo modelo = AutoModelForCausalLM.from_config(config) # Função para calcular o tamanho do modelo def model_size(model): return sum(t.numel() for t in modelo.parameters()) print(f'Tamanho do Modelo: {model_size(modelo)/1000**2:.1f}M parâmetros') #Tamanho do Modelo: 15.0M parâmetros #Este modelo tem 15 milhões de parâmetros em vez dos 111 milhões de parâmetros da configuração padrão "gpt2". type(modelo) transformers.models.gpt2.modeling_gpt2.GPT2LMHeadModel # Salva o modelo em disco modelo.save_pretrained("modelos/modelo_inicial") # Definindo uma classe chamada NumberTokenizer, que é usada para tokenizar os números class DSATokenizer: # Método construtor da classe, que é executado quando um objeto dessa classe é criado def __init__(self, numbers_qty = 10): # Lista de tokens possíveis que o tokenizador pode encontrar vocab = ['+', '=', '-1', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] # Definindo a quantidade de números que o tokenizador pode lidar self.numbers_qty = numbers_qty # Definindo o token de preenchimento (padding) self.pad_token = '-1' # Criando um dicionário que mapeia cada token para um índice único self.encoder = {str(v):i for i,v in enumerate(vocab)} # Criando um dicionário que mapeia cada índice único de volta ao token correspondente self.decoder = {i:str(v) for i,v in enumerate(vocab)} # Obtendo o índice do token de preenchimento no encoder self.pad_token_id = self.encoder[self.pad_token] # Método para decodificar uma lista de IDs de token de volta para uma string def decode(self, token_ids): return ' '.join(self.decoder[t] for t in token_ids) # Método que é chamado quando o objeto da classe é invocado como uma função def __call__(self, text): # Dividindo o texto em tokens individuais e retornando uma lista dos IDs correspondentes return [self.encoder[t] for t in text.split()] # Cria o objeto do tokenizador tokenizer = DSATokenizer(vocab_size) # Decoder do tokenizador tokenizer.decoder # Testando o tokenizador tokenizer("1 + 1 = 2") # Definindo uma classe chamada CriaDataset, que herda da classe Dataset do PyTorch class CriaDataset(Dataset): # Método construtor da classe, que é executado quando um objeto dessa classe é criado def __init__(self, split, length = 6): # Verificando se a divisão do dataset (split) é 'treino' ou 'teste' assert split in {'treino', 'teste'} self.split = split self.length = length # Definindo o método len que retorna o tamanho do dataset. # Nesse caso, o tamanho é fixo e igual a 1 milhão. def __len__(self): return 1000000 # Definindo o método getitem que é usado para obter um item específico do dataset def __getitem__(self, idx): # Criando uma lista com todos os números disponíveis que não são tokens de padding e são numéricos available_numbers = [int(n) for n in tokenizer.decoder.values() if n != tokenizer.pad_token and str(n).isnumeric()] # Selecionando aleatoriamente números da lista de números disponíveis para criar uma entrada (input) inp = torch.tensor(np.random.choice(available_numbers, size = result_length)) # Calculando a soma dos números selecionados e criando um tensor sol = torch.tensor([int(i) for i in str(inp.sum().item())]) # Preenchendo o tensor com zeros para que tenha o tamanho desejado sol = torch.nn.functional.pad(sol, (1 if sol.size()[0] == 1 else 0,0), 'constant', 0) # Concatenando a entrada e a solução em um tensor cat = torch.cat((inp, sol), dim = 0) # Criando os tensores de entrada e alvo para o treinamento do modelo x = cat[:-1].clone() y = cat[1:].clone() # Definindo o primeiro elemento do tensor alvo como o token de padding y[:1] = int(tokenizer.pad_token) # Transformando os tensores x e y em strings x = str(x[0].item()) + ' + ' + str(x[1].item()) + ' = ' + str(x[2].item()) y = '-1 ' + str(y[0].item()) + ' -1 ' + str(y[1].item()) + ' ' + str(y[2].item()) # Tokenizando as strings de entrada e alvo tokenized_input = tokenizer(x) tokenized_output = tokenizer(y) # Retornando os tensores de entrada e alvo como itens do dataset return torch.tensor(tokenized_input), torch.tensor(tokenized_output) dataset_treino = CriaDataset('treino', length = sequence_length) dataset_teste = CriaDataset('teste', length = sequence_length) x, y = dataset_treino[0] x y print(tokenizer.decode(x.numpy())) print(tokenizer.decode(y.numpy())) num_epochs = 2 batch_size = 100 optimizer = torch.optim.Adam(modelo.parameters()) dados = torch.utils.data.DataLoader(dataset_treino, shuffle = True, batch_size = batch_size) import accelerate from accelerate import Accelerator accelerator = Accelerator() modelo, optimizer, dados = accelerator.prepare(modelo, optimizer, dados) modelo.train() # Iniciando o loop para as épocas de treinamento for epoch in range(num_epochs): # Iterando por cada batch (conjunto) de dados de entrada e alvos no dataset de treinamento for source, targets in dados: # Resetando os gradientes acumulados no otimizador optimizer.zero_grad() # Calculando a perda (loss) através da entropia cruzada entre as previsões do modelo e os alvos verdadeiros. # Os tensores são "achatados" para que possam ser passados para a função de entropia cruzada. # O índice do token de preenchimento (pad_token) é ignorado no cálculo da perda. loss = F.cross_entropy(modelo(source).logits.flatten(end_dim = 1), targets.flatten(end_dim = 1), ignore_index = tokenizer.pad_token_id) # Calculando os gradientes da perda em relação aos parâmetros do modelo accelerator.backward(loss) # Atualizando os parâmetros do modelo utilizando os gradientes calculados optimizer.step() # Recalculando a perda após a etapa de otimização. loss = F.cross_entropy(modelo(source).logits.flatten(end_dim = 1), targets.flatten(end_dim = 1), ignore_index = tokenizer.pad_token_id) # Imprimindo a época atual e a perda após cada época de treinamento print(f'Epoch: {epoch+1}/{num_epochs} --- Erro: {loss.item()}') # Definindo a função gera_solution com três parâmetros: input, solution_length e model def faz_previsao(entrada, solution_length = 6, model = modelo): # Colocando o modelo em modo de avaliação. model.eval() # Convertendo a entrada (string) em tensor utilizando o tokenizer. # O tensor é uma estrutura de dados que o modelo de aprendizado de máquina pode processar. entrada = torch.tensor(tokenizer(entrada)) # Enviando o tensor de entrada para o dispositivo de cálculo disponível (CPU ou GPU) entrada = entrada.to(accelerator.device) # Iniciando uma lista vazia para armazenar a solução solution = [] # Loop que gera a solução de comprimento solution_length for i in range(solution_length): # Alimentando a entrada atual ao modelo e obtendo a saída saida = model(entrada) # Pegando o índice do maior valor no último conjunto de logits (log-odds) da saída, # que é a previsão do modelo para o próximo token predicted = saida.logits[-1].argmax() # Concatenando a previsão atual com a entrada atual. # Isso servirá como a nova entrada para a próxima iteração. entrada = torch.cat((entrada, predicted.unsqueeze(0)), dim = 0) # Adicionando a previsão atual à lista de soluções e convertendo o tensor em um número Python padrão solution.append(predicted.cpu().item()) # Decodificando a lista de soluções para obter a string de saída e retornando-a return tokenizer.decode(solution) # Definindo a função avalia_modelo com dois parâmetros: num_samples e log def avalia_modelo(num_samples = 1000, log = False): # Iniciando um contador para as previsões corretas correct = 0 # Loop que itera num_samples vezes for i in range(num_samples): # Obtendo a entrada e o alvo (resposta correta) do i-ésimo exemplo do conjunto de teste entrada, target = dataset_teste[i] # Convertendo os tensores de entrada e alvo em arrays numpy para processamento posterior entrada = entrada.cpu().numpy() target = target.cpu().numpy() # Decodificando a entrada e o alvo utilizando o tokenizer entrada = tokenizer.decode(entrada[:sequence_length]) target = tokenizer.decode(target[sequence_length-1:]) # Gerando a previsão utilizando a função faz_previsao predicted = faz_previsao(entrada, solution_length = result_length, model = modelo) # Se a previsão for igual ao alvo, incrementa o contador de previsões corretas if target == predicted: correct += 1 # Se log for True, imprime detalhes do exemplo e a previsão correta if log: print(f'Acerto do Modelo: Input: {entrada} Target: {target} Previsão: {predicted}') else: # Se log for True, imprime detalhes do exemplo e a previsão errada if log: print(f'Erro do Modelo: Input: {entrada} Target: {target} Previsão: {predicted}') # Ao final do loop, calcula a acurácia (número de previsões corretas dividido pelo número total de exemplos) print(f'Acurácia: {correct/num_samples}') # Executa a função avalia_modelo(num_samples = 10, log = True) # Executa a função avalia_modelo(num_samples = 1000, log = False) type(modelo) modelo.save_pretrained("modelos/modelo_final")