AI手写数字识别(二)

理解代码

上文主要介绍了人工智能模型的集成过程。人工智能模型的正确集成,是我们案例中人工智能应用开发的核心步骤。但要让一个人工智能应用顺利地被使用,除了集成模型之外的一些工作也是必不可少的,比如处理输入的数据,进行界面交互等。

应用的主体逻辑都在 MNIST.App 项目的 文件中,该文件包括了界面联动、数据预处理两部分的代码,以及一行推理预测的代码。剩下的都是自动生成的代码。界面联动是为了实现手写输入时的良好体验;而数据预处理部分是在推理前,将用户输入的笔迹变为模型所需要输入的浮点数组。MainWindow.cs

小提示

代码中包含了非常详尽的注释。建议在读完本章节后,再通读代码中的注释,以便更深入地理解整个代码逻辑。

界面联动

  1. 展开 MNIST.App 项目,找到 文件。所有的界面设计和代码都在这里。先双击它,打开设计界面。可以看到,设计界面和程序实际运行起来的效果非常像。窗体上包含了三个控件:,,以及 。稍后会介绍控件对应的变量名称,以及绑定的控件事件响应函数、这些控件用途等。MainWindow.csPictureBoxLabelButton

  2. 在窗体的设计界面上右击,并选择 查看代码,可以看到核心的代码。如图所示。

AI手写数字识别(二)_第1张图片

  1. 打开代码后,可以看到,几乎每一行代码都有对应的注释。除了类里的 等几个变量外,剩下的逻辑几乎都在事件响应函数中。这些响应函数是在控件属性的事件面板中添加的。每个控件都在代码中可以通过变量名称来使用。界面逻辑的一些重要信息如下:ImageSize
变量名称 控件类型 绑定的事件响应函数 控件的用途
Form1 Form Form1_Load 主窗体。加载时会一次性初始化部分变量。
writeArea PictureBox writeArea_MouseDown
writeArea_MouseMove
writeArea_MouseUp
手写区域。
在鼠标操作时,响应鼠标的按下(MouseDown)、移动(MouseMove)、释放(MouseUp)事件。
在触摸屏操作时,与鼠标操作类似,会响应手指的接触屏幕、在屏幕上移动、离开屏幕的事件。
outputText Label 文本标签。显示推理结果的数字。
clean Button clean_Click 清除按钮。在每次推理前清除手写区内容,及文本标签显示的数字。

数据预处理

数据预处理是 AI 应用的重要一环。在大部分 AI 应用中,特别是本文的图片分类应用中,通过监督学习来训练模型。即先提供一些标记过分类的图片来训练出模型,然后输入未知的图片,推理预测出此图片的类别。因此,在训练和推理过程中,每次输入模型的数据格式必须完全一致,这样才能保证预测推理的效果。

本示例已知了训练数据的输入形式,所以按图索骥就能写出代码。在 AI 的实际应用中,一定要了解模型输入数据的格式细节,严格的实现它。如果数据格式细节不一致,通常会降低推理结果的正确率。而这类问题几乎不会产生编译或运行错误,而且数据是不易直观理解的浮点数组,所以对此类问题的诊断和修正较困难。

下面会介绍一下本例中的数据预处理过程,从而体会一下数据预处理中的细节问题。

  1. 数据预处理的第一步,在窗体设计时,手写区域调整为了正方形,和训练数据的形状保持一致。

  2. 定义了类变量 常量等于 。这是训练数据的实际图片尺寸。界面上的正方形最终会缩小为 。ImageSize2828x28

private const int ImageSize = 28;
  1. clear_Click函数中设置了手写区域的背景为白色。训练数据是黑白的,需要将前景、背景颜色同样设置,而与训练数据一样,达到最大的对比度。
graphics.Clear(Color.White);
  1. writeArea_MouseMove事件中设置了手写笔风格。手写笔迹宽度是40,颜色为黑色,开始、结束位置设成圆头。

笔迹宽度与图片尺寸的比例基本匹配了训练数据的比例。但无法控制用户输入的文字大小,还会有一些误差。好在深度学习的模型适应性较强,对识别准确率的影响不太大。

黑色笔迹配合了白色背景,形成最大的对比度。

笔迹的开始、结束位置为圆头。在书写过程中会多次调用到鼠标移动事件中,每次根据上一次的结束位置到当前位置画了一条直线。如果不将笔头设置为圆头,就会像图 13中一样,这些直线会形成矩形拼接起来,形成很多不连续的位置。既影响识别,也不美观。

Pen penStyle = new Pen(Color.Black, 40) 
{
    StartCap = LineCap.Round, 
    EndCap = LineCap.Round 
};

AI手写数字识别(二)_第2张图片

  1. writeArea_MouseUp事件中包含了其它的数据处理逻辑。首先构造了 28x28 的图片,将手写的图片缩放到了新的图片对象中。
Bitmap clonedBmp = new Bitmap(writeArea.Image, ImageSize, ImageSize);
  1. 按行、列遍历了 28x28 位图中的所有节点,并取出了每个像素,处理后存入数组中。
for (int y = 0; y < ImageSize; y++)
{
    for (int x = 0; x < ImageSize; x++)
    {
        Color color = clonedBmp.GetPixel(x, y);
        // 此处略过处理过程。包括reversed变量的定义,请参见下文。
        image.Add((float)reversed);
    }
}
  1. 将红绿蓝通道加和并平均,完成了像素灰度化。手写识别模型的输入数据是黑白图。

手写区是白底黑字,每个颜色通道的值其实是一样的。为了逻辑上的严密和便于理解,所以对值取了均值,进行灰度化。

double average = (color.R + color.G + color.B) / 3.0;
  1. 将取值范围变换到了 0~1。机器学习中取值范围变化很大,因此绝大部分机器学习模型都用浮点数进行计算。
double oneValue = average / 255;
  1. 将数值翻转,并做+0.5 的位移。这一步可以减少输入数据中零值的数量。

过多的零,会让中间结果也出现更多的零,使得神经网络容易丢失信息。白色背景通过灰度化之后灰度值是 255,经变换后为 -0.5;黑色笔迹灰度化之后灰度值是0,经变换后为 0.5。这样处理后,大部分值都成为了非零值。最重要的是我们所依赖的模型(衍生自脚本),对输入数据的处理也是如此。mnist.py

对于数据的取反,是经验的做法。通过试验,在很多情况下取反后的训练效果会更好。MNIST数据集的数据也是取反保存的。

double reversed = 0.5 - oneValue;

推理

推理即输入数据并获取模型的预测结果。这一步非常简单,输入变换后的一维数组,输出预测结果即可。推理函数每次调用可以输入多张图片,进行批量预测。

long inferResult = model.Infer(new List> { image }).First().First();

小提示

如果有兴趣的话,可以看看Mnist 项目中生成的用于访问模型代码。该项目里还包含了优化后的模型数据文件。

实际场景

本示例非常简单,还不足以成为实际的手写识别产品。但经过这小小的一步,就迈入了开发 AI 应用的门槛。通过组合多个模型,混合传统的编程方法,就能打造出强大的 AI 产品。

这里抛砖引玉,提出一些实际中会遇到的问题,并提出一些解决方案供参考。除此之外,在真正应用时还会发现更多的问题,如连笔文字,歪斜矫正,梯形校正等等。所以,虽然有了 AI 模型,可以将更多的不可能变为可能,但所需要的软件开发工程量还是很大。要做出一款产品,除了与 AI 相关的新问题外,传统软件开发中的问题一样都不会少。

思考实际场景中问题的同时,也会培养 AI 的应用意识。做产品时,如果遇到了有很多数据,但很难找到规律的情况。不妨思考一下,能否使用 AI 模型来解决这个问题?有了这样的 AI 意识,就能更好的将 AI 和传统软件相结合,创造出让人眼前一亮、用户体验自然的产品。

在大部分 AI 应用中,数据非常重要。我们经常可以看到一些 AI 讨论中会说,数据是 AI 应用的基础。在下面的解决方案中也能看出,很多方案依赖于大量的、有代表性的数据才能实现。

另外,从下面的方案中也可以看到,传统方案很易于理解和实现,但处理噪音等数据却比较棘手。AI 模型的质量则取决于数据和算法。究竟哪个更好,要在实际应用中进行验证,才能有结论。也可以同时使用多种方案,再通过一个 AI 模型来决定使用哪个方法的结果。

大小不一或没有居中

在尝试书写的时候也会发现,当字写得小一点,或者写偏一点,错误识别的情况就会变多。这是因为训练数据为了提高识别率,对数据都进行了居中、缩放的处理。可能的解决方案有两种:

采用图像处理算法,找到笔迹像素的分布中心,将其居中。并根据外围的笔迹像素来进行缩放,适配到识别区中。此方法逻辑清晰,不需要数据进行训练。缺点是如果手写数据来源于摄像头,会有很多噪点,会造成误判。

训练一个目标检测(Object Detect)的 AI 模型来自动框出合适的大小。这种方法会将需要的目标(即字符)用矩形框出来、并基本保证其居中。将矩形做适当的缩放后,既可作为输入。如果数据量足够且有代表性,这种方法的效果会不错。但目标检测需要的数据标记工作却很繁琐,需要对每个训练数据中的字符画框标记,标记的质量也直接影响到了识别的质量。

一次识别多个字符

在书写时,实际情况通常是书写一句话或者一个等式,很少只输入一个字符。多字符识别时,需要将其分割为单字符进行逐个识别。其中主要有两个问题,一是将字符分割开,二是将字符进行排序。

  1. 分割字符

传统的解决方案较多,这里列举两种:

a.	找到相邻的点进行扩展,找到每一笔,然后根据矩形是否重合来找到每个字。这种方法的缺点是如果在识别中文等情况下,遇到左右或者上下结构,可能会被识别成两个字符。

b.	检查每一列,如果没有任何笔迹像素,则将左右两方切割开来。但这样在左右结构时仍然会遇到问题,也不支持纵向书写。

用 AI 模型,可以使用上文提到的目标检测(Object Detect)方法,框出每个字符。

  1. 字符排序

单个字符本身的意义还不够,语言文字中需要将字符组成词语和句子,才有更丰富的含义。在上一步中,虽然分割出了单个的字符,在完成识别后,还需要将它们组合成词语和句子。除了常见的横向书写外,还有纵向书写。如果纸上没有画线,还可能越写越歪。这一步可以用传统的方法根据各个方向的距离等因素判断是否是连续的字符串。

个人风格迥异

识别不同书写风格,是个数据问题。如果有了有代表性的数据集,这个问题就迎刃而解了。

识别字母、符号、中文

同上,数据是最重要的。另外,随着问题规模的变大,使用的 模型有可能也需要做相应的修改,需要有足够的复杂度来容纳新的信息。如,中文字符比较复杂,可能 28x28 的大小不够表达足够多的细节,需要原始输入的尺寸更大。AI

常见问题

克隆代码出现错误

在教育网等网络环境里,访问 GitHub 较不稳定。如果不能找到其它网络环境的话,只能试着在网络的非高峰使用期进行克隆。

创建模型项目时出现错误

如果在添加模型项目完成后,没有在目录下看到 文件。通常是因为网络不稳定,下载超时造成的。请参考下面的过程手工下载依赖的库。packages.config

右击 Model 项目,选择管理 NuGet 程序包...

AI手写数字识别(二)_第3张图片

如下图所示,在弹出的 NuGet 页面里,点击 浏览,然后输入 ml.scoring,来手工搜索需要的 NuGet 包。

AI手写数字识别(二)_第4张图片

点选搜索出的 Microsoft.ML.Scoring,点击右边的 安装,然后点击 确定,如下图所示。然后我们会在输出界面看到开始添加 Microsoft.ML.Scoring 的信息。如果网速较慢,这一步会花费一些时间。完成后,即可根据本文内容进行下一步操作。

AI手写数字识别(二)_第5张图片

运行时提示"系统找不到指定的路径"

AI手写数字识别(二)_第6张图片

一般是由于路径中有中文字符造成的,需要将整个解决方案移动到不包含中文的路径中。

你可能感兴趣的:(人工智能,microsoft,机器学习,深度学习)