Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions src/CSharp/CSharpExamples/ForwardForward.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. See LICENSE in the project root for license information.
using System;
using System.IO;
using System.Collections.Generic;
using System.Diagnostics;

using TorchSharp;
using static TorchSharp.torchvision;

using TorchSharp.Examples;
using TorchSharp.Examples.Utils;

using static TorchSharp.torch;
using static TorchSharp.torch.nn;
using static TorchSharp.torch.nn.functional;

namespace CSharpExamples
{
/// <summary>
/// Forward-Forward MNIST classification
///
/// Based on: https://github.com/pytorch/examples/tree/main/mnist_forward_forward
///
/// Implements the Forward-Forward algorithm (Geoffrey Hinton, 2022). Instead of
/// backpropagation, each layer is trained independently using a local contrastive loss.
/// Positive examples have the correct label overlaid, negative examples have wrong labels.
/// </summary>
public class ForwardForward
{
internal static void Run(int epochs, int timeout, string logdir)
{
var device =
torch.cuda.is_available() ? torch.CUDA :
torch.mps_is_available() ? torch.MPS :
torch.CPU;

Console.WriteLine();
Console.WriteLine($"\tRunning Forward-Forward MNIST on {device.type} for {epochs} epochs.");
Console.WriteLine();

torch.random.manual_seed(1);

var dataset = "mnist";
var datasetPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "..", "Downloads", dataset);

var sourceDir = datasetPath;
var targetDir = Path.Combine(datasetPath, "test_data");

if (!Directory.Exists(targetDir)) {
Directory.CreateDirectory(targetDir);
Decompress.DecompressGZipFile(Path.Combine(sourceDir, "train-images-idx3-ubyte.gz"), targetDir);
Decompress.DecompressGZipFile(Path.Combine(sourceDir, "train-labels-idx1-ubyte.gz"), targetDir);
Decompress.DecompressGZipFile(Path.Combine(sourceDir, "t10k-images-idx3-ubyte.gz"), targetDir);
Decompress.DecompressGZipFile(Path.Combine(sourceDir, "t10k-labels-idx1-ubyte.gz"), targetDir);
}

Console.WriteLine($"\tLoading data...");

// Load full training set as a single batch for the Forward-Forward algorithm
int trainSize = 50000;
int testSize = 10000;

using (MNISTReader trainReader = new MNISTReader(targetDir, "train", trainSize, device: device),
testReader = new MNISTReader(targetDir, "t10k", testSize, device: device))
{
Stopwatch totalTime = new Stopwatch();
totalTime.Start();

// Get one big batch of training data
Tensor x = null, y = null, xTe = null, yTe = null;

foreach (var (data, target) in trainReader) {
// Flatten the images: (N, 1, 28, 28) -> (N, 784)
x = data.view(data.shape[0], -1);
y = target;
break; // Just the first (and only) batch
}

foreach (var (data, target) in testReader) {
xTe = data.view(data.shape[0], -1);
yTe = target;
break;
}

Console.WriteLine($"\tCreating Forward-Forward network [784, 500, 500]...");

var net = new ForwardForwardNet(new int[] { 784, 500, 500 }, device);

// Create positive and negative examples
var xPos = ForwardForwardNet.OverlayLabelOnInput(x, y);
var yNeg = ForwardForwardNet.GetNegativeLabels(y);
var xNeg = ForwardForwardNet.OverlayLabelOnInput(x, yNeg);

Console.WriteLine($"\tTraining...");
net.Train(xPos, xNeg, epochs, lr: 0.03, logInterval: 10);

// Evaluate
var trainPred = net.Predict(x);
var trainError = 1.0f - trainPred.eq(y).to_type(ScalarType.Float32).mean().item<float>();
Console.WriteLine($"\tTrain error: {trainError:F4}");

var testPred = net.Predict(xTe);
var testError = 1.0f - testPred.eq(yTe).to_type(ScalarType.Float32).mean().item<float>();
Console.WriteLine($"\tTest error: {testError:F4}");

totalTime.Stop();
Console.WriteLine($"Elapsed time: {totalTime.Elapsed.TotalSeconds:F1} s.");
}
}
}
}
123 changes: 123 additions & 0 deletions src/CSharp/CSharpExamples/GAT.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. See LICENSE in the project root for license information.
using System;
using System.Diagnostics;

using TorchSharp;
using TorchSharp.Examples;

using static TorchSharp.torch;
using static TorchSharp.torch.nn;
using static TorchSharp.torch.nn.functional;

namespace CSharpExamples
{
/// <summary>
/// Graph Attention Network (GAT) for node classification
///
/// Based on: https://github.com/pytorch/examples/tree/main/gat
///
/// Implements a 2-layer GAT with multi-head attention for semi-supervised
/// node classification. Uses synthetic graph data for demonstration.
/// </summary>
public class GAT
{
internal static void Run(int epochs, int timeout, string logdir)
{
var device =
torch.cuda.is_available() ? torch.CUDA :
torch.mps_is_available() ? torch.MPS :
torch.CPU;

Console.WriteLine();
Console.WriteLine($"\tRunning GAT on {device.type} for {epochs} epochs, terminating after {TimeSpan.FromSeconds(timeout)}.");
Console.WriteLine();

torch.random.manual_seed(13);

// Synthetic graph data (simulating Cora-like structure)
int numNodes = 2708;
int numFeatures = 1433;
int numClasses = 7;
int hiddenDim = 64;
int numHeads = 8;

Console.WriteLine($"\tGenerating synthetic graph data...");
Console.WriteLine($"\t Nodes: {numNodes}, Features: {numFeatures}, Classes: {numClasses}");
Console.WriteLine($"\t Hidden: {hiddenDim}, Heads: {numHeads}");

var features = torch.randn(numNodes, numFeatures, device: device);
var labels = torch.randint(numClasses, numNodes, device: device);

// Create adjacency matrix with self-loops
var adjMat = torch.eye(numNodes, device: device);
// Add some random edges to simulate graph structure
var rng = new Random(13);
int numEdges = 10556;
for (int e = 0; e < numEdges; e++) {
int i = rng.Next(numNodes);
int j = rng.Next(numNodes);
adjMat[i, j] = 1.0f;
adjMat[j, i] = 1.0f;
}

// Split
var idx = torch.randperm(numNodes, device: device);
var idxTrain = idx.slice(0, 1600, numNodes, 1);
var idxVal = idx.slice(0, 1200, 1600, 1);
var idxTest = idx.slice(0, 0, 1200, 1);
Comment on lines +65 to +67
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slice operations for train/val/test splits appear to be incorrect. Line 65 uses idx.slice(0, 1600, numNodes, 1) which attempts to slice from index 1600 to numNodes with step 1, but based on the context (training should use indices 1600 onwards), this appears backwards. Similarly, lines 66-67 have the same issue. The correct usage should ensure training gets the largest subset, validation gets a medium subset, and test gets the smallest. Verify the slice parameters match the intended data split.

Suggested change
var idxTrain = idx.slice(0, 1600, numNodes, 1);
var idxVal = idx.slice(0, 1200, 1600, 1);
var idxTest = idx.slice(0, 0, 1200, 1);
int numTrain = 1600;
int numVal = 400;
int trainEnd = numTrain;
int valEnd = numTrain + numVal;
var idxTrain = idx.slice(0, 0, trainEnd, 1);
var idxVal = idx.slice(0, trainEnd, valEnd, 1);
var idxTest = idx.slice(0, valEnd, numNodes, 1);

Copilot uses AI. Check for mistakes.

Console.WriteLine($"\tCreating GAT model...");

var model = new GATModel("gat", numFeatures, hiddenDim, numHeads, numClasses,
concat: false, dropout: 0.6, leakyReluSlope: 0.2, device: device);

var optimizer = optim.Adam(model.parameters(), lr: 0.005, weight_decay: 5e-4);
var criterion = NLLLoss();

Console.WriteLine($"\tTraining...");

Stopwatch totalTime = new Stopwatch();
totalTime.Start();

for (int epoch = 1; epoch <= epochs; epoch++) {
using (var d = torch.NewDisposeScope()) {
model.train();
optimizer.zero_grad();

var output = model.forward(features, adjMat);
var loss = criterion.forward(output.index(idxTrain), labels.index(idxTrain));
loss.backward();
optimizer.step();

if (epoch % 20 == 0 || epoch == 1) {
model.eval();
using (torch.no_grad()) {
var evalOutput = model.forward(features, adjMat);

var trainAcc = evalOutput.index(idxTrain).argmax(1)
.eq(labels.index(idxTrain)).to_type(ScalarType.Float32).mean().item<float>();
var valAcc = evalOutput.index(idxVal).argmax(1)
.eq(labels.index(idxVal)).to_type(ScalarType.Float32).mean().item<float>();

Console.WriteLine($"\tEpoch {epoch:D4} | Loss: {loss.item<float>():F4} | Train Acc: {trainAcc:F4} | Val Acc: {valAcc:F4}");
}
}
}

if (totalTime.Elapsed.TotalSeconds > timeout) break;
}

// Final test
model.eval();
using (torch.no_grad()) {
var testOutput = model.forward(features, adjMat);
var testAcc = testOutput.index(idxTest).argmax(1)
.eq(labels.index(idxTest)).to_type(ScalarType.Float32).mean().item<float>();
Console.WriteLine($"\tTest accuracy: {testAcc:F4}");
}

totalTime.Stop();
Console.WriteLine($"Elapsed time: {totalTime.Elapsed.TotalSeconds:F1} s.");
}
}
}
137 changes: 137 additions & 0 deletions src/CSharp/CSharpExamples/GCN.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (c) .NET Foundation and Contributors. All Rights Reserved. See LICENSE in the project root for license information.
using System;
using System.Diagnostics;
using System.Linq;

using TorchSharp;
using TorchSharp.Examples;

using static TorchSharp.torch;
using static TorchSharp.torch.nn;
using static TorchSharp.torch.nn.functional;

namespace CSharpExamples
{
/// <summary>
/// Graph Convolutional Network (GCN) for node classification
///
/// Based on: https://github.com/pytorch/examples/tree/main/gcn
///
/// Implements a 2-layer GCN for semi-supervised node classification.
/// Uses synthetic graph data for demonstration since the Cora dataset
/// requires external download infrastructure.
/// </summary>
public class GCN
{
internal static void Run(int epochs, int timeout, string logdir)
{
var device =
torch.cuda.is_available() ? torch.CUDA :
torch.mps_is_available() ? torch.MPS :
torch.CPU;

Console.WriteLine();
Console.WriteLine($"\tRunning GCN on {device.type} for {epochs} epochs, terminating after {TimeSpan.FromSeconds(timeout)}.");
Console.WriteLine();

torch.random.manual_seed(42);

// Create synthetic graph data for demonstration
// In practice, you would load a real graph dataset like Cora
int numNodes = 2708;
int numFeatures = 1433;
int numClasses = 7;
int hiddenDim = 16;

Console.WriteLine($"\tGenerating synthetic graph data...");
Console.WriteLine($"\t Nodes: {numNodes}, Features: {numFeatures}, Classes: {numClasses}");

// Random features and labels
var features = torch.randn(numNodes, numFeatures, device: device);
var labels = torch.randint(numClasses, numNodes, device: device);

// Create a random sparse adjacency matrix (simulating graph structure)
int numEdges = 10556;
var edgeIdx1 = torch.randint(numNodes, numEdges, device: device);
var edgeIdx2 = torch.randint(numNodes, numEdges, device: device);
var adjMat = torch.zeros(numNodes, numNodes, device: device);

// Add edges and self-loops
for (int i = 0; i < numNodes; i++) {
adjMat[i, i] = 1.0f; // self-loops
}
// Note: In a real implementation, you'd construct the adjacency matrix properly
// and apply the renormalization trick D^(-1/2) A D^(-1/2)
// For now, use identity + random edges normalized by degree
adjMat = adjMat + torch.eye(numNodes, device: device) * 0.1f;

Comment on lines +59 to +67
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code adds an identity matrix scaled by 0.1 to the adjacency matrix that already has self-loops (line 66). This results in diagonal values of 1.1 instead of 1.0, which may not be the intended behavior. The comment on line 63 mentions "In a real implementation, you'd construct the adjacency matrix properly", but this double-addition of self-loops should be corrected even in this synthetic example.

Suggested change
// Add edges and self-loops
for (int i = 0; i < numNodes; i++) {
adjMat[i, i] = 1.0f; // self-loops
}
// Note: In a real implementation, you'd construct the adjacency matrix properly
// and apply the renormalization trick D^(-1/2) A D^(-1/2)
// For now, use identity + random edges normalized by degree
adjMat = adjMat + torch.eye(numNodes, device: device) * 0.1f;
// Add self-loops
for (int i = 0; i < numNodes; i++) {
adjMat[i, i] = 1.0f; // self-loops
}
// Note: In a real implementation, you'd construct the adjacency matrix properly,
// add edges based on the dataset, and apply the renormalization trick D^(-1/2) A D^(-1/2).
// Here we only add self-loops for demonstration; the adjacency is then normalized below.

Copilot uses AI. Check for mistakes.
// Normalize adjacency matrix (simplified)
var degree = adjMat.sum(dim: 1);
var degreeInvSqrt = torch.sqrt(1.0f / degree);
degreeInvSqrt = torch.where(degreeInvSqrt.isinf(), torch.zeros_like(degreeInvSqrt), degreeInvSqrt);
var degreeMatrix = torch.diag(degreeInvSqrt);
adjMat = torch.mm(torch.mm(degreeMatrix, adjMat), degreeMatrix);

// Split into train/val/test
var idx = torch.randperm(numNodes, device: device);
var idxTrain = idx.slice(0, 1500, numNodes, 1);
var idxVal = idx.slice(0, 1000, 1500, 1);
var idxTest = idx.slice(0, 0, 1000, 1);
Comment on lines +76 to +79
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slice operations for train/val/test splits are incorrect. The indices are specified as (dim, start, end, step), but the order should be (dim, start, end, step) where start and end define the range. Line 77 uses idx.slice(0, 1500, numNodes, 1) which would try to slice from index 1500 to numNodes, but based on the context this should be slicing indices 1500 onwards. The correct call should be idx.slice(0, 1500, numNodes, 1) or more simply idx[TensorIndex.Slice(1500, numNodes)]. Similar issues exist on lines 78-79.

Copilot uses AI. Check for mistakes.

Console.WriteLine($"\tCreating GCN model...");

var model = new GCNModel("gcn", numFeatures, hiddenDim, numClasses,
useBias: true, dropoutP: 0.5, device: device);

var optimizer = optim.Adam(model.parameters(), lr: 0.01, weight_decay: 5e-4);
var criterion = NLLLoss();

Console.WriteLine($"\tTraining...");

Stopwatch totalTime = new Stopwatch();
totalTime.Start();

for (int epoch = 1; epoch <= epochs; epoch++) {
using (var d = torch.NewDisposeScope()) {
// Training
model.train();
optimizer.zero_grad();

var output = model.forward(features, adjMat);
var loss = criterion.forward(output.index(idxTrain), labels.index(idxTrain));
loss.backward();
optimizer.step();

if (epoch % 20 == 0 || epoch == 1) {
// Evaluate
model.eval();
using (torch.no_grad()) {
var evalOutput = model.forward(features, adjMat);

var trainAcc = evalOutput.index(idxTrain).argmax(1)
.eq(labels.index(idxTrain)).to_type(ScalarType.Float32).mean().item<float>();
var valAcc = evalOutput.index(idxVal).argmax(1)
.eq(labels.index(idxVal)).to_type(ScalarType.Float32).mean().item<float>();

Console.WriteLine($"\tEpoch {epoch:D4} | Loss: {loss.item<float>():F4} | Train Acc: {trainAcc:F4} | Val Acc: {valAcc:F4}");
}
}
}

if (totalTime.Elapsed.TotalSeconds > timeout) break;
}

// Final test evaluation
model.eval();
using (torch.no_grad()) {
var testOutput = model.forward(features, adjMat);
var testAcc = testOutput.index(idxTest).argmax(1)
.eq(labels.index(idxTest)).to_type(ScalarType.Float32).mean().item<float>();
Console.WriteLine($"\tTest accuracy: {testAcc:F4}");
}

totalTime.Stop();
Console.WriteLine($"Elapsed time: {totalTime.Elapsed.TotalSeconds:F1} s.");
}
}
}
Loading