2.3. From Files To Typed Minibatches
A model type tells Lean the shape of one input and one output. A dataset must eventually provide values of exactly those shapes, but real data begins in a less orderly form: rows in a CSV file, arrays in an NPY file, text tokens, simulator output, or tensors generated on demand.
TorchLean treats loading as a boundary. Parsing and dimension checks happen before a value becomes a typed training sample. Once the boundary succeeds, the training loop does not need to ask on every step whether a row had the right number of columns.
2.3.1. The Dataset Type
The trainer-facing type is:
Trainer.Dataset inputShape targetShape
It describes one training item. The scalar type is intentionally absent. Its build field
materializes a concrete dataset after the trainer chooses Float, IEEE32Exec, or another
supported executable scalar. Materialization chooses one scalar type for every numeric input and
target in that run; it is not a per-column dtype schema.
This lets the same Float-authored data feed several scalar runtimes while keeping the conversion visible at materialization. It also means a proof-level real tensor is not accidentally passed to an IO training loop.
2.3.2. Begin With Four Samples
The XOR table is small enough to see in full:
import NN.API
open TorchLean
def xs : Tensor.T Float (shape![4, 2]) :=
tensor! [
[0.0, 0.0],
[0.0, 1.0],
[1.0, 0.0],
[1.0, 1.0]
]
def ys : Tensor.T Float (shape![4, 1]) :=
tensor! [[0.0], [1.0], [1.0], [0.0]]
def xorData : Trainer.Dataset (shape![2]) (shape![1]) :=
Data.tensorDataset xs ys
The leading dimension of xs and ys is the sample count. Data.tensorDataset checks that both
counts agree and removes that leading axis from the item shapes:
whole input tensor [4, 2] one input sample [2] whole target tensor [4, 1] one target sample [1]
Try changing the target annotation to shape![3,1] while leaving four rows in the literal. The
literal itself fails to elaborate. If the mismatch instead arrives from runtime files, the loader
returns an error before constructing the dataset.
Data.samples, Data.singleton, and Data.floatSamples serve list-backed or generated data.
Data.regressionGrid builds the deterministic grid used by the running MLP.
2.3.3. A Real CSV Run
TorchLean includes a 25-row regression file with columns x1,x2,y. Run:
lake exe torchlean data_csv \ --device cpu --batch 5 --steps 5 --seed 2026
The loader prints the model and boundary choices before training:
model:
Sequential: [5, 2] -> [5, 1], layers=3, params=33
[0] Linear(2, 8): [5, 2] -> [5, 8]
[1] ReLU: [5, 8] -> [5, 8]
[2] Linear(8, 1): [5, 8] -> [5, 1]
data_dir = NN/Examples/Data
csv_path = NN/Examples/Data/small_regression.csv
seed = 2026
train = Adam(lr=0.05), steps=5,
batch_size=5, shuffle=true, drop_last=true
dataset size = 5
mean_loss(before) = 1.367492
mean_loss(after) = 0.323823
“Dataset size = 5” counts materialized minibatches, not source rows. Each item already has input
shape [5,2] and target shape [5,1].
The constructor in
Csv.lean
is:
let csvOptions : Data.CsvOptions :=
{ skipHeader := true }
let data :=
Data.tabularCsvDataset csvPath batch 2 1
(csvOptions := csvOptions)
(shuffle := true)
(seed := seed)
The arguments 2 and 1 state how many columns belong to the input and target. Malformed numbers,
wrong column counts, missing files, or too few rows become explicit IO errors when build runs.
2.3.4. Break The File Boundary Deliberately
Run the same command with a nonexistent path:
lake exe torchlean data_csv --csv /tmp/no-such-data.csv
The program stops at Data.requireFile; no randomly initialized model is reported as having
trained on an empty dataset.
For a second experiment, copy the small CSV and remove one value from a row. The CSV parser may still recognize the row as text, but the supervised loader rejects its column count. These two failures are different:
-
file existence is an operating-system boundary;
-
row width is a data-schema boundary.
Neither is a theorem about the data-generating process. They establish that the accepted artifact has the structure requested by the model.
2.3.5. NPY And Other Numeric Sources
NPY preserves numeric dtype and array dimensions, making it a cleaner boundary for already prepared numeric tensors:
def source : Data.SupervisedSource :=
Data.SupervisedSource.ofPaths
.npy
"data/x.npy"
"data/y.npy"
100
[2]
[1]
def data : Trainer.Dataset (shape![2]) (shape![1]) :=
Data.supervisedDataset source
The source declares:
-
the file format;
-
input and target paths;
-
sample count;
-
one-sample input dimensions;
-
one-sample target dimensions.
Data.supervisedNpyDataset is the convenience constructor. Data.LabeledSource reads integer
labels and constructs one-hot targets for classification.
TorchLean does not maintain a second parser for every ecosystem format. .pt, .pth, .npz,
image folders, and specialized scientific containers can be converted with Python into a small
numeric boundary such as NPY or CSV. The converter is an untrusted producer; the Lean loader checks
the resulting artifact.
2.3.6. Text Is Not A Numeric Matrix
Language models need a vocabulary, tokenization rule, context window, and target shift. Those are semantic choices, not generic CSV parsing.
A next-token dataset typically turns tokens:
t_0,t_1,\ldots,t_n
into windows:
x_i=(t_i,\ldots,t_{i+L-1}),\qquad
y_i=(t_{i+1},\ldots,t_{i+L}).
The shapes may both be [batch,L], but the one-position shift is the learning problem. TorchLean's
text helpers make integer tokens and window construction explicit. A tokenizer file or vocabulary
mapping should be stored with the run because changing it changes the meaning of every integer in
the dataset.
2.3.6.1. GPT-2 Byte-Level BPE Files
NN.API.Text.Bpe loads the conventional GPT-2 tokenizer pair directly in Lean:
-
vocab.jsonis a flat JSON object from byte-escaped token spellings to natural-number ids; -
merges.txtgives one adjacent symbol pair per non-comment line. File order determines rank, and a lower rank is applied first.
The two files form one artifact: using a vocabulary with a merge table from a different tokenizer
can change token boundaries or leave a merged piece absent from the vocabulary. Pass both paths and
record both files, preferably with content hashes, alongside a run. A malformed vocabulary or a
missing file produces an error. The current merge parser skips malformed non-comment lines, so a
successful load alone is not a complete integrity check for an untrusted merges.txt.
Encoding has four visible stages. It applies the GPT-2 pre-tokenizer, converts each fragment's UTF-8
bytes through GPT-2's reversible byte-to-Unicode table, repeatedly applies the best-ranked adjacent
merge, and finally looks up every piece in vocab.json. Gpt2Bpe.encode returns an error when a
piece has no id. Gpt2Bpe.decode? performs the inverse vocabulary lookup and byte decoding, and
reports an unknown id or invalid UTF-8 rather than inventing text.
Here is a direct round-trip from the repository root after placing a matching tokenizer pair at the shown paths:
import NN.API.Text.Bpe
open TorchLean.text
def inspectBpe : IO Unit := do
let tok ← Gpt2Bpe.load
"data/real/gpt2/vocab.json"
"data/real/gpt2/merges.txt"
match Gpt2Bpe.encode tok "naïve café" with
| .error e => throw <| IO.userError e
| .ok ids =>
IO.println s!"ids = {repr ids}"
match Gpt2Bpe.decode? tok ids with
| .error e => throw <| IO.userError e
| .ok decoded => IO.println s!"decoded = {decoded}"
decodeD and the generic-tokenizer adapter are intended for display and convenience: they collapse
decode or encode failures to empty output. Use the error-reporting encode and decode? functions
at an artifact-validation boundary.
2.3.6.2. Unicode Is Part Of The Tokenizer Version
GPT-2 pre-tokenization distinguishes Unicode letters, numbers, whitespace, contractions, and other
symbols. NN.API.Text.Unicode supplies checked-in Unicode 15.1 category ranges for L* and N*
plus the Unicode White_Space set. Tokenization therefore does not silently inherit an operating
system regex engine, but the checked-in tables are still a semantic dependency: category changes
between Unicode versions can change segmentation. Record the TorchLean revision as well as the BPE
files when exact reproducibility matters.
2.3.6.3. Run The Maintained BPE Trainer
First prepare the example corpus, then run the CUDA trainer with both tokenizer paths:
python3 scripts/datasets/download_example_data.py --tiny-shakespeare lake -R -K cuda=true exe torchlean text_gpt2 --device cuda \ --data-file data/real/text/tiny_shakespeare.txt \ --bpe-vocab data/real/gpt2/vocab.json \ --bpe-merges data/real/gpt2/merges.txt \ --allow-small-data --max-chars 20000 --steps 1 \ --prompt "First Citizen:" --generate 0
The data script prepares the corpus; it does not supply the tokenizer pair. The command prints BPE loading progress, a first shifted token window, and before/after loss. Both BPE flags are required together. Omitting both selects the runner's byte-token path instead.
This runner is CUDA-only. It does not load OpenAI or Hugging Face model weights. Its BPE mode trains a randomly initialized TorchLean Transformer with batch size two, a one-token context, and a local projection of at most 512 observed GPT-2 ids; ids outside that retained set map to the local fallback id. It exercises real file parsing, tokenization, shifted-window construction, training, and decode plumbing, but it is neither GPT-2-small nor evidence of checkpoint-level tokenizer equivalence.
That completes the text-specific detour. The rest of the chapter returns to a question shared by numeric, image, and text datasets: what a batch means and when it is materialized.
2.3.7. Two Meanings Of “Batch Size”
The distinction here is important.
2.3.7.1. Tensor minibatches
Data.batchDataset changes the item shapes:
def batched :
Trainer.Dataset (shape![2, 2]) (shape![2, 1]) :=
Data.batchDataset 2 xorData
(shuffle := true)
(seed := 42)
The model must accept [2,2] and return [2,1]. One forward/backward operation processes the whole
tensor minibatch.
Using batch size five for this four-row XOR dataset would produce no typed batches because
Data.batchDataset defaults to dropLast := true. Choosing two here makes both batches full and
keeps the example executable.
2.3.7.2. Groups of unbatched samples
Trainer.TrainOptions.batchSize counts dataset items per optimizer update. On a model [2] → [1],
ordinary dataset items are individual samples. Every selected item is differentiated at the same
parameter point, the resulting gradient packs are averaged, and the optimizer is applied once. The
reported loss is the corresponding mean of the item losses. This path does not insert a leading
tensor axis.
For a Data.batchDataset, each item already contains a fixed-size tensor minibatch. Keep
TrainOptions.batchSize := 1 to perform one vectorized device pass per update. A larger value
accumulates gradients across several tensor minibatches. On CUDA, generic accumulation currently
synchronizes the parameter pack for the host optimizer update, so one typed batch item per update
is the fast path for large batches.
2.3.8. Why The Final Partial Batch Is Dropped
A model accepting [5,2] cannot receive [3,2]; these are different Lean types. Therefore the
fixed-size typed batching path uses dropLast := true. In a 23-sample dataset with batch size five,
four full minibatches are produced and three samples remain.
Dropping is one policy, not a law of machine learning. Alternatives include:
-
padding to five and carrying a validity mask;
-
bucketing examples so each group has a fixed length;
-
using a dynamic wrapper at a lower runtime layer;
-
choosing a batch size that divides the dataset.
Each alternative changes the data contract. TorchLean refuses to silently change the shape of the last item.
2.3.9. Materialized Loaders And Epoch State
Trainer.Dataset delays scalar choice. Lower-level manual code may already own a
materialized:
Data.Dataset α inputShape targetShape
Data.batchLoader constructs a loader whose batch size appears in its type.
Data.BatchLoader.epoch name loader returns both:
-
the full typed batches for this epoch;
-
updated deterministic shuffle state for the next epoch.
This functional state transition makes data order reproducible. There is no hidden global RNG whose position depends on unrelated code.
Use the lower loader API for a custom epoch loop. Ordinary model training should prefer
Data.batchDataset, Data.tabularCsvDataset, or another dataset constructor.
2.3.10. Generated Streams
Some workloads are not finite passes over a stored dataset. PINNs resample collocation points, reinforcement-learning agents collect new transitions, and language models may generate windows from a large file on demand.
Trainer.Manual.StepBatchStream α shapes represents a source indexed by the training step. The
shape list remains fixed in the type, while values can be generated or loaded lazily.
This gives the loop explicit control over:
-
the step number;
-
generator state;
-
file position;
-
simulator state;
-
checkpoint restoration.
A generated stream should log enough state to reproduce a batch. “Seed 42” is insufficient if the generator also depends on an evolving environment or file cursor.
2.3.11. Reproducibility Needs More Than One Seed
At minimum, record:
Choice | Example |
|---|---|
model initialization | trainer seed |
sample order | loader/shuffle seed |
source identity | file path and preferably content hash |
preprocessing | tokenizer, normalization, column split |
batch policy | size, shuffling, dropping, padding |
runtime | scalar semantics, backend, device |
The CSV example uses 2026 for both model initialization and shuffling for convenience. They are
conceptually separate choices and may be configured independently in a larger experiment.
Training can write a JSON TrainLog:
let trained ← trainer.train data
{ steps := 200
log := .json outPath
title := "small regression" }
The log is an execution artifact. It can support debugging and reproducibility, but it is not a certificate of convergence or generalization.
2.3.12. Continue With Training
The complete minibatch example is runnable as:
lake exe torchlean quickstart_minibatch_mlp \ --device cpu --batch 5 --steps 5 --seed 2026
It exercises CSV parsing, deterministic shuffling, fixed-size typed batching, model execution, autograd, Adam, and prediction. The next chapter opens the training loop and explains what state changes at each optimizer step.
Sources: