MartialTerran commited on
Commit
96a54b7
1 Parent(s): 7369314

Create Gettysburg_GPT2_v1.4.2.py

Browse files
Files changed (1) hide show
  1. Gettysburg_GPT2_v1.4.2.py +461 -0
Gettysburg_GPT2_v1.4.2.py ADDED
@@ -0,0 +1,461 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Added into v1.4.2.py
2
+
3
+ # Dynamic vocab_size Update: After the Tokenizer is created, in the def Main, the calculated tokenizer.vocab_size is assigned back to hyperparameters["vocab_size"]: hyperparameters["vocab_size"] = tokenizer.vocab_size This guarantees that the ToyGPT2 model is constructed before training with the correct vocabulary size derived from the Dataset (Gettysburg Address) plus special tokens. This helps prevent runtime errors when user makes a change to the Dataset resulting in a different number of tokens in the model vocabulary. [If the hyperparameters["vocab_size"] is any larger than the number of Tokenizer-defined tokens, the as-initialized output level might select an undefined token as the next-token and then produce an out-of-range crash. Checkpoints (when enabled within the def Training) will be saved to include the updated hyperparameters["vocab_size"]. A future feature, not yet implemented, should include automatically writing the hyperparameters["vocab_size"] to the filename of the saved checkpoint.
4
+
5
+ # Note: If the token length of the Training Dataset is changed (even if the hyperparameters["vocab_size"] is not changed), the values for ["max_sequence_len": 264, # Maximum sequence length] and [min_training_input_seq_len = 32] may need to be changed to avoid a crash. Generally, the Dataset has to have at least about 20 more tokens than the [min_training_input_seq_len = 32]. This has something to do with how the dataloader defines batches. It is fastest and probably most efficient to train the model with ["batch_size": 1] and only provide enough tokens in the dataset to define one unique batch?
6
+
7
+ # The ["max_sequence_len": 264] Hyperparameter only truncates input token (prompt) sequences. Despite the ["max_sequence_len": 264] the model will still output a larger number of tokens in its whole response. This is a still-mysterious feature of the model python code. A max-new-tokens limit has not been implemented. a <end of doc> special token has not been defined.
8
+
9
+ # Features of v1.4final.py
10
+ # This script runs and computes loss down to under 0.001 at epoch 101, then after epoch 110 the loss rises up again. Then at epoch 150 the loss goes downward again. Next version will report the particular words that are causing the error/loss.
11
+ # # The tokenize method now uses the last special token in the self.special_tokens list (which is assumed to be the padding token <pad> in this case) as the default token for unknown words.
12
+ #text separate_punctuation focuses solely on separating the defined punctuation marks from words.
13
+
14
+ #Carriage returns [unntested] are treated as a distinct case and are replaced with the <cr> token after a punctuation-separate step.
15
+ # The detokenizer does not yet auto-remove spaces preceding punctuations. This is because tokens are defined without leading spaces, and spaces are autoappended to all tokens in detokenizer.
16
+
17
+ # It's possible to increase training_input_seq_len over epochs. However, directly modifying training_input_seq_len inside the Dataset class after it's created isn't ideal. A better approach is to control the sequence length during batch creation within the DataLoader. You can achieve this using a custom collate_fn ?
18
+
19
+
20
+ print("loading libraries")
21
+ import os # to get filename of this script
22
+ import datetime
23
+ import torch
24
+ import torch.nn as nn
25
+ import torch.optim as optim
26
+ from torch.utils.data import Dataset, DataLoader
27
+ import torch.optim as optim
28
+ from torch.optim.lr_scheduler import ReduceLROnPlateau # Import the learning rate scheduler
29
+ import math
30
+ import inspect
31
+ #import string # replaced with self.punctuation_list = ['.', ',', '/', '\\', '[', ']', '<', '?', '>', '-']] # Specific list of punctuations
32
+ print("done loading libraries")
33
+
34
+ print("Hardcoding Memorized_Speech = Gettysburg Address") #(for simplicity in this toy example)
35
+ Memorized_Speech = """
36
+ Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.
37
+
38
+ Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.
39
+
40
+ But, in a larger sense, we can not dedicate - we can not consecrate - we can not hallow-this ground. The brave men, living and dead, who struggled here, have consecrated it, far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us - that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion - that we here highly resolve that these dead shall not have died in vain - that this nation, under God, shall have a new birth of freedom - and that government of the people, by the people, for the people, shall not perish from the earth.
41
+ """
42
+ print(f'Length of Memorized_Speech = {len(Memorized_Speech)} characters, as follows:')
43
+ print(Memorized_Speech)
44
+
45
+ # Add special tokens here. "<pad>" is also used for unknown words. The carriage-return specialtoken will be auto-inserted into the received text before tokenization. But tabs and newlines are not implemented/supported.
46
+ # Hyperparameters
47
+ hyperparameters = {
48
+ "vocab_size": 152, # Estimated vocabulary size for Gettysburg Address + special tokens
49
+ "special_tokens": ["<FreetheLLM>", "<cr>", "<pad>"],
50
+ "n_embd": 512, # Embedding dimension
51
+ "n_layer": 4, # Number of layers
52
+ "n_head": 16, # Number of attention heads
53
+ "n_inner": 4 * 512, # Inner dimension of feedforward network (4 times n_embd)
54
+ "max_sequence_len": 264, # Maximum sequence length
55
+ "epochs": 200, # Number of training epochs
56
+ "learning_rate": 1e-3, # [Initial] Learning rate
57
+ "batch_size": 1, # Batch size (since the dataset is small)
58
+ "dropout": 0.2 # Dropout probability
59
+ }
60
+ # More Script/Training parameters:
61
+ min_training_input_seq_len = 32
62
+ Early_stopping_loss = 0.01
63
+
64
+
65
+ def print_with_line(message):
66
+ frame = inspect.currentframe().f_back # needs import inspect
67
+ line_number = frame.f_lineno
68
+ print(f"{message} at script line {line_number}")
69
+
70
+ # --- Tokenizer and Detokenizer ---
71
+ class Tokenizer:
72
+ def __init__(self, text, special_tokens, vocab_size_hyperparameter):
73
+ self.special_tokens = special_tokens
74
+ self.cr_token = special_tokens[1]
75
+ #self.punctuation = string.punctuation # Store punctuation characters
76
+ self.punctuation_list = ['.', ',', '/', '\\', '[', ']', '<', '?', '>', '-'] # Specific list of punctuations
77
+ estimated_vocab_size = vocab_size_hyperparameter #hyperparameters["vocab_size"]
78
+
79
+ # Preprocess text to separate existing punctuation from words, and then auto-inserts <cr> special tokens at carriage returns.
80
+ text = self.separate_punctuation(text)
81
+
82
+ in_text_words = []
83
+ in_text_punctuations = []
84
+ for candidate in text.split(): # Split into tokens (space-separated words and punctuation; includes words attached to punctuation)
85
+ cleaned_words = ''.join(c for c in candidate if c not in self.punctuation_list) #strip punctuation from words
86
+ if cleaned_words:
87
+ in_text_words.append(cleaned_words.lower())
88
+ for char in candidate: # Iterate through each character in the candidates
89
+ if char in self.punctuation_list:
90
+ in_text_punctuations.append(char) # Add in-text punctuation as separate tokens
91
+
92
+ # Ensure unique and sorted word and punctuation tokens
93
+ in_text_words = list(set(in_text_words))
94
+ in_text_words.sort()
95
+ in_text_punctuations = list(set(in_text_punctuations))
96
+ in_text_punctuations.sort()
97
+
98
+ self.vocab = self.special_tokens + in_text_punctuations + in_text_words # Vocab starts with special tokens, then punctuation, then whole words.
99
+ self.vocab_size = len(self.vocab) # Calculate vocabulary size dynamically
100
+ # Alert if vocab_size is different from a predefined hyperparameter estimate (optional)
101
+ if self.vocab_size != estimated_vocab_size:
102
+ print(f"Warning: Calculated vocab_size ({self.vocab_size}) differs from estimated size ({estimated_vocab_size}).")
103
+
104
+ self.word_to_index = {word: i for i, word in enumerate(self.vocab)}
105
+ self.index_to_word = {i: word for i, word in enumerate(self.vocab)}
106
+
107
+ def separate_punctuation(self, text): # text passed to the tokenize method is also preprocessed to have separated punctuation before tokenization #separate_punctuation(self, text) method, as currently implemented, does not directly affect carriage returns (\r) in the original text.
108
+ #Adds spaces around punctuation to separate them from words.
109
+ for char in self.punctuation_list:
110
+ text = text.replace(char, f' {char} ')
111
+ #Replace carriage returns (backslash-r) in the input text with a special token (e.g., <cr>).
112
+ text = text.replace('\r', f' {self.cr_token} ') # Replace \r with <cr> token and pad with spaces.
113
+ #print(f"Carriage-Return's special token inserted as {self.cr_token}")
114
+ return text
115
+
116
+
117
+
118
+ def tokenize(self, text):
119
+ # Apply punctuation separation before tokenizing
120
+ text = self.separate_punctuation(text)
121
+ words = text.lower().split() #preserves special tokens like the auto-inserted <cr>
122
+ token_ids = []
123
+ for word in words:
124
+ if word in self.word_to_index:
125
+ token_ids.append(self.word_to_index[word])
126
+ else:
127
+ #token_ids.append(self.word_to_index['<pad>'])
128
+ token_ids.append(self.word_to_index[self.special_tokens[-1]]) # Use last special token as default (e.g., <pad>) # The tokenize method now uses the last special token in the self.special_tokens list (which is assumed to be the padding token <pad> in this case) as the default token for unknown words.
129
+ return token_ids
130
+
131
+ def detokenize(self, tokens):
132
+ return " ".join([self.index_to_word[token] for token in tokens if token in self.index_to_word])
133
+
134
+ # --- GPT-2 Model ---
135
+ class CausalSelfAttention(nn.Module):
136
+ def __init__(self, config):
137
+ super().__init__()
138
+ assert config["n_embd"] % config["n_head"] == 0
139
+ # key, query, value projections for all heads, but in a batch
140
+ self.c_attn = nn.Linear(config["n_embd"], 3 * config["n_embd"])
141
+ # output projection
142
+ self.c_proj = nn.Linear(config["n_embd"], config["n_embd"])
143
+ # regularization
144
+ self.attn_dropout = nn.Dropout(0.1)
145
+ self.resid_dropout = nn.Dropout(0.1)
146
+ self.n_head = config["n_head"]
147
+ self.n_embd = config["n_embd"]
148
+ self.register_buffer("bias", torch.tril(torch.ones(config["max_sequence_len"], config["max_sequence_len"]))
149
+ .view(1, 1, config["max_sequence_len"], config["max_sequence_len"]))
150
+
151
+ def forward(self, x):
152
+ B, T, C = x.size() # batch size, sequence length, embedding dimensionality (n_embd)
153
+
154
+ # calculate query, key, values for all heads in batch and move head forward to be the batch dim
155
+ q, k ,v = self.c_attn(x).split(self.n_embd, dim=2)
156
+ k = k.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
157
+ q = q.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
158
+ v = v.view(B, T, self.n_head, C // self.n_head).transpose(1, 2) # (B, nh, T, hs)
159
+
160
+ # causal self-attention; Self-attend: (B, nh, T, hs) x (B, nh, hs, T) -> (B, nh, T, T)
161
+ att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
162
+ att = att.masked_fill(self.bias[:,:,:T,:T] == 0, float('-inf'))
163
+ att = torch.softmax(att, dim=-1)
164
+ att = self.attn_dropout(att)
165
+ y = att @ v # (B, nh, T, T) x (B, nh, T, hs) -> (B, nh, T, hs)
166
+ y = y.transpose(1, 2).contiguous().view(B, T, C) # re-assemble all head outputs side by side
167
+
168
+ # output projection
169
+ y = self.resid_dropout(self.c_proj(y))
170
+ return y
171
+
172
+ class Block(nn.Module):
173
+ def __init__(self, config):
174
+ super().__init__()
175
+ self.ln_1 = nn.LayerNorm(config["n_embd"])
176
+ self.attn = CausalSelfAttention(config)
177
+ self.ln_2 = nn.LayerNorm(config["n_embd"])
178
+ self.mlp = nn.Sequential(
179
+ nn.Linear(config["n_embd"], config["n_inner"]),
180
+ nn.GELU(),
181
+ nn.Linear(config["n_inner"], config["n_embd"]),
182
+ nn.Dropout(0.1),
183
+ )
184
+
185
+ def forward(self, x):
186
+ x = x + self.attn(self.ln_1(x))
187
+ x = x + self.mlp(self.ln_2(x))
188
+ return x
189
+
190
+ class ToyGPT2(nn.Module):
191
+ def __init__(self, config):
192
+ super().__init__()
193
+ self.config = config
194
+ self.token_embedding_table = nn.Embedding(config["vocab_size"], config["n_embd"])
195
+ self.position_embedding_table = nn.Embedding(config["max_sequence_len"], config["n_embd"])
196
+ self.blocks = nn.Sequential(*[Block(config) for _ in range(config["n_layer"])])
197
+ self.ln_f = nn.LayerNorm(config["n_embd"]) # final layer norm
198
+ self.lm_head = nn.Linear(config["n_embd"], config["vocab_size"])
199
+
200
+ # Initialize weights to be small for better training
201
+ self.apply(self._init_weights)
202
+
203
+ # Tie the weights of the embedding and the output layer
204
+ self.lm_head.weight = self.token_embedding_table.weight
205
+
206
+ def _init_weights(self, module):
207
+ #if isinstance(module, nn.Linear):
208
+ # torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
209
+ if isinstance(module, nn.Linear) and module.bias is not None:
210
+ #print("isinstance(module, nn.Linear) and module.bias is not None")
211
+ torch.nn.init.zeros_(module.bias)
212
+ elif isinstance(module, nn.Embedding):
213
+ torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
214
+ #print("torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)")
215
+
216
+ def forward(self, idx, targets=None):
217
+ B, T = idx.shape
218
+ # idx and targets are both (B,T) tensor of integers
219
+ tok_emb = self.token_embedding_table(idx) # (B,T,C)
220
+ pos_emb = self.position_embedding_table(torch.arange(T, device=idx.device)) # (T,C)
221
+ x = tok_emb + pos_emb # (B,T,C)
222
+ x = self.blocks(x) # (B,T,C)
223
+ x = self.ln_f(x) # (B,T,C)
224
+ logits = self.lm_head(x) # (B,T,vocab_size)
225
+
226
+ if targets is None:
227
+ loss = None
228
+ else:
229
+ B, T, C = logits.shape
230
+ logits = logits.view(B*T, C)
231
+ targets = targets.view(B*T)
232
+ loss = nn.functional.cross_entropy(logits, targets)
233
+
234
+ return logits, loss
235
+
236
+ def generate(self, input_ids, max_new_tokens, temperature=1.0):
237
+ self.eval() # Set model to evaluation mode
238
+ with torch.no_grad(): # Disable gradient calculation during generation
239
+ for _ in range(max_new_tokens):
240
+ # Limit input_ids to the last max_sequence_len tokens
241
+ input_ids_truncated = input_ids[:, -self.config["max_sequence_len"]:]
242
+
243
+ # Get logits from the model
244
+ logits, _ = self(input_ids_truncated) # No need for loss during generation
245
+
246
+ # Focus on the logits for the last time step (next token prediction)
247
+ logits = logits[:, -1, :] / temperature
248
+
249
+ # Apply softmax to get probabilities
250
+ probs = torch.softmax(logits, dim=-1)
251
+
252
+ # Sample the next token
253
+ next_token = torch.multinomial(probs, num_samples=1)
254
+
255
+
256
+ # Append next token to input sequence
257
+ input_ids = torch.cat((input_ids, next_token), dim=1)
258
+
259
+ self.train() # Return model to training mode
260
+ return input_ids
261
+
262
+ # --- Dataset ---
263
+ class Dataset(Dataset):
264
+ def __init__(self, data, tokenizer, seq_len):
265
+ self.tokenizer = tokenizer
266
+ self.seq_len = seq_len
267
+
268
+ print_with_line("# Tokenize the entire data")
269
+ self.tokens = self.tokenizer.tokenize(data)
270
+ print(f"DEBUG: Total tokens: {len(self.tokens)} in Dataset(") # Add this line
271
+
272
+ # Calculate token counts
273
+ self.token_counts = self._calculate_token_counts() # Store counts in the object
274
+
275
+ # Create input-target pairs
276
+ self.data = []
277
+ for i in range(0, len(self.tokens) - seq_len - 1, seq_len):
278
+ input_seq = self.tokens[i:i + seq_len]
279
+ target_seq = self.tokens[i + 1:i + seq_len + 1]
280
+ self.data.append((torch.tensor(input_seq), torch.tensor(target_seq)))
281
+
282
+ print(f"DEBUG Dataset(Dataset): Number of data samples created in class Dataset(Dataset): {len(self.data)}") # Add this line
283
+
284
+ # Print token-vocabulary information
285
+ print_with_line("# Print token-vocabulary information:")
286
+ self.print_vocabulary_info() # Call the new method
287
+
288
+ def _calculate_token_counts(self):
289
+ #Calculates the frequency of each token in self.tokens.
290
+ counts = {}
291
+ for token in self.tokens:
292
+ if token in counts:
293
+ counts[token] += 1
294
+ #print(f"token {token} count has been incremented to {counts[token]}")
295
+ else:
296
+ counts[token] = 1
297
+ return counts
298
+
299
+ def print_vocabulary_info(self):
300
+ print_with_line("# Print token-vocabulary information:")
301
+ for token_id in range(self.tokenizer.vocab_size): # Iterate through indices
302
+ token = self.tokenizer.index_to_word[token_id] # Get token string from index
303
+ count = self.token_counts.get(token_id, 0) # Correct: token_id is an integer ID # Get count, default to 0 if not found
304
+ #print(f" Token {token_id}: '{token}' occurs {count} times in the dataset")
305
+ print(f" Token {token_id}:'{token}' \t\t occurs {count} times in the dataset")
306
+
307
+ def __len__(self):
308
+ return len(self.data)
309
+
310
+ def __getitem__(self, idx):
311
+ return self.data[idx] # Return the pre-processed tensor pairs
312
+
313
+
314
+
315
+ # --- Trainer ---
316
+ class Trainer:
317
+ def __init__(self, model, tokenizer, train_loader, hyperparameters, device):
318
+ self.model = model
319
+ self.tokenizer = tokenizer
320
+ self.train_loader = train_loader # notice this change
321
+ self.hyperparameters = hyperparameters
322
+ self.Early_stopping_loss = Early_stopping_loss # Set Early stopping loss
323
+ self.device = device # Store the device
324
+
325
+
326
+ self.optimizer = optim.AdamW(self.model.parameters(), lr=hyperparameters["learning_rate"])
327
+ self.scheduler = ReduceLROnPlateau(self.optimizer, mode='min', factor=0.99, patience=10)
328
+ # mode='min': Indicates that you want to minimize the loss.
329
+ # factor=0.1: The factor by which the learning rate is reduced (e.g., 0.1 means reduce to 10%).
330
+ # patience=10: Number of epochs with no improvement after which the learning rate will be reduced.
331
+ # verbose=True: Prints a message when the learning rate is adjusted.
332
+ # Step the Scheduler: Call self.scheduler.step(average_loss) after calculating average_loss. This tells the scheduler to update the learning rate based on the current loss.
333
+ # Automated Adjustment: The scheduler automatically adjusts the learning rate, removing the need for manual tuning during training.
334
+ # Improved Convergence: Can help the model converge more smoothly and potentially reach a better solution.
335
+ # Reduced Fluctuations: Helps reduce the fluctuations in the loss.
336
+
337
+ def train(self):
338
+ self.model.train() # Set model to training mode
339
+ for epoch in range(self.hyperparameters["epochs"]):
340
+ total_loss = 0
341
+ for batch_idx, (input_seq, target_seq) in enumerate(self.train_loader): # Use enumerate to get batch index # Directly use the loaded batches
342
+ input_seq = input_seq.to(self.device) # Move to device
343
+ target_seq = target_seq.to(self.device) # Move to device
344
+
345
+ self.optimizer.zero_grad()
346
+ logits, loss = self.model(input_seq, targets=target_seq) # logits are the raw predictions
347
+ loss.backward()
348
+ self.optimizer.step()
349
+ total_loss += loss.item()
350
+
351
+ average_loss = total_loss / len(self.train_loader) # Consider number of batches
352
+ print(f"Epoch {epoch+1}/{self.hyperparameters['epochs']}, Loss: {average_loss:.4f}")
353
+ if loss < 0.01: # Check loss for current batch
354
+ print(" LOSS IS BELOW 0.01")
355
+ if loss < 0.001: # Check loss for current batch
356
+ print(" LOSS IS BELOW 0.001")
357
+
358
+ self.scheduler.step(average_loss) # Update the lossrate-scheduler with the current loss
359
+
360
+ # Check if the learning rate has changed and print it
361
+ current_lr = self.optimizer.param_groups[0]['lr']
362
+ last_lr = self.scheduler.get_last_lr()[0] # Get the last learning rate
363
+ if current_lr != last_lr:
364
+ print(f"Learning rate reduced to {last_lr:.6f}")
365
+
366
+ if(epoch%100 ==0):
367
+ current_lr = self.optimizer.param_groups[0]['lr'] # Get the current learning rate from the optimizer
368
+ print(f"Epoch {epoch + 1}: Current learning rate: {current_lr:.6f}") #current_lr Retrieval: Inside the if (epoch % 100 == 0) block, the current learning rate is obtained using self.optimizer.param_groups[0]['lr']. This is the standard way to access the learning rate of the first (and often only) parameter group in PyTorch optimizers.
369
+
370
+ #self.save_checkpoint(f"model_checkpoint_epoch_{epoch+1}.pth")
371
+ self.save_checkpoint(f"model_checkpoint_epoch_{epoch + 1}.pth", epoch, average_loss) # Pass epoch and average_loss
372
+
373
+ # Early stopping condition
374
+ if average_loss < self.Early_stopping_loss:
375
+ print(f"Early stopping: Average loss {average_loss:.4f} is below the threshold ({self.Early_stopping_loss}).")
376
+ self.save_checkpoint(f"model_checkpoint_early_stop.pth", epoch, average_loss) # Save checkpoint
377
+ break # Exit the training loop
378
+
379
+
380
+ def save_checkpoint(self, path, epoch, average_loss):
381
+ # Get the current script's filename
382
+ script_filename = os.path.basename(__file__) # Get filename from the current script path
383
+
384
+ # Get the current date and time
385
+ current_datetime = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
386
+
387
+ # Construct the new filename
388
+ base_filename, extension = os.path.splitext(path) # Split original filename
389
+ new_filename = f"{base_filename}_{script_filename}_{current_datetime}{extension}"
390
+
391
+ torch.save({
392
+ 'epoch': epoch,
393
+ 'model_state_dict': self.model.state_dict(),
394
+ 'optimizer_state_dict': self.optimizer.state_dict(),
395
+ 'loss': average_loss,
396
+ 'hyperparameters': self.hyperparameters
397
+ }, new_filename)
398
+
399
+
400
+ # --- Main Execution ---
401
+ def main():
402
+ # Determine device (GPU if available, else CPU)
403
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
404
+ print(f"Using device: {device}")
405
+
406
+ print_with_line("# Initialize tokenizer")
407
+ #tokenizer = Tokenizer(Memorized_Speech)
408
+ tokenizer = Tokenizer(Memorized_Speech, hyperparameters["special_tokens"], hyperparameters["vocab_size"]) # The special_tokens list is now defined in the hyperparameters dictionary.
409
+ print(f"Tokenizer Vocabulary Size: {tokenizer.vocab_size}")
410
+
411
+ # Update vocab_size in hyperparameters
412
+ hyperparameters["vocab_size"] = tokenizer.vocab_size
413
+ print(f"The Hyperparamter vocab_size (Vocabulary Size) is set to: {tokenizer.vocab_size}") # Print calculated size
414
+
415
+ print_with_line("# Prepare dataset")
416
+ #dataset = Dataset(Memorized_Speech, tokenizer, hyperparameters["max_sequence_len"])
417
+ dataset = Dataset(Memorized_Speech, tokenizer, min_training_input_seq_len) # Common values of min_training_input_seq_len for smaller models or experiments are 32, 64, 128, or 256.
418
+ train_loader = DataLoader(dataset, batch_size=hyperparameters["batch_size"])
419
+
420
+ print_with_line("# Initialize model")
421
+ print(f"HyperParamters = {hyperparameters}")
422
+ model = ToyGPT2(hyperparameters).to(device)
423
+
424
+ print_with_line("# Initialize trainer")
425
+ trainer = Trainer(model, tokenizer, train_loader, hyperparameters, device)
426
+ # Update vocab_size in hyperparameters
427
+
428
+ print_with_line("# Train the model")
429
+ trainer.train()
430
+
431
+ print("") # space
432
+ print_with_line("# --- Inference Examples ---")
433
+ model.eval()
434
+
435
+ # Example 1: Recite the Gettysburg Address
436
+ print_with_line("# Example 1: Recite the Gettysburg Address")
437
+ start_text = "four score"
438
+ start_tokens = torch.tensor(tokenizer.tokenize(start_text)).unsqueeze(0).to(device)
439
+ print("Prompt:", start_text)
440
+ generated_tokens = model.generate(start_tokens, max_new_tokens=len(dataset.tokens)-len(start_tokens), temperature=1.0) # Generate a completion for the whole dataset
441
+ generated_text = tokenizer.detokenize(generated_tokens.squeeze().tolist())
442
+ print("\nResponse:\n", generated_text)
443
+
444
+ print("") # space
445
+ # Example 2: Free text generation after encountering <FreetheLLM> #### Eventually, modify to request user text inxlusinf only Gettysburg vocabulary]
446
+ print_with_line("# Example 2: Free text generation after encountering <FreetheLLM>")
447
+
448
+ start_text = "we here highly resolve that these dead shall not have died in vain and that this nation under god shall have a new "
449
+ special_token = tokenizer.special_tokens[0] # Get the <FreetheLLM> token
450
+ start_text += special_token # Append the special token directly to the string
451
+ print("Prompt:", start_text)
452
+
453
+ start_tokens = torch.tensor(tokenizer.tokenize(start_text)).unsqueeze(0).to(device) # Tokenize the combined string
454
+
455
+ generated_tokens = model.generate(start_tokens, max_new_tokens=100, temperature=1.0)
456
+ generated_text = tokenizer.detokenize(generated_tokens.squeeze().tolist())
457
+ print("\nFreestyle Generation:\n", generated_text)
458
+
459
+
460
+ if __name__ == "__main__":
461
+ main()