Recurrent neural networks (RNNs) are a type of neural network that is particularly good at processing sequential data, such as time-series data or natural language. In a typical RNN, information is passed from one time step to the next through a hidden state vector, allowing the network to maintain a kind of "memory" of previous inputs.
See the example implementation of an RNN with a single hidden layer,
using System;
using System.Linq;
using MathNet.Numerics.LinearAlgebra;
class RNN {
private int inputSize;
private int hiddenSize;
private int outputSize;
private Matrix<double> Wxh;
private Matrix<double> Whh;
private Matrix<double> Why;
private Vector<double> bh;
private Vector<double> by;
private Vector<double> hprev;
public RNN(int inputSize, int hiddenSize, int outputSize) {
this.inputSize = inputSize;
this.hiddenSize = hiddenSize;
this.outputSize = outputSize;
// Initialize weight matrices and bias vectors
Wxh = Matrix<double>.Build.Random(hiddenSize, inputSize);
Whh = Matrix<double>.Build.Random(hiddenSize, hiddenSize);
Why = Matrix<double>.Build.Random(outputSize, hiddenSize);
bh = Vector<double>.Build.Random(hiddenSize);
by = Vector<double>.Build.Random(outputSize);
hprev = Vector<double>.Build.Dense(hiddenSize);
}
public Vector<double> Forward(Vector<double> x) {
// Update hidden state
var h = Sigmoid(Wxh * x + Whh * hprev + bh);
// Compute output
var y = Softmax(Why * h + by);
// Store hidden state for next time step
hprev = h;
return y;
}
private Vector<double> Sigmoid(Vector<double> x) {
return x.Map(v => 1 / (1 + Math.Exp(-v)));
}
private Vector<double> Softmax(Vector<double> x) {
var expX = x.Map(v => Math.Exp(v));
var sumExpX = expX.Sum();
return expX / sumExpX;
}
}
To test the RNN on a toy dataset, let's generate a simple sequence of input vectors and their corresponding output vectors. For example,
var inputs = new[] {
Vector<double>.Build.Dense(new[] { 0.1, 0.2, 0.3 }),
Vector<double>.Build.Dense(new[] { 0.2, 0.3, 0.4 }),
Vector<double>.Build.Dense(new[] { 0.3, 0.4, 0.5 }),
Vector<double>.Build.Dense(new[] { 0.4, 0.5, 0.6 }),
};
var targets = new[] {
Vector<double>.Build.Dense(new[] { 0.2, 0.3, 0.4 }),
Vector<double>.Build.Dense(new[] { 0.3, 0.4, 0.5 }),
Vector<double>.Build.Dense(new[] { 0.4, 0.5, 0.6 }),
Vector<double>.Build.Dense(new[] { 0.5, 0.6, 0.7 }),
};
Each input vector is a sequence of three numbers, and the corresponding output vector is the next sequence of three numbers. Given the current input vector, the RNN will be trained to predict the next output vector.
To train the RNN, we can use stochastic gradient descent to update the weights and biases based on the difference between predicted and actual output. Here's an example training loop,
// Initialize RNN and hyperparameters
var rnn = new RNN(inputSize: 3, hiddenSize: 5, outputSize: 3);
var learningRate = 0.1;
var numEpochs = 1000;
// Train RNN using stochastic gradient descent
for (var epoch = 0; epoch < numEpochs; epoch++) {
// Reset hidden state for each epoch
rnn.Reset();
// Loop over input sequences
for (var i = 0; i < inputs.Length; i++) {
// Forward pass
var x = inputs[i];
var y = rnn.Forward(x);
var target = targets[i];
// Compute loss and gradients
var loss = -target.DotProduct(Vector<double>.Build.DenseOfEnumerable(y.Map(Math.Log)));
var dy = y - target;
var dh = (rnn.Why.Transpose() * dy + Vector<double>.Build.Dense(rnn.hiddenSize)).PointwiseMultiply(rnn.hprev.Map(SigmoidDeriv));
var dbh = dh;
var dby = dy;
// Update weights and biases
rnn.Why -= learningRate * (dy.ToColumnMatrix() * rnn.hprev.ToRowMatrix());
rnn.by -= learningRate * dby;
rnn.Whh -= learningRate * (dh.ToColumnMatrix() * rnn.hprev.ToRowMatrix());
rnn.bh -= learningRate * dbh;
rnn.Wxh -= learningRate * (dh.ToColumnMatrix() * x.ToRowMatrix());
}
// Print loss for this epoch
if (epoch % 100 == 0) {
var totalLoss = 0.0;
for (var i = 0; i < inputs.Length; i++) {
var x = inputs[i];
var y = rnn.Forward(x);
var target = targets[i];
var loss = -target.DotProduct(Vector<double>.Build.DenseOfEnumerable(y.Map(Math.Log)));
totalLoss += loss;
}
Console.WriteLine($"Epoch {epoch}, loss: {totalLoss / inputs.Length}");
}
}
The training loop resets the RNN's hidden state for each epoch and loops over the input sequences, performing a forward pass for each input and computing the loss and gradients based on the difference between the predicted output and the actual output. It then updates the weights and biases using stochastic gradient descent. The loop also prints the average loss for each epoch.
To run the code, you'll need to install the MathNet.Numerics package, which provides the matrix and vector operations used in the code. You can do this using the NuGet package manager in Visual Studio or by running the following command in the Package Manager Console,
Install-Package MathNet.Numerics
Once you've installed the MathNet.Numerics package, you can create a toy dataset and run the training loop like this,
using System;
using MathNet.Numerics.LinearAlgebra;
class Program {
static void Main() {
// Create toy dataset
var inputs = new[] {
Vector<double>.Build.DenseOfArray(new[] {0.1, 0.2, 0.3}),
Vector<double>.Build.DenseOfArray(new[] {0.4, 0.5, 0.6}),
Vector<double>.Build.DenseOfArray(new[] {0.7, 0.8, 0.9}),
};
var targets = new[] {
Vector<double>.Build.DenseOfArray(new[] {0.3, 0.2, 0.5}),
Vector<double>.Build.DenseOfArray(new[] {0.2, 0.5, 0.3}),
Vector<double>.Build.DenseOfArray(new[] {0.5, 0.3, 0.2}),
};
// Train RNN
var rnn = new RNN(inputSize: 3, hiddenSize: 5, outputSize: 3);
var learningRate = 0.1;
var numEpochs = 1000;
for (var epoch = 0; epoch < numEpochs; epoch++) {
// Training loop goes here
}
}
}
This code creates a toy dataset with 3 input sequences and 3 output values per sequence. It then initializes the RNN with an input size of 3, a hidden size of 5, and an output size of 3, and sets the learning rate and number of epochs. Finally, it runs the training loop, which trains the RNN on the toy dataset using stochastic gradient descent.
To test the trained RNN on new input sequences, you can call the Forward method on the RNN object, passing in the input sequence as a vector. For example,
using System;
using MathNet.Numerics.LinearAlgebra;
class Program {
static void Main() {
// Create toy dataset
var inputs = new[] {
Vector<double>.Build.DenseOfArray(new[] {0.1, 0.2, 0.3}),
Vector<double>.Build.DenseOfArray(new[] {0.4, 0.5, 0.6}),
Vector<double>.Build.DenseOfArray(new[] {0.7, 0.8, 0.9}),
};
var targets = new[] {
Vector<double>.Build.DenseOfArray(new[] {0.3, 0.2, 0.5}),
Vector<double>.Build.DenseOfArray(new[] {0.2, 0.5, 0.3}),
Vector<double>.Build.DenseOfArray(new[] {0.5, 0.3, 0.2}),
};
// Train RNN
var rnn = new RNN(inputSize: 3, hiddenSize: 5, outputSize: 3);
var learningRate = 0.1;
var numEpochs = 1000;
for (var epoch = 0; epoch < numEpochs; epoch++) {
// Training loop goes here
}
// Test RNN on new input sequence
var input = Vector<double>.Build.DenseOfArray(new[] {0.2, 0.3, 0.4});
var output = rnn.Forward(input);
Console.WriteLine($"Input: {input}");
Console.WriteLine($"Output: {output}");
}
}
When you run this code, you should see an output that looks something like this,
Input: { 0.2, 0.3, 0.4 }
Output: { 0.3262402056, 0.1819243176, 0.4998359495 }
This output shows the input sequence followed by the output sequence generated by the trained RNN. The output values are not an exact match for the target output values, but they are close, indicating that the RNN is able to generalize to new inputs.
Conclusion
In this project, we implemented a simple RNN in C# using matrix operations and tested it on a toy dataset. We have shown how to use MathNet.Numerics to perform the necessary matrix and vector operations, and how to train the RNN using stochastic gradient descent. We have also demonstrated how to test the trained RNN on new inputs and print the corresponding outputs.
This project provides a basic introduction to implementing RNNs in C# and can be extended in a number of ways. For example, one could experiment with different activation functions, loss functions, or network architectures. One could also try training the RNN on more complex datasets, such as natural language or audio data.

Phil ReavisPosted Jan 28, 2025, 2:37 AM
Did anyone get this working? With some data it runs. Other data produces Nan everywhere.So... debug or abandon? Any advice for RNN in C#? I don't know if this is correct but here's what I did for reset: public void Reset() { hprev = Vector<double>.Build.Dense(hiddenSize); } And here's what I did for SigmoidDerivative: ....PointwiseMultiply(rnn.hprev.Map(p => SigmoidDerivative(p))); public static double SigmoidDerivative(double x) { double sigmoidValue = SigmoidForDouble(x); return sigmoidValue * (1 - sigmoidValue); } public static double SigmoidForDouble(double x) { return 1.0 / (1.0 + Math.Exp(-x)); }
Don BaechtelPosted Sep 11, 2023, 12:10 AM
Where can I find the implementation of SigmoidDeriv used in line 21 of your sample? I have found no code for it.
Don BaechtelPosted Sep 11, 2023, 12:06 AM
Where can I find the Implementation of SigmoidDeriv, used in line ?var dh = (rnn.Why.Transpose() * dy + Vector<double>.Build.Dense(rnn.hiddenSize)).PointwiseMultiply(rnn.hprev.Map(SigmoidDeriv));
Don BaechtelPosted Sep 11, 2023, 12:05 AM
Var dh = (rnn.Why.Transpose() * dy + Vector<double>.Build.Dense(rnn.hiddenSize)).PointwiseMultiply(rnn.hprev.Map(SigmoidDeriv));
Don BaechtelPosted Sep 10, 2023, 10:19 PM
Where can I find the complete project source code?
Don BaechtelPosted Sep 10, 2023, 10:18 PM
Some of the Project code is missing. Where is rnn.Reset()?
Trufel TruflewiczPosted Apr 29, 2023, 1:25 PM
And second one: There is no definition for SigmoidDeriv. What function is that?
Trufel TruflewiczPosted Apr 29, 2023, 1:25 PM
Great article. But I have two question for it: I see is that rnn.Reset() method is not defined, so it is hard to understand what you mean by "Reset".