Please modify the code to save a single model instead of saving it every epoch, and also make the code save a json file that contains the configuration of the model as well as another txt file that contains the vocabulary of the model, code: import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
from tqdm import tqdm
import matplotlib.pyplot as plt
import os

# Define parameters and directory paths
SEQUENCE_LENGTH = 40
BATCH_SIZE = 128
VOCAB_SIZE = 192 # Number of ASCII characters considered in the vocabulary
HIDDEN_SIZE = 384
NUM_LAYERS = 2
LEARNING_RATE = 0.001
NUM_EPOCHS = 10
GRAD_CLIP = 5.0 # Gradient clipping to prevent exploding gradients
CHECKPOINT_FOLDER = “checkpoints”
os.makedirs(CHECKPOINT_FOLDER, exist_ok=True)

# Replace ‘your_file.txt’ with the path to your actual text file.
file_path = “C:/Users/Dell-PC/Desktop/The-Physics-Dataset.txt”

# Read the file and filter out blank lines
with open(file_path, “r”, encoding=“utf-8”) as file:
lines = file.readlines()
text = “”.join(line for line in lines if not line.isspace())

# Convert text to ASCII values
ascii_characters = [ord© for c in text]

# Prepare datasets
inputs = []
targets = []
for i in range(0, len(ascii_characters) - SEQUENCE_LENGTH):
input_seq = ascii_characters[i:i+SEQUENCE_LENGTH]
target = ascii_characters[i+1:i+SEQUENCE_LENGTH+1]
inputs.append(torch.tensor(input_seq, dtype=torch.long))
targets.append(torch.tensor(target, dtype=torch.long))

input_tensor = torch.stack(inputs)
target_tensor = torch.stack(targets)

# Load data into DataLoader for batch processing
dataset = TensorDataset(input_tensor, target_tensor)
data_loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)

# Define the sophisticated RNN model architecture
class AdvancedRNN(nn.Module):
def init(self, vocab_size, hidden_size, num_layers):
super(AdvancedRNN, self).init()
self.hidden_size = hidden_size
self.num_layers = num_layers
self.embedding = nn.Embedding(vocab_size, hidden_size)
self.rnn = nn.LSTM(hidden_size, hidden_size, num_layers, batch_first=True)
self.fc = nn.Linear(hidden_size, vocab_size)

def forward(self, x, hidden):
x = self.embedding(x)
out, hidden = self.rnn(x, hidden)
out = self.fc(out.reshape(out.size(0) * out.size(1), out.size(2)))
return out, hidden

def init_hidden(self, batch_size):
weight = next(self.parameters())
hidden = (weight.new_zeros(self.num_layers, batch_size, self.hidden_size),
weight.new_zeros(self.num_layers, batch_size, self.hidden_size))
return hidden

# Initialize model, loss function, and optimizer
model = AdvancedRNN(VOCAB_SIZE, HIDDEN_SIZE, NUM_LAYERS)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)

# Training loop with added production-ready features and tqdm progress
def train_model(model, data_loader, optimizer, loss_fn, epochs, batch_size):
loss_history = [] # Initialize a list to store average epoch losses
for epoch in range(epochs):
model.train()
hidden = model.init_hidden(batch_size)
running_loss = 0.0
# Wrap the data_loader with tqdm for a progress bar
progress_bar = tqdm(data_loader, desc=f"Epoch {epoch + 1}/{epochs}“)

for input_batch, target_batch in progress_bar:
if input_batch.size(0) != batch_size:
hidden = model.init_hidden(input_batch.size(0))

optimizer.zero_grad()
output, hidden = model(input_batch, hidden)
hidden = tuple([h.detach() for h in hidden]) # Detach hidden state
loss = loss_fn(output, target_batch.view(-1))
loss.backward()

# Clip gradients to mitigate exploding gradients
nn.utils.clip_grad_norm_(model.parameters(), GRAD_CLIP)
optimizer.step()

running_loss += loss.item()
progress_bar.set_postfix(loss=loss.item())

# Calculate and store the average epoch loss
epoch_loss = running_loss / len(data_loader) # Change to len(data_loader)
loss_history.append(epoch_loss)
print(f"Epoch {epoch + 1}/{epochs}, Loss: {epoch_loss:.4f}”)

# Save the model checkpoint (with enhanced cross-platform directory usage)
checkpoint_path = os.path.join(CHECKPOINT_FOLDER, f"model_epoch_{epoch + 1}.pth")
torch.save(model.state_dict(), checkpoint_path)

# Plot training loss per epoch outside the training loop
plt.figure() # Create a new figure
plt.plot(range(1, epochs + 1), loss_history, label=“Training Loss”)
plt.xlabel(“Epoch”)
plt.ylabel(“Loss”)
plt.legend()
plt.grid(True)
plt.title(“Training Loss Over Epochs”)
plt.show()

return model # Return the trained model

# Predict the next character
def predict(model, char, hidden=None, temperature=1.0):
# Convert to ASCII and then tensor
input_tensor = torch.tensor([ord(char)], dtype=torch.long).unsqueeze(0)
if hidden is None:
hidden = model.init_hidden(1)
output, hidden = model(input_tensor, hidden)
output_dist = output.view(-1).div(temperature).exp()
top_i = torch.multinomial(output_dist, 1)[0]
predicted_char = chr(top_i)
return predicted_char, hidden

# Generate text
def generate_text(model, start_char=“P”, length=200):
model.eval()
hidden = model.init_hidden(1)
input_char = start_char
for i in range(length):
input_char, hidden = predict(model, input_char, hidden)
print(input_char, end=“”)

if name == “main”:
# Train the model
train_model(model, data_loader, optimizer, loss_fn, NUM_EPOCHS, BATCH_SIZE)

# Generate text from the trained model
generate_text(model, start_char=“P”, length=200)