Spaces:
Sleeping
Sleeping
# 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") | |
modelo_llm = AutoModelForCausalLM.from_pretrained("modelos/modelo_final") | |
# 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 | |
tokenizer = DSATokenizer(13) | |
# 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_llm): | |
# 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)) | |
# 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) | |
# Testa a função | |
faz_previsao('3 + 5 =', solution_length = 2) | |
# Função para retornar a função que faz a previsão | |
def funcsolve(entrada): | |
return faz_previsao(entrada, solution_length = 2) | |
# Cria a web app | |
webapp = gr.Interface(fn = funcsolve, | |
inputs = [gr.Textbox(label = "Dados de Entrada", | |
lines = 1, | |
info = "Os dados devem estar na forma: '1 + 2 =' com um único espaço entre cada caractere e apenas números de um dígito são permitidos.")], | |
outputs = [gr.Textbox(label = "Resultado (Previsão do Modelo)", lines = 1)], | |
title = "Deploy de LLM Após o Fine-Tuning", | |
description = "Digite os dados de entrada e clique no botão Submit para o modelo fazer a previsão.", | |
examples = ["5 + 3 =", "2 + 9 ="]) | |
webapp.launch() |