PyIntell 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.
- pyintell/__init__.py +31 -0
- pyintell/attention.py +126 -0
- pyintell/autograd.py +50 -0
- pyintell/builder.py +211 -0
- pyintell/embeddings.py +33 -0
- pyintell/finetuning.py +29 -0
- pyintell/focus.py +81 -0
- pyintell/generation.py +73 -0
- pyintell/layers.py +75 -0
- pyintell/loss.py +39 -0
- pyintell/model.py +224 -0
- pyintell/optim.py +94 -0
- pyintell/quantization.py +25 -0
- pyintell/scheduling.py +16 -0
- pyintell/serialization.py +313 -0
- pyintell/system.py +122 -0
- pyintell/tokenization.py +147 -0
- pyintell/training.py +15 -0
- pyintell/transformer.py +90 -0
- pyintell/utilities.py +444 -0
- pyintell-0.1.0.dist-info/METADATA +226 -0
- pyintell-0.1.0.dist-info/RECORD +25 -0
- pyintell-0.1.0.dist-info/WHEEL +5 -0
- pyintell-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyintell-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""Model serialization and named-model management."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import pickle
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_REGISTRY_PATH = Path(os.path.expanduser("~/.pyintell/models.json"))
|
|
9
|
+
_CURRENT_MODEL = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def set_current_model(model):
|
|
13
|
+
"""Set the model used by save_model when no explicit model is supplied."""
|
|
14
|
+
global _CURRENT_MODEL
|
|
15
|
+
_CURRENT_MODEL = model
|
|
16
|
+
return model
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def state_dict(model):
|
|
20
|
+
return {
|
|
21
|
+
name: value.copy()
|
|
22
|
+
for name, value in vars(model).items()
|
|
23
|
+
if hasattr(value, "shape") and hasattr(value, "copy")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_state_dict(model, state):
|
|
28
|
+
for name, value in state.items():
|
|
29
|
+
setattr(model, name, value)
|
|
30
|
+
return model
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def save_weights(model, path):
|
|
34
|
+
import numpy as np
|
|
35
|
+
np.savez(path, **state_dict(model))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def load_weights(model, path):
|
|
39
|
+
import numpy as np
|
|
40
|
+
with np.load(path) as data:
|
|
41
|
+
return load_state_dict(model, {key: data[key] for key in data.files})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read_registry():
|
|
45
|
+
try:
|
|
46
|
+
with _REGISTRY_PATH.open("r", encoding="utf-8") as file:
|
|
47
|
+
data = json.load(file)
|
|
48
|
+
return data if isinstance(data, dict) else {}
|
|
49
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
50
|
+
return {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _write_registry(registry):
|
|
54
|
+
_REGISTRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
temporary = _REGISTRY_PATH.with_suffix(".tmp")
|
|
56
|
+
with temporary.open("w", encoding="utf-8") as file:
|
|
57
|
+
json.dump(registry, file, ensure_ascii=False, indent=2)
|
|
58
|
+
os.replace(temporary, _REGISTRY_PATH)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _validate_model_name(model_name):
|
|
62
|
+
if not isinstance(model_name, str) or not model_name.strip():
|
|
63
|
+
raise TypeError("model_name must be a non-empty string")
|
|
64
|
+
name = model_name.strip()
|
|
65
|
+
if name in {".", ".."} or any(char in name for char in "\\/"):
|
|
66
|
+
raise ValueError("model_name must not contain path separators")
|
|
67
|
+
return name
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _model_file(directory, name):
|
|
71
|
+
"""Return the canonical saved-model path inside a directory."""
|
|
72
|
+
directory = Path(directory).expanduser()
|
|
73
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
if not directory.is_dir():
|
|
75
|
+
raise NotADirectoryError(f"path must be a directory: {directory}")
|
|
76
|
+
return directory / f"{name}.pyintell"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def save_model(model_name, path=None, model=None):
|
|
80
|
+
"""Save a model under a unique name.
|
|
81
|
+
|
|
82
|
+
``path`` is the directory where ``<model_name>.pyintell`` is created.
|
|
83
|
+
If ``model`` is omitted, the most recently built/loaded model is used.
|
|
84
|
+
"""
|
|
85
|
+
global _CURRENT_MODEL
|
|
86
|
+
name = _validate_model_name(model_name)
|
|
87
|
+
if model is None:
|
|
88
|
+
model = _CURRENT_MODEL
|
|
89
|
+
if model is None:
|
|
90
|
+
raise RuntimeError("no active model; build or load a model first")
|
|
91
|
+
if path is None:
|
|
92
|
+
raise TypeError("path is required")
|
|
93
|
+
|
|
94
|
+
registry = _read_registry()
|
|
95
|
+
if name in registry:
|
|
96
|
+
raise FileExistsError(f"model name '{name}' already exists")
|
|
97
|
+
|
|
98
|
+
model_path = _model_file(path, name)
|
|
99
|
+
if model_path.exists():
|
|
100
|
+
raise FileExistsError(f"model file already exists: {model_path}")
|
|
101
|
+
|
|
102
|
+
# Store the name on the model so the active-model state is unambiguous.
|
|
103
|
+
model.model_name = name
|
|
104
|
+
|
|
105
|
+
temporary = model_path.with_suffix(model_path.suffix + ".tmp")
|
|
106
|
+
try:
|
|
107
|
+
with temporary.open("wb") as file:
|
|
108
|
+
pickle.dump(model, file, protocol=pickle.HIGHEST_PROTOCOL)
|
|
109
|
+
os.replace(temporary, model_path)
|
|
110
|
+
|
|
111
|
+
registry[name] = {"path": str(model_path.resolve())}
|
|
112
|
+
try:
|
|
113
|
+
_write_registry(registry)
|
|
114
|
+
except Exception:
|
|
115
|
+
try:
|
|
116
|
+
model_path.unlink()
|
|
117
|
+
except OSError:
|
|
118
|
+
pass
|
|
119
|
+
raise
|
|
120
|
+
finally:
|
|
121
|
+
try:
|
|
122
|
+
temporary.unlink()
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
_CURRENT_MODEL = model
|
|
127
|
+
return str(model_path)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def load_model(model_name):
|
|
131
|
+
"""Load a named model and make it the active model."""
|
|
132
|
+
global _CURRENT_MODEL
|
|
133
|
+
name = _validate_model_name(model_name)
|
|
134
|
+
registry = _read_registry()
|
|
135
|
+
if name not in registry:
|
|
136
|
+
raise FileNotFoundError(f"model '{name}' was not found")
|
|
137
|
+
|
|
138
|
+
model_path = Path(registry[name].get("path", "")).expanduser()
|
|
139
|
+
if not model_path.is_file():
|
|
140
|
+
raise FileNotFoundError(f"model file for '{name}' no longer exists: {model_path}")
|
|
141
|
+
|
|
142
|
+
with model_path.open("rb") as file:
|
|
143
|
+
model = pickle.load(file)
|
|
144
|
+
model.model_name = name
|
|
145
|
+
_CURRENT_MODEL = model
|
|
146
|
+
return model
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_model(model_name):
|
|
150
|
+
"""Load a named model without changing the active model."""
|
|
151
|
+
name = _validate_model_name(model_name)
|
|
152
|
+
registry = _read_registry()
|
|
153
|
+
if name not in registry:
|
|
154
|
+
raise FileNotFoundError(f"model '{name}' was not found")
|
|
155
|
+
model_path = Path(registry[name].get("path", "")).expanduser()
|
|
156
|
+
if not model_path.is_file():
|
|
157
|
+
raise FileNotFoundError(f"model file for '{name}' no longer exists: {model_path}")
|
|
158
|
+
with model_path.open("rb") as file:
|
|
159
|
+
model = pickle.load(file)
|
|
160
|
+
model.model_name = name
|
|
161
|
+
return model
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def delete_model(model_name):
|
|
165
|
+
"""Delete a named model from disk and remove it from the registry."""
|
|
166
|
+
global _CURRENT_MODEL
|
|
167
|
+
name = _validate_model_name(model_name)
|
|
168
|
+
registry = _read_registry()
|
|
169
|
+
if name not in registry:
|
|
170
|
+
raise FileNotFoundError(f"model '{name}' was not found")
|
|
171
|
+
|
|
172
|
+
model_path = Path(registry[name].get("path", "")).expanduser()
|
|
173
|
+
if not model_path.is_file():
|
|
174
|
+
del registry[name]
|
|
175
|
+
_write_registry(registry)
|
|
176
|
+
raise FileNotFoundError(f"model file for '{name}' no longer exists: {model_path}")
|
|
177
|
+
|
|
178
|
+
model_path.unlink()
|
|
179
|
+
del registry[name]
|
|
180
|
+
_write_registry(registry)
|
|
181
|
+
|
|
182
|
+
if _CURRENT_MODEL is not None and getattr(_CURRENT_MODEL, "model_name", None) == name:
|
|
183
|
+
_CURRENT_MODEL = None
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def edit_model(model_name, **changes):
|
|
188
|
+
"""Edit supported metadata of a saved model."""
|
|
189
|
+
global _CURRENT_MODEL
|
|
190
|
+
name = _validate_model_name(model_name)
|
|
191
|
+
registry = _read_registry()
|
|
192
|
+
if name not in registry:
|
|
193
|
+
raise FileNotFoundError(f"model '{name}' was not found")
|
|
194
|
+
if not changes:
|
|
195
|
+
raise ValueError("at least one model field must be provided")
|
|
196
|
+
|
|
197
|
+
model_path = Path(registry[name].get("path", "")).expanduser()
|
|
198
|
+
if not model_path.is_file():
|
|
199
|
+
raise FileNotFoundError(f"model file for '{name}' no longer exists: {model_path}")
|
|
200
|
+
|
|
201
|
+
with model_path.open("rb") as file:
|
|
202
|
+
model = pickle.load(file)
|
|
203
|
+
|
|
204
|
+
allowed = {"focus", "parameters", "settings", "model_name"}
|
|
205
|
+
unknown = set(changes) - allowed
|
|
206
|
+
if unknown:
|
|
207
|
+
raise TypeError(f"unsupported model field(s): {', '.join(sorted(unknown))}")
|
|
208
|
+
|
|
209
|
+
if "focus" in changes:
|
|
210
|
+
focus = changes["focus"]
|
|
211
|
+
if isinstance(focus, str):
|
|
212
|
+
model.focus = (focus,)
|
|
213
|
+
elif isinstance(focus, (list, tuple, set)) and focus:
|
|
214
|
+
model.focus = tuple(focus)
|
|
215
|
+
else:
|
|
216
|
+
raise TypeError("focus must be a non-empty string, list, tuple, or set")
|
|
217
|
+
|
|
218
|
+
if "parameters" in changes:
|
|
219
|
+
parameters = changes["parameters"]
|
|
220
|
+
if isinstance(parameters, bool) or not isinstance(parameters, (int, float)):
|
|
221
|
+
raise TypeError("parameters must be a number")
|
|
222
|
+
if parameters < 0:
|
|
223
|
+
raise ValueError("parameters must not be negative")
|
|
224
|
+
model.parameters = int(parameters)
|
|
225
|
+
|
|
226
|
+
if "settings" in changes:
|
|
227
|
+
settings = changes["settings"]
|
|
228
|
+
if not isinstance(settings, dict):
|
|
229
|
+
raise TypeError("settings must be a dictionary")
|
|
230
|
+
protected = {"layers", "heads", "embedding_size", "hidden_size", "context_length"}
|
|
231
|
+
changed_architecture = protected.intersection(settings)
|
|
232
|
+
if changed_architecture:
|
|
233
|
+
raise ValueError("cannot edit architecture settings in place; rebuild the model instead")
|
|
234
|
+
model.settings.update(settings)
|
|
235
|
+
|
|
236
|
+
new_name = name
|
|
237
|
+
if "model_name" in changes:
|
|
238
|
+
new_name = _validate_model_name(changes["model_name"])
|
|
239
|
+
if new_name != name and new_name in registry:
|
|
240
|
+
raise FileExistsError(f"model name '{new_name}' already exists")
|
|
241
|
+
new_path = model_path.with_name(f"{new_name}.pymodel")
|
|
242
|
+
if new_path != model_path and new_path.exists():
|
|
243
|
+
raise FileExistsError(f"model file already exists: {new_path}")
|
|
244
|
+
else:
|
|
245
|
+
new_path = model_path
|
|
246
|
+
|
|
247
|
+
model.model_name = new_name
|
|
248
|
+
temporary = new_path.with_suffix(new_path.suffix + ".tmp")
|
|
249
|
+
with temporary.open("wb") as file:
|
|
250
|
+
pickle.dump(model, file, protocol=pickle.HIGHEST_PROTOCOL)
|
|
251
|
+
os.replace(temporary, new_path)
|
|
252
|
+
|
|
253
|
+
if new_name != name:
|
|
254
|
+
del registry[name]
|
|
255
|
+
registry[new_name] = {"path": str(new_path.resolve())}
|
|
256
|
+
if model_path != new_path and model_path.exists():
|
|
257
|
+
model_path.unlink()
|
|
258
|
+
else:
|
|
259
|
+
registry[name] = {"path": str(new_path.resolve())}
|
|
260
|
+
|
|
261
|
+
_write_registry(registry)
|
|
262
|
+
_CURRENT_MODEL = model
|
|
263
|
+
return model
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def save(model, path):
|
|
267
|
+
"""Backward-compatible anonymous model save."""
|
|
268
|
+
with open(path, "wb") as file:
|
|
269
|
+
pickle.dump(model, file, protocol=pickle.HIGHEST_PROTOCOL)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def load(path):
|
|
273
|
+
"""Backward-compatible anonymous model load."""
|
|
274
|
+
with open(path, "rb") as file:
|
|
275
|
+
return pickle.load(file)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def checkpoint(model, path):
|
|
279
|
+
return save(model, path)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def save_checkpoint(model, path):
|
|
283
|
+
return save(model, path)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def load_checkpoint(path):
|
|
287
|
+
return load(path)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def save_vocab(vocabulary, path):
|
|
291
|
+
with open(path, "w", encoding="utf-8") as file:
|
|
292
|
+
json.dump(vocabulary, file, ensure_ascii=False, indent=2)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def load_vocab(path):
|
|
296
|
+
with open(path, "r", encoding="utf-8") as file:
|
|
297
|
+
return json.load(file)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def serialize(value):
|
|
301
|
+
return pickle.dumps(value)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def deserialize(value):
|
|
305
|
+
return pickle.loads(value)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def export_model(model, path):
|
|
309
|
+
return save(model, path)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def import_model(path):
|
|
313
|
+
return load(path)
|
pyintell/system.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""System resource inspection used by model builders."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import psutil
|
|
9
|
+
except ImportError:
|
|
10
|
+
psutil = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def ram():
|
|
14
|
+
"""Return total, available, and used system RAM in bytes."""
|
|
15
|
+
if psutil is not None:
|
|
16
|
+
try:
|
|
17
|
+
info = psutil.virtual_memory()
|
|
18
|
+
return {"total": info.total, "available": info.available, "used": info.used}
|
|
19
|
+
except Exception:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
# Android/Pydroid may not expose virtual memory through psutil.
|
|
23
|
+
try:
|
|
24
|
+
values = {}
|
|
25
|
+
with open("/proc/meminfo", "r", encoding="utf-8") as file:
|
|
26
|
+
for line in file:
|
|
27
|
+
key, value = line.split(":", 1)
|
|
28
|
+
parts = value.strip().split()
|
|
29
|
+
if parts:
|
|
30
|
+
values[key] = int(parts[0]) * 1024
|
|
31
|
+
|
|
32
|
+
total = values.get("MemTotal")
|
|
33
|
+
available = values.get("MemAvailable")
|
|
34
|
+
if available is None:
|
|
35
|
+
available = values.get("MemFree", 0) + values.get("Buffers", 0) + values.get("Cached", 0)
|
|
36
|
+
used = total - available if total is not None and available is not None else None
|
|
37
|
+
return {"total": total, "available": available, "used": used}
|
|
38
|
+
except (OSError, ValueError, IndexError):
|
|
39
|
+
return {"total": None, "available": None, "used": None}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cpu_info():
|
|
43
|
+
return {"name": platform.processor(), "count": os.cpu_count(), "architecture": platform.machine()}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def storage_info(path="."):
|
|
47
|
+
usage = shutil.disk_usage(path)
|
|
48
|
+
return {"total": usage.total, "free": usage.free, "used": usage.used}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def memory_info():
|
|
52
|
+
process = None
|
|
53
|
+
if psutil is not None:
|
|
54
|
+
try:
|
|
55
|
+
process = psutil.Process(os.getpid()).memory_info().rss
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
return {"ram": ram(), "process": process}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def device_info():
|
|
62
|
+
return {"cpu": cpu_info(), "gpu": gpu_info()}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def device_count():
|
|
66
|
+
return os.cpu_count() or 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def gpu_info():
|
|
70
|
+
try:
|
|
71
|
+
import torch
|
|
72
|
+
if torch.cuda.is_available():
|
|
73
|
+
return {"available": True, "count": torch.cuda.device_count(), "name": torch.cuda.get_device_name(0)}
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
return {"available": False, "count": 0, "name": None}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def is_gpu_available():
|
|
80
|
+
return bool(gpu_info()["available"])
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def cuda():
|
|
84
|
+
return is_gpu_available()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cpu():
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def clear_cache():
|
|
92
|
+
try:
|
|
93
|
+
import torch
|
|
94
|
+
if torch.cuda.is_available():
|
|
95
|
+
torch.cuda.empty_cache()
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def free_memory():
|
|
101
|
+
return ram().get("available")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def gpu_memory():
|
|
105
|
+
return gpu_info()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cpu_memory():
|
|
109
|
+
return ram()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def system_info():
|
|
113
|
+
return {
|
|
114
|
+
"platform": platform.platform(),
|
|
115
|
+
"system": platform.system(),
|
|
116
|
+
"machine": platform.machine(),
|
|
117
|
+
"processor": platform.processor(),
|
|
118
|
+
"cpu_count": os.cpu_count(),
|
|
119
|
+
"ram": ram(),
|
|
120
|
+
"storage": storage_info(),
|
|
121
|
+
"gpu": gpu_info(),
|
|
122
|
+
}
|
pyintell/tokenization.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Basic vocabulary and tokenization utilities."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def tokenizer(text):
|
|
5
|
+
if not isinstance(text, str):
|
|
6
|
+
raise TypeError("text must be a string")
|
|
7
|
+
return text.split()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def tokenize(text):
|
|
11
|
+
return tokenizer(text)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def detokenize(tokens):
|
|
15
|
+
return " ".join(tokens)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def vocab(tokens):
|
|
19
|
+
if isinstance(tokens, str):
|
|
20
|
+
tokens = tokenizer(tokens)
|
|
21
|
+
if tokens and isinstance(tokens[0], (list, tuple)):
|
|
22
|
+
tokens = [t for seq in tokens for t in seq]
|
|
23
|
+
result = {}
|
|
24
|
+
for token in tokens:
|
|
25
|
+
if token not in result:
|
|
26
|
+
result[token] = len(result)
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_vocab(tokens):
|
|
31
|
+
return vocab(tokens)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def update_vocab(vocabulary, tokens):
|
|
35
|
+
result = dict(vocabulary)
|
|
36
|
+
new = vocab(tokens)
|
|
37
|
+
for token in new:
|
|
38
|
+
if token not in result:
|
|
39
|
+
result[token] = len(result)
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def merge_vocab(*vocabularies):
|
|
44
|
+
result = {}
|
|
45
|
+
for vocabulary in vocabularies:
|
|
46
|
+
for token in vocabulary:
|
|
47
|
+
if token not in result:
|
|
48
|
+
result[token] = len(result)
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def reverse_vocab(vocabulary):
|
|
53
|
+
return {index: token for token, index in vocabulary.items()}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def vocab_size(vocabulary):
|
|
57
|
+
return len(vocabulary)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def token_id(token, vocabulary, default=None):
|
|
61
|
+
return vocabulary.get(token, default)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def id_token(index, reverse):
|
|
65
|
+
return reverse[index]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def token_exists(token, vocabulary):
|
|
69
|
+
return token in vocabulary
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def add_token(vocabulary, token):
|
|
73
|
+
result = dict(vocabulary)
|
|
74
|
+
if token not in result:
|
|
75
|
+
result[token] = len(result)
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def remove_token(vocabulary, token):
|
|
80
|
+
result = dict(vocabulary)
|
|
81
|
+
result.pop(token, None)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def special_tokens(pad="<PAD>", unk="<UNK>", bos="<BOS>", eos="<EOS>"):
|
|
86
|
+
return {"pad": pad, "unk": unk, "bos": bos, "eos": eos}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def add_special_token(vocabulary, token):
|
|
90
|
+
return add_token(vocabulary, token)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def encode(text, vocabulary, unknown_token=None):
|
|
94
|
+
tokens = tokenizer(text)
|
|
95
|
+
unknown_id = vocabulary.get(unknown_token) if unknown_token is not None else None
|
|
96
|
+
return [vocabulary[token] if token in vocabulary else unknown_id for token in tokens]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def decode(ids, vocabulary):
|
|
100
|
+
reverse = vocabulary if vocabulary and all(isinstance(key, int) for key in vocabulary) else reverse_vocab(vocabulary)
|
|
101
|
+
return " ".join(reverse[index] for index in ids)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def encode_batch(texts, vocabulary, unknown_token=None):
|
|
105
|
+
return [encode(text, vocabulary, unknown_token) for text in texts]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def decode_batch(ids, reverse):
|
|
109
|
+
return [decode(item, reverse) for item in ids]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def tokenize_batch(texts):
|
|
113
|
+
return [tokenizer(text) for text in texts]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def detokenize_batch(batch):
|
|
117
|
+
return [detokenize(tokens) for tokens in batch]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def normalize_text(text):
|
|
121
|
+
return " ".join(str(text).split())
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def clean_text(text):
|
|
125
|
+
return normalize_text(text)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def split_text(text, separator=None):
|
|
129
|
+
return str(text).split(separator)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def truncate(text, length):
|
|
133
|
+
return str(text)[:length]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def pad_sequence(sequence, length, value=0, pad_id=None):
|
|
137
|
+
"""Pad or truncate a sequence to ``length``.
|
|
138
|
+
|
|
139
|
+
``value`` is the original padding argument. ``pad_id`` is supported as
|
|
140
|
+
an API-compatible alias used by the public test suite and is preferred
|
|
141
|
+
when supplied.
|
|
142
|
+
"""
|
|
143
|
+
if pad_id is not None:
|
|
144
|
+
value = pad_id
|
|
145
|
+
|
|
146
|
+
result = list(sequence)[:length]
|
|
147
|
+
return result + [value] * max(0, length - len(result))
|
pyintell/training.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Training helpers."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def train(model, dataset, epochs=1, **kwargs):
|
|
5
|
+
"""Run a model's training method when available."""
|
|
6
|
+
if hasattr(model, "train"):
|
|
7
|
+
return model.train(dataset, epochs=epochs, **kwargs)
|
|
8
|
+
raise TypeError("model must provide a train() method")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def evaluate(model, dataset, **kwargs):
|
|
12
|
+
"""Evaluate a model using its evaluate() method."""
|
|
13
|
+
if hasattr(model, "evaluate"):
|
|
14
|
+
return model.evaluate(dataset, **kwargs)
|
|
15
|
+
raise TypeError("model must provide an evaluate() method")
|
pyintell/transformer.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Persistent, parameterized Transformer building blocks."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from .attention import multihead_attention, cross_attention, causal_mask
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _norm(x, eps=1e-5):
|
|
9
|
+
x = np.asarray(x, dtype=np.float32)
|
|
10
|
+
return (x - x.mean(axis=-1, keepdims=True)) / np.sqrt(x.var(axis=-1, keepdims=True) + eps)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _gelu(x):
|
|
14
|
+
return 0.5 * x * (1.0 + np.tanh(np.sqrt(2.0 / np.pi) * (x + 0.044715 * x ** 3)))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def init_block(d_model, heads, hidden_size, rng=None):
|
|
18
|
+
"""Create one persistent Transformer block state."""
|
|
19
|
+
rng = np.random.default_rng() if rng is None else rng
|
|
20
|
+
scale = 1.0 / np.sqrt(max(d_model, 1))
|
|
21
|
+
return {
|
|
22
|
+
"q": (rng.standard_normal((d_model, d_model)) * scale).astype(np.float32),
|
|
23
|
+
"k": (rng.standard_normal((d_model, d_model)) * scale).astype(np.float32),
|
|
24
|
+
"v": (rng.standard_normal((d_model, d_model)) * scale).astype(np.float32),
|
|
25
|
+
"o": (rng.standard_normal((d_model, d_model)) * scale).astype(np.float32),
|
|
26
|
+
"ff1": (rng.standard_normal((d_model, hidden_size)) * scale).astype(np.float32),
|
|
27
|
+
"ff2": (rng.standard_normal((hidden_size, d_model)) * (1.0 / np.sqrt(hidden_size))).astype(np.float32),
|
|
28
|
+
"ff1_bias": np.zeros(hidden_size, dtype=np.float32),
|
|
29
|
+
"ff2_bias": np.zeros(d_model, dtype=np.float32),
|
|
30
|
+
"q_bias": np.zeros(d_model, dtype=np.float32),
|
|
31
|
+
"k_bias": np.zeros(d_model, dtype=np.float32),
|
|
32
|
+
"v_bias": np.zeros(d_model, dtype=np.float32),
|
|
33
|
+
"o_bias": np.zeros(d_model, dtype=np.float32),
|
|
34
|
+
"norm1_scale": np.ones(d_model, dtype=np.float32),
|
|
35
|
+
"norm1_bias": np.zeros(d_model, dtype=np.float32),
|
|
36
|
+
"norm2_scale": np.ones(d_model, dtype=np.float32),
|
|
37
|
+
"norm2_bias": np.zeros(d_model, dtype=np.float32),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def transformer_block(x, heads=8, hidden_size=None, causal=True, weights=None):
|
|
42
|
+
"""Apply one pre-norm Transformer block."""
|
|
43
|
+
d_model = x.shape[-1]
|
|
44
|
+
hidden_size = int(hidden_size or d_model * 4)
|
|
45
|
+
if weights is None:
|
|
46
|
+
weights = init_block(d_model, heads, hidden_size)
|
|
47
|
+
n1 = _norm(x) * weights["norm1_scale"] + weights["norm1_bias"]
|
|
48
|
+
mask = causal_mask(x.shape[-2]) if causal else None
|
|
49
|
+
x = x + multihead_attention(n1, heads=heads, mask=mask, weights=weights)
|
|
50
|
+
n2 = _norm(x) * weights["norm2_scale"] + weights["norm2_bias"]
|
|
51
|
+
ff = _gelu(n2 @ weights["ff1"] + weights["ff1_bias"]) @ weights["ff2"] + weights["ff2_bias"]
|
|
52
|
+
return x + ff
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def transformer(x, layers=4, heads=8, hidden_size=None, causal=True, weights=None):
|
|
56
|
+
"""Stack persistent Transformer blocks."""
|
|
57
|
+
hidden_size = int(hidden_size or x.shape[-1] * 4)
|
|
58
|
+
if weights is None:
|
|
59
|
+
weights = [init_block(x.shape[-1], heads, hidden_size) for _ in range(int(layers))]
|
|
60
|
+
if len(weights) != int(layers):
|
|
61
|
+
raise ValueError("number of Transformer weight sets must equal layers")
|
|
62
|
+
for block in weights:
|
|
63
|
+
x = transformer_block(x, heads=heads, hidden_size=hidden_size, causal=causal, weights=block)
|
|
64
|
+
return _norm(x)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def encoder(x, layers=4, heads=8, hidden_size=None, weights=None):
|
|
68
|
+
for _ in []:
|
|
69
|
+
pass
|
|
70
|
+
hidden_size = int(hidden_size or x.shape[-1] * 4)
|
|
71
|
+
if weights is None:
|
|
72
|
+
weights = [init_block(x.shape[-1], heads, hidden_size) for _ in range(int(layers))]
|
|
73
|
+
for block in weights:
|
|
74
|
+
x = transformer_block(x, heads=heads, hidden_size=hidden_size, causal=False, weights=block)
|
|
75
|
+
return _norm(x)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def decoder(x, context=None, memory=None, layers=4, heads=8, hidden_size=None, weights=None):
|
|
79
|
+
if context is not None and memory is not None:
|
|
80
|
+
raise ValueError("provide either context or memory, not both")
|
|
81
|
+
if memory is not None:
|
|
82
|
+
context = memory
|
|
83
|
+
hidden_size = int(hidden_size or x.shape[-1] * 4)
|
|
84
|
+
if weights is None:
|
|
85
|
+
weights = [init_block(x.shape[-1], heads, hidden_size) for _ in range(int(layers))]
|
|
86
|
+
for block in weights:
|
|
87
|
+
x = transformer_block(x, heads=heads, hidden_size=hidden_size, causal=True, weights=block)
|
|
88
|
+
if context is not None:
|
|
89
|
+
x = x + cross_attention(_norm(x), context)
|
|
90
|
+
return _norm(x)
|