.Net 9下使用Tensorflow.net---CNN_使用卷积模型识别手写数字

.Net 9下使用Tensorflow.net---DNN_使用卷积模型识别手写数字

在这个例子里,使用了Tensorflow.net中的典型的卷积网络神经模型,加载MNIST数据集,经过训练,可以识别手写的数字,正确率可以达到 95%左右。
本例的基本步骤为
1、导入数据集并初始化获得训练集、测试集;
2、创建CNN模型,定义各神经网络层,并选择激活函数,各神经网络超参数;
3、训练完成后 保存模型;
4、并使用模型 通过MNIST测试集验证效果;
5、通过picturebox手写数字,测试效果。

一、创建项目,并导入各依赖项

本例子使用.net9的winform作为示例。

创建后,通过nuget,添加依赖项

TensorFlow.NET

TensorFlow.Keras

SciSharp.TensorFlow.Redist--如果使用GPU训练,请使用不同的依赖包

二、初始化

1、添加命名空间

using Tensorflow;
using Tensorflow.Keras.Engine;
using Tensorflow.Keras.Layers;
//using NumSharp;
using static Tensorflow.Binding;
using static Tensorflow.KerasApi;
using Tensorflow.NumPy;
using Newtonsoft.Json.Linq;

2、直接在窗口类中定义变量

        //--定义模型
        IModel model;
        //Keras中的层API,用于定义神经网络各个层,包括卷积层、池化层、全连接层
        LayersApi layers = new LayersApi();
        //定义示例中使用的,训练集、测试集
        NDArray x_train, y_train, x_test, y_test, x_test_raw;//x_test_raw for image show

        //本例中保存训练好的模型,后续随时可以load出来使用
        const string modelFile = "model.wts";

        //以下是使用picturebox绘制手写数字的变量
        Bitmap drawingBitmap;
        bool isDrawing = false;
        Point previousPoint;

3、在窗口类构造函数中初始化后续用到的变量

public Form1()
{
    InitializeComponent();
    this.DoubleBuffered = true; // 启用双缓冲以减少闪烁
    this.pictureBox_Image.MouseDown += PictureBox_Image_MouseDown; ;
    pictureBox_Image.MouseMove += PictureBox_Image_MouseMove; ;
    pictureBox_Image.MouseUp += PictureBox_Image_MouseUp ;
    pictureBox_Image.Paint += PictureBox_Image_Paint;
    //pictureBox_Image.Paint += new PaintEventHandler(Redraw); // 重绘事件处理
    this.DoubleBuffered = true;

    // 创建绘图画布
    drawingBitmap = new Bitmap(pictureBox_Image.Width, pictureBox_Image.Height, PixelFormat.Format24bppRgb);
    using (Graphics g = Graphics.FromImage(drawingBitmap))
    {
        g.Clear(Color.Black);
    }
}

4、加载MNIST数据集并归一化

 private void button1_Click_1(object sender, EventArgs e)
 {
     (x_train, y_train, x_test_raw, y_test) = keras.datasets.mnist.load_data();
     x_train = x_train.reshape((60000, 28, 28, 1)) / 255f;
     x_test = x_test_raw.reshape((10000, 28, 28, 1)) / 255f;
 }

以上这几个步骤,前几篇示例中都有相同的操作,就不再赘述解释

三、创建模型

这是本示例的重点,本示例需要做图像识别,而卷积神经网络(CNN)就是专业用于视觉图像分类和视觉图像目标检测的。本例的重点是使用了哪些神经网络层及各层中的激活函数。

1、layers.Conv2D,本例中使用该二维卷积层函数,说明下几个重要的参数:

1、filters:filters指的是这个卷积层中卷积核的数量,也就是指定输出特征图(feature maps)的深度(通道数)。卷积核越多,每个卷积核可以学习一个单独的特征,比如 纹理啊,颜色啊等,这样 模型的表达能力、学习能力就更强,但是也就意味着更大的计算。所以 一般的浅层网络中,较小的学习任务中,通常使用较小的 filters 值(例如 32、64)。而对应的

在深层网络中,使用大一些的 filters 的值(例如 128、256、512)。

2、kernel_size: kernel_size 参数定义了卷积核(filter)的大小,本示例中,kernel_size=(3, 3) 表示一个 3x3 的卷积核。而在实际训练中,卷积核在输入数据上滑动,通过点乘和求和操作提取局部特征。

所以 kernel_size 控制卷积核的空间尺寸(高度和宽度)。

filters 控制卷积核的数量,即输出特征图的深度。卷积层的参数量由 kernel_size 和 filters 共同决定。

一般通用的为 3*3

3、激活函数,这个就不再赘述了,这里用的就是具有生物特征的 Relu

2、layers.MaxPooling2D 该层称为 二维最大池化层

它的主要作用是通过下采样(downsampling)减少特征图的空间尺寸,从而降低计算复杂度并提取主要特征。换句话说就是 他的主要作用是就是按照池化层的最大二维参数,以保留最大特征值的方式,减少特征图的空间尺寸,最大池化层可以显著减少后续层的参数量和计算量,也就是降维,保留有效值。

3、layers.Flatten 展平层

用于将多维的输出张量进行展开,变为一维张量

最终通过 keras.Model() 方法 来构建模型,通过model.compile()【来编译该模型,编译过程其实就是 给模型设置训练参数,创建静态、动态图,为接下来的训练Model.fit()做好准备】

        private IModel CreateModel()
       {
           // input layer 输入层,每个手写的数字,像素都是28*28的,所以这里标准输入,按照MNIST的格式,就是 28,28,1
           var inputs = keras.Input(shape: (28, 28, 1));
           // 第一层,卷积层,用于二维图像
           var outputs = layers.Conv2D(32, kernel_size: (3, 3), activation: keras.activations.Relu).Apply(inputs);
           // 第二层,池化层,用于降维
           outputs = layers.MaxPooling2D(2, strides: 2).Apply(outputs);
           //  第三层,卷积层
           outputs = layers.Conv2D(64, kernel_size: (3, 3), activation: keras.activations.Relu).Apply(outputs);
           // 第四层,池化层
           outputs = layers.MaxPooling2D(2, strides: 2).Apply(outputs);
           // 第五层,展平层,用于将多维的输出张量进行展开,变为一维张量
           outputs = layers.Flatten().Apply(outputs);
          
           outputs = layers.Dense(128, activation: keras.activations.Relu).Apply(outputs);
          
           outputs = layers.Dense(10).Apply(outputs);
          
           model = keras.Model(inputs, outputs, name: "mnist_model");
           // model.summary()---输出模型的内容摘要
           model.summary();
           // 构建编译,配置参数,生成静态图
           model.compile(loss: keras.losses.SparseCategoricalCrossentropy(from_logits: true),
               optimizer: keras.optimizers.Adam(learning_rate: 0.001f),
               metrics: new[] { "accuracy" });

           return model;
       }

四、训练模型,保存模型

训练模型代码如下:

private void TrainModel()
{

   (x_train, y_train, x_test_raw, y_test) = keras.datasets.mnist.load_data();
   x_train = x_train.reshape((60000, 28, 28, 1)) / 255f;
   x_test = x_test_raw.reshape((10000, 28, 28, 1)) / 255f;

   model = CreateModel();
   // train model by feeding data and labels.
   model.fit(x_train, y_train, batch_size: 64, epochs: 5, validation_split: 0.2f);
   // evluate the model
   model.evaluate(x_test, y_test, verbose: 2);

   model.save_weights(modelFile, true);

}

重点是三个函数:

1、model.fit() 该函数就是 训练创建好的模型

batch_size:指的是样本批次的数量,

epchs:一共训练几轮

validation_split:验证集划分比,指的是 按照该参数比例,从训练集中划分出数据作为验证集。

2、model.evaluate()评估函数,评估模型的损失度和精度

其中参数 verbose:

verbose=0:静默模式,不输出任何日志信息。

verbose=1:输出进度条和每个 epoch 的评估结果。

verbose=2:输出每个 batch 的评估结果,但不显示进度条。

3、model.save_weights()将模型作为文件保存,后面可以加载使用

五、通过MNIST测试集验证效果

以下直接粘贴代码:

思路是 从 测试集中(x_test)随机挑选 一个张量 进行预测,看预测的结果和 测试标签集中(y_test)中的实际结果是否一致。

本例中没有 其他要注意事项,仅仅用到了 model.load_weights()来加载模型,model.predict()来进行模型预测。

唯一要注意的是 预测的张量的shape要和 训练使用的训练集保持一致,此处为new[] { 1, 28, 28, 1 }

private void button1_Click(object sender, EventArgs e)
{
    if (System.IO.File.Exists(modelFile))
    {
        model = CreateModel();
        model.load_weights(modelFile);
    }
    int num = Convert.ToInt32(txtResult.Text);
    string str = "真实数字是" + y_test[num];
    textBox_history.Text += "\r\n" + "Real Label is:" + y_test[num] + "\r\n";
    var predict_result = model.predict(x_test[num].reshape(new[] { 1, 28, 28, 1 }));
    var predict_label = np.argmax(predict_result[0].numpy(), axis: 1);
    str += "测试结果是:" + predict_label[0].ToString();
    label1.Text = str;
  }

测试结果,准确率能达到 98%

六、通过手写数字来验证效果

1、在picturebox上实现手写数字,

1)、定义变量

//以下是使用picturebox绘制手写数字的变量
Bitmap drawingBitmap;
bool isDrawing = false;
Point previousPoint;

2)窗体构造方法中定义事件

 public Form1()
{
    InitializeComponent();
    this.DoubleBuffered = true; // 启用双缓冲以减少闪烁
    this.pictureBox_Image.MouseDown += PictureBox_Image_MouseDown; ;
    pictureBox_Image.MouseMove += PictureBox_Image_MouseMove; ;
    pictureBox_Image.MouseUp += PictureBox_Image_MouseUp ;
    pictureBox_Image.Paint += PictureBox_Image_Paint;
    //pictureBox_Image.Paint += new PaintEventHandler(Redraw); // 重绘事件处理
    this.DoubleBuffered = true;

    // 创建绘图画布
    drawingBitmap = new Bitmap(pictureBox_Image.Width, pictureBox_Image.Height, PixelFormat.Format24bppRgb);
    using (Graphics g = Graphics.FromImage(drawingBitmap))
    {
        g.Clear(Color.Black);
    }
}

3)通过事件实现picturebox绘图

private void PictureBox_Image_MouseUp(object? sender, MouseEventArgs e)
{
    isDrawing = false;

    drawingBitmap.Save("orginal.bmp");
}

private void PictureBox_Image_MouseMove(object? sender, MouseEventArgs e)
{
    if (isDrawing)
    {
        // 在绘图画布上绘制线条
        using (Graphics g = Graphics.FromImage(drawingBitmap))
        {                    
            g.DrawLine(new Pen(Color.White, 10), previousPoint, e.Location);
        }

        // 更新上一个点的位置
        previousPoint = e.Location;

        // 刷新 PictureBox
        pictureBox_Image.Invalidate();
    }
}

private void PictureBox_Image_MouseDown(object? sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        isDrawing = true;
        previousPoint = e.Location;
    }
}

private void PictureBox_Image_Paint(object? sender, PaintEventArgs e)
{
    e.Graphics.DrawImage(drawingBitmap, Point.Empty);
}

绘图完成后保存成了 图片 orginal.bmp

4)定义工具类,将手绘的图片orginal.bmp实现 灰度转化和归一化

using System;
using System.Collections.Generic;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tensorflow;
using Tensorflow.NumPy;

namespace WinFormCNN
{
  public class MnistConverter
  {
      public static float[] ConvertToMnistFormat()
      {
          //1、灰度化
          Bitmap grayBitmap = ConvertToGrayscale();

          // 2. 归一化像素值并转换为 MNIST 格式
          float[] mnistData = new float[28 * 28];
          for (int y = 0; y < 28; y++)
          {
              for (int x = 0; x < 28; x++)
              {
                  Color pixel = grayBitmap.GetPixel(x, y);
                  float normalizedValue = pixel.R / 255.0f; // 归一化到 0-1 之间
                  mnistData[y * 28 + x] = normalizedValue;
              }
          }
          return mnistData;
      }

      private static Bitmap ConvertToGrayscale()
      {
        
          Bitmap bitmap = new Bitmap(Image.FromFile("orginal.bmp"),28,28);
         
          Bitmap grayBitmap = new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format24bppRgb);
          for (int y = 0; y < bitmap.Height; y++)
          {
              for (int x = 0; x < bitmap.Width; x++)
              {
                  Color pixel = bitmap.GetPixel(x, y);
                  if (pixel.R > 0 || pixel.G > 0 || pixel.B > 0) {

                      Console.WriteLine(pixel.R + "," + pixel.G + "," + pixel.B);
                  }
                  var test= (pixel.R * 0.299 + pixel.G * 0.587 + pixel.B * 0.114);
                  int grayValue = (int)(pixel.R * 0.299 + pixel.G * 0.587 + pixel.B * 0.114);
                  grayBitmap.SetPixel(x, y, Color.FromArgb(grayValue, grayValue, grayValue));
              }
          }          
          return grayBitmap;
      }       
  }
}

5)测试效果

要先 加载数据集

 private void button1_Click_1(object sender, EventArgs e)
 {
     (x_train, y_train, x_test_raw, y_test) = keras.datasets.mnist.load_data();
     x_train = x_train.reshape((60000, 28, 28, 1)) / 255f;
     x_test = x_test_raw.reshape((10000, 28, 28, 1)) / 255f;
 }

跳过模型训练,直接加载模型后进行预测

       private void button1_Click(object sender, EventArgs e)
       {           
           if (System.IO.File.Exists(modelFile))
           {
               model = CreateModel();
               model.load_weights(modelFile);
           }
          //将上面步骤绘制的数字转化为浮点数组
           float[] imageBytes = MnistConverter.ConvertToMnistFormat();
   		//将浮点型数组转化为预测张量
           var tensor = tf.constant(imageBytes, dtype: tf.float32);
           tensor = tf.reshape(tensor, (1, 28, 28, 1));
        //进行预测,观察结果
           var predict_result = model.predict(tensor);
           var predict_label = np.argmax(predict_result[0].numpy(), axis: 1);
           var str = "测试结果是:" + predict_label[0].ToString();

           label1.Text = str;
       }

经过多次测试,准确率很高。

相关推荐
极客BIM工作室21 分钟前
QKV 注意力机制在Transformer架构中的作用,和卷积在卷积神经网络中的地位,有哪些相似之处?
深度学习·cnn·transformer
码观~天工1 小时前
AI与.NET技术实操系列(二):开始使用ML.NET
ai·.net·ml.net
机器学习之心1 小时前
JCRQ1河马算法+四模型对比!HO-CNN-GRU-Attention系列四模型多变量时序预测
算法·cnn·gru·cnn-gru·四模型多变量时序预测
武狐肆骸14 小时前
第十三站:卷积神经网络(CNN)的优化
人工智能·神经网络·cnn
步、步、为营21 小时前
C# 应用程序中,输入法操控
开发语言·c#·.net
朝野布告1 天前
记一次.NET内存居高不下排查解决与启示
.net·dotnet·内存泄露·k8s部署
董林夕1 天前
CSS Selectors
前端·css·tensorflow
极客BIM工作室1 天前
如何计算卷积神经网络每一层的参数数量和特征图大小?
人工智能·神经网络·cnn
码观~天工1 天前
AI与.NET技术实操系列 - 开篇
ai·.net·ml.net