Architecture - xkqg/DeepSharp GitHub Wiki
Architecture
In short
This page gives the decisions behind DeepSharp's design, and why each was taken. The one rule to remember: anything that learns from the data is fitted on the training split alone and replayed unchanged everywhere after. The rest of the design hangs off four seams β where the arithmetic runs, where the rows come from, what learns, and what a model trained elsewhere looks like here β and a package implements exactly one of them.
| You want to⦠| Write |
|---|---|
| run on the light engine | nothing β it is the default, new CpuBackend() |
| train on libtorch | new FitOptions(seed) { Backend = TorchBackend.OnCpu() } |
| serve predictions on an engine | trained.Predict(rows, engine) |
| say what an engine's name in a declaration stands for | new Engines().Use("torch", TorchBackend.OnCpu()) |
| train the network a pipeline declares | pipeline.Train(engines) |
| run a pipeline for what one learner needs | pipeline.RunFor(needs) |
| read a model trained elsewhere | importer.Read(file), on an IImporter |
| hand numbers to a network by the path of each slot | network.Load(entries) |
| make a tensor from values | Tensor.From(shape, values) |
The rest of this page explains each decision, grouped by the part of the library it shapes.
The version kept beside the code is
ARCHITECTURE.md; this page is the same
reasoning for a reader who has not cloned anything.
PDD β pipeline-driven design
The idea this library is built around: the course from raw data to a validated model is declared in advance as one artefact and then replayed, rather than performed again at each stage.
The rule that gives it teeth is that anything which learns from the data β a mean, the value that fills a gap, the categories an encoder knows β is fitted on the training split alone and replayed unchanged everywhere after.
PDD has the whole of it: the sequence, what the artefact holds, what it deliberately does not, and the mistake it removes.
The shape of it
Src/DeepSharp/Tensors/ Shape, Tensor and its TensorStorage, Window, ITensorBackend and what it refuses,
CpuBackend, RecordingBackend
Src/DeepSharp/Networks/ the layers, networks, losses, optimizers, the loop, a network's file, and the load
every number trained elsewhere goes in through (IImporter, SlotEntry)
Src/DeepSharp.Pipelines/ the data half: the steps, the rules, the file, the one walk every run is, a run for
each learner, the report
Src/DeepSharp.Pipelines.Parquet/ a reader of Apache Parquet files, through Parquet.Net
Src/DeepSharp.Pipelines.Excel/ a reader of Excel workbooks, through ExcelDataReader
Src/DeepSharp.Pipelines.Images/ a reader of a folder of pictures, a folder to a class, through StbImageSharp
Src/DeepSharp.Pipelines.Sound/ a reader of a folder of WAV recordings, a folder to a class, through NAudio.Core
Src/DeepSharp.Pipelines.Json/ a reader of JSON files holding an array of records
Src/DeepSharp.Pipelines.DataFrame/ a reader of Microsoft's DataFrame, through MatPlotLibNet.DataFrame
Src/DeepSharp.Pipelines.Indicators/ indicators over a series, as verbs
Src/DeepSharp.Pipelines.Binance/ a live source: the candles of a market on Binance, fetched once over a closed window
and landed as a file beside its record; the HTTP and the retries, through Polly.Core
Src/DeepSharp.Learners.Networks/ where a network meets a pipeline: trained behind it, serving, the one file
Src/DeepSharp.Learners.ML/ where a trainer from ML.NET is declared, and its model read and written; no ML.NET
Src/DeepSharp.Learners.MLNet/ the half that carries ML.NET: the rows handed over, the tree fitted, the file written
Src/DeepSharp.Backends.TorchSharp/ an engine on libtorch, through TorchSharp, on the processor or a graphics card
Src/DeepSharp.Import.PyTorch/ a reader of a safetensors file and of what torch.save writes
Src/DeepSharp.Import.Keras/ a reader of the .keras and .h5 files Keras saves, through PureHDF
Src/DeepSharp.Import.Onnx/ a reader of ONNX graphs, through OnnxSharp
Src/DeepSharp.Export.Onnx/ a writer of ONNX graphs from a network's steps, through OnnxSharp's messages
Src/DeepSharp.Charts/ every chart, as SVG, drawn with MatPlotLibNet; the report, as HTML
Src/DeepSharp.Verso.Notebooks/ a pipeline written as a notebook in Verso
Src/DeepSharp.Verso.Api/ an application of your own that hosts the notebook
Src/DeepSharp.Verso.Serve/ DeepSharp's own server, the notebook in a browser
Samples/ runnable programs, a sample notebook, and the published data they read
tools/ the coverage check, and the checks that start what a person installs: the server, the
notebook as Verso installs it, the libtorch engine, the ML.NET learner and the landing
Tst/DeepSharp/ the tests of the libraries
Tst/DeepSharp.Import.TestParts/ importers those tests read files with, written as another reader's would be
Tst/DeepSharp.Backends.TestParts/ engines those tests hand a network, written outside the library's internals
Tst/DeepSharp.Backends.Contract/ what every engine is held to, written once against what the library publishes
Tst/DeepSharp.Backends.TorchSharp/ the libtorch engine's tests: every engine contract, on the processor and a card
Tst/DeepSharp.Learners.MLNet/ the ML.NET learner's tests, which carry ML.NET and the native side it comes with
Tst/DeepSharp.Verso.Notebooks/ the notebook's tests, run in the host an application of your own uses
Tst/DeepSharp.Verso.Api/ the host's tests, run on Verso's own engine the way an application runs it
Tst/DeepSharp.Verso.Serve/ the server's tests, run against the server started for real
Tst/DeepSharp.Verso.Serve.TestParts/ parts the server's tests carry beside it, found as another's parts are found
The four seams
A package that implements a seam implements exactly one, and the four are not interchangeable:
| seam | what it decides | implemented by |
|---|---|---|
ITensorBackend |
where the arithmetic runs | the light engine and libtorch |
IRowSource |
where the rows come from | a file format, a database reader, a landed fetch |
| a learner | what learns from prepared data | a network, or a trainer from ML.NET |
IImporter |
what a model trained elsewhere looks like here | a reader of one framework's files |
A seam is only real when two implementations of it differ in kind, and each is held to that:
- a pure-managed engine beside a native one;
- rows from a file and from a database;
- an importer that reads numbers alone into a network it is handed, beside one that builds the network its file describes.
Packages that implement no seam
Four kinds of package implement none of the four, on purpose, and each is a package for a reason of its own:
- The notebook is a way of writing a pipeline and looking at it, and sits beside the seams as the charts do.
- A live source feeds the second seam without implementing it. What it lands is a file, and the core's own
comma-separated reader is the
IRowSourceof that landed fetch. So it is a package for what it carries β the HTTP, the retries and the pacing a project serving a trained model has no use for. See Live sources. - The indicators are verbs over a series, a package for the library whose arithmetic they borrow.
- The ONNX writer is the mirror of the importer and no fifth seam: what it writes is a file for another runtime, produced from the model, so a project that only trains carries neither the messages it writes with nor the proof of them. See Exporting a model.
The notebook's packages
Two packages follow the notebook: DeepSharp.Verso.Api, with which an application of your own hosts it, and
DeepSharp.Verso.Serve, DeepSharp's own server that shows it in a browser, built on Api.
A folder is created the day it has code in it. The notebook package stays the same code in every place. What two of them would share is kept in one package beside them rather than written into each β a package that appears with the first type both sides compile against; none does yet.
The pipeline
One declaration, whichever way it is written
A pipeline can be written four ways β as a C# chain, through the extension point another package uses, as a file, or as a notebook β and all four become one declaration. Its constructor is where the rules live:
- nothing learns before the split;
- one source, one schema, one split, one order, one output;
- the schema directly after the source;
- rows dropped and ordered before the split;
- every column read where it exists.
A rule enforced by one door only is advice with a lock on one of them.
What a step does is the one capability it implements β opening rows, declaring columns, ordering, adding, dropping, dividing, learning, producing evidence. Every run, replay and view is the same walk over the steps in the order they were written.
Each step lists its parameters once, and the file's schema, the verb reference, the template and the notebook's form are all generated from those lists.
A row is known by what it says
Every row carries a key made from its own contents. A split ranks rows by it, so the same rows land in the same parts whatever order a file lists them in, and every copy of a repeated row lands where its first copy does.
What a fit learned is keyed by the steps it was learned behind, so a fit is never used under steps that changed after it.
A column the schema calls the id travels with its rows and is never a feature. A source that joins two files says what becomes of a row without a partner and never guesses.
A split at random can deal its test rows by a seed of their own, so a search that deals the rest again many times never chooses on rows another deal learned from.
What a model is asked to predict, and the way back
The answer is named right below the split, before anything that learns, and each kind of answer is a verb of its own:
- one column;
- the shares of a whole;
- labels that are each nought or one;
- several free numbers;
- a value read some rows on.
A package adds a kind the way it adds any verb.
Shares can be in an order, which the output says, and a measure or a loss that follows an order is refused against one that does not. A flock that arrived short has what is left of the whole made as one more answer, outside the order.
An answer read from later rows is allowed only below a split in time whose gap is at least as wide, so the last training rows never learn their answers from the rows a model is measured on.
Predictions come back in the units each answer was read in: the steps are walked backwards, and on through the column an answer was made from. A way back that needs the row a number belongs to β a share of a flock, a return on a price β reads that row as it was read, and the run checks every way back before anything is handed over.
What can be done to a column is said once
Taking a column in, leaving it out and changing its kind are one set of operations for every door β the notebook's grid and its list, a take-over, code. They judge nothing: the rules every declaration keeps do that, so what is offered and what is allowed cannot drift apart.
What a pipeline decided about its columns can be saved on its own and taken over by another pipeline, written again next month over new rows. A take-over lists every decision it would change before anything is applied.
What a column holds is proposed, and said by a person
A source's cells are read into a proposal of each column's kind β true or false, whole numbers, numbers, moments, a category, text β so nobody types what a file already shows, and nobody finds a kind decided for them.
The schema is still what a person writes, and nothing takes the proposal in by itself. A pipeline that did would work its schema out again from whatever the file held on the day it ran.
The proposal reads every cell of every row with the reading the schema binds by. A kind is a declaration and comes before the split, and a verdict drawn from some rows is broken by a cell among the others.
- A moment written with its day and month in an order the cells cannot settle is offered with both formats, never proposed as one.
- A source that knows what its columns hold, as a database does, says so, and that outranks what the cells look like.
- What depends on how the values are spread β the categories there are, the extremes, what should not be there β is measured on the training rows, by the steps and the profile.
The notebook
A notebook is one more way of writing the declaration
Each block of the Notebook is one step. What a block shows is worked out from the steps above it each time it is asked for β never saved with the notebook, because it is somebody's own data.
The same package runs in Verso's VS Code extension, in verso serve, in DeepSharp's own server and in an application
of your own, with the same code in each.
The notebook outside Verso's editors
DeepSharp.Verso.Api hosts the notebook in an application of your own the way Verso's editors host it.
- One file is one open notebook however many views show it.
- One thing at a time. Whatever is done to it β typing, running, a click, a button β takes its turn, one at a time, because the engine serves one caller at a time.
- Every view is told what changed, version by version. The engine says nothing when a cell is cleared, inserted or taken away, so the host compares the notebook with its last version instead of passing on what the engine says.
- Every version says what Verso's editors show beside the cells: the run under way and what the engine runs that no run owns, the toolbar, whether anything is unsaved, what became of the kernels, the layout and the theme and what the layout draws of its own, and what the notebook says of itself.
- Opening and saving work as Verso's browser editor opens and saves a notebook, with nothing written into it that the engine only falls back on.
- It registers the notebook's parts itself, so a program published as a single file, with nothing beside it for the engine to find, has them too.
- The drawing stays the application's: it is handed every cell, and what the cell shows, as plain values.
DeepSharp's own server
DeepSharp.Verso.Serve is DeepSharp's own server built on it, the tool deepsharp-serve. The notebook it serves runs
code as the person who started it, so the server is shut to everyone else β this computer alone, the token it printed,
a name of this computer, and nothing served from its folder; Security has the detail.
Its page is one file the tool carries, with no framework and nothing fetched from anywhere. The libraries Verso's editor fetches for diagrams and formulas are carried inside it, and so is DOMPurify, which every piece of HTML the page places passes first. So what a notebook file carries is drawn and runs nothing, where Verso's editor places it as written.
The page takes over what Verso's browser editor does, with that editor as the reference, duty by duty: one socket for each tab, the panels, the layouts and the themes, the keys, what a cell shows and what the page says. It sends what a person does the way Verso's own router means to: a button on its click, a box or a select on its change, and nothing on a key.
Before a package leaves a run of the workflows, the one just made is installed and started as a person starts it, on each runtime it can run on. A package can lack what the build had while every test, which runs the build, still passes.
The learner
What learns is a plug
The pipeline ends at the handover, and what learns plugs in there: a network built here, a trainer from ML.NET β Microsoft's machine-learning library β or something you wrote. All are fed the same prepared rows, which is the only reason two of them can honestly be compared.
The first learner is the network, in DeepSharp.Learners.Networks: the one package that references both the tensors
and the pipeline, which reference neither it nor each other. A trainer from ML.NET is the second, in packages of its
own: ML.NET learner.
A learner says what it needs of its features where they are handed to it. A network takes every feature between minus one and one, and the handover refuses, all at once, every feature the steps do not declare to land there. That is read from the declaration rather than the rows, so a feature that happens to lie in range on these rows is not taken for one that always will, and a pipeline loaded from its file is held to the same answer.
A pipeline runs for the learner that learns from it. The same prepared data is not the same representation for
every learner β a tree is indifferent to scale and harmed by a wide one-hot expansion. RunFor(needs):
- leaves out a step that only scales a feature, for a learner indifferent to scale;
- hands each category over as its place in the list the training rows held, to a learner that takes categories itself.
A step says which learners need it, and what stands in its place for the others, by IMeetsANeed, and only a step that
learns can. A step stays whenever leaving it out would change more than what the learner does without.
What is left out is decided from the declaration and the need alone, and written into the run's file as skipped, never dropped in silence. So the artefact still says what each learner was fed β the only reason two learners measured by one report can be compared.
The learner is declared with the pipeline, and the pipeline still knows no learner
The course from raw data to a validated model is declared in advance as one artefact, and its last stretch used to be outside it: a network was written beside the pipeline and fitted by hand, so two runs of one declaration could train different models and the file said nothing about which. A declaration now names the learner it is written for, and the file says what was trained behind its steps.
The pipeline does that without knowing a learner. INamesTheLearner is a step that acts on nothing, as an output
that only names the answer and a report that only names its measures do. It says one thing the pipeline uses: what the
learner needs of its features. Everything else is the step's own parameters, which only the package that brings the
verb reads, so DeepSharp.Pipelines still references no learner, no engine and no network. There is at most one,
below the output it learns to answer.
The model is text in the words a saved network already writes β dense with its units, adam with its rate.
Every setting a word takes is written down, because a declaration is replayed from what it says, and a number nobody
wrote would make two runs of one file differ as soon as a default moved.
So the file names no vocabulary: TensorFlow's words and PyTorch's lower onto one model, and each door writes the number its own library leaves where a caller says none. Keras's Adam and PyTorch's differ in their epsilon, so the two write different files, rightly, being different runs.
The engine is a name, light unless said, since an engine could not be a file and a package name would tie a model
to a machine. The application says what it stands for through Engines.Use, and a name nothing was given is refused
rather than answered by an engine that would quietly give other numbers.
Training is the learner package's. Run() still answers the prepared rows; pipeline.Train(), in
DeepSharp.Learners.Networks, runs the pipeline for the learner the declaration names and gives the same
TrainedNetwork a network written as code does, number for number. The file gains a step and nothing else, so a model
saved by an earlier release still loads: a network is held to its pipeline by the digest of the text that pipeline
carries.
Tensors and engines
A tensor knows nothing about arithmetic
Tensor is a shape and the values that fill it. Everything done to one is done by an ITensorBackend.
The alternative reads better for exactly one line β a.Add(b) β and then decides the architecture. The tensor would
have to know where its arithmetic happens, so it would have to hold a backend, so either every tensor carries one or
there is a shared one somewhere. A shared one is a global that any code in the process can reach and replace, and then
two halves of one program can disagree about where their work runs.
So the backend is handed to what needs it, and the model stays independent of it. The seam exists from the first line rather than being retrofitted, because retrofitting means touching every operation written without it.
A tensor's values live where its engine keeps them
Tensor stays one sealed type, and where its values live is its TensorStorage: this machine's memory for a tensor
made here, or the storage of the engine that made it β memory outside .NET, a graphics card. An engine hands back what
it makes as Tensor.On(shape, storage) and knows its own when it comes back.
An engine's own tensor type in the signatures, or a tensor generic over its storage, would each have changed every member that takes a tensor β every network written as code among them β and a model would have had to name its engine.
What the seam refuses is said once, in TensorOperandExtensions, and every engine asks it first, so each refuses what
the light one refuses, in the same words.
What an engine may have beyond the seam is a capability
A faster way to do what the seam already asks is an interface an engine may implement, not a member every engine must: a
product that leaves out a copy (IMultipliesTurnedLeft), a step of Adam in one pass (IStepsAdam), and matrices stacked
on their rows multiplied block by block (IMultipliesInBlocks, which attention is built on). Whoever needs one asks for it
through the seam's own words β backend.MatMulInBlocks(...) β and an engine that has not got it is asked for the operations
it has, to the same numbers, only slower. So an engine written before a capability existed still compiles and answers as it
did. The cost of that choice is that an engine which wraps another has to pass a capability on, or it is the engine that has
not got it.
Every engine is held to one contract, per operation and per step
The tests of the seam, the layers, the losses, the optimizers, the loop and the network's file are written once, against what the library publishes, and run on every engine:
- the light one;
- an engine of the tests' own on memory allocated outside .NET, whose totals are single precision;
- libtorch on the processor and a card.
A total lies within three roundings of the size of the terms it adds up, since two engines add the same terms in their own order. A value worked out value by value lies within PyTorch's own float tolerance.
The promise is per step, not per run. Two engines that round in another order drift apart over many steps, as any two float engines do β the networks sample's Titanic run ended with weights 0.08 apart on libtorch's processor build. That is why a checkpoint names the engine its run was on, and a run goes on from it on that engine alone.
The engine is a choice
An engine gives you fast arithmetic. It does not give you a way to describe a network in C#, pour real data into it, drive a training loop, keep checkpoints or show what happened.
So CpuBackend and TorchBackend stand side by side behind the seam. The line that picks one is written where the
arithmetic is asked for β FitOptions.Backend for a run, trained.Predict(rows, engine) for serving β and nothing
keeps it: the network holds no engine and its file names none.
libtorch is a dependency like any other, borrowed rather than written, and its package brings TorchSharp alone. The application brings libtorch, for the processor on its platform or for a card, since a package that brought it would bring every platform's to every application.
Gradients are worked out by the same recording on every engine, libtorch included, rather than by libtorch's own history, which marks the tensors a network and its checkpoints share and switches a mode on for a thread. TorchSharp backend has what it is held to and when it is the faster.
A gradient is worked out by a backend that records one pass
The recording is a backend like any other: RecordingBackend wraps the one the arithmetic runs on, is handed to the
pass that wants gradients, writes down each operation as it runs it, and is dropped once it has worked back from the
loss.
Nothing holds one or reaches for one, so there is no mode switched on somewhere and no recording that outlives its pass. What it keeps is a record of one pass's arithmetic, not a second description of the model, so a model written as plain code is differentiated as it was written.
Every operation on the seam carries its rule for sending a gradient back, written in the seam's own operations. So an operation added without one does not compile, and the way back runs wherever the arithmetic does. The numbers asked about are known by which tensor they are, never by what they hold.
Each rule is checked by nudging every input a little either way and watching the loss, on the wrapped backend alone, so the recording never checks itself.
A tensor never changes
Handing the same tensor to two layers is safe, and Tensor.From copies what it is given, so a caller reusing a scratch
buffer cannot rewrite a tensor they have already passed on. An in-place variant arrives when a measurement shows the
copying costs something that matters β not before.
Measured on one machine, a training step on one thread:
| step | time | allocated | collector's pause |
|---|---|---|---|
| thirty-two Titanic rows through a dense layer of 16 | 19.5 Β΅s | 51 KB | less than half a per cent of its time |
| one of thirty-two 28-by-28 images through eight 3-by-3 filters | 11.9 ms on .NET 10, 11.8 ms on .NET 8 | 12.6 MB | between 0.7 and 1.4 per cent |
So nothing is changed in place yet. ARCHITECTURE.md has the full measurement.
A shape has no meaningless default
A struct can always be brought into existence without running a constructor: default(Shape), an array of them, a
field nobody assigned. So that state means something sensible β no axes, which is a single value, which is what a loss
is. Count is read rather than stored for that reason alone.
The product of the axes is accumulated as a long and refused above int.MaxValue. An int product wraps around in
silence and buys a buffer far too small for what is about to be written into it, which surfaces as memory corruption
thousands of operations later, nowhere near the shape that caused it.
The CPU backend uses .NET's own primitives
System.Numerics.Tensors gives vectorised element-wise arithmetic with nothing native behind it. That is the promise:
a model that travels inside an ordinary application. For comparison, libtorch for the processor is 56.9 MB to 128.2 MB
to download for each platform, and 245 MB to 498 MB installed, before any build for a card.
Those primitives hold no matrix product, so the light engine writes its own. Every total it adds up is kept in double precision: added up in single precision, ten million tenths came to a mean of 0.1087937.
At a small network's size it is the faster engine: a Titanic training step took 38.8 Β΅s on it and 336 Β΅s on libtorch, which spends longer handing each small operation over than the operation takes. Work that outgrows it β a convolution's step took 12.1 ms on it and 3.5 on libtorch on one thread β goes to libtorch behind the same seam, which never becomes required.
Every draw is counted, and none is kept
What a layer starts at, the order an epoch takes its rows in and which values a dropout leaves out are all drawn from one source that keeps nothing but its seed. Each draw is counted from the seed, what it is for, the epoch and the step β Philox, the generator NumPy ships.
So a run resumed at an epoch draws what it would have drawn, and a layer added to a network moves no other layer's start.
Networks
A layer is code, and a network is layers
A network is a layer that holds layers, written as code or as a stack, and every number it holds is a slot with a dotted path. A slot either learns, and an optimizer moves it, or is measured, and only a training pass moves it β so the validation rows never shape what a network keeps.
A pass says whether it trains or evaluates, so no layer carries a mode.
The layers, the losses and the optimizers start and work as PyTorch's do, and each was matched against PyTorch on the same rows; the loop and its early stopping are Keras's.
A layer begins with one value
Every question about how a layer starts β how its weights begin, how its bias begins, whether it has a bias at all,
which draws it all comes from β travels as one value, a Beginning. It sits in the place the draws were in each
constructor, because every layer's constructor was already at the four parameters a method may have.
new Beginning(draws)starts a layer as it always was started.- A layer without a bias has no slot for one and draws nothing for it.
- No slot is renamed, because a slot's path is its identity in a file's key, in a checkpoint's slots and in a checkpoint's memory.
- Neither vocabulary's habit is imposed: a dense layer in Keras's words starts as PyTorch's does, and Keras's own start is asked for.
A start is how a layer is begun and never reaches a file, which holds the numbers it learned.
A parameter carries the rules of its numbers
A penalty (L1, L2), a bound (a largest norm for each unit) and a hold belong to the parameter whose numbers they are about, not to the layer or the run, so a normalisation can penalise its scale and not its shift.
The penalty is Keras's regularizer and sits in the loss the run reports; the weight decay is PyTorch's and sits in the optimizer's step. They are two doors, each in its library's vocabulary, and not one thing said twice.
A held parameter is out of the loop altogether β no gradient is worked out for it, and the optimizer remembers nothing of it. A checkpoint records which layers were held, so a run is never gone on with others.
A part says only what it holds
The settings added to a part are optional, mean what the part always meant by being left out, and are not written when they hold that meaning. A part of a network's file is written as the oldest version that says what it holds.
So a network that uses nothing that was added is the file it always was, and the files the published packages wrote are read, written again and served by every release after them.
One model, two vocabularies
Stack the layers in Keras's words, or write the forward pass as code. The words are read once and lowered onto the same stack of layers code would build, every width worked out from the rows, every start drawn by the layer's place β so both give the same network bit for bit, and the same file.
A network described in words is built at its first fit, from the rows it learns from and the run's seed, so one recorded number reproduces the whole run.
A network and its pipeline are one file
A network trained behind a pipeline is kept with it: the network's part names what it was trained on, down to the SHA-256 of the pipeline's own file as it was fitted, and the pipeline's file stands beside it. The same names are not the same fit, so a network is read only beside the very fit it was trained behind.
Every fault in the file is named at once, at its line and column, and only kinds registered with the reader are made.
A checkpoint adds what the run needs to go on, and what the run went under β the seed, the batch size, the early stopping, the clips, the window of batches, the average, the layers held and the engine. Each decides what the run does next, and a run handed others is refused, naming each difference.
Models trained elsewhere
Numbers trained elsewhere go in by path, under the same rule
A model trained somewhere else reaches a network here through an importer, IImporter, which hands back what reading a
network's own file does: the network, its slots holding the file's numbers, and its loss. There are two kinds:
- an importer of numbers alone is handed the network they belong to;
- one of a file that describes the network builds it, in the same words a network described here is built in, so a graph beside the model is never kept.
Either ends in network.Load, which holds the numbers to the network as PyTorch's load_state_dict does with
strict=True β every slot once, each of its shape, every value finite, all of them or none. A network's own file goes
through the same load, so both refuse in the same words.
Layouts are what each layer or node declares, never what the numbers look like: a square matrix turned round is as square as one that is not. Importing a model has each reader.
A pickle is a program, so it is read as PyTorch reads weights
The file torch.save writes holds a pickle, and a pickle is a program whose instructions name functions and call them.
The protection PyTorch itself settled on is not a library but an interpreter of its own,
torch.load(weights_only=True). The one part of reading this format that cannot be borrowed is the refusal, so
DeepSharp does the same: an interpreter that carries out only PyTorch's own instructions and builds only what a state
dictionary is made of, refusing every other name at the byte that names it.
The pickle reader for .NET was measured first and set aside. It is taught names only in a dictionary every reader in the process shares, and it builds something for every name a file holds before anything could refuse it.
What a file can make the reader do is bounded by the file and by the network β Security has the bounds.
What a file says is shown as words
A refusal names what a file holds β a tensor's name, a record, a key β and whoever wrote the file chose those words. So
every message that shows them shows them through one rule, Quoted(): a character that would break the message's
line, hide what follows it or turn it round is written as its escape, and a long name is cut short with its length.
The pipeline's package carries none of the deep-learning core, so it keeps the same rule as a copy of its own, and a test hands both copies every kind of text the rule treats apart and holds them to the same answer.
Searching and drawing
A search chooses by the validation rows
Trying many networks is a Study: the pipeline is run once for each deal, every candidate is trained on those rows and
judged by the validation loss, and only the winner's networks β with the report's measures of the test rows β are
kept.
A sampler is a seam; random search ships, and a smarter one is written elsewhere. A Comparison asks the same of two
pipelines, deal by deal, and reports the differences with their spread. The test rows are for the winner alone.
Charts come from the loop, not from the caller
The charts are drawn from what the loop and the measures already keep β the caller never assembles arrays to plot.
DeepSharp.Charts draws them as SVG with MatPlotLibNet, in a package of its own, so a trainer on a headless
machine carries no renderer.
Deliberately absent
- A global backend, context or session. Nothing reaches for a shared instance. The notebook keeps a
session of its own β which block shows what β held by that notebook's block type and handed to every part
through the host that loaded it, never static and never shared. One thing is as wide as the process, because
what it guards already is: the turn C# runs take in
DeepSharp.Verso.Api, since Verso's C# kernel takes over the process's console while it runs. - A graph that is not a model. The declarative front door produces the same object the imperative one does. Two representations of one network means two engines to keep in step, and they diverge on the first unusual model.
- A native dependency in the core package. Optional, behind the seam, permanently.
- A random source the process shares. Every draw is counted from a seed that is handed in.
- A schedule that watches the validation loss. The rows a model is chosen on would shape the weights it is chosen for.
- A start in a file. A file holds the numbers a layer learned, never how they began.
- Every regularizer and constraint Keras has. A penalty is L1, L2 or both and a bound is a largest norm for each unit; the rest is refused by name where a Keras file says it.
- A word of your own in a pipeline's file. A loss or an optimizer of your own is code; a file names the words every reader has.
- A class label from a prediction. A network answers a chance or a share; where the line between classes lies is for the use to say.