利用神经网络算法的C#手写数字识别

转发来自云加社区,用于学习机器学习与神经网络

欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~

介绍

这是一篇基于Mike O'Neill 写的一篇很棒的文章:神经网络的手写字符识别(Neural Network for Recognition of Handwritten Digits)而给出的一个人工神经网络实现手写字符识别的例子。尽管在过去几年已经有许多系统和分类算法被提出,但是手写识别任然是模式识别中的一项挑战。Mike O'Neill的程序对想学习通过神经网络算法实现一般手写识别的程序员来说是一个极好的例子,尤其是在神经网络的卷积部分。那个程序是用MFC/ C++编写的,对于不熟悉的人来说有些困难。所以,我决定用C#重新写一下我的一些程序。我的程序已经取得了良好的效果,但还并不优秀(在收敛速度,错误率等方面)。但这次仅仅是程序的基础,目的是帮助理解神经网络,所以它比较混乱,有重构的必要。我一直在试把它作为一个库的方式重建,那将会很灵活,很简单地通过一个INI文件来改变参数。希望有一天我能取得预期的效果。

字符检测

模式检测和字符候选检测是我在程序中必须面对的最重要的问题之一。事实上,我不仅仅想利用另一种编程语言重新完成Mike的程序,而且我还想识别文档图片中的字符。有一些研究提出了我在互联网上发现的非常好的目标检测算法,但是对于像我这样的业余项目来说,它们太复杂了。在教我女儿绘画时发现的一个方法解决了这个问题。当然,它仍然有局限性,但在第一次测试中就超出了我的预期。在正常情况下,字符候选检测分为行检测,字检测和字符检测几种,分别采用不同的算法。我的做法和这有一点点不同。检测使用相同的算法:

public static Rectangle GetPatternRectangeBoundary
(Bitmap original,int colorIndex, int hStep, int vStep, bool bTopStart)

以及:

public static List<Rectangle> PatternRectangeBoundaryList
(Bitmap original, int colorIndex, int hStep, int vStep,
bool bTopStart,int widthMin,int heightMin)

通过改变参数hStep (水平步进)和vStep (垂直步进)可以简单地检测行,字或字符。矩形边界也可以通过更改bTopStart true false实现从上到下和从左到右不同方式进行检测。矩形被widthMin 和d限制。我的算法的最大优点是:它可以检测不在同一行的字或字符串。

public void PatternRecognitionThread(Bitmap bitmap)
{
_originalBitmap = bitmap;
if (_rowList == null)
{
_rowList = AForge.Imaging.Image.PatternRectangeBoundaryList
(_originalBitmap,255, 30, 1, true, 5, 5);
_irowIndex = 0; }
foreach(Rectangle rowRect in _rowList)
{
_currentRow = AForge.Imaging.ImageResize.ImageCrop
(_originalBitmap, rowRect);
if (_iwordIndex == 0)
{
_currentWordsList = AForge.Imaging.Image.PatternRectangeBoundaryList
(_currentRow, 255, 20, 10, false, 5, 5);
} foreach (Rectangle wordRect in _currentWordsList)
{
_currentWord = AForge.Imaging.ImageResize.ImageCrop
(_currentRow, wordRect);
_iwordIndex++;
if (_icharIndex == 0)
{
_currentCharsList =
AForge.Imaging.Image.PatternRectangeBoundaryList
(_currentWord, 255, 1, 1, false, 5, 5);
} foreach (Rectangle charRect in _currentCharsList)
{
_currentChar = AForge.Imaging.ImageResize.ImageCrop
(_currentWord, charRect);
_icharIndex++;
Bitmap bmptemp = AForge.Imaging.ImageResize.FixedSize
(_currentChar, 21, 21);
bmptemp = AForge.Imaging.Image.CreateColorPad
(bmptemp,Color.White, 4, 4);
bmptemp = AForge.Imaging.Image.CreateIndexedGrayScaleBitmap
(bmptemp);
byte[] graybytes = AForge.Imaging.Image.GrayscaletoBytes(bmptemp);
PatternRecognitionThread(graybytes);
m_bitmaps.Add(bmptemp);
}
string s = " \n";
_form.Invoke(_form._DelegateAddObject, new Object[] { 1, s });
If(_icharIndex ==_currentCharsList.Count)
{
_icharIndex =0;
}
}
If(_iwordIndex==__currentWordsList.Count)
{
_iwordIndex=0;
}
}

字符识别

原程序中的卷积神经网络(CNN)包括输入层在内本质上是有五层。卷积体系结构的细节已经在Mike和Simard博士在他们的文章《应用于视觉文件分析的卷积神经网络的最佳实践》中描述过了。这种卷积网络的总体方案是用较高的分辨率去提取简单的特征,然后以较低的分辨率将它们转换成复杂的特征。生成较低分辨的最简单方法是对子层进行二倍二次采样。这反过来又为卷积核的大小提供了参考。核的宽度以一个单位(奇数大小)为中心被选定,需要足够的重叠从而不丢失信息(对于一个单位3重叠显得过小),同时不至于冗余(7重叠将会过大,5重叠能实现超过70%的重叠)。因此,在这个网络中我选择大小为5的卷积核。填充输入(调整到更大以实现特征单元居中在边界上)并不能显着提高性能。所以不填充,内核大小设定为5进行二次采样,每个卷积层将特征尺寸从n减小到(n-3)/2。由于在MNIST的初始输入的图像大小为28x28,所以在二次卷积后产生整数大小的近似值是29x29。经过两层卷积之后,5x5的特征尺寸对于第三层卷积而言太小。Simard博士还强调,如果第一层的特征少于五个,则会降低性能,然而使用超过5个并不能改善(Mike使用了6个)。类似地,在第二层上,少于50个特征会降低性能,而更多(100个特征)没有改善。关于神经网络的总结如下:

#0层:是MNIST数据库中手写字符的灰度图像,填充到29x29像素。输入层有29x29 = 841个神经元。

#1层:是一个具有6个特征映射的卷积层。从层#1到前一层有13×13×6 = 1014个神经元,(5×5 + 1)×6 = 156个权重,以及1014×26 = 26364个连接。

#2层:是一个具有五十(50)个特征映射的卷积层。从#2层到前一层有5x5x50 = 1250个神经元,(5x5 + 1)x6x50 = 7800个权重,以及1250x(5x5x6 + 1)= 188750个连接。(在Mike的文章中不是有32500个连接)。

#3层:是一个100个单元的完全连接层。有100个神经元,100x(1250 + 1)= 125100权重,和100x1251 = 125100连接。

#4层:是最后的,有10个神经元,10×(100 + 1)= 1010个权重,以及10×10 1 = 1010个连接。

反向传播

反向传播是更新每个层权重变化的过程,从最后一层开始,向前移动直到达到第一个层。

在标准的反向传播中,每个权重根据以下公式更新:

(1)

其中eta是“学习率”,通常是类似0.0005这样的小数字,在训练过程中会逐渐减少。但是,由于收敛速度慢,标准的反向传播在程序中不需要使用。相反,LeCun博士在他的文章《Efficient BackProp》中提出的称为“随机对角列文伯格-马夸尔特法(Levenberg-Marquardt)”的二阶技术已得到应用,尽管Mike说它与标准的反向传播并不相同,理论应该帮助像我这样的新人更容易理解代码。

在Levenberg-Marquardt方法中,rw 计算如下:

假设平方代价函数是:

那么梯度是:

而Hessian遵循如下规则:

Hessian矩阵的简化近似为Jacobian矩阵,它是一个维数为N×O的半矩阵。

用于计算神经网络中的Hessian矩阵对角线的反向传播过程是众所周知的。假设网络中的每一层都有:

(7)

使用Gaus-Neuton近似(删除包含|'(y))的项,我们得到:

(8)(9)

以及:

随机对角列文伯格-马夸尔特(Levenberg-Marquardt)法

事实上,使用完整Hessian矩阵信息(Levenberg-Marquardt,Gaus-Newton等)的技术只能应用于以批处理模式训练的非常小的网络,而不能用于随机模式。为了获得Levenberg- Marquardt算法的随机模式,LeCun博士提出了通过关于每个参数的二阶导数的运算估计来计算Hessian对角线的思想。瞬时二阶导数可以通过反向传播获得,如公式(7,8,9)所示。只要我们利用这些运算估计,可以用它们来计算每个参数各自的学习率:

其中e是全局学习速率,并且

是关于h ki的对角线二阶导数的运算估计。m是防止h ki在二阶导数较小的情况下(即优化在误差函数的平坦部分移动时)的参数。可以在训练集的一个子集(500随机化模式/ 60000训练集的模式)中计算二阶导数。由于它们变化非常缓慢,所以只需要每隔几个周期重新估计一次。在原来的程序中,对角线Hessian是每个周期都重新估算的。

这里是C#中的二阶导数计算函数:

public void BackpropagateSecondDerivatives(DErrorsList d2Err_wrt_dXn /* in */,
DErrorsList d2Err_wrt_dXnm1 /* out */)
{
// 命名(从NeuralNetwork类继承)
// 注意:尽管我们正在处理二阶导数(而不是一阶),
// 但是我们使用几乎相同的符号,就好像有一阶导数
// 一样,否则ASCII的显示会令人误解。 我们添加一
// 个“2”而不是两个“2”,比如“d2Err_wrt_dXn”,以简
// 单地强调我们使用二阶导数
//
// Err是整个神经网络的输出误差
// Xn是第n层上的输出向量
// Xnm1是前一层的输出向量
// Wn是第n层权重的向量
// Yn是第n层的激活值,
// 即,应用挤压功能之前的输入的加权和
// F是挤压函数:Xn = F(Yn)
// F'是挤压函数的导数
// 简单说,对于F = tanh,则F'(Yn)= 1-Xn ^ 2,即,
// 可以从输出中计算出导数,而不需要知道输入 int ii, jj;
uint kk;
int nIndex;
double output;
double dTemp; var d2Err_wrt_dYn = new DErrorsList(m_Neurons.Count);
//
// std::vector< double > d2Err_wrt_dWn( m_Weights.size(), 0.0 );
// important to initialize to zero
//////////////////////////////////////////////////
//
///// 设计 TRADEOFF: REVIEW !!
//
// 请注意,此命名的方案与NNLayer :: Backpropagate()
// 函数中的推理相同,即从该函数派生的
// BackpropagateSecondDerivatives()函数
//
// 我们希望对数组“d2Err_wrt_dWn”使用STL向量(为了便于编码)
// ,这是图层中当前模式的错误权重的二阶微分。 但是,对于
// 具有许多权重的层(例如完全连接的层),也有许多权重。 分
// 配大内存块时,STL向量类的分配器非常愚蠢,并导致大量的页
// 面错误,从而导致应用程序总体执行时间减慢。 // 为了解决这个问题,我尝试使用一个普通的C数组,
// 并从堆中取出所需的空间,并在函数结尾处删除[]。
// 但是,这会导致相同数量的页面错误错误,并
// 且不会提高性能。 // 所以我试着在栈上分配一个普通的C数组(即不是堆)。
// 当然,我不能写double d2Err_wrt_dWn [m_Weights.size()];
// 因为编译器坚持一个编译时间为数组大小的已知恒定值。
// 为了避免这个需求,我使用_alloca函数来分配堆栈上的内存。
// 这样做的缺点是堆栈使用过多,可能会出现堆栈溢出问题。
// 这就是为什么将它命名为“Review” double[] d2Err_wrt_dWn = new double[m_Weights.Count];
for (ii = 0; ii < m_Weights.Count; ++ii)
{
d2Err_wrt_dWn[ii] = 0.0;
}
// 计算 d2Err_wrt_dYn = ( F'(Yn) )^2 *
// dErr_wrt_Xn (其中dErr_wrt_Xn实际上是二阶导数) for (ii = 0; ii < m_Neurons.Count; ++ii)
{
output = m_Neurons[ii].output;
dTemp = m_sigmoid.DSIGMOID(output);
d2Err_wrt_dYn.Add(d2Err_wrt_dXn[ii] * dTemp * dTemp);
}
// 计算d2Err_wrt_Wn =(Xnm1)^ 2 * d2Err_wrt_Yn
// (其中dE2rr_wrt_Yn实际上是二阶导数)
// 对于这个层中的每个神经元,通过先前层的连接
// 列表,并更新相应权重的差分 ii = 0;
foreach (NNNeuron nit in m_Neurons)
{
foreach (NNConnection cit in nit.m_Connections)
{
try
{
kk = (uint)cit.NeuronIndex;
if (kk == 0xffffffff)
{
output = 1.0;
// 这是隐含的联系; 隐含的神经元输出“1”
}
else
{
output = m_pPrevLayer.m_Neurons[(int)kk].output;
} // ASSERT( (*cit).WeightIndex < d2Err_wrt_dWn.size() );
// 因为在将d2Err_wrt_dWn更改为C风格的
// 数组之后,size()函数将不起作用 d2Err_wrt_dWn[cit.WeightIndex] = d2Err_wrt_dYn[ii] * output * output;
}
catch (Exception ex)
{ }
} ii++;
}
// 计算d2Err_wrt_Xnm1 =(Wn)^ 2 * d2Err_wrt_dYn
// (其中d2Err_wrt_dYn是不是第一个二阶导数)。
// 需要d2Err_wrt_Xnm1作为d2Err_wrt_Xn的
// 二阶导数反向传播的输入值
// 对于下一个(即先前的空间)层
// 对于这个层中的每个神经元 ii = 0;
foreach (NNNeuron nit in m_Neurons)
{
foreach (NNConnection cit in nit.m_Connections)
{
try
{
kk = cit.NeuronIndex;
if (kk != 0xffffffff)
{
// 我们排除了ULONG_MAX,它表示具有恒定输出“1”的
// 虚偏置神经元,因为我们不能正真训练偏置神经元 nIndex = (int)kk;
dTemp = m_Weights[(int)cit.WeightIndex].value;
d2Err_wrt_dXnm1[nIndex] += d2Err_wrt_dYn[ii] * dTemp * dTemp;
}
}
catch (Exception ex)
{
return;
}
} ii++; // ii 跟踪神经元迭代器
}
double oldValue, newValue; // 最后,使用dErr_wrt_dW更新对角线的层
// 神经元的权重。通过设计,这个函数
// 以及它对许多(约500个模式)的迭代被
// 调用,而单个线程已经锁定了神经网络,
// 所以另一个线程不可能改变Hessian的值。
// 不过,由于这很容易做到,所以我们使用一
// 个原子比较交换操作,这意味着另一个线程
// 可能在二阶导数的反向传播过程中,而且Hessians
// 可能会稍微移动 for (jj = 0; jj < m_Weights.Count; ++jj)
{
oldValue = m_Weights[jj].diagHessian;
newValue = oldValue + d2Err_wrt_dWn[jj];
m_Weights[jj].diagHessian = newValue;
}
}
//////////////////////////////////////////////////////////////////

训练和实验

尽管MFC / C ++和C#之间存不兼容,但是我的程序与原程序相似。使用MNIST数据库,网络在60,000个训练集模式中执行后有291次错误识别。这意味着错误率只有0.485%。然而,在10000个模式中,有136个错误识别,错误率为1.36%。结果并不像基础测试那么好,但对我来说,用我自己的手写字符集做实验已经足够了。首先将输入的图像从上到下分为字符组,然后在每组中把字符从左到右进行检测,调整到29x29像素,然后由神经网络系统识别。该方案满足我的基本要求,我自己的手写数字是可以被正确识别的。在AForge.Net的图像处理库中添加了检测功能,以便使用。但是,因为它只是在我的业余时间编程,我相信它有很多的缺陷需要修复。反向传播时间就是一个例子。每个周期使用大约3800秒的训练时间,但是只需要2400秒。(我的电脑使用了英特尔奔腾双核E6500处理器)。与Mike的程序相比,速度相当慢。我也希望能有一个更好的手写字符数据库,或者与其他人合作,继续我的实验,使用我的算法开发一个真正的应用程序。

参考文献

翻译人:青橙.,该成员来自云+社区翻译社

原文链接:https://www.codeproject.com/Articles/143059/Neural-Network-for-Recognition-of-Handwritten-Di

原文作者:Vietdungiitb

相关阅读

此文已由作者授权腾讯云+技术社区发布,转载请注明文章出处

海量技术实践经验,尽在云加社区! https://cloud.tencent.com/developer

利用神经网络算法的C#手写数字识别(一)的更多相关文章

  1. 【机器学习】k-近邻算法应用之手写数字识别

    上篇文章简要介绍了k-近邻算法的算法原理以及一个简单的例子,今天再向大家介绍一个简单的应用,因为使用的原理大体差不多,就没有没有过多的解释. 为了具有说明性,把手写数字的图像转换为txt文件,如下图所 ...

  2. 卷积神经网络应用于tensorflow手写数字识别(第三版)

    import tensorflow as tf from tensorflow.examples.tutorials.mnist import input_data mnist = input_dat ...

  3. 基于sk_learn的k近邻算法实现-mnist手写数字识别且要求97%以上精确率

    1. 导入需要的库 from sklearn.datasets import fetch_openml import numpy as np from sklearn.neighbors import ...

  4. MLP 之手写数字识别

    0. 前言 前面我们利用 LR 模型实现了手写数字识别,但是效果并不好(不到 93% 的正确率). LR 模型从本质上来说还只是一个线性的分类器,只不过在线性变化之后加入了非线性单调递增 sigmoi ...

  5. 利用神经网络算法的C#手写数字识别

    欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~ 下载Demo - 2.77 MB (原始地址):handwritten_character_recognition.zip 下载源码 - 70. ...

  6. 利用神经网络算法的C#手写数字识别(二)

    利用神经网络算法的C#手写数字识别(二)   本篇主要内容: 让项目编译通过,并能打开图片进行识别.   1. 从上一篇<利用神经网络算法的C#手写数字识别>中的源码地址下载源码与资源, ...

  7. C#中调用Matlab人工神经网络算法实现手写数字识别

    手写数字识别实现 设计技术参数:通过由数字构成的图像,自动实现几个不同数字的识别,设计识别方法,有较高的识别率 关键字:二值化  投影  矩阵  目标定位  Matlab 手写数字图像识别简介: 手写 ...

  8. 实现手写数字识别(数据集50000张图片)比较3种算法神经网络、灰度平均值、SVM各自的准确率—Jason niu

    对手写数据集50000张图片实现阿拉伯数字0~9识别,并且对结果进行分析准确率, 手写数字数据集下载:http://yann.lecun.com/exdb/mnist/ 首先,利用图片本身的属性,图片 ...

  9. 利用c++编写bp神经网络实现手写数字识别详解

    利用c++编写bp神经网络实现手写数字识别 写在前面 从大一入学开始,本菜菜就一直想学习一下神经网络算法,但由于时间和资源所限,一直未展开比较透彻的学习.大二下人工智能课的修习,给了我一个学习的契机. ...

随机推荐

  1. ubutun下安装phantomjs配置chromedriver

    我们在进行动态html页面获取时候,需要用到selenium,phantomjs,chromedriver selenium可以通过命令python的pip进行安装, phantomjs与chrome ...

  2. malloc/free与new/delete的区别(转)

    相同点:都可用于申请动态内存和释放内存 不同点:(1)操作对象有所不同.malloc与free是C++/C 语言的标准库函数,new/delete 是C++的运算符.对于非内部数据类的对象而言,光用m ...

  3. FreeSql接入CAP的实践

    CAP CAP 是一个基于 .NET Standard 的 C# 库,它是一种处理分布式事务的解决方案,同样具有 EventBus 的功能,它具有轻量级.易使用.高性能等特点. https://git ...

  4. tar命令打包和压缩与解压

    Linux里压缩与打包时分开的: 打包:多个文件变一个文件.该一个文件会大于整体所有文件,因为会添加各个信息说明哪到哪是一个文件. 压缩:大文件变小文件. 归档:将多个文件变成一个文件,这个文件就是归 ...

  5. MFC的窗口句柄

    1.窗口.控件的指针和句柄的相互转化 1)指针转化为句柄在MFC应用程序中首先要获得窗口的指针,然后将其转化为句柄 CWnd* pWnd; HANDLE hWnd = pWnd->GetSafe ...

  6. 面试官:你说你精通源码,那你知道ArrayList 源码的设计思路吗?

    Arraylist源码分析 ArrayList 我们几乎每天都会使用到,但是通常情况下我们只是知道如何去使用,至于其内部是怎么实现的我们不关心,但是有些时候面试官就喜欢问与ArrayList 的源码相 ...

  7. 修改pycharm中的flask项目名遇到的坑

    曾修改过自己的项目名,并在settings中的解释器也更正过来了.然后执行pip list 报错: failed to create process. 解决方法如下: 到你的项目的venv目录下的Sc ...

  8. Lambda表达式(一)入门认识篇

    Lambda表达式(一)入门认识篇 Lambda简介 Lambda 表达式是 JDK8 的一个新特性,可以取代大部分的匿名内部类,写出更优雅的 Java 代码,尤其在集合的遍历和其他集合操作中,可以极 ...

  9. 牛客练习赛71 数学考试 题解(dp)

    题目链接 题目大意 要你求出有多少个长度为n的排列满足m个限制条件 第i个限制条件 p[i]表示前 p[i]个数不能是1-p[i]的排列 题目思路 这个感觉是dp但是不知道怎么dp 首先就是要明白如果 ...

  10. C语言基础知识:几种特殊的函数宏封装方式

    函数宏介绍 函数宏,即包含多条语句的宏定义,其通常为某一被频繁调用的功能的语句封装,且不想通过函数方式封装来降低额外的弹栈压栈开销. 函数宏本质上为宏,可以直接进行定义,例如: #define INT ...