Toro
PyTorch semantics, idiomatic F#. Powered by TorchSharp.
Toro wraps TorchSharp with an F#-idiomatic API. It keeps PyTorch's tensor semantics while using records, computation expressions, and direct method chaining.
- Direct API -- operations throw on failure for concise method chaining, just like PyTorch.
- Records as models -- models are plain F# records.
Model.trainableVarscollects parameters by reflection, so there is no base class to inherit. - Ownership management --
scoped { }CE automatically disposes intermediate tensors. Return values are kept alive past the scope. - Computation expressions for composition --
sequential { }composes layers;pipeline { }enables composition for heterogeneous signatures. - PyTorch-compatible surface -- tensor operations, neural network layers, and training utilities mirror PyTorch naming and behavior.
| Package | Purpose |
|---|---|
Toro | Tensors, DType, Device, SafeTensors I/O |
Toro.NN | Layers, loss functions, optimizers, metrics |
Toro.GNN | Graph neural network layers and batching |
Toro.Vision | Image I/O and transforms (SkiaSharp + Torch) |
Toro.Text | Tokenization via Microsoft.ML.Tokenizers |
Toro.Hub | Hugging Face Hub client for pre-trained models |
Install
dotnet add package Toro
dotnet add package Toro.NN
dotnet add package TorchSharp-cpu
# Optional domain packages
dotnet add package Toro.GNN # Graph neural networks
dotnet add package Toro.Vision # Image transforms
dotnet add package Toro.Text # Tokenization and text preprocessing
Train an XOR model
open Toro
open Toro.NN
let x = Tensor.ofList ([ [0f;0f]; [0f;1f]; [1f;0f]; [1f;1f] ], Cpu)
let y = Tensor.ofList ([ [0f]; [1f]; [1f]; [0f] ], Cpu)
let l1 = Linear.init 2 16 F32 Cpu
let l2 = Linear.init 16 1 F32 Cpu
let model = sequential { l1; Relu; l2 }
let opt = AdamW.createWithLr 0.01 (Model.trainableVars model)
for _ in 1..500 do
scoped {
opt.zeroGrad ()
let pred = model.forward x
let loss = Loss.mse pred y
loss.backward ()
opt.step ()
}
Operations throw on failure. scoped { } disposes intermediate tensors at the end of each iteration.
Define a CNN with records
Models are plain F# records. Each part defines a forward member:
type Features = {
Conv1: Conv2d; Bn1: BatchNorm; Pool1: MaxPool2d
Conv2: Conv2d; Bn2: BatchNorm; Pool2: MaxPool2d
} with
member this.forward(train, x) =
x
|> this.Conv1.forward
|> this.Bn1.forwardT train
|> _.relu()
|> this.Pool1.forward
|> this.Conv2.forward
|> this.Bn2.forwardT train
|> _.relu()
|> this.Pool2.forward
type Classifier = { Fc1: Linear; Drop: Dropout; Fc2: Linear } with
member this.forward(train, x) =
x
|> _.flatten(1, -1)
|> this.Fc1.forward
|> _.relu()
|> this.Drop.forwardT train
|> this.Fc2.forward
type CnnModel = { Features: Features; Classifier: Classifier } with
member this.forward(train, x) =
x |> this.Features.forward(train) |> this.Classifier.forward(train)
Nested records compose naturally. Model.trainableVars walks the entire structure by reflection.
Evaluate
Toro.noGrad (fun () ->
let pred = model.forward testX
let argmax = pred.argmax 1
let eq = (argmax .=. testY).toDType F32
let acc = eq.meanAll ()
printfn "Accuracy: %.1f%%" (acc.item () * 100.0)
)
Examples
| Example | Description |
|---|---|
| LinearRegression | Gradient descent with raw tensors |
| SimpleTraining | XOR with sequential { } CE |
| MnistTraining | CNN image classification |
| MnistCnn | CNN with BatchNorm, Dropout |
| MnistAutoencoder | Autoencoder with image output |
| MnistGan | GAN image generation |
| CharRnn | Character-level text generation with LSTM |
| TextClassifier | Transformer-based text classification |
| SimpleGcn | GNN node classification with GCNConv |
| HubSentiment | Load DistilBERT from Hugging Face Hub for sentiment analysis |