aiforge-studio 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.
- aiforge/__init__.py +7 -0
- aiforge/__main__.py +43 -0
- aiforge/data.py +383 -0
- aiforge/examples/color_images/blue/0.png +0 -0
- aiforge/examples/color_images/blue/1.png +0 -0
- aiforge/examples/color_images/blue/10.png +0 -0
- aiforge/examples/color_images/blue/11.png +0 -0
- aiforge/examples/color_images/blue/12.png +0 -0
- aiforge/examples/color_images/blue/13.png +0 -0
- aiforge/examples/color_images/blue/14.png +0 -0
- aiforge/examples/color_images/blue/15.png +0 -0
- aiforge/examples/color_images/blue/16.png +0 -0
- aiforge/examples/color_images/blue/17.png +0 -0
- aiforge/examples/color_images/blue/18.png +0 -0
- aiforge/examples/color_images/blue/19.png +0 -0
- aiforge/examples/color_images/blue/2.png +0 -0
- aiforge/examples/color_images/blue/20.png +0 -0
- aiforge/examples/color_images/blue/21.png +0 -0
- aiforge/examples/color_images/blue/22.png +0 -0
- aiforge/examples/color_images/blue/23.png +0 -0
- aiforge/examples/color_images/blue/24.png +0 -0
- aiforge/examples/color_images/blue/25.png +0 -0
- aiforge/examples/color_images/blue/26.png +0 -0
- aiforge/examples/color_images/blue/27.png +0 -0
- aiforge/examples/color_images/blue/28.png +0 -0
- aiforge/examples/color_images/blue/29.png +0 -0
- aiforge/examples/color_images/blue/3.png +0 -0
- aiforge/examples/color_images/blue/4.png +0 -0
- aiforge/examples/color_images/blue/5.png +0 -0
- aiforge/examples/color_images/blue/6.png +0 -0
- aiforge/examples/color_images/blue/7.png +0 -0
- aiforge/examples/color_images/blue/8.png +0 -0
- aiforge/examples/color_images/blue/9.png +0 -0
- aiforge/examples/color_images/red/0.png +0 -0
- aiforge/examples/color_images/red/1.png +0 -0
- aiforge/examples/color_images/red/10.png +0 -0
- aiforge/examples/color_images/red/11.png +0 -0
- aiforge/examples/color_images/red/12.png +0 -0
- aiforge/examples/color_images/red/13.png +0 -0
- aiforge/examples/color_images/red/14.png +0 -0
- aiforge/examples/color_images/red/15.png +0 -0
- aiforge/examples/color_images/red/16.png +0 -0
- aiforge/examples/color_images/red/17.png +0 -0
- aiforge/examples/color_images/red/18.png +0 -0
- aiforge/examples/color_images/red/19.png +0 -0
- aiforge/examples/color_images/red/2.png +0 -0
- aiforge/examples/color_images/red/20.png +0 -0
- aiforge/examples/color_images/red/21.png +0 -0
- aiforge/examples/color_images/red/22.png +0 -0
- aiforge/examples/color_images/red/23.png +0 -0
- aiforge/examples/color_images/red/24.png +0 -0
- aiforge/examples/color_images/red/25.png +0 -0
- aiforge/examples/color_images/red/26.png +0 -0
- aiforge/examples/color_images/red/27.png +0 -0
- aiforge/examples/color_images/red/28.png +0 -0
- aiforge/examples/color_images/red/29.png +0 -0
- aiforge/examples/color_images/red/3.png +0 -0
- aiforge/examples/color_images/red/4.png +0 -0
- aiforge/examples/color_images/red/5.png +0 -0
- aiforge/examples/color_images/red/6.png +0 -0
- aiforge/examples/color_images/red/7.png +0 -0
- aiforge/examples/color_images/red/8.png +0 -0
- aiforge/examples/color_images/red/9.png +0 -0
- aiforge/examples/fruit_size.csv +601 -0
- aiforge/examples/house_prices.csv +501 -0
- aiforge/examples/movie_reviews.csv +801 -0
- aiforge/fixes.py +192 -0
- aiforge/guide.py +770 -0
- aiforge/layers.py +450 -0
- aiforge/model_io.py +266 -0
- aiforge/predict.py +82 -0
- aiforge/spec.py +484 -0
- aiforge/sysinfo.py +214 -0
- aiforge/trainer.py +241 -0
- aiforge/tui/__init__.py +0 -0
- aiforge/tui/app.py +23 -0
- aiforge/tui/banner.py +73 -0
- aiforge/tui/browser.py +448 -0
- aiforge/tui/builder.py +489 -0
- aiforge/tui/chart.py +73 -0
- aiforge/tui/export.py +84 -0
- aiforge/tui/fixdialog.py +106 -0
- aiforge/tui/guide.py +81 -0
- aiforge/tui/home.py +280 -0
- aiforge/tui/modals.py +223 -0
- aiforge/tui/predict.py +129 -0
- aiforge/tui/resize.py +62 -0
- aiforge/tui/styles.tcss +81 -0
- aiforge/tui/sysmon.py +207 -0
- aiforge/tui/train.py +738 -0
- aiforge_studio-0.1.0.dist-info/METADATA +747 -0
- aiforge_studio-0.1.0.dist-info/RECORD +96 -0
- aiforge_studio-0.1.0.dist-info/WHEEL +5 -0
- aiforge_studio-0.1.0.dist-info/entry_points.txt +2 -0
- aiforge_studio-0.1.0.dist-info/licenses/LICENSE +21 -0
- aiforge_studio-0.1.0.dist-info/top_level.txt +1 -0
aiforge/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""AIForge - a no-code neural network studio in your terminal."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
__version__ = "0.1.0"
|
|
5
|
+
|
|
6
|
+
# Small sample datasets shipped inside the package (CSV tables, text, an image folder).
|
|
7
|
+
EXAMPLES_DIR = Path(__file__).resolve().parent / "examples"
|
aiforge/__main__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Entry point: ``python -m aiforge`` or the ``aiforge`` command."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
import warnings
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def default_workspace() -> Path:
|
|
13
|
+
"""Where models live unless -w / AIFORGE_WORKSPACE says otherwise.
|
|
14
|
+
|
|
15
|
+
A ``./workspace`` folder in the current directory wins (that's where earlier versions and
|
|
16
|
+
source checkouts keep their models); otherwise models go to ``~/AIForge``.
|
|
17
|
+
"""
|
|
18
|
+
env = os.environ.get("AIFORGE_WORKSPACE")
|
|
19
|
+
if env:
|
|
20
|
+
return Path(env)
|
|
21
|
+
local = Path.cwd() / "workspace"
|
|
22
|
+
if (local / "models").is_dir():
|
|
23
|
+
return local
|
|
24
|
+
return Path.home() / "AIForge"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
parser = argparse.ArgumentParser(prog="aiforge", description="No-code neural network studio in your terminal.")
|
|
29
|
+
parser.add_argument("-w", "--workspace", default=None,
|
|
30
|
+
help="Folder for models, datasets and exports "
|
|
31
|
+
"(default: ./workspace if it exists, otherwise ~/AIForge)")
|
|
32
|
+
parser.add_argument("-V", "--version", action="version", version=f"aiforge {__version__}")
|
|
33
|
+
args = parser.parse_args()
|
|
34
|
+
warnings.filterwarnings("ignore") # keep library warnings from drawing over the UI
|
|
35
|
+
|
|
36
|
+
from .tui.app import AIForgeApp
|
|
37
|
+
|
|
38
|
+
workspace = Path(args.workspace) if args.workspace else default_workspace()
|
|
39
|
+
AIForgeApp(workspace.expanduser().resolve()).run()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
aiforge/data.py
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""Datasets. Every loader returns train/val datasets plus a ``DataInfo`` describing
|
|
2
|
+
what the model must look like and how raw inputs are preprocessed for prediction."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
from collections import Counter
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import torch
|
|
13
|
+
from torch.utils.data import Dataset, Subset, TensorDataset, random_split
|
|
14
|
+
|
|
15
|
+
SOURCES = {
|
|
16
|
+
"synthetic": "Synthetic toy data (no files needed)",
|
|
17
|
+
"csv": "CSV table - numeric/categorical columns + a target column",
|
|
18
|
+
"csv_text": "CSV with a text column + label column (text classification)",
|
|
19
|
+
"image_folder": "Image folder - one sub-folder per class",
|
|
20
|
+
"mnist": "MNIST handwritten digits (auto-download, 28×28×1)",
|
|
21
|
+
"fashion_mnist": "Fashion-MNIST clothing (auto-download, 28×28×1)",
|
|
22
|
+
"cifar10": "CIFAR-10 objects (auto-download, 32×32×3)",
|
|
23
|
+
}
|
|
24
|
+
SYNTHETIC = ["moons", "spirals", "circles", "blobs", "regression"]
|
|
25
|
+
IMAGE_EXT = {".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class DataInfo:
|
|
30
|
+
input_shape: list[int]
|
|
31
|
+
input_dtype: str
|
|
32
|
+
task: str
|
|
33
|
+
class_names: list[str]
|
|
34
|
+
preprocess: dict = field(default_factory=dict)
|
|
35
|
+
n_train: int = 0
|
|
36
|
+
n_val: int = 0
|
|
37
|
+
description: str = ""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def n_outputs(self) -> int:
|
|
41
|
+
return len(self.class_names) if self.task == "classification" else 1
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load(cfg: dict, data_dir: Path, spec=None) -> tuple[Dataset, Dataset, DataInfo]:
|
|
45
|
+
src = cfg["source"]
|
|
46
|
+
val_split = float(cfg.get("val_split", 0.2))
|
|
47
|
+
if src == "synthetic":
|
|
48
|
+
ds, info = _synthetic(cfg.get("kind", "moons"), int(cfg.get("samples", 2000)))
|
|
49
|
+
elif src == "csv":
|
|
50
|
+
ds, info = _csv_table(Path(cfg["path"]).expanduser(), cfg.get("target") or None,
|
|
51
|
+
cfg.get("task") or None)
|
|
52
|
+
elif src == "csv_text":
|
|
53
|
+
seq_len = int(spec.input_shape[0]) if spec and spec.input_dtype == "long" else 100
|
|
54
|
+
prev = spec.data.get("preprocess", {}) if spec else {}
|
|
55
|
+
ds, info = _csv_text(Path(cfg["path"]).expanduser(), cfg.get("target") or None,
|
|
56
|
+
cfg.get("text_column") or None, seq_len, _vocab_size(spec),
|
|
57
|
+
prev.get("vocab") if prev.get("type") == "text" else None)
|
|
58
|
+
elif src == "image_folder":
|
|
59
|
+
ds, info = _image_folder(Path(cfg["path"]).expanduser(), spec)
|
|
60
|
+
elif src in ("mnist", "fashion_mnist", "cifar10"):
|
|
61
|
+
return _torchvision(src, data_dir, spec)
|
|
62
|
+
else:
|
|
63
|
+
raise ValueError(f"Unknown data source {src}")
|
|
64
|
+
n_val = max(1, int(len(ds) * val_split)) if val_split > 0 else 0
|
|
65
|
+
gen = torch.Generator().manual_seed(int(cfg.get("seed", 42)))
|
|
66
|
+
train, val = random_split(ds, [len(ds) - n_val, n_val], generator=gen)
|
|
67
|
+
info.n_train, info.n_val = len(train), len(val)
|
|
68
|
+
return train, val, info
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _vocab_size(spec) -> int:
|
|
72
|
+
if spec:
|
|
73
|
+
for l in spec.layers:
|
|
74
|
+
if l.type == "Embedding":
|
|
75
|
+
return int(l.config["num_embeddings"])
|
|
76
|
+
return 20000
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Synthetic
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _synthetic(kind: str, n: int):
|
|
85
|
+
g = torch.Generator().manual_seed(0)
|
|
86
|
+
if kind == "regression":
|
|
87
|
+
x = torch.rand(n, 4, generator=g) * 4 - 2
|
|
88
|
+
y = (torch.sin(x[:, 0]) + x[:, 1] ** 2 * 0.5 - x[:, 2] * x[:, 3]
|
|
89
|
+
+ torch.randn(n, generator=g) * 0.1)
|
|
90
|
+
info = DataInfo([4], "float", "regression", [], {"type": "tabular",
|
|
91
|
+
"features": ["x1", "x2", "x3", "x4"], "mean": [0.0] * 4, "std": [1.0] * 4,
|
|
92
|
+
"categorical": {}}, description="y = sin(x1) + x2²/2 - x3·x4 + noise")
|
|
93
|
+
return TensorDataset(x, y.unsqueeze(1)), info
|
|
94
|
+
if kind == "moons":
|
|
95
|
+
h = n // 2
|
|
96
|
+
t = torch.rand(h, generator=g) * math.pi
|
|
97
|
+
a = torch.stack([torch.cos(t), torch.sin(t)], 1)
|
|
98
|
+
b = torch.stack([1 - torch.cos(t), 0.5 - torch.sin(t)], 1)
|
|
99
|
+
x = torch.cat([a, b]) + torch.randn(2 * h, 2, generator=g) * 0.1
|
|
100
|
+
y = torch.cat([torch.zeros(h), torch.ones(h)]).long()
|
|
101
|
+
classes = ["upper", "lower"]
|
|
102
|
+
elif kind == "circles":
|
|
103
|
+
h = n // 2
|
|
104
|
+
t = torch.rand(2 * h, generator=g) * 2 * math.pi
|
|
105
|
+
r = torch.cat([torch.ones(h), torch.full((h,), 0.4)])
|
|
106
|
+
x = torch.stack([r * torch.cos(t), r * torch.sin(t)], 1) + torch.randn(2 * h, 2, generator=g) * 0.06
|
|
107
|
+
y = torch.cat([torch.zeros(h), torch.ones(h)]).long()
|
|
108
|
+
classes = ["outer", "inner"]
|
|
109
|
+
elif kind == "spirals":
|
|
110
|
+
k, per = 3, n // 3
|
|
111
|
+
xs, ys = [], []
|
|
112
|
+
for c in range(k):
|
|
113
|
+
r = torch.linspace(0.05, 1, per)
|
|
114
|
+
t = c * 2 * math.pi / k + r * 3.5 + torch.randn(per, generator=g) * 0.15
|
|
115
|
+
xs.append(torch.stack([r * torch.cos(t), r * torch.sin(t)], 1))
|
|
116
|
+
ys.append(torch.full((per,), c))
|
|
117
|
+
x, y = torch.cat(xs), torch.cat(ys).long()
|
|
118
|
+
classes = ["arm-0", "arm-1", "arm-2"]
|
|
119
|
+
elif kind == "blobs":
|
|
120
|
+
k, per = 4, n // 4
|
|
121
|
+
centers = torch.tensor([[-2, -2], [2, 2], [-2, 2], [2, -2]], dtype=torch.float)
|
|
122
|
+
x = torch.cat([centers[c] + torch.randn(per, 2, generator=g) * 0.8 for c in range(k)])
|
|
123
|
+
y = torch.arange(k).repeat_interleave(per)
|
|
124
|
+
classes = [f"blob-{c}" for c in range(k)]
|
|
125
|
+
else:
|
|
126
|
+
raise ValueError(f"unknown synthetic dataset {kind}")
|
|
127
|
+
info = DataInfo([2], "float", "classification", classes,
|
|
128
|
+
{"type": "tabular", "features": ["x", "y"], "mean": [0.0, 0.0],
|
|
129
|
+
"std": [1.0, 1.0], "categorical": {}}, description=f"2D {kind}")
|
|
130
|
+
return TensorDataset(x.float(), y), info
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# CSV tables
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _read_csv(path: Path) -> tuple[list[str], list[list[str]]]:
|
|
139
|
+
if not path.exists():
|
|
140
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
141
|
+
with path.open(newline="", encoding="utf-8-sig") as f:
|
|
142
|
+
sample = f.read(4096)
|
|
143
|
+
f.seek(0)
|
|
144
|
+
try:
|
|
145
|
+
dialect = csv.Sniffer().sniff(sample, delimiters=",;\t|")
|
|
146
|
+
except csv.Error:
|
|
147
|
+
dialect = csv.excel
|
|
148
|
+
rows = [r for r in csv.reader(f, dialect) if any(c.strip() for c in r)]
|
|
149
|
+
if len(rows) < 2:
|
|
150
|
+
raise ValueError("CSV needs a header row and at least one data row")
|
|
151
|
+
return [h.strip() for h in rows[0]], rows[1:]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def csv_columns(path: str) -> list[str]:
|
|
155
|
+
try:
|
|
156
|
+
return _read_csv(Path(path).expanduser())[0]
|
|
157
|
+
except Exception: # noqa: BLE001
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _is_num(v: str) -> bool:
|
|
162
|
+
try:
|
|
163
|
+
float(v)
|
|
164
|
+
return True
|
|
165
|
+
except ValueError:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _csv_table(path: Path, target: str | None, task: str | None):
|
|
170
|
+
header, rows = _read_csv(path)
|
|
171
|
+
target = target or header[-1]
|
|
172
|
+
if target not in header:
|
|
173
|
+
raise ValueError(f"Target column '{target}' not in CSV columns: {', '.join(header)}")
|
|
174
|
+
ti = header.index(target)
|
|
175
|
+
rows = [r for r in rows if len(r) == len(header)]
|
|
176
|
+
feats = [h for i, h in enumerate(header) if i != ti]
|
|
177
|
+
cols = {h: [r[header.index(h)].strip() for r in rows] for h in header}
|
|
178
|
+
|
|
179
|
+
categorical: dict[str, list[str]] = {}
|
|
180
|
+
matrix = []
|
|
181
|
+
for h in feats:
|
|
182
|
+
vals = cols[h]
|
|
183
|
+
if all(_is_num(v) for v in vals if v != ""):
|
|
184
|
+
nums = [float(v) if v != "" else math.nan for v in vals]
|
|
185
|
+
known = [v for v in nums if not math.isnan(v)]
|
|
186
|
+
fill = sum(known) / len(known) if known else 0.0
|
|
187
|
+
matrix.append([fill if math.isnan(v) else v for v in nums])
|
|
188
|
+
else:
|
|
189
|
+
cats = sorted(set(vals))
|
|
190
|
+
categorical[h] = cats
|
|
191
|
+
matrix.append([float(cats.index(v)) for v in vals])
|
|
192
|
+
x = torch.tensor(matrix, dtype=torch.float).T
|
|
193
|
+
mean, std = x.mean(0), x.std(0).clamp_min(1e-8)
|
|
194
|
+
x = (x - mean) / std
|
|
195
|
+
|
|
196
|
+
yv = cols[target]
|
|
197
|
+
numeric_target = all(_is_num(v) for v in yv)
|
|
198
|
+
if task is None:
|
|
199
|
+
task = "regression" if numeric_target and len(set(yv)) > 20 else "classification"
|
|
200
|
+
if task == "regression":
|
|
201
|
+
if not numeric_target:
|
|
202
|
+
raise ValueError("Regression needs a numeric target column")
|
|
203
|
+
y = torch.tensor([float(v) for v in yv]).unsqueeze(1)
|
|
204
|
+
y_mean, y_std = y.mean().item(), max(y.std().item(), 1e-8)
|
|
205
|
+
y = (y - y_mean) / y_std # train on a standardised target; predictions are scaled back
|
|
206
|
+
classes = []
|
|
207
|
+
else:
|
|
208
|
+
classes = sorted(set(yv), key=lambda v: (float(v) if numeric_target else 0, v))
|
|
209
|
+
y = torch.tensor([classes.index(v) for v in yv])
|
|
210
|
+
info = DataInfo([len(feats)], "float", task, classes, {
|
|
211
|
+
"type": "tabular", "features": feats, "mean": mean.tolist(), "std": std.tolist(),
|
|
212
|
+
"categorical": categorical, **({"target_mean": y_mean, "target_std": y_std}
|
|
213
|
+
if task == "regression" else {})},
|
|
214
|
+
description=f"{path.name}: {len(rows)} rows, "
|
|
215
|
+
f"{len(feats)} features, target '{target}'")
|
|
216
|
+
return TensorDataset(x, y), info
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
# Text CSV
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
TOKEN_RE = re.compile(r"[a-z0-9']+|[^\sa-z0-9]")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def tokenize(text: str) -> list[str]:
|
|
227
|
+
return TOKEN_RE.findall(text.lower())
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def encode_text(text: str, vocab: dict[str, int], seq_len: int) -> list[int]:
|
|
231
|
+
ids = [vocab.get(t, 1) for t in tokenize(text)][:seq_len]
|
|
232
|
+
# Left-pad so the real tokens end the sequence (RNNs read the last step). 0 = <pad>, 1 = <unk>
|
|
233
|
+
return [0] * (seq_len - len(ids)) + ids
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _csv_text(path: Path, target: str | None, text_col: str | None, seq_len: int,
|
|
237
|
+
vocab_size: int, vocab: dict[str, int] | None = None):
|
|
238
|
+
header, rows = _read_csv(path)
|
|
239
|
+
target = target or header[-1]
|
|
240
|
+
text_col = text_col or next(h for h in header if h != target)
|
|
241
|
+
for c in (target, text_col):
|
|
242
|
+
if c not in header:
|
|
243
|
+
raise ValueError(f"Column '{c}' not in CSV columns: {', '.join(header)}")
|
|
244
|
+
ti, xi = header.index(target), header.index(text_col)
|
|
245
|
+
rows = [r for r in rows if len(r) == len(header)]
|
|
246
|
+
if vocab is None: # keep an existing vocab when fine-tuning so embeddings stay meaningful
|
|
247
|
+
counts = Counter(t for r in rows for t in tokenize(r[xi]))
|
|
248
|
+
words = [w for w, _ in counts.most_common(max(0, vocab_size - 2))]
|
|
249
|
+
vocab = {w: i + 2 for i, w in enumerate(words)}
|
|
250
|
+
x = torch.tensor([encode_text(r[xi], vocab, seq_len) for r in rows])
|
|
251
|
+
classes = sorted(set(r[ti].strip() for r in rows))
|
|
252
|
+
y = torch.tensor([classes.index(r[ti].strip()) for r in rows])
|
|
253
|
+
info = DataInfo([seq_len], "long", "classification", classes, {
|
|
254
|
+
"type": "text", "vocab": vocab, "seq_len": seq_len, "vocab_size": vocab_size},
|
|
255
|
+
description=f"{path.name}: {len(rows)} texts, vocab {len(vocab) + 2}")
|
|
256
|
+
return TensorDataset(x, y), info
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# Images
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _image_transform(pre: dict, train: bool = False):
|
|
265
|
+
from torchvision import transforms as T
|
|
266
|
+
|
|
267
|
+
ops = [T.Resize(tuple(pre["size"]))]
|
|
268
|
+
if pre["channels"] == 1:
|
|
269
|
+
ops.append(T.Grayscale(1))
|
|
270
|
+
else:
|
|
271
|
+
ops.append(T.Lambda(lambda im: im.convert("RGB")))
|
|
272
|
+
if train and pre.get("augment"):
|
|
273
|
+
ops.append(T.RandomHorizontalFlip())
|
|
274
|
+
ops += [T.ToTensor(), T.Normalize(pre["mean"], pre["std"])]
|
|
275
|
+
return T.Compose(ops)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class _ImageFiles(Dataset):
|
|
279
|
+
def __init__(self, items, transform):
|
|
280
|
+
self.items, self.transform = items, transform
|
|
281
|
+
|
|
282
|
+
def __len__(self):
|
|
283
|
+
return len(self.items)
|
|
284
|
+
|
|
285
|
+
def __getitem__(self, i):
|
|
286
|
+
from PIL import Image
|
|
287
|
+
|
|
288
|
+
path, label = self.items[i]
|
|
289
|
+
with Image.open(path) as im:
|
|
290
|
+
return self.transform(im), label
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _image_folder(root: Path, spec):
|
|
294
|
+
if not root.is_dir():
|
|
295
|
+
raise FileNotFoundError(f"Folder not found: {root}")
|
|
296
|
+
classes = sorted(d.name for d in root.iterdir() if d.is_dir() and not d.name.startswith("."))
|
|
297
|
+
if len(classes) < 2:
|
|
298
|
+
raise ValueError("Image folder needs at least 2 sub-folders (one per class)")
|
|
299
|
+
items = [(p, ci) for ci, c in enumerate(classes) for p in sorted((root / c).rglob("*"))
|
|
300
|
+
if p.suffix.lower() in IMAGE_EXT]
|
|
301
|
+
if not items:
|
|
302
|
+
raise ValueError("No images found (.png/.jpg/...)")
|
|
303
|
+
shape = list(spec.input_shape) if spec and len(spec.input_shape) == 3 else [3, 64, 64]
|
|
304
|
+
c = shape[0] if shape[0] in (1, 3) else 3
|
|
305
|
+
imagenet = c == 3
|
|
306
|
+
pre = {"type": "image", "size": shape[1:], "channels": c,
|
|
307
|
+
"mean": [0.485, 0.456, 0.406] if imagenet else [0.5],
|
|
308
|
+
"std": [0.229, 0.224, 0.225] if imagenet else [0.5]}
|
|
309
|
+
ds = _ImageFiles(items, _image_transform(pre))
|
|
310
|
+
info = DataInfo([c, *shape[1:]], "float", "classification", classes, pre,
|
|
311
|
+
description=f"{root.name}: {len(items)} images, {len(classes)} classes")
|
|
312
|
+
return ds, info
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _torchvision(name: str, data_dir: Path, spec):
|
|
316
|
+
from torchvision import datasets, transforms as T
|
|
317
|
+
|
|
318
|
+
meta = {
|
|
319
|
+
"mnist": (datasets.MNIST, [1, 28, 28], [0.1307], [0.3081]),
|
|
320
|
+
"fashion_mnist": (datasets.FashionMNIST, [1, 28, 28], [0.2860], [0.3530]),
|
|
321
|
+
"cifar10": (datasets.CIFAR10, [3, 32, 32], [0.4914, 0.4822, 0.4465], [0.2470, 0.2435, 0.2616]),
|
|
322
|
+
}[name]
|
|
323
|
+
cls, shape, mean, std = meta
|
|
324
|
+
pre = {"type": "image", "size": shape[1:], "channels": shape[0], "mean": mean, "std": std}
|
|
325
|
+
tf = T.Compose([T.ToTensor(), T.Normalize(mean, std)])
|
|
326
|
+
root = data_dir / name
|
|
327
|
+
train = cls(str(root), train=True, download=True, transform=tf)
|
|
328
|
+
val = cls(str(root), train=False, download=True, transform=tf)
|
|
329
|
+
info = DataInfo(shape, "float", "classification", list(train.classes), pre,
|
|
330
|
+
n_train=len(train), n_val=len(val),
|
|
331
|
+
description=f"{name}: {len(train)} train / {len(val)} test images")
|
|
332
|
+
return train, val, info
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def limit(ds: Dataset, n: int | None) -> Dataset:
|
|
336
|
+
"""Use only the first n samples (quick experiments)."""
|
|
337
|
+
if not n or n >= len(ds):
|
|
338
|
+
return ds
|
|
339
|
+
return Subset(ds, range(n))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
# Raw input -> tensor (for prediction)
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def prepare_input(spec, raw: str) -> torch.Tensor:
|
|
348
|
+
pre = spec.data.get("preprocess", {})
|
|
349
|
+
kind = pre.get("type")
|
|
350
|
+
if kind == "image" or (kind is None and len(spec.input_shape) == 3):
|
|
351
|
+
if kind is None:
|
|
352
|
+
c = spec.input_shape[0]
|
|
353
|
+
pre = {"size": spec.input_shape[1:], "channels": c, "mean": [0.5] * c, "std": [0.5] * c}
|
|
354
|
+
from PIL import Image
|
|
355
|
+
|
|
356
|
+
p = Path(raw.strip().strip("'\"")).expanduser()
|
|
357
|
+
if not p.exists():
|
|
358
|
+
raise FileNotFoundError(f"Image not found: {p}")
|
|
359
|
+
with Image.open(p) as im:
|
|
360
|
+
return _image_transform(pre)(im).unsqueeze(0)
|
|
361
|
+
if kind == "text":
|
|
362
|
+
return torch.tensor([encode_text(raw, pre["vocab"], pre["seq_len"])])
|
|
363
|
+
# tabular / generic numbers
|
|
364
|
+
parts = [v.strip() for v in re.split(r"[,\s;]+", raw.strip()) if v.strip()]
|
|
365
|
+
feats = pre.get("features")
|
|
366
|
+
n = int(torch.tensor(spec.input_shape).prod())
|
|
367
|
+
if len(parts) != n:
|
|
368
|
+
hint = f" ({', '.join(feats)})" if feats else ""
|
|
369
|
+
raise ValueError(f"Expected {n} values{hint}, got {len(parts)}")
|
|
370
|
+
vals = []
|
|
371
|
+
cats = pre.get("categorical", {})
|
|
372
|
+
for i, v in enumerate(parts):
|
|
373
|
+
name = feats[i] if feats else None
|
|
374
|
+
if name in cats:
|
|
375
|
+
if v not in cats[name]:
|
|
376
|
+
raise ValueError(f"'{v}' is not a known value for {name}: {', '.join(cats[name])}")
|
|
377
|
+
vals.append(float(cats[name].index(v)))
|
|
378
|
+
else:
|
|
379
|
+
vals.append(float(v))
|
|
380
|
+
x = torch.tensor(vals)
|
|
381
|
+
if "mean" in pre:
|
|
382
|
+
x = (x - torch.tensor(pre["mean"])) / torch.tensor(pre["std"])
|
|
383
|
+
return x.reshape(1, *spec.input_shape)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|