1. 程式人生 > >手寫數字閱讀器使用者介面

手寫數字閱讀器使用者介面

目錄

介紹

背景

使用程式碼

解決方案

為什麼這麼多框架?

解析Mnist資料

例項化和訓練神經網路

Mnist培訓師

前進通行證

反向傳播

Mnist測試使用者介面

影象預處理

最後


C#面向物件的神經網路,培訓師和Windows窗體使用者介面,用於識別手寫數字。

介紹

該原始碼演示瞭如何訓練和使用神經網路來解釋手寫數字。
本文不會有任何數學知識。這完全是關於基礎的機器學習的C#實現。

https://www.codeproject.com/KB/AI/1273125/Mnist_Test_Form_5.png

背景

使用Mnist手寫數字資料集2訓練人工神經網路人工神經網路(ANN) 

。這是資料科學領域的經典問題。它也被稱為機器學習的Hello World應用程式 Code Project上已經發布了一些關於此主題的演示應用程式,但我認為我的原始碼可以幫助某人。複雜的問題可能需要不止一個解釋,我試圖讓它儘可能簡單。

使用程式碼

Visual Studio 2017中下載,解壓縮並開啟解決方案。

解決方案

https://www.codeproject.com/KB/AI/1273125/SystemArchitecture.png

該解決方案包含五個專案:

專案

描述

骨架

DeepLearningConsole

訓練控制檯的入口點

.NET Core 2.2

DeepLearning

主庫

.NET Standard 2.0

Data

解析資料。目前只有Mnist

.NET Standard 2.0

MnistTestUi

用於手動測試的使用者介面

.NET Framework 4.7.1

Tests

一些單元測試

.NET Core 2.2

為什麼這麼多框架?

我意識到.NET Core.NET Framework

快了大約30%,我想在Windows Forms應用程式中使用.NET Framework
遺憾的是,您無法從.NET Framework引用.NET Core庫。它們不相容。
為解決這個問題,我使用.NET Standard作為通用元件。

.NET Standard不是一個框架。它是.NET API的正式規範。
所有.NET實現都應該與它相容,不僅僅是.NET Core.NET Framework,還有XamarineMonoUnityWindows Mobile
這就是為可重用元件選擇目標框架(Target Framework)的原因。

解析Mnist資料

這些是Mnist資料庫中的四個檔案:

  • t10k-images-idx3-ubyte——測試影象
  • t10k-labels-idx1-ubyte——測試影象的標籤
  • train-images-idx3-ubyte——培訓影象(60000
  • train-labels-idx1-ubyte——60000次培訓影象的標籤

Mnist中的影象必須從位元組陣列轉換為從01的雙精度陣列。
這些檔案還包含一些標題欄位。

private static List<Sample> LoadMnistImages(string imgFileName, string idxFileName, int imgCount)
{
    var imageReader = File.OpenRead(imgFileName);
    var byte4 = new byte[4];
    imageReader.Read(byte4, 0, 4); //magic number
    imageReader.Read(byte4, 0, 4); //magic number
    Array.Reverse(byte4);
    //var imgCount = BitConverter.ToInt32(byte4, 0);

    imageReader.Read(byte4, 0, 4); //width (28)
    imageReader.Read(byte4, 0, 4); //height (28)
    var samples = new Sample[imgCount];

    var labelReader = File.OpenRead(idxFileName);
    labelReader.Read(byte4, 0, 4);//magic number
    labelReader.Read(byte4, 0, 4);//count
    var targets = GetTargets();

    for (int i = 0; i < imgCount; i++)
    {
        samples[i].Data = new double[784];
        var buffer = new byte[784];
        imageReader.Read(buffer, 0, 784);
        for (int b = 0; b < buffer.Length; b++)
            samples[i].Data[b] = buffer[b] / 256d;

        samples[i].Label = labelReader.ReadByte();
        samples[i].Targets = targets[samples[i].Label];
     }
     return samples.ToList();
}

解析過程產生兩個訓練和測試樣本列表。
樣本由影象畫素陣列和長度為10的目標陣列組成,目標陣列是影象所在的數字的資訊。
數字零是陣列:1,0,0,0,0,0,0,0,0,0
數字五是:0,0,0,0,1,0,0,0,0,0(五分之一),依此類推。

例項化和訓練神經網路

要例項化新的人工神經網路(ANN),您需要提供其拓撲,層數和每層中的神經元數量。
必須有784個神經元輸入用於mnist影象(28x28畫素)。輸出層必須具有10
培訓師類使用TrainData指定的學習速率訓練神經網路。

var neuralNetwork = new NeuralNetwork(rndSeed: 0, sizes: new[] { 784, 200, 10 });
neuralNetwork.LearnRate = 0.3;
var trainer = new Trainer(neuralNetwork, Mnist.Data);
trainer.Train();

接下來,每個訓練樣本都被送到網路,以便學習。
我發現隱藏層中的200個神經元使得人工神經網路(人工神經網路(ANN))的準確率達到98.5%,這似乎已經足夠了。
400個神經元,精度最高可達98.8%,但需要兩倍的時間訓練。

Mnist培訓師

培訓師反覆訓練人工神經網路(ANN),讓它看到一個訓練樣本。
所有60000張訓練影象中的一個迴圈稱為紀元。

在每個紀元之後,人工神經網路(ANN)被序列化並儲存到檔案中。
然後針對測試樣本測試人工神經網路(ANN),並將結果記錄到csv檔案中。
訓練影象也在每個紀元之間進行混洗。

public void Train(int epochs = 100)
{            
    var rnd = new Random(0);
    var name = $"Sigmoid LR{NeuralNetwork.LearnRate} HL{NeuralNetwork.Layers[1].Count}";
    var csvFile = $"{name}.csv";
    var bestResult = 0d;
    for (int epoch = 1; epoch < epochs; epoch++)
    {
        Shuffle(TrainData.TrainSamples, rnd);
        TrainEpoch();                
        var result = Test();
        Log($"Epoch {epoch} {result.ToString("P")}");
        File.AppendAllText(csvFile, $"{epoch};{result};{NeuralNetwork.TotalError}\r\n");
        if (result > bestResult)
        {
            NeuralNetwork.Save($"{name}.bin");
            Log($"Saved {name}.bin");
            bestResult = result;
        }
    }
 }

前進通行證

這通過總結所有先前神經元乘以其權重來計算每個神經元值。
然後通過啟用函式傳遞該值。然後可以從最後一層(也稱為輸出層)獲得結果或輸出。

private void Compute(Sample sample, bool train)
{
    for (int i = 0; i < sample.Data.Length; i++)
        Layers[0][i].Value = sample.Data[i];

    for (int l = 0; l < Layers.Length - 1; l++)
    {
        for (int n = 0; n < Layers[l].Count; n++)
        {
            var neuron = Layers[l][n];
            foreach (var weight in neuron.Weights)
                weight.ConnectedNeuron.Value += weight.Value * neuron.Value;
        }

        var neuronCount = Layers[l + 1].Count;
        if (l + 1 < Layers.Count() - 1)
             neuronCount--; //skipping bias

        for (int n = 0; n < neuronCount; n++)
        {
            var neuron = Layers[l + 1][n];
            neuron.Value = LeakyReLU(neuron.Value / Layers[l].Count);
        }
    }
}

反向傳播

該演算法調整神經元之間的所有權重。它使網路學習並逐步提高其效能。

private void ComputeNextWeights(double[] targets)
{
    var output = OutputLayer;
    for (int t = 0; t < output.Count; t++)
        output[t].Target = targets[t];

    //Output Layer
    foreach (var neuron in output)
    {
        neuron.Error = Math.Pow(neuron.Target - neuron.Value, 2) / 2;
        neuron.Delta = (neuron.Value - neuron.Target) * (neuron.Value > 0 ? 1 : 1 / 20d));
    }
    this.TotalError = output.Sum(n => n.Error);

    foreach (var neuron in Layers[1])
    {
        foreach (var weight in neuron.Weights)
            weight.Delta = neuron.Value * weight.ConnectedNeuron.Delta;
    }
    
    //Hidden Layer
    Parallel.ForEach(Layers[0], GetParallelOptions(), (neuron) => {

        foreach (var weight in neuron.Weights)
        {
            foreach (var connectedWeight in weight.ConnectedNeuron.Weights)
                weight.Delta += connectedWeight.Value * connectedWeight.ConnectedNeuron.Delta;
            var cv = weight.ConnectedNeuron.Value;
            weight.Delta *= (cv > 0 ? 1 : 1 / 20d);
            weight.Delta *= neuron.Value;
        }

    });

    //All deltas are done. Now calculate new weights.
    for (int l = 0; l < Layers.Length - 1; l++)
    {
        var layer = Layers[l];
        foreach (var neuron in layer)
            foreach (var weight in neuron.Weights)
                weight.Value -= (weight.Delta * this.LearnRate);
    }
}

Mnist測試使用者介面

Test UI用於測試您自己的手寫內容。它有兩個面板。小面板解釋單個繪製的數字,而在較大的底部,您可以繪製一個數字。

影象預處理

Mnist資料庫主頁說明:

來自NIST的原始黑白(雙層)影象的大小被標準化以適應20x20畫素的盒子,同時保留它們的寬高比。由於歸一化演算法使用的抗鋸齒技術,得到的影象包含灰度級。通過計算畫素的質心,並將影象平移以便將該點定位在28x28域的中心,影象以28x28影象為中心。

以下是如何使用點陣圖和Windows窗體圖形進行操作的說明。

https://www.codeproject.com/KB/AI/1273125/preprocess.png

首先,找到繪製數字周圍的最小方塊。

public Rectangle DrawnSquare()
{
    var fromX = int.MaxValue;
    var toX = int.MinValue;
    var fromY = int.MaxValue;
    var toY = int.MinValue;
    var empty = true;
    for (int y = 0; y < Bitmap.Height; y++)
    {
        for (int x = 0; x < Bitmap.Width; x++)
        {
            var pixel = Bitmap.GetPixel(x, y);
            if (pixel.A > 0)
            {
                empty = false;
                if (x < fromX)
                    fromX = x;
                if (x > toX)
                    toX = x;
                if (y < fromY)
                    fromY = y;
                if (y > toY)
                    toY = y;
            }
        }
    }
    if (empty)
        return Rectangle.Empty;
    var dx = toX - fromX;
    var dy = toY - fromY;
    var side = Math.Max(dx, dy);
    if (dy > dx)
        fromX -= (side - dx) / 2;
    else
        fromY -= (side - dy)/ 2;

    return new Rectangle(fromX, fromY, side, side);
}

裁剪出正方形並調整大小為20x20的新點陣圖。

public DirectBitmap CropToSize(Rectangle drawnRect, int width, int height)
{
    var bmp = new DirectBitmap(width, height);
    bmp.Bitmap.SetResolution(Bitmap.HorizontalResolution, Bitmap.VerticalResolution);

    var gfx = Graphics.FromImage(bmp.Bitmap);
    gfx.CompositingQuality = CompositingQuality.HighQuality;
    gfx.InterpolationMode = InterpolationMode.HighQualityBicubic;
    gfx.PixelOffsetMode = PixelOffsetMode.HighQuality;
    gfx.SmoothingMode = SmoothingMode.AntiAlias;
    var rect = new Rectangle(0, 0, width, height);
    gfx.DrawImage(Bitmap, rect, drawnRect, GraphicsUnit.Pixel);
    return bmp;
}

最後,繪製20 x 20的影象,其質心集中在28x28點陣圖內。

public Point GetMassCenterOffset()
{
    var path = new List<Vector2>();
    for (int y = 0; y < Height; y++)
    {
        for (int x = 0; x < Width; x++)
        {
            var c = GetPixel(x, y);
            if (c.A > 0)
                path.Add(new Vector2(x, y));
        }
    }
    var centroid = path.Aggregate(Vector2.Zero, (current, point) => current + point) / path.Count();
    return new Point((int)centroid.X - Width / 2, (int)centroid.Y - Height / 2);
}

protected DirectBitmap PadAndCenterImage(DirectBitmap bitmap)
{
    var drawnRect = bitmap.DrawnRectangle();
    if (drawnRect == Rectangle.Empty)
        return null;

    var bmp2020 = bitmap.CropToSize(drawnRect, 20, 20);

    //Make image larger and center on center of mass
    var off = bmp2020.GetMassCenterOffset();
    var bmp2828 = new DirectBitmap(28, 28);
    var gfx2828 = Graphics.FromImage(bmp2828.Bitmap);
    gfx2828.DrawImage(bmp2020.Bitmap, 4 - off.X, 4 - off.Y);

    bmp2020.Dispose();
    return bmp2828;
}

然後,只需從影象中提取位元組並使用它們查詢人工神經網路(ANN)

public byte[] ToByteArray()
{
    var bytes = new List<byte>();
    for (int y = 0; y < Bitmap.Height; y++)
    {
        for (int x = 0; x < Bitmap.Width; x++)
        {
            var color = Bitmap.GetPixel(x, y);
            var i = color.A;
            bytes.Add(i);
        }
     }
     return bytes.ToArray();
}

如果您對它們的外觀感到好奇,介面(UI)還具有顯示Mnist影象的功能。但是我不會過多地介紹介面(UI)的每個細節,因為我覺得我們正在脫離主題。

最後

我希望你喜歡我的文章,也許你學到了一些你還不知道的東西。如果您有任何問題,意見或想法,請留言。

 

原文地址:https://www.codeproject.com/Articles/1273125/Handwritten-Digits-Reader-UI