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.cnblogs.com/qcloud1001/p/8385402.html