PROWAREtech

articles » current » dot-net » convolutional-neural-network-supervised-machine-learning

.NET: Convolutional Neural Network, Deep Learning, Supervised Learning Example in C#

An example 1D/2D CNN, deep learning library written 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 and type of CNN filters (a.k.a. kernels). This example usage code requires SixLabors.ImageSharp (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 void NormalizeBetweenNegativeAlphaAnd1(double alpha, double[] flattened, double min, double max)
			{
				double range = max - min;
				for (int i = 0; i < flattened.Length; i++)
					flattened[i] = (flattened[i] - min) / range * (1.0 + Math.Abs(alpha)) - Math.Abs(alpha);
			}
			public static void NormalizeJustBelow0And1(double[] flattened, double min, double max) // this is in beta
			{
				double range = max - min;
				for (int i = 0; i < flattened.Length; i++)
					flattened[i] = (flattened[i] - min) / range * 1.01 - 0.01; // values from -0.01 to 1.0 - check for improved results using LeakyReLU with the neural network
			}
			public static double Linear(double x) // use this when applying filters to Word2Vec (or similar) values since it will not modify the values
			{
				return x;
			}
			// alpha might work at 0.01 but can be modified, bigger or smaller
			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.


This site uses cookies. Cookies are simple text files stored on the user's computer. They are used for adding features and security to this site. Read the privacy policy.
CLOSE