modelmakerlibrary 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
src/model.py ADDED
@@ -0,0 +1,343 @@
1
+ from .packages import torch, nn, json, pathlib, sft, plt, csv
2
+ from .NeuralNetwork_create import TextGenerationModel, init_nn, init_text_generator
3
+ from .dataset_function import load_dataset
4
+ from accelerator import init_accelerator
5
+
6
+
7
+ # Display the model specifications like parameters, batch size, learning rate, optimizer, loss function, and number of epochs.
8
+ # Model params should be calculated based on the input size, output size, and hidden layers.
9
+ def model_specs(parameters: dict, batch_size: int, lr_rate: float, optimizer: str, loss_function: str, epochs: int):
10
+ print("Model Specifications:")
11
+ input_size = parameters.get("input_size", 0)
12
+ output_size = parameters.get("output_size", 0)
13
+ hidden_layers = parameters.get("hidden_layers", 0)
14
+
15
+ if isinstance(hidden_layers, (list, tuple)):
16
+ hidden_sizes = list(hidden_layers)
17
+ elif isinstance(hidden_layers, int) and hidden_layers > 0:
18
+ hidden_sizes = [hidden_layers]
19
+ else:
20
+ hidden_sizes = []
21
+
22
+ layer_sizes = [input_size, *hidden_sizes, output_size]
23
+ parameter_count = sum(
24
+ (layer_sizes[index] + 1) * layer_sizes[index + 1]
25
+ for index in range(len(layer_sizes) - 1)
26
+ if isinstance(layer_sizes[index], int)
27
+ and isinstance(layer_sizes[index + 1], int)
28
+ and layer_sizes[index] >= 0
29
+ and layer_sizes[index + 1] >= 0
30
+ )
31
+
32
+ print(f"Input size: {input_size}")
33
+ print(f"Hidden layers: {hidden_layers}")
34
+ print(f"Output size: {output_size}")
35
+ print(f"Trainable parameters: {parameter_count}")
36
+ print(f"Batch size: {batch_size}")
37
+ print(f"Learning rate: {lr_rate}")
38
+ print(f"Optimizer: {optimizer}")
39
+ print(f"Loss function: {loss_function}")
40
+ print(f"Epochs: {epochs}")
41
+
42
+
43
+ # Save model in .pt ot .safetensors or .pkl format. The function takes the model, path, and format as input and saves the model accordingly.
44
+ def model_save(model: nn.Module, path: str, format: str):
45
+ if format == "pt":
46
+ torch.save(model.state_dict(), path)
47
+ print(f"Model saved in .pt format at {path}")
48
+ elif format == "safetensors":
49
+ sft.torch.save_file(model.state_dict(), path)
50
+ print(f"Model saved in .safetensors format at {path}")
51
+ elif format == "pkl":
52
+ with open(path, 'wb') as f:
53
+ torch.save(model, f)
54
+ print(f"Model saved in .pkl format at {path}")
55
+ else:
56
+ raise ValueError("Unsupported format. Please choose from 'pt', 'safetensors', or 'pkl'.")
57
+
58
+
59
+
60
+ # To quantize already trained models
61
+ def model_quantization(model: nn.Module, model_file: str, out_path: str, quant_dtype: int):
62
+ if model is None:
63
+ raise ValueError("A model instance is required for quantization.")
64
+
65
+ if not quant_dtype:
66
+ raise ValueError("quant_dtype cannot be empty. Use values like 'int8', 'fp16', or 'bf16'.")
67
+
68
+ if model_file:
69
+ checkpoint = torch.load(model_file, map_location="cpu")
70
+
71
+ if isinstance(checkpoint, dict):
72
+ if "state_dict" in checkpoint:
73
+ state_dict = checkpoint["state_dict"]
74
+ elif all(isinstance(key, str) for key in checkpoint.keys()):
75
+ state_dict = checkpoint
76
+ else:
77
+ raise ValueError("Unsupported checkpoint format. Expected a state_dict or a dict containing 'state_dict'.")
78
+
79
+ model.load_state_dict(state_dict)
80
+ else:
81
+ raise TypeError("The model checkpoint must be a dictionary or a state dict.")
82
+
83
+ dtype_name = str(quant_dtype).strip().lower()
84
+
85
+ if dtype_name in ("", "int8", "qint8", "8"):
86
+ target_dtype = torch.qint8
87
+ elif dtype_name in ("float16", "fp16", "16"):
88
+ target_dtype = torch.float16
89
+ elif dtype_name in ("bfloat16", "bf16", "32"):
90
+ target_dtype = torch.bfloat16
91
+ elif isinstance(quant_dtype, int):
92
+ if quant_dtype == 8:
93
+ target_dtype = torch.qint8
94
+ elif quant_dtype == 16:
95
+ target_dtype = torch.float16
96
+ elif quant_dtype == 32:
97
+ target_dtype = torch.bfloat16
98
+ else:
99
+ raise ValueError("Unsupported quant_dtype. Use 'int8', 'fp16', or 'bf16'.")
100
+ else:
101
+ raise ValueError("Unsupported quant_dtype. Use 'int8', 'fp16', or 'bf16'.")
102
+
103
+ model.eval()
104
+
105
+ if target_dtype == torch.qint8:
106
+ quantized_model = torch.quantization.quantize_dynamic(
107
+ model,
108
+ {nn.Linear},
109
+ dtype=torch.qint8,
110
+ )
111
+ torch.save(quantized_model.state_dict(), out_path)
112
+ print(f"Model quantized to int8 and saved at {out_path}")
113
+ return quantized_model
114
+
115
+ model = model.to(dtype=target_dtype)
116
+ torch.save(model.state_dict(), out_path)
117
+ print(f"Model converted to {dtype_name.upper()} and saved at {out_path}")
118
+ return model
119
+
120
+
121
+
122
+ def model_inference(model_type: str, path: str, test_data_path: str):
123
+ """Evaluate a saved model on labeled test data and display its accuracy."""
124
+ try:
125
+ checkpoint = torch.load(path, map_location="cpu", weights_only=False)
126
+ except TypeError:
127
+ checkpoint = torch.load(path, map_location="cpu")
128
+
129
+ if isinstance(checkpoint, nn.Module):
130
+ model = checkpoint
131
+ else:
132
+ model = checkpoint.get("model") if isinstance(checkpoint, dict) else None
133
+ if not isinstance(model, nn.Module):
134
+ model = init_nn(model_type)
135
+ state_dict = checkpoint.get("state_dict", checkpoint) if isinstance(checkpoint, dict) else checkpoint
136
+ model.load_state_dict(state_dict)
137
+
138
+ suffix = pathlib.Path(test_data_path).suffix.lower()
139
+ if suffix in (".pt", ".pth"):
140
+ try:
141
+ test_data = torch.load(test_data_path, map_location="cpu", weights_only=False)
142
+ except TypeError:
143
+ test_data = torch.load(test_data_path, map_location="cpu")
144
+ elif suffix == ".json":
145
+ with open(test_data_path, "r", encoding="utf-8") as file:
146
+ test_data = json.load(file)
147
+ elif suffix == ".csv":
148
+
149
+ with open(test_data_path, "r", newline="", encoding="utf-8") as file:
150
+ rows = list(csv.reader(file))
151
+ if rows:
152
+ try:
153
+ [float(value) for value in rows[0]]
154
+ except ValueError:
155
+ rows = rows[1:]
156
+ if not rows or any(len(row) < 2 for row in rows):
157
+ raise ValueError("CSV test data must contain feature columns and a label column.")
158
+ rows = [[float(value) for value in row] for row in rows]
159
+ test_data = ([row[:-1] for row in rows], [row[-1] for row in rows])
160
+ else:
161
+ raise ValueError("Test data must be a .pt, .pth, .json, or .csv file.")
162
+
163
+ if isinstance(test_data, dict):
164
+ features = next((test_data[key] for key in ("features", "inputs", "X", "data") if key in test_data), None)
165
+ labels = next((test_data[key] for key in ("labels", "targets", "y") if key in test_data), None)
166
+ elif isinstance(test_data, (list, tuple)) and len(test_data) == 2:
167
+ features, labels = test_data
168
+ elif isinstance(test_data, list) and test_data and isinstance(test_data[0], (list, tuple)):
169
+ features = [row[:-1] for row in test_data]
170
+ labels = [row[-1] for row in test_data]
171
+ else:
172
+ raise ValueError("Test data must contain both features and labels.")
173
+
174
+ if features is None or labels is None:
175
+ raise ValueError("Test data must include both features and labels.")
176
+ features = torch.as_tensor(features, dtype=torch.float32)
177
+ labels = torch.as_tensor(labels)
178
+ if features.ndim == 1:
179
+ features = features.unsqueeze(0)
180
+
181
+ model.eval()
182
+ with torch.no_grad():
183
+ outputs = model(features)
184
+ if isinstance(outputs, (tuple, list)):
185
+ outputs = outputs[0]
186
+ if outputs.ndim > 1 and outputs.shape[-1] > 1:
187
+ predictions = outputs.argmax(dim=1)
188
+ else:
189
+ predictions = (outputs.reshape(-1) >= 0.5).long()
190
+ accuracy = (predictions.reshape(-1) == labels.reshape(-1).long()).float().mean().item() * 100
191
+
192
+ print(f"Model accuracy: {accuracy:.2f}%")
193
+
194
+ plt.figure()
195
+ plt.bar(["Accuracy"], [accuracy], color="steelblue")
196
+ plt.ylim(0, 100)
197
+ plt.ylabel("Performance (%)")
198
+ plt.title("Model Test Performance")
199
+ plt.text(0, accuracy, f"{accuracy:.2f}%", ha="center", va="bottom")
200
+ plt.tight_layout()
201
+ plt.show()
202
+ return accuracy
203
+
204
+
205
+ def train_text_generator(
206
+ dataset_path: str,
207
+ output_path: str,
208
+ epochs: int = 10,
209
+ batch_size: int = 16,
210
+ steps_per_epoch: int = 10,
211
+ learning_rate: float = 3e-4,
212
+ device: str = "cpu",
213
+ max_context_length: int = 64,
214
+ max_new_tokens: int = 120,
215
+ prompt: str = "ModelMakerLibrary",
216
+ ) -> dict:
217
+ """Train and save a causal byte-level text model."""
218
+ for value, name in (
219
+ (epochs, "Epoch count"),
220
+ (batch_size, "Batch size"),
221
+ (steps_per_epoch, "Steps per epoch"),
222
+ (max_context_length, "Maximum context length"),
223
+ (max_new_tokens, "Maximum new tokens"),
224
+ ):
225
+ if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
226
+ raise ValueError(f"{name} must be a positive integer.")
227
+ if isinstance(learning_rate, bool) or not isinstance(learning_rate, (int, float)) or learning_rate <= 0:
228
+ raise ValueError("Learning rate must be a positive number.")
229
+ if not isinstance(prompt, str) or not prompt:
230
+ raise ValueError("Prompt must be a non-empty string.")
231
+
232
+ dataset = load_dataset(dataset_path, model_type="text")
233
+ corpus = "\n".join(
234
+ text.strip()
235
+ for text, _ in dataset
236
+ if isinstance(text, str) and text.strip()
237
+ )
238
+ token_ids = torch.tensor(list(corpus.encode("utf-8")), dtype=torch.long)
239
+ if token_ids.numel() < 9:
240
+ raise ValueError("The training dataset must contain at least 9 UTF-8 bytes.")
241
+
242
+ context_length = min(max_context_length, max(8, token_ids.numel() // 3))
243
+ if token_ids.numel() <= context_length:
244
+ raise ValueError("The training dataset must be longer than the model context.")
245
+ target_device = init_accelerator(device)
246
+ model = init_text_generator(context_length=context_length).to(target_device)
247
+ parameter_count = sum(parameter.numel() for parameter in model.parameters())
248
+ optimizer = torch.optim.AdamW(model.parameters(), lr=learning_rate)
249
+ losses = []
250
+
251
+ model.train()
252
+ for epoch in range(1, epochs + 1):
253
+ total_loss = 0.0
254
+ for _ in range(steps_per_epoch):
255
+ starts = torch.randint(token_ids.numel() - context_length, (batch_size,))
256
+ offsets = torch.arange(context_length)
257
+ indices = starts.unsqueeze(1) + offsets
258
+ inputs = token_ids[indices].to(target_device)
259
+ targets = token_ids[indices + 1].to(target_device)
260
+
261
+ optimizer.zero_grad(set_to_none=True)
262
+ logits = model(inputs)
263
+ loss = nn.functional.cross_entropy(
264
+ logits.reshape(-1, model.vocab_size),
265
+ targets.reshape(-1),
266
+ )
267
+ loss.backward()
268
+ optimizer.step()
269
+ total_loss += loss.item()
270
+ average_loss = total_loss / steps_per_epoch
271
+ losses.append(average_loss)
272
+ print(f"Epoch {epoch:02d}/{epochs}: loss={average_loss:.4f}")
273
+
274
+ checkpoint_path = pathlib.Path(output_path).expanduser().resolve()
275
+ checkpoint_path.parent.mkdir(parents=True, exist_ok=True)
276
+ torch.save(
277
+ {
278
+ "state_dict": model.state_dict(),
279
+ "context_length": model.context_length,
280
+ "vocab_size": model.vocab_size,
281
+ "model_width": model.model_width,
282
+ "attention_heads": model.attention_heads,
283
+ "transformer_layers": model.transformer_layers,
284
+ "parameter_count": parameter_count,
285
+ "losses": losses,
286
+ },
287
+ checkpoint_path,
288
+ )
289
+ sample = generate_text(model, prompt, max_new_tokens=max_new_tokens)
290
+ return {
291
+ "checkpoint_path": str(checkpoint_path),
292
+ "parameter_count": parameter_count,
293
+ "context_length": context_length,
294
+ "losses": losses,
295
+ "sample": sample,
296
+ }
297
+
298
+
299
+ def load_text_generator(path: str, device: str = "cpu") -> TextGenerationModel:
300
+ """Load a text-generation checkpoint saved by train_text_generator."""
301
+ target_device = init_accelerator(device)
302
+ try:
303
+ checkpoint = torch.load(path, map_location=target_device, weights_only=True)
304
+ except TypeError:
305
+ checkpoint = torch.load(path, map_location=target_device)
306
+ if not isinstance(checkpoint, dict) or "state_dict" not in checkpoint:
307
+ raise ValueError("Unsupported text-generator checkpoint.")
308
+ model = init_text_generator(
309
+ context_length=checkpoint["context_length"],
310
+ vocab_size=checkpoint["vocab_size"],
311
+ model_width=checkpoint["model_width"],
312
+ attention_heads=checkpoint["attention_heads"],
313
+ transformer_layers=checkpoint["transformer_layers"],
314
+ )
315
+ model.load_state_dict(checkpoint["state_dict"])
316
+ model.to(target_device)
317
+ model.eval()
318
+ return model
319
+
320
+
321
+ def generate_text(
322
+ model: TextGenerationModel,
323
+ prompt: str,
324
+ max_new_tokens: int = 120,
325
+ ) -> str:
326
+ """Generate text from a prompt using a library-created language model."""
327
+ if not isinstance(model, TextGenerationModel):
328
+ raise TypeError("Model must be a TextGenerationModel created by ModelMakerLibrary.")
329
+ if not isinstance(prompt, str) or not prompt:
330
+ raise ValueError("Prompt must be a non-empty string.")
331
+ if isinstance(max_new_tokens, bool) or not isinstance(max_new_tokens, int) or max_new_tokens <= 0:
332
+ raise ValueError("Maximum new tokens must be a positive integer.")
333
+
334
+ device = next(model.parameters()).device
335
+ generated = torch.tensor(list(prompt.encode("utf-8")), dtype=torch.long, device=device)
336
+ model.eval()
337
+ with torch.no_grad():
338
+ for _ in range(max_new_tokens):
339
+ context = generated[-model.context_length :].unsqueeze(0)
340
+ logits = model(context)[0, -1]
341
+ next_token = torch.multinomial(torch.softmax(logits, dim=-1), num_samples=1)
342
+ generated = torch.cat((generated, next_token))
343
+ return bytes(generated.tolist()).decode("utf-8", errors="replace")
src/packages.py ADDED
@@ -0,0 +1,17 @@
1
+ import torch
2
+ from torch import optim
3
+ from torch import nn
4
+ from torch.utils.data import DataLoader
5
+ from torchvision import datasets
6
+ from torchvision.transforms import v2
7
+ import json
8
+ import csv
9
+ import tsv
10
+ import pandas as pd
11
+ import transformers
12
+ import os
13
+ import tokenizers
14
+ import pathlib
15
+ from accelerator import *
16
+ import matplotlib.pyplot as plt
17
+ import safetensors.torch as sft
@@ -0,0 +1,109 @@
1
+ from src.packages import os, transformers, torch
2
+ from src.dataset_function import init_dataset
3
+
4
+ def init_tokenizer(tokenizer_name, tokenizer_path=None, token=False):
5
+ """Load a tokenizer, ignoring saved Hugging Face credentials unless provided."""
6
+ if tokenizer_name is None:
7
+ raise ValueError("Tokenizer name must not be None.")
8
+ if tokenizer_name == "bert":
9
+ if tokenizer_path is not None:
10
+ tokenizer = transformers.AutoTokenizer.from_pretrained(tokenizer_path, token=token)
11
+ else:
12
+ tokenizer = transformers.AutoTokenizer.from_pretrained('bert-base-uncased', token=token)
13
+ elif tokenizer_name == "gpt2":
14
+ if tokenizer_path is not None:
15
+ tokenizer = transformers.AutoTokenizer.from_pretrained(tokenizer_path, token=token)
16
+ else:
17
+ tokenizer = transformers.AutoTokenizer.from_pretrained('gpt2', token=token)
18
+ elif tokenizer_name == "roberta":
19
+ if tokenizer_path is not None:
20
+ tokenizer = transformers.AutoTokenizer.from_pretrained(tokenizer_path, token=token)
21
+ else:
22
+ tokenizer = transformers.AutoTokenizer.from_pretrained('roberta-base', token=token)
23
+ elif tokenizer_name == "custom":
24
+ if tokenizer_path is None:
25
+ raise ValueError("Tokenizer path must be provided for custom tokenizer.")
26
+ tokenizer = transformers.AutoTokenizer.from_pretrained(tokenizer_path, token=token)
27
+ else:
28
+ raise ValueError("Unsupported tokenizer name. Supported names are: 'bert', 'gpt2', 'roberta', 'custom'.")
29
+ return tokenizer
30
+
31
+
32
+ def tokenize_dataset(tokenizer, dataset_path, output_path, max_length=512):
33
+ if tokenizer is None:
34
+ raise ValueError("Tokenizer must not be None.")
35
+ if dataset_path is None or output_path is None:
36
+ raise ValueError("Dataset path and output path must not be None.")
37
+ if isinstance(max_length, bool) or not isinstance(max_length, int) or max_length <= 0:
38
+ raise ValueError("Maximum length must be a positive integer.")
39
+ if not os.path.exists(dataset_path):
40
+ raise ValueError("Dataset path does not exist.")
41
+
42
+ output_path = os.fspath(output_path)
43
+ absolute_output_path = os.path.abspath(output_path)
44
+ os.makedirs(os.path.dirname(absolute_output_path), exist_ok=True)
45
+ torch.save([], output_path)
46
+ print(f"Created token output file at '{absolute_output_path}'.")
47
+
48
+ _, dataset_path = init_dataset("text", os.fspath(dataset_path))
49
+ text_files = []
50
+ found_files = []
51
+ supported_extensions = {".txt", ".json", ".csv", ".tsv"}
52
+ if os.path.isfile(dataset_path):
53
+ found_files.append(dataset_path)
54
+ if os.path.splitext(dataset_path)[1].lower() in supported_extensions:
55
+ text_files.append(dataset_path)
56
+ else:
57
+ for root, _, files in os.walk(dataset_path):
58
+ for filename in files:
59
+ file_path = os.path.join(root, filename)
60
+ found_files.append(file_path)
61
+ if os.path.splitext(filename)[1].lower() in supported_extensions:
62
+ text_files.append(file_path)
63
+ if not text_files:
64
+ details = "; ".join(
65
+ f"{os.path.relpath(path, dataset_path) if os.path.isdir(dataset_path) else os.path.basename(path)} "
66
+ f"({os.path.getsize(path)} bytes)"
67
+ for path in found_files[:5]
68
+ )
69
+ if len(found_files) > 5:
70
+ details += f"; and {len(found_files) - 5} more file(s)"
71
+ found_message = f" Files found: {details}." if details else " The directory contains no files."
72
+ raise ValueError(
73
+ f"Cannot tokenize dataset at '{os.path.abspath(dataset_path)}': no supported text data "
74
+ "was found. Supported inputs are UTF-8 .txt, JSON, CSV, and TSV files."
75
+ f"{found_message} Check that the input path points to the source text dataset or a "
76
+ "text dataset exported with dataset_convert."
77
+ )
78
+ tokenized_data = []
79
+
80
+ for text_file in sorted(text_files):
81
+ extension = os.path.splitext(text_file)[1].lower()
82
+ with open(text_file, encoding="utf-8", newline="") as file:
83
+ if extension == ".txt":
84
+ texts = [file.read()]
85
+ elif extension == ".json":
86
+ records = json.load(file)
87
+ if isinstance(records, dict):
88
+ records = records.get("data", [records])
89
+ if not isinstance(records, list):
90
+ raise ValueError(f"Expected a JSON object or list of records in '{text_file}'.")
91
+ texts = [
92
+ record["text"] for record in records
93
+ if isinstance(record, dict) and isinstance(record.get("text"), str)
94
+ ]
95
+ else:
96
+ reader = csv.DictReader(file, delimiter="\t" if extension == ".tsv" else ",")
97
+ if not reader.fieldnames or "text" not in reader.fieldnames:
98
+ raise ValueError(f"Expected a 'text' column in '{text_file}'.")
99
+ texts = [row["text"] for row in reader if row.get("text")]
100
+ if not texts:
101
+ raise ValueError(f"No text records found in '{text_file}'.")
102
+ for text in texts:
103
+ tokenized_batch = tokenizer([text], padding=True, truncation=True, max_length=max_length, return_tensors='pt')
104
+ tokenized_data.append(tokenized_batch)
105
+
106
+ # Save the tokenized data to the output path
107
+ torch.save(tokenized_data, output_path)
108
+ print(f"Tokenized {len(tokenized_data)} text record(s) and saved them to '{absolute_output_path}'.")
109
+ return output_path
src/train.py ADDED
@@ -0,0 +1,36 @@
1
+ """Example training run using the ModelMakerLibrary public API."""
2
+
3
+ from pathlib import Path
4
+
5
+ import modelMakerLibrary as mml
6
+
7
+
8
+ def main():
9
+ project_dir = Path(__file__).resolve().parents[1]
10
+ result = mml.train_text_generator(
11
+ dataset_path=str(project_dir / "data" / "train"),
12
+ output_path=str(project_dir / "data" / "text_generator.pt"),
13
+ epochs=10,
14
+ batch_size=16,
15
+ steps_per_epoch=10,
16
+ learning_rate=3e-4,
17
+ device="cpu",
18
+ max_context_length=64,
19
+ max_new_tokens=120,
20
+ )
21
+ parameter_count = result["parameter_count"]
22
+ print(
23
+ f"Trainable parameters: {parameter_count:,} "
24
+ f"({parameter_count / 1_000_000:.3f}M)"
25
+ )
26
+ if not 90_000 <= parameter_count <= 110_000:
27
+ raise RuntimeError(
28
+ f"Expected about 0.1M parameters, got {parameter_count:,}."
29
+ )
30
+ print(f"Saved checkpoint: {result['checkpoint_path']}")
31
+ print("Sample generation:")
32
+ print(result["sample"].encode("ascii", errors="backslashreplace").decode("ascii"))
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()