File size: 14,660 Bytes
af228ba
3cd80ad
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dcf41c9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354

# 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()