使用 .NET 中的神经网络进行图像分类





5.00/5 (8投票s)
使用 .NET 中的深度置信网络和卷积神经网络实现图像分类
基本原理
图像分类是非循环神经网络最常见的用例之一。基本概念是,神经网络接收输入图像,其输入层中的神经元数量与图像中的像素数量相同(假设图像为灰度)。另外,根据要进行的分类数量,此神经网络应具有相同数量的输出神经元。神经网络可以使用卷积层、全连接层或两者的组合。卷积网络速度更快,因为它们会压缩输入图像并使用多个核对其进行卷积以提取重要特征。有关卷积的更多详细信息,请参阅 此处。卷积大大减小了全连接网络的大小,这些网络在经过一系列卷积和池化后用于对图像进行分类。由于使用适当激活函数的神经网络的输入和输出只能是 0 到 1 之间的双精度值,因此将图像输入神经网络需要进行一些预处理,以将像素归一化到此形式。稍后章节中将介绍使用深度置信网络和卷积神经网络进行分类的代码实现。
图像预处理
将原始图像灰度化然后缩小到 50x50 像素将大大减少输入神经元的数量,约 2500 个。这还将降低神经网络需要分析的复杂性和特征。 EmguCV 库(OpenCV 的 .NET 包装器)在执行这些任务时非常方便。也可以进行直方图均衡化,但仅灰度化图像会更自然地保留特征。可以使用 75x75 或 100x100 等任何尺寸,但增加分辨率也会增加输入神经元的数量,并在使用非卷积网络时大大增加训练和检测时间。
public static Image<Gray, Byte>
ConvertOriginalImageToGrayScaleAndProcess(Image<Bgr, Byte> orginalImage)
{
var grayScale = orginalImage.Convert<Gray, Byte>();
return grayScale.Resize(50,50, Emgu.CV.CvEnum.Inter.Cubic, false);
}
像素值归一化
由于像素值是字节类型,因此每个灰度像素值除以 256,将其转换为 0 到 1 之间的值,以便可以输入神经网络。EmguCV 图像有一个名为 Bytes
的属性,它提供一个像素值数组。
public static double[] GetNetworkFeedArray(Image<Gray, Byte> image)
{
var imageBytes = image.Bytes;
double[] networkFeed = new double[imageBytes.Count()];
for (int i = 0; i < imageBytes.Length; i++)
{
networkFeed[i] = ((double)imageBytes[i] / 256);
}
return networkFeed;
}
训练网络
任何神经网络都可以使用与之关联的可选训练算法进行训练。但无论使用哪种类型的神经网络或训练算法,都会提供一个包含输入数据和相应输出数据的训练数据集。对于分类,预期的输出值将是 0 或 1。下面是一个具有三种输入数据分类的示例。每个输出神经元的输出本身是抽象的,但可以与分类类型相关联,例如“car
”、“bird
”、“cat
”、“human
”等。
使用此数据集训练许多迭代(前提是训练算法和其他超参数的参数设置得当),并且错误率持续降低后,网络将达到一种状态,即它可以根据对输入的理解生成有效输出。所有归一化的图像数据都必须转换为组合图像的 double
交错数组,以便神经网络能够对其进行训练。无论选择哪种类型的神经网络,此步骤都是通用的。将图像组合成批次而不是一次训练一个图像可以加快训练过程。
public (double[][], double[][]) GetBatchDataFromImages(IPagedList<LocalImage> localImages)
{
var numberOfImages = localImages.Count;
double[][] batchInputs = new double[numberOfImages][];
double[][] batchOutputs = new double[numberOfImages][];
foreach (int i in Enumerable.Range(0, numberOfImages))
{
var currentLocalImage = localImages[i];
(double[] normalizedImageData, ImageType imageType) =
LocalImage.GetImageInformationForNeuralNetwork(currentLocalImage);
batchInputs[i] = normalizedImageData;
batchOutputs[i] = new double[]
{ currentLocalImage.ImageType == ImageType.CAT ? 1 : 0,
currentLocalImage.ImageType == ImageType.CAT ? 0 : 1 };
}
return (batchInputs, batchOutputs);
}
生成输出
神经网络在转发/计算输入值后,从输出神经元输出 0-1 范围内的值。对于分类,输出值必须进行阈值处理,因为生成的值通常不是具体的 0 或 1。这些值还可以用于生成有效分类的概率。以下是用于将图像分类为“cat
”或非“cat
”并返回布尔值的代码,这可能是因为此神经网络只有两个输出。在此,代码使用了 0.95 的上限阈值和 0.05 的下限阈值。这些值是任意的,可以根据神经网络的训练量以及为了平衡 过拟合 的程度来使用更接近 1 和 0 的值。此方法可以扩展到任意数量的分类。
private static bool DetectCat(INeuralNetwork neuralNetwork, Image<Gray, Byte> image)
{
double[] networkFeed = LocalImage.GetNetworkFeedArray(image);
var networkOutput = neuralNetwork.GenerateOutput(networkFeed);
var outputValue = networkOutput[0];
var complementaryOutputValue = networkOutput[1];
return outputValue > 0.95 && complementaryOutputValue < 0.05;
}
使用全连接网络进行分类
具有足够隐藏层数量的全连接神经网络可以对任何图像数据集进行分类,并且增加数据集中的数据点将对应于增加隐藏层中的神经元数量以获得正确的预测。由于这些是非常简单的前馈网络,并且隐藏层或输入层中添加的每个神经元都会使连接数量呈指数级增长,因此它们都容易遗忘,并且需要很长时间才能训练。此网络只能通过学习进行训练。根据用例和输入数据集的长度,微调训练类的学习率和动量(无论是 反向传播 还是 弹性反向传播)可以使我们在此方法中获得最佳结果。测试表明,使用全连接网络在训练数据集上可以达到大约 90% 的准确率,尽管结果可能会有所不同。为了提高准确率至 100%,可以使用深度置信网络。
使用深度置信网络进行分类
深度置信网络是一种特殊的神经网络,为了使其能够正确学习,它必须经过每个层的无监督学习阶段,然后对整个网络进行监督学习。它被定义为一系列 受限玻尔兹曼机,其中每个层都与前一层和下一层进行通信。
有关这些网络的更多信息,请参阅 此处 和 此处。但即使它最初看起来很复杂,该网络只需使用每个受限玻尔兹曼机上的整个数据集进行 无监督 学习,并在达到较低的错误率阈值后,就可以继续对整个网络进行监督学习,使用与全连接网络相同的反向传播或弹性反向传播训练类。本文稍后章节将介绍使用 Accord.NET 实现此类网络及代码,在 500 张图像的训练数据集上实现了 100% 的准确率。
使用卷积神经网络进行分类
卷积是一种强大的图像处理工具,它使用一个核通过遍历图像并使用该核执行计算来转换图像。可以使用 不同类型的 核来创建边缘检测、模糊和其他许多效果。每个卷积神经网络通常由一个卷积层、一个 整流线性激活层 和一个 池化层 组成。这些三层可以串联使用,参数不同,以创建适合图像分类问题的神经网络。然后将卷积层的输出馈送到全连接网络,该网络通常比深度置信网络和类似网络类型的网络小得多。由于使用适当的核进行图像卷积会从图像中提取某些特征,然后整流线性激活函数会消除负值,最后池化层在保留重要特征的同时减小输出的尺寸,因此它非常适合提取定义分类的适当特征,无论其多么抽象,例如狗和猫的区别。
位于全连接网络之前的卷积层基本上是一个非常简单但强大的预处理步骤,可以减小输入尺寸同时保留重要特征。最后,全连接网络会产生与任何常规神经网络相同的分类输出。在 .NET 中,有一个名为 ConvNetSharp 的 NuGet 包,可以使用不同顺序的各种层来创建任何类型的卷积神经网络。本文稍后章节将详细介绍使用此库实现图像分类器的代码。
使用锚框进行图像定位
任何分类神经网络都可以修改以执行图像中的对象定位。定位基本上是指可以确定图像中对象的位置,并用一个框将其框起来。锚框是图像中随机生成的区域,每个区域都可以缩放到输入尺寸并输入神经网络,以确定该区域是否可以被分类为任何定义的分类类型。如果一个区域被正确分类,则在该区域周围绘制一个矩形,可以认为该区域包含所关注的对象。通常生成约 1000-2000 个随机锚框,并将它们的相应区域按顺序输入神经网络并分析输出。下面是图像上随机生成锚框的示例。
在将每个锚框处理到分类网络后,可以在正面检测结果周围绘制矩形。有效且相互重叠的矩形可以合并成最终的边界矩形。下面是生成 1000 个随机锚框的代码。
public static AnchorBox GenerateNew()
{
double x = (random.NextDouble()) / (double)1.1;
double y = (random.NextDouble()) / (double)1.1;
var smallerValue = x > y ? x : y;
double width = random.Next(100000,
(int)(((double)1 - smallerValue) * 10000000)) / (double)10000000;
double height = width;
return new AnchorBox()
{
X = x,
Y = y,
Width = width,
Height = height
};
}
public static List<AnchorBox> GenerateRandomAnchorBoxes()
{
var anchorBoxList = new List<AnchorBox>() { new AnchorBox()
{ Height = 1, Width = 1, X = 0, Y = 0 } };
foreach (int i in Enumerable.Range(0, 1000))
{
anchorBoxList.Add(AnchorBox.GenerateNew());
}
return anchorBoxList;
}
每个锚框用于生成一个 ROI(感兴趣区域),可以使用 EmguCV 将原始图像裁剪到另一个图像,该图像被缩放到与原始图像相同的尺寸。将这些区域通过神经网络运行可以确定对象是否存在于这些子区域中。所有检测到的锚框都被组合起来以生成最终的边界矩形。
private static (Image<Gray, Byte>, System.Drawing.Rectangle)
GetAreaUnderAnchorBox(Image<Bgr, Byte> originalImage, AnchorBox anchorBox)
{
var originalImageCopy = originalImage.Copy();
var rectangle = GetRectangleFromAnchroBox(originalImageCopy, anchorBox);
originalImageCopy.ROI = rectangle;
var croppedImage = LocalImage.ConvertOriginalImageToGrayScaleAndProcess
(originalImageCopy.Copy());
originalImageCopy.ROI = System.Drawing.Rectangle.Empty;
return (croppedImage, rectangle);
}
生成锚框后,可以确定哪些区域具有有效检测结果。下面是实现检测图像中猫的代码。
private static List<System.Drawing.Rectangle> GetDetectionRectangles
(INeuralNetwork neuralNetwork, Image<Bgr, Byte> originalImage, Action<double> progressUpdater)
{
List<System.Drawing.Rectangle> DetectedRectangles = new List<System.Drawing.Rectangle>();
var anchorBoxes = AnchorBox.GenerateRandomAnchorBoxes();
int counter = 0;
Parallel.ForEach(anchorBoxes, new ParallelOptions()
{ MaxDegreeOfParallelism = 10 }, (anchorBox) => {
(var croppedImage, var rectangle) = GetAreaUnderAnchorBox(originalImage, anchorBox);
var catDetected = DetectCat(neuralNetwork, croppedImage);
if (catDetected)
{
DetectedRectangles.Add(rectangle);
}
progressUpdater(((double)counter / (double)anchorBoxes.Count()) * 100);
counter++;
});
return DetectedRectangles;
}
这种定位方法非常慢,因为数千个区域在一个循环中通过网络,但这是最容易实现的。
使用 Accord.NET 实现深度置信网络进行分类
此库可在 NuGet 上找到,以下命令可将其安装。文档可在 此处 获取。
如本文前面所述,深度置信网络通过对深度置信网络的每个受限玻尔兹曼机进行无监督学习,然后对整个网络进行监督学习来正确学习。因此,需要两种不同的教师算法来完成此任务。Accord.NET 提供了许多类可供使用,但要创建深度置信网络,有一个 DeepBeliefNetwork
类。对于无监督学习,有一个 DeepBeliefNetworkLearning
类;对于监督学习,可以选择 BackpropagationLearning
、ResilientBackpropagationLearning
或 ParallelResilientBackpropagationLearning
类。深度置信网络使用 NguyenWidrow
类以随机权重初始化。下面的代码用于二元分类,因此输出神经元的数量只有 2 个。它有两个隐藏层,分别有 1200 和 600 个神经元。
public AccordNetwork()
{
network = new DeepBeliefNetwork(new BernoulliFunction(), inputLength, 1200, 600, 2);
new NguyenWidrow(network).Randomize();
network.UpdateVisibleWeights();
unsuperVisedTeacher = GetUnsupervisedTeacherForNetwork(network);
supervisedTeacher = GetSupervisedTeacherForNetwork(network);
}
private DeepBeliefNetworkLearning
GetUnsupervisedTeacherForNetwork(DeepBeliefNetwork deepNetwork)
{
var teacher = new DeepBeliefNetworkLearning(deepNetwork)
{
Algorithm = (hiddenLayer, visibleLayer, i) =>
new ContrastiveDivergenceLearning(hiddenLayer, visibleLayer)
{
LearningRate = 0.1,
Momentum = 0.5
}
};
return teacher;
}
private ResilientBackpropagationLearning GetSupervisedTeacherForNetwork
(DeepBeliefNetwork deepNetwork)
{
var teacher = new ResilientBackpropagationLearning(deepNetwork)
{
LearningRate = 0.1
//Momentum = 0.5
};
return teacher;
}
在所有受限玻尔兹曼机上进行无监督训练
从数据集中,仅使用输入以无监督方式进行训练。下面是从我们的数据集中获取输入并通过迭代训练所有受限玻尔兹曼机的代码。
private void TrainUnsupervised(double[][] batchInputs, int iterations,
Action<double, int, string> progressCallback)
{
for (int layerIndex = 0; layerIndex < network.Machines.Count - 1; layerIndex++)
{
unsuperVisedTeacher.LayerIndex = layerIndex;
var layerData = unsuperVisedTeacher.GetLayerInput(batchInputs);
foreach (int i in Enumerable.Range(1, iterations))
{
var error = unsuperVisedTeacher.RunEpoch(layerData) / batchInputs.Length;
if (progressCallback != null)
{
progressCallback(error, i, $"Unsupervised Layer {layerIndex}");
}
if (this.ShouldStopTraning)
{
this.ShouldStopTraning = false;
break;
}
}
}
}
在整个网络上进行监督训练
第二步需要数据集中的输入和输出。这些用于以监督方式训练网络。下面是将数据集传递给监督教师并报告错误率的代码。此错误率可以在图表中或其他用途中可视化。
private void TrainSupervised(double[][] batchInputs, double[][] batchOutputs,
int iterations, Action<double, int, string> progressCallback)
{
foreach (int i in Enumerable.Range(1, iterations))
{
var error = supervisedTeacher.RunEpoch(batchInputs, batchOutputs) / batchInputs.Length;
if (progressCallback != null)
{
progressCallback(error, i, "Supervised");
}
if (this.ShouldStopTraning)
{
this.ShouldStopTraning = false;
break;
}
}
}
计算输出
单张图像输入的输出计算在一行中完成。输出数组的长度与网络中的输出神经元数量相同。
public double[] GenerateOutput(double[] inputs)
{
return network.Compute(inputs);
}
序列化/反序列化训练模型
成功训练任何网络后,可以对其状态进行序列化,以便以后可以随时随地保存、加载和测试。Accord.NET 类中有两个用于此目的的方法:Load
和 Save
。
public void LoadNetworkFromFile(string filePath)
{
network = DeepBeliefNetwork.Load(filePath);
supervisedTeacher = GetSupervisedTeacherForNetwork(network);
unsuperVisedTeacher = GetUnsupervisedTeacherForNetwork(network);
}
public void SaveNetwork(string filePath)
{
network.Save(filePath);
}
使用 ConvNetSharp 实现卷积神经网络进行分类
以下命令将 ConvNetSharp 安装到项目中。它仍处于 alpha 版本,但效果非常好。文档可在 此处 获取。
在 ConvNetSharp 库中,可以选择卷积层、池化层和全连接层来构建专门用于图像分类的神经网络。在下面的代码中,通过构造函数指定了层参数和顺序。
public ConvNetSharpNetwork()
{
network = new Net<double>();
network.AddLayer(new InputLayer(50, 52, 1));
network.AddLayer(new ConvLayer(3, 3, 8) { Stride = 1, Pad = 2 });
network.AddLayer(new ReluLayer());
network.AddLayer(new PoolLayer(3, 3) { Stride = 2 });
network.AddLayer(new ConvLayer(3, 3, 16) { Stride = 1, Pad = 2 });
network.AddLayer(new ReluLayer());
network.AddLayer(new PoolLayer(3, 3) { Stride = 2 });
network.AddLayer(new ConvLayer(3, 3, 32) { Stride = 1, Pad = 2 });
network.AddLayer(new FullyConnLayer(20));
network.AddLayer(new FullyConnLayer(50));
network.AddLayer(new FullyConnLayer(2));
network.AddLayer(new SoftmaxLayer(2));
trainer = GetTrainerForNetwork(network);
}
上面的网络包含三个卷积层和一个最后的全连接神经网络用于分类。这个全连接神经网络中的神经元数量远少于全连接网络所需的数量。
该网络的教师是随机梯度下降器——由同一库提供的 SgdTrainer
类。
private TrainerBase<double> GetTrainerForNetwork(Net<double> net)
{
return new SgdTrainer<double>(net)
{
LearningRate = 0.003
};
}
训练网络
在此卷积网络中,输入和输出必须转换为具有与输入和输出层维度相同的 Shape
的 Volume
。ConvNetSharp 中提供了辅助类来轻松完成此操作。首先,将图像组合成批次,然后转换为 Volume 以加快训练过程。批次大小为 50 对此网络效果很好,可以平衡收敛时间和准确性。
public void BatchTrain(double[][] batchInputs, double[][] batchOutputs,
int iterations, Action<double, int, string> progressCallback)
{
trainer.BatchSize = batchInputs.Length;
foreach (int currentIteration in Enumerable.Range(1, iterations))
{
Randomizer.Shuffle(batchInputs, batchOutputs);
(Volume<double> inputs, Volume<double> outputs) =
GetVolumeDataSetsFromArrays(batchInputs, batchOutputs);
trainer.Train(inputs, outputs);
var error = network.GetCostLoss(inputs, outputs);
if (progressCallback != null)
{
progressCallback(error, currentIteration, "Supervised");
}
inputs.Dispose();
outputs.Dispose();
if(this.ShouldStopTraning)
{
this.ShouldStopTraning = false;
break;
}
}
}
首先将双精度数组输入数据转换为兼容的 Shape
和 Volume
,输入网络,然后此网络的输出是一个 Volume,将其转换为双精度数组并返回。
public double[] GenerateOutput(double[] inputs)
{
Volume<double> inputImageData = BuilderInstance<double>.Volume.From
(inputs, new Shape(50, 52, 1));
Volume<double> calculatedPrediction = network.Forward(inputImageData);
inputImageData.Dispose();
return calculatedPrediction.ToArray();
}
序列化/反序列化训练模型
在 ConvNetSharp 中,FromJson
和 ToJson
是用于序列化和反序列化网络模型的两个可用方法。
public void LoadNetworkFromFile(string filePath)
{
var networkJSON = File.ReadAllText(filePath);
network = SerializationExtensions.FromJson<double>(networkJSON);
trainer = GetTrainerForNetwork(network);
}
public void SaveNetwork(string filePath)
{
var networkJSON = network.ToJson();
FileHelper.WriteTextAsync
(filePath, Encoding.ASCII.GetBytes(networkJSON)).RunSynchronously();
}
应用程序——猫分类器
使用上面详述的技术,本文的应用程序部分实现了一个猫分类器,该分类器将图像分类为“猫”或“非猫”的二元分类。该应用程序使用 WPF 创建,以便用户轻松使用并注重外观。它将在 .NET 4.5 下运行,因此,如果用户仍在使用 .NET 4(在旧版 Windows 7 OS 的情况下),他们必须下载 .NET 4.5 运行时。应用程序的源代码也已链接在上面,可以使用 Visual Studio 打开,适合更偏重代码的用户。
网络选择
应用程序显示的第一个窗口是网络选择窗口,可用于选择 Accord.NET 或 ConvNetSharp 实现的两个神经网络之一。定义了一个名为 INeuralNetwork
的接口,以确保来自不同源的网络可以与通用代码和 UI 无缝运行。
选择网络后,可以使用下一个窗口对其进行训练或测试。
数据采集
图像是通过 CSV 文件收集的,该文件包含图像的 URL 和图像的类型(是否为猫)。文件如下所示。
ImageType,Url
CAT,https://proxy.duckduckgo.com/iu/?u=http%3A%2F%2Fi.dailymail.co.uk% ...
CAT,https://proxy.duckduckgo.com/iu/?u=https%3A%2F%2Ftse3.mm.bing.net% ...
NOT_CAT,https://proxy.duckduckgo.com/iu/?u=https%3A%2F%2Ftse3.mm.bing. ...
NOT_CAT,https://proxy.duckduckgo.com/iu/?u=https%3A%2F%2Ftse2.mm.bing. ...
NOT_CAT,https://proxy.duckduckgo.com/iu/?u=https%3A%2F%2Ftse3.mm.bing. ...
使用 CsvHelper 库,可以将此文件读取到“远程图像”列表中。在 UI 中,可以使用“训练网络”窗口下载这些图像,然后将图像收集到“Images/”目录下的一个文件夹中,并加载到 UI 的“本地图像”列表中。在上述链接的 zip 文件中提供的 CSV 文件中收集了大约 500 张图像,以及应用程序。文件名是 'remote-image-urls.csv'。
如果图像已下载并存在于“Images/CAT”(用于猫)和“Images/ NOT_CAT”(用于非猫)文件夹中,则可以使用 UI 通过“从磁盘加载图像”按钮将其加载到应用程序中。
然后,这些图像将准备好使用选定的神经网络进行训练或测试。
测试选定的网络
在应用程序窗口中,有一个“加载模型”按钮,可以使用它来选择一个预训练模型以继续测试应用程序。Accord.NET 网络模型具有 '*.accmodel' 扩展名,ConvNetSharp 网络模型具有 '*.convmodel' 扩展名。选择模型后,应用程序就可以通过将图像拖放到窗口中来测试任何图像,或者在“训练网络”窗口中一次性测试所有本地图像。目前,应用程序“Models/”文件夹中包含的两个模型在训练过的 500 张图像上均具有 100% 的准确率。
训练选定的网络
将图像加载到本地图像列表后,可以使用“使用图像开始训练”按钮开始训练。可以在“显示错误图”窗口中监视错误率。错误率的降低是适当的参数和训练的迹象。由于图像是分批次传递到神经网络的,因此随着图像批次的训练,图表将如下所示。错误率达到较低阈值,然后训练继续到下一批图像,错误率再次飙升。
批次大小和阈值在两个网络中有所不同,经过一些试错后选定。.
ConvNetSharp 参数
private int batchSubSize = 50;
private int maxIterationsOnEachPage = 500;
private double stopPageTrainingErrorRate = 0.02;
private double stopIterationTrainingErrorRate = 1;
Accord.NET 参数
private int batchSubSize = 500;
private int maxIterationsOnEachPage = 1000;
private int maxUnSupervisedIterationsOnEachPage = 100;
private double stopPageTrainingErrorRate = 0.0001;
private double stopIterationTrainingErrorRate = 0.001;
脚注
随着 Keras、CNTK、TensorFlow 等的出现,创建复杂的神经网络变得轻而易举,.NET 也已赶上。一旦 ConvNetSharp 走出 alpha 版本,它就可以可靠地用于创建同样强大的神经网络,因为它还支持“计算图”功能。使用此功能创建的网络示例如图 此处 所示。
每个神经网络本身都很愚蠢,但通过将不同种类和功能的多个网络以特定顺序组合起来,包括循环网络以及少量过程代码,就可以创建一个我们称之为“人工智能”的智能网络。
参考文献
- http://web.pdx.edu/~jduh/courses/Archive/geog481w07/Students/Ludwig_ImageConvolution.pdf
- https://en.wikipedia.org/wiki/Deep_belief_network
- https://skymind.ai/wiki/deep-belief-network
- https://en.wikipedia.org/wiki/Unsupervised_learning
- https://en.wikipedia.org/wiki/Kernel_(image_processing)
- https://en.wikipedia.org/wiki/Rectifier_(neural_networks)
- https://en.wikipedia.org/wiki/Convolutional_neural_network#Pooling_layer
- https://www.coursera.org/lecture/convolutional-neural-networks/anchor-boxes-yNwO0
- https://medium.com/@andersasac/anchor-boxes-the-key-to-quality-object-detection-ddf9d612d4f9
- http://cs231n.github.io/convolutional-networks/