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.
Files changed (96) hide show
  1. aiforge/__init__.py +7 -0
  2. aiforge/__main__.py +43 -0
  3. aiforge/data.py +383 -0
  4. aiforge/examples/color_images/blue/0.png +0 -0
  5. aiforge/examples/color_images/blue/1.png +0 -0
  6. aiforge/examples/color_images/blue/10.png +0 -0
  7. aiforge/examples/color_images/blue/11.png +0 -0
  8. aiforge/examples/color_images/blue/12.png +0 -0
  9. aiforge/examples/color_images/blue/13.png +0 -0
  10. aiforge/examples/color_images/blue/14.png +0 -0
  11. aiforge/examples/color_images/blue/15.png +0 -0
  12. aiforge/examples/color_images/blue/16.png +0 -0
  13. aiforge/examples/color_images/blue/17.png +0 -0
  14. aiforge/examples/color_images/blue/18.png +0 -0
  15. aiforge/examples/color_images/blue/19.png +0 -0
  16. aiforge/examples/color_images/blue/2.png +0 -0
  17. aiforge/examples/color_images/blue/20.png +0 -0
  18. aiforge/examples/color_images/blue/21.png +0 -0
  19. aiforge/examples/color_images/blue/22.png +0 -0
  20. aiforge/examples/color_images/blue/23.png +0 -0
  21. aiforge/examples/color_images/blue/24.png +0 -0
  22. aiforge/examples/color_images/blue/25.png +0 -0
  23. aiforge/examples/color_images/blue/26.png +0 -0
  24. aiforge/examples/color_images/blue/27.png +0 -0
  25. aiforge/examples/color_images/blue/28.png +0 -0
  26. aiforge/examples/color_images/blue/29.png +0 -0
  27. aiforge/examples/color_images/blue/3.png +0 -0
  28. aiforge/examples/color_images/blue/4.png +0 -0
  29. aiforge/examples/color_images/blue/5.png +0 -0
  30. aiforge/examples/color_images/blue/6.png +0 -0
  31. aiforge/examples/color_images/blue/7.png +0 -0
  32. aiforge/examples/color_images/blue/8.png +0 -0
  33. aiforge/examples/color_images/blue/9.png +0 -0
  34. aiforge/examples/color_images/red/0.png +0 -0
  35. aiforge/examples/color_images/red/1.png +0 -0
  36. aiforge/examples/color_images/red/10.png +0 -0
  37. aiforge/examples/color_images/red/11.png +0 -0
  38. aiforge/examples/color_images/red/12.png +0 -0
  39. aiforge/examples/color_images/red/13.png +0 -0
  40. aiforge/examples/color_images/red/14.png +0 -0
  41. aiforge/examples/color_images/red/15.png +0 -0
  42. aiforge/examples/color_images/red/16.png +0 -0
  43. aiforge/examples/color_images/red/17.png +0 -0
  44. aiforge/examples/color_images/red/18.png +0 -0
  45. aiforge/examples/color_images/red/19.png +0 -0
  46. aiforge/examples/color_images/red/2.png +0 -0
  47. aiforge/examples/color_images/red/20.png +0 -0
  48. aiforge/examples/color_images/red/21.png +0 -0
  49. aiforge/examples/color_images/red/22.png +0 -0
  50. aiforge/examples/color_images/red/23.png +0 -0
  51. aiforge/examples/color_images/red/24.png +0 -0
  52. aiforge/examples/color_images/red/25.png +0 -0
  53. aiforge/examples/color_images/red/26.png +0 -0
  54. aiforge/examples/color_images/red/27.png +0 -0
  55. aiforge/examples/color_images/red/28.png +0 -0
  56. aiforge/examples/color_images/red/29.png +0 -0
  57. aiforge/examples/color_images/red/3.png +0 -0
  58. aiforge/examples/color_images/red/4.png +0 -0
  59. aiforge/examples/color_images/red/5.png +0 -0
  60. aiforge/examples/color_images/red/6.png +0 -0
  61. aiforge/examples/color_images/red/7.png +0 -0
  62. aiforge/examples/color_images/red/8.png +0 -0
  63. aiforge/examples/color_images/red/9.png +0 -0
  64. aiforge/examples/fruit_size.csv +601 -0
  65. aiforge/examples/house_prices.csv +501 -0
  66. aiforge/examples/movie_reviews.csv +801 -0
  67. aiforge/fixes.py +192 -0
  68. aiforge/guide.py +770 -0
  69. aiforge/layers.py +450 -0
  70. aiforge/model_io.py +266 -0
  71. aiforge/predict.py +82 -0
  72. aiforge/spec.py +484 -0
  73. aiforge/sysinfo.py +214 -0
  74. aiforge/trainer.py +241 -0
  75. aiforge/tui/__init__.py +0 -0
  76. aiforge/tui/app.py +23 -0
  77. aiforge/tui/banner.py +73 -0
  78. aiforge/tui/browser.py +448 -0
  79. aiforge/tui/builder.py +489 -0
  80. aiforge/tui/chart.py +73 -0
  81. aiforge/tui/export.py +84 -0
  82. aiforge/tui/fixdialog.py +106 -0
  83. aiforge/tui/guide.py +81 -0
  84. aiforge/tui/home.py +280 -0
  85. aiforge/tui/modals.py +223 -0
  86. aiforge/tui/predict.py +129 -0
  87. aiforge/tui/resize.py +62 -0
  88. aiforge/tui/styles.tcss +81 -0
  89. aiforge/tui/sysmon.py +207 -0
  90. aiforge/tui/train.py +738 -0
  91. aiforge_studio-0.1.0.dist-info/METADATA +747 -0
  92. aiforge_studio-0.1.0.dist-info/RECORD +96 -0
  93. aiforge_studio-0.1.0.dist-info/WHEEL +5 -0
  94. aiforge_studio-0.1.0.dist-info/entry_points.txt +2 -0
  95. aiforge_studio-0.1.0.dist-info/licenses/LICENSE +21 -0
  96. 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