下载Demo-2.77MB(原文地址)handwritten_character_recognition.zip下载源代码-70.64KB(原文地址)nnhandwrittencharreccssource.zipIntroduction这是一篇基于MikeO'写的文章的文章Neill的一篇很棒的文章:NeuralNetworkforRecognitionofHandwrittenDigits(手写数字识别的神经网络)给出了一个用于手写字符识别的人工神经网络示例。尽管在过去几年中提出了许多系统和分类算法,但手写识别仍然是模式识别中的一个挑战。MikeO'Neill的程序对于想要通过神经网络算法学习一般手写识别的程序员来说是一个很好的例子,尤其是在神经网络的卷积部分。该程序是用MFC/C++编写的,对于不熟悉它的人来说有些困难。所以,我决定用C#重写我的一些程序。我的程序取得了不错的效果,但还算不上优秀(在收敛速度、错误率等方面)。但是这次只是程序的基础,目的是为了帮助理解神经网络,所以比较迷惑,需要重构。我一直在尝试将它重建为一个库,这将非常灵活,可以通过INI文件轻松更改参数。希望有一天我能达到想要的效果。字符检测模式检测和字符候选检测是我在程序中必须面对的最重要的问题之一。其实我不仅想用另一种编程语言重新做Mike的程序,我还想识别文档图片中的字符。我在互联网上找到了一些提出非常好的目标检测算法的研究,但对于像我这样的业余项目来说,它们太复杂了。我在教女儿画画时发现的一个解决方案解决了这个问题。当然,它仍然有局限性,但它在第一次测试时超出了我的预期。通常情况下,字符候选检测分为线检测、字检测和字符检测,分别使用不同的算法。我的方法有点不同。检测使用相同的算法:publicstaticRectangleGetPatternRectangeBoundary(Bitmaporiginal,intcolorIndex,inthStep,intvStep,boolbTopStart)和:publicstaticListPatternRectangeBoundaryList(Bitmaporiginal,intcolorIndex,inthStep,intvStep,boolbTopStart,intwidthMin)并通过参数MStep更改级别步长,在vStep(垂直步长)可以简单地检测线条、单词或字符。通过将bTopStart更改为true或false,也可以从上到下和从左到右以不同的方式检测矩形边界。矩形由widthMin和d界定。我的算法最大的优点是:它可以检测出不在同一行的单词或字符串。字符时选择识别别可以通过以下方法实现:publicvoidPatternRecognitionThread(Bitmapbitmap){_originalBitmap=bitmap;if(_rowList==null){_rowList=AForge.Imaging.Image.PatternRectangeBoundaryList(_originalBitmap,255,30,5,1,);_irowIndex=0;}foreach(RectanglerowRectin_rowList){_currentRow=AForge.Imaging.ImageResize.ImageCrop(_originalBitmap,rowRect);if(_iwordIndex==0){_currentWordsList=AForge.Imaging.Image.PatternRectangeBoundaryList(_currentRow,255,20,10,false,5,5);}foreach(RectanglewordRectin_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(RectanglecharRectin_currentCharsList){_currentChar=AForge.Imaging.ImageResize.ImageCrop(_currentWord,charRect);_icharIndex++;Bitmapbmptemp=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);}字符串="\n";_form.Invoke(_form._DelegateAddObject,newObject[]{1,s});If(_icharIndex==_currentCharsList.Count){_icharIndex=0;}}If(_iwordIndex==__currentWordsList.Count){_iwordIndex=0;}}原始字符识别程序中的卷积神经网络(CNN),包括输入层在内,本质上是一个五层卷积架构。Dr.Mike和Simard在他们的文章《应用于视觉文件分析的卷积神经网络的***实践》中描述了架构的细节。这个卷积网络的总体方案是在较高的分辨率下提取简单的特征,然后在较低的分辨率下将其转化为复杂的特征。生成较低分辨率的最简单方法是对子层进行二倍子采样。这反过来为卷积核的大小提供了参考。内核的宽度选择以一个单元为中心(奇数大小),需要足够的重叠而不丢失信息(3个重叠对于一个单元来说太小),并且不冗余(7个重叠太大,5个重叠可以实现超过70%的重叠)。因此,在这个网络中,我选择了一个大小为5的卷积核。填充输入(将大小调整到更大,以便特征单元位于边界的中心)不会显着提高性能。所以没有padding,kernelsize设置为5进行subsampling,每个卷积层将featuresize从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×101=1010个连接。Backpropagation反向传播是更新每一层权重变化的过程,从最后一层开始,一直向前,直到到达第一层。在标准的反向传播中,每个权重根据以下公式更新:(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-MaLevenberg-Marquardt方法事实上,使用全Hessian矩阵信息的技术(Levenberg-Marquardt、Gaus-Newton等)只能应用于以批处理模式训练的非常小的网络,而不是随机模式。为了得到Levenberg-Marquardt算法的随机模式,LeCun博士提出了通过对每个参数的二阶导数的运算估计来计算Hessian的对角线的想法。瞬时二阶导数可以通过反向传播获得,如等式(7,8,9)所示。只要我们利用这些操作估计,我们就可以使用它们来计算每个参数的单独学习率:其中e是全局学习率,是对角线关于hki的二阶导数的操作估计。m是在二阶导数较小的情况下(即当优化在误差函数的平坦部分移动时)防止hki的参数。可以在训练集的子集中计算二阶导数(训练集的500个随机模式/60000个模式)。由于它们变化非常缓慢,因此只需每隔几个周期重新估计一次。在原来的程序中,对角Hessian在每个周期都被重新估计。下面是C#中的二阶导数计算函数:publicvoidBackpropagateSecondDerivatives(DErrorsListd2Err_wrt_dXn/*in*/,DErrorsListd2Err_wrt_dXnm1/*out*/){//命名(继承自NeuralNetwork类)//注意:虽然我们处理的是二阶导数(而不是一阶),//但我们使用几乎相同的符号,就好像存在一阶导数一样//否则ASCII显示会产生误导。我们添加一个//一个“2”而不是两个“2”,比如“d2Err_wrt_dXn”,来简单地//简单地强调我们使用的是二阶导数////Err是整个神经网络的输出误差//Xn是第n层的输出向量//Xnm1是前一层的输出向量//Wn是第n层的权重向量//Yn是第n层的激活值,//即应用前一层的输入squeezefunctionWeightedsum//F是挤压函数:Xn=F(Yn)//F'是挤压函数的导数//简单的说,对于F=tanh,则F'(Yn)=1-Xn^2、即,//可以在不知道输入的情况下从输出计算导数m_Weights.size(),0.0);//importanttoinitializetozero/////////////////////////////////////////////////////////设计权衡:回顾!!////请注意,此命名方案与NNLayer::Backpropagate()//函数中的推理相同,//BackpropagateSecondDerivatives()函数派生自此函数////我们要使用STL数组“d2Err_wrt_dWn”的向量(为了便于编码)//这是层中当前模式的错误权重的二阶导数。但是,对于权重很多的层(例如全连接层),权重也很多。STL向量类的分配器在分配//分配大块内存时非常愚蠢,会导致大量页面错误,从而减慢应用程序的整体执行时间。//为了解决这个问题,我尝试使用普通的C数组//并从堆中获取所需的空间并删除函数末尾的[]。//但是,这会导致相同数量的页面错误,并且//不会提高性能。//所以我尝试在堆栈(即不是堆)上分配一个普通的C数组。//当然不能写doubled2Err_wrt_dWn[m_Weights.size()];//因为编译器坚持使用编译时已知的数组大小常量值。//为避免这种需要,我使用_alloca函数在堆栈上分配内存。//这样做的缺点是栈使用过多,可能会出现栈溢出问题。//这就是为什么它被命名为“Review”double[]d2Err_wrt_dWn=newdouble[m_Weights.Count];for(ii=0;ii