PROWAREtech








.NET: Convolutional Neural Network, Deep Learning, Supervised Learning Example in C#
Supervised learning is a machine learning paradigm for problems where the available data consists of labeled examples, meaning that each data point contains features (covariates) and an associated label. The goal of supervised learning algorithms is learning a function that maps feature vectors (inputs) to labels (output), based on example input-output pairs.
See unsupervised learning version.
Learn about convolutional neural networks.
This code is built upon the neural network example (NeuralNetwork.cs) which is used for the fully connected layers.
See this code in action here and here.
Download these files including example training and the MNIST image files with labels: NEURALNETWORKMNIST.zip. Experiment with the number of neurons and layers. This example usage code requires SixLabors.ImageSharp v3.0.1 (NuGet package).
The Convolutional Neural Network code:
// ConvolutionalNeuralNetwork.cs
using System;
namespace ML
{
namespace CNN1D
{
public class Kernel1D
{
public float[] filter;
public double divisor;
public Kernel1D(float[] filter, double divisor = 1)
{
this.filter = filter;
this.divisor = divisor;
}
}
public static class ConvolutionalNetwork
{
// for 1D CNNs
private static readonly float[] twoGram1DFilter = new float[] { 1, 1 };
private static readonly float[] threeGram1DFilter = new float[] { 1, 1, 1 };
private static readonly float[] fourGram1DFilter = new float[] { 1, 1, 1, 1 };
// for 1D CNNs
public static Kernel1D TwoGram1DFilter = new Kernel1D(twoGram1DFilter);
public static Kernel1D ThreeGram1DFilter = new Kernel1D(threeGram1DFilter);
public static Kernel1D FourGram1DFilter = new Kernel1D(fourGram1DFilter);
// for 1D CNNs
public static void ApplyFilter(double[,] vectors, Kernel1D kernel, double[,] filtered, CNN.IActivation activation)
{
if (filtered.GetLength(0) != vectors.GetLength(0) || filtered.GetLength(1) != vectors.GetLength(1) - (kernel.filter.Length - 1))
throw new ArgumentException($"The filtered and vectors arguments are not a match, or the wrong kernel is being applied.");
for (int row = 0; row < vectors.GetLength(0); row++)
for (int i = 0; i < filtered.GetLength(1); i++)
{
double result = 0;
for (int j = 0; j < kernel.filter.Length; j++)
result += vectors[row, i + j] * kernel.filter[j];
filtered[row, i] = activation.Function(result / kernel.divisor);
}
}
// for 1D CNNs
public static void ApplyMaxOverTimePooling(double[,] filtered, double[,] pooled, int pooledColumn)
{
if (pooled.GetLength(0) != filtered.GetLength(0))
throw new ArgumentException("The pooled argument must have a rows length equal to the rows length of the filtered argument.");
double max;
for (int row = 0; row < filtered.GetLength(0); row++)
{
max = filtered[row, 0];
for (int col = 0; col < filtered.GetLength(1); col++)
max = Math.Max(max, filtered[row, col]);
pooled[row, pooledColumn] = max;
}
}
// for 1D CNNs
public static void FlattenPooled(double[,] pooled, double[] flattened, out double min, out double max)
{
if (flattened.Length < pooled.GetLength(0) * pooled.GetLength(1))
throw new ArgumentException("The flattened argument must have a length greater than or equal to the pooled argument's dimensions multiplied.");
min = max = pooled[0, 0];
// Loop through the rows and columns of the image
for (int row = 0; row < pooled.GetLength(0); row++)
for (int column = 0; column < pooled.GetLength(1); column++)
{
var v = pooled[row, column];
min = Math.Min(min, v);
max = Math.Max(max, v);
flattened[row * pooled.GetLength(1) + column] = v;
}
for (int i = pooled.GetLength(0) * pooled.GetLength(1); i < flattened.Length; i++)
flattened[i] = 0;
}
}
}
namespace CNN2D
{
public class Kernel2D
{
public float[,] filter;
public double divisor;
public Kernel2D(float[,] filter, double divisor = 1)
{
this.filter = filter;
this.divisor = divisor;
}
}
public static class ConvolutionalNetwork
{
// for 2D CNNs
public const int FILTER_2D_SIZE = 3;
public const int POOL_2D_SIZE = 2;
private static readonly float[,] vertEdge2DFilter = new float[,] {
{ -1, 0, 1 },
{ -1, 0, 1 },
{ -1, 0, 1 } };
private static readonly float[,] horzEdge2DFilter = new float[,] {
{ -1, -1, -1 },
{ 0, 0, 0 },
{ 1, 1, 1 } };
private static readonly float[,] diagEdge2DFilter = new float[,] {
{ -1, 0, 0 },
{ 0, 1, 0 },
{ 0, 0, -1 } };
private static readonly float[,] sobelX2DFilter = new float[,] {
{ 1, 0, -1 },
{ 2, 0, -2 },
{ 1, 0, -1 } };
private static readonly float[,] sobelY2DFilter = new float[,] {
{ 1, 2, 1 },
{ 0, 0, 0 },
{ -1, -2, -1 } };
private static readonly float[,] identity2DFilter = new float[,] { // does nothing
{ 0, 0, 0 },
{ 0, 1, 0 },
{ 0, 0, 0 } };
private static readonly float[,] laplacian2DFilter = new float[,] { // used for detecting edges and sharp intensity changes in an image, regardless of their direction
{ -1, -1, -1 },
{ -1, 8, -1 },
{ -1, -1, -1 } };
private static readonly float[,] sharpen2DFilter = new float[,] {
{ 0, -1, 0 },
{ -1, 5, -1 },
{ 0, -1, 0 } };
private static readonly float[,] boxBlur2DFilter = new float[,] { // must apply a divisor of 9.0 to the sum
{ 1, 1, 1 },
{ 1, 1, 1 },
{ 1, 1, 1 } };
// for 2D CNNs
public static Kernel2D VerticalEdge2DFilter = new Kernel2D(vertEdge2DFilter);
public static Kernel2D HorizontalEdge2DFilter = new Kernel2D(horzEdge2DFilter);
public static Kernel2D DiagonalEdge2DFilter = new Kernel2D(diagEdge2DFilter);
public static Kernel2D SobelX2DFilter = new Kernel2D(sobelX2DFilter);
public static Kernel2D SobelY2DFilter = new Kernel2D(sobelY2DFilter);
public static Kernel2D Identity2DFilter = new Kernel2D(identity2DFilter);
public static Kernel2D Laplacian2DFilter = new Kernel2D(laplacian2DFilter);
public static Kernel2D Sharpen2DFilter = new Kernel2D(sharpen2DFilter);
public static Kernel2D BoxBlur2DFilter = new Kernel2D(boxBlur2DFilter, 9);
// for 2D CNNs
public static void ApplyFilterToRightWithZeroPadding(double[,] image, Kernel2D kernel, double[,] filtered, CNN.IActivation activation, bool mergeMaximum = false)
{
if (image.GetLength(0) != image.GetLength(1))
throw new ArgumentException("The image argument must be square.");
if (filtered.GetLength(0) != image.GetLength(0) || filtered.GetLength(1) != image.GetLength(1))
throw new ArgumentException("The filtered argument must be same size as image argument.");
// Calculate padding size
const int padding = FILTER_2D_SIZE / 2;
int image_size = image.GetLength(0);
// Loop through the image with padding
for (int j = padding; j < image_size - padding; j++)
for (int i = padding; i < image_size - padding; i++)
{
double sum = 0;
// Convolve the filter with the image region
for (int fi = 0; fi < FILTER_2D_SIZE; fi++)
for (int fj = 0; fj < FILTER_2D_SIZE; fj++)
sum += image[i + fi - padding, j + fj - padding] * kernel.filter[fi, fj];
// Store the result in the output matrix
if (mergeMaximum)
filtered[i, j] = Math.Max(activation.Function(sum / kernel.divisor), filtered[i, j]);
else
filtered[i, j] = activation.Function(sum / kernel.divisor);
}
}
// for 2D CNNs
public static void ApplyFilterToBottomWithZeroPadding(double[,] image, Kernel2D kernel, double[,] filtered, CNN.IActivation activation, bool mergeMaximum = false)
{
if (image.GetLength(0) != image.GetLength(1))
throw new ArgumentException("The image argument must be square.");
if (filtered.GetLength(0) != image.GetLength(0) || filtered.GetLength(1) != image.GetLength(1))
throw new ArgumentException("The filtered argument must be same size as image argument.");
// Calculate padding size
const int padding = FILTER_2D_SIZE / 2;
int image_size = image.GetLength(0);
// Loop through the image with padding
for (int i = padding; i < image_size - padding; i++)
for (int j = padding; j < image_size - padding; j++)
{
double sum = 0;
// Convolve the filter with the image region
for (int fi = 0; fi < FILTER_2D_SIZE; fi++)
for (int fj = 0; fj < FILTER_2D_SIZE; fj++)
sum += image[i + fi - padding, j + fj - padding] * kernel.filter[fi, fj];
// Store the result in the output matrix
if (mergeMaximum)
filtered[i, j] = Math.Max(activation.Function(sum / kernel.divisor), filtered[i, j]);
else
filtered[i, j] = activation.Function(sum / kernel.divisor);
}
}
// for 2D CNNs - function to apply 2D convolution with replication padding
public static void ApplyFilterWithReplicationPadding(double[,] image, Kernel2D kernel, double[,] filtered, CNN.IActivation activation, bool mergeMaximum = false)
{
if (image.GetLength(0) != image.GetLength(1))
throw new ArgumentException("The image argument must be square.");
if (filtered.GetLength(0) != image.GetLength(0) || filtered.GetLength(1) != image.GetLength(1))
throw new ArgumentException("The filtered argument must be same size as image argument.");
int rows = image.GetLength(0), cols = image.GetLength(1);
const int padding = FILTER_2D_SIZE / 2;
// Apply convolution with replication padding
for (int i = 0; i < rows; i++)
for (int j = 0; j < cols; j++)
{
double sum = 0.0;
// Convolve the kernel with the image region
for (int ki = 0; ki < FILTER_2D_SIZE; ki++)
{
for (int kj = 0; kj < FILTER_2D_SIZE; kj++)
{
// Calculate the pixel position in the image with padding
int x = i + ki - padding;
int y = j + kj - padding;
// Handle replication padding for out-of-bound pixels
x = (x < 0) ? 0 : ((x >= rows) ? (rows - 1) : x);
y = (y < 0) ? 0 : ((y >= cols) ? (cols - 1) : y);
sum += image[x, y] * kernel.filter[ki, kj];
}
}
// Store the result in the output matrix "filtered"
if (mergeMaximum)
filtered[i, j] = Math.Max(activation.Function(sum / kernel.divisor), filtered[i, j]);
else
filtered[i, j] = activation.Function(sum / kernel.divisor);
}
}
// for 2D CNNs
public static void CropImage(double[,] image, double[,] croppedImage)
{
int originalWidth = image.GetLength(0), originalHeight = image.GetLength(1);
const int padding = 1;
// Calculate the dimensions of the cropped image
int croppedWidth = originalWidth - (2 * padding);
int croppedHeight = originalHeight - (2 * padding);
if (croppedImage.GetLength(0) != croppedWidth || croppedImage.GetLength(1) != croppedHeight)
throw new ArgumentException($"The croppedImage argument must have the dimensions [{croppedWidth}, {croppedHeight}]");
// Copy the relevant part of the image to the cropped image
for (int i = padding; i < originalWidth - padding; i++)
for (int j = padding; j < originalHeight - padding; j++)
croppedImage[i - padding, j - padding] = image[i, j];
}
// for 2D CNNs
public static void ApplyMaxPooling(double[,] filtered, double[,] pooled)
{
if (filtered.GetLength(0) != filtered.GetLength(1))
throw new ArgumentException("The filtered argument must be square.");
if (pooled.GetLength(0) != filtered.GetLength(0) / POOL_2D_SIZE || pooled.GetLength(1) != filtered.GetLength(1) / POOL_2D_SIZE)
throw new ArgumentException("The dimensions of the pooled argument must be half that of filtered argument.");
for (int i = 0; i < filtered.GetLength(0); i += POOL_2D_SIZE)
for (int j = 0; j < filtered.GetLength(1); j += POOL_2D_SIZE)
{
double max_val = filtered[i, j];
// Find the maximum value in the pooling window
for (int m = 0; m < POOL_2D_SIZE; m++)
for (int n = 0; n < POOL_2D_SIZE; n++)
if (filtered[i + m, j + n] > max_val)
max_val = filtered[i + m, j + n];
// Store the maximum value in the pooled matrix
pooled[i / POOL_2D_SIZE, j / POOL_2D_SIZE] = max_val;
}
}
// for 2D CNNs
public static void FlattenPooled(double[,] pooled, double[] flattened, out double min, out double max)
{
if (flattened.Length != pooled.GetLength(0) * pooled.GetLength(1))
throw new ArgumentException("The flattened argument must have a length equal to the pooled argument's dimensions multiplied.");
min = max = pooled[0, 0];
// Loop through the rows and columns of the image
for (int row = 0; row < pooled.GetLength(0); row++)
for (int column = 0; column < pooled.GetLength(1); column++)
{
var v = pooled[row, column];
min = Math.Min(min, v);
max = Math.Max(max, v);
flattened[row * pooled.GetLength(1) + column] = v;
}
}
}
}
namespace CNN
{
public interface IActivation
{
public double Function(double x);
}
public class ActivationLinear : IActivation
{
public double Function(double x)
{
return CommonFunctions.Linear(x);
}
}
public class ActivationLeakyReLU : IActivation
{
private readonly double alpha;
public ActivationLeakyReLU(double alpha = 0.01)
{
this.alpha = alpha;
}
public double Function(double x)
{
return CommonFunctions.LeakyReLU(x, alpha);
}
}
public class ActivationReLU : IActivation
{
public double Function(double x)
{
return CommonFunctions.ReLU(x);
}
}
internal static class CommonFunctions
{
public static void NormalizeBetween0and1(double[] flattened, double min, double max)
{
double range = max - min;
for (int i = 0; i < flattened.Length; i++)
flattened[i] = (flattened[i] - min) / range;
}
public static void NormalizeBetweenNegative1and1(double[] flattened, double min, double max)
{
double range = max - min;
for (int i = 0; i < flattened.Length; i++)
flattened[i] = (flattened[i] - min) / range * 2 - 1;
}
public static double Linear(double x) // NOTE: use this when applying filters to Word2Vec (or similar) values since it will not modify the values
{
return x;
}
// NOTE: alpha can be modified, bigger or smaller, tensorflow uses 0.3 for example
public static double LeakyReLU(double x, double alpha) // Rectified Linear Unit function ("Leaky" version)
{
return x >= 0 ? x : (alpha * x);
}
public static double ReLU(double x) // Rectified Linear Unit function
{
return x > 0 ? x : 0;
}
}
}
}
The Neural Network code for the "fully connected" layers can be found here.