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.
- accelerator/__init__.py +28 -0
- accelerator/gpu.py +14 -0
- accelerator/npu.py +30 -0
- accelerator/tpu.py +24 -0
- modelMakerLibrary.py +68 -0
- modelmakerlibrary-0.1.0.dist-info/METADATA +86 -0
- modelmakerlibrary-0.1.0.dist-info/RECORD +17 -0
- modelmakerlibrary-0.1.0.dist-info/WHEEL +5 -0
- modelmakerlibrary-0.1.0.dist-info/licenses/LICENSE +202 -0
- modelmakerlibrary-0.1.0.dist-info/top_level.txt +3 -0
- src/NeuralNetwork_create.py +193 -0
- src/__init__.py +0 -0
- src/dataset_function.py +614 -0
- src/model.py +343 -0
- src/packages.py +17 -0
- src/tokenizers_function.py +109 -0
- src/train.py +36 -0
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()
|