LLM_gradio / app.py
camilaaeromoca's picture
Update app.py
40da916 verified
raw
history blame
14.7 kB
# 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()