.Net 9下使用Tensorflow.net---DNN_使用卷积模型识别手写数字
- 一、创建项目,并导入各依赖项
- 二、初始化
- 三、创建模型
-
- 1、layers.Conv2D,本例中使用该二维卷积层函数,说明下几个重要的参数:
- [2、layers.MaxPooling2D 该层称为 二维最大池化层](#2、layers.MaxPooling2D 该层称为 二维最大池化层)
- [3、layers.Flatten 展平层](#3、layers.Flatten 展平层)
- 四、训练模型,保存模型
-
- [1、model.fit() 该函数就是 训练创建好的模型](#1、model.fit() 该函数就是 训练创建好的模型)
- 2、model.evaluate()评估函数,评估模型的损失度和精度
- 3、model.save_weights()将模型作为文件保存,后面可以加载使用
- 五、通过MNIST测试集验证效果
- 六、通过手写数字来验证效果
-
- 1、在picturebox上实现手写数字,
-
- 1)、定义变量
- 2)窗体构造方法中定义事件
- 3)通过事件实现picturebox绘图
- [4)定义工具类,将手绘的图片orginal.bmp实现 灰度转化和归一化](#4)定义工具类,将手绘的图片orginal.bmp实现 灰度转化和归一化)
- 5)测试效果
在这个例子里,使用了Tensorflow.net中的典型的卷积网络神经模型,加载MNIST数据集,经过训练,可以识别手写的数字,正确率可以达到 95%左右。
本例的基本步骤为
1、导入数据集并初始化获得训练集、测试集;
2、创建CNN模型,定义各神经网络层,并选择激活函数,各神经网络超参数;
3、训练完成后 保存模型;
4、并使用模型 通过MNIST测试集验证效果;
5、通过picturebox手写数字,测试效果。
一、创建项目,并导入各依赖项
本例子使用.net9的winform作为示例。
创建后,通过nuget,添加依赖项
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;
}
经过多次测试,准确率很高。