使用 UNIPEN 数据库的在线手写识别系统库。






4.94/5 (33投票s)
一个用于手写识别系统的库,可以识别 99% 的数字或 90% 的大写字母+数字。
- 下载 capital_letters__digit_89_.zip - 5.6 MB
- 下载 lowcase_letter_89_.zip - 5.6 MB
- 下载 numberic_97_.zip - 2.3 MB
- 下载源代码 - 1 MB
- 下载演示 - 114.9 KB
引言
我启动这个项目是因为我想在平板电脑(Windows 8 或 Android 平板电脑)上创建一个小型程序,能够识别我 5 岁的女儿在上面画的东西,并帮助她学习数字和字母。我知道这涉及到机器学习和模式识别,是一项非常艰巨的工作。这个程序可能要到我女儿完成她的中学课程才能完成,但这足以让我利用业余时间来做这件事。目前,该项目已经取得了一些不错的成果,例如:一个用于处理 UNIPEN 数据库的库,一个用于在运行时动态创建神经网络的库,以及一些用于字符分割的类等等。这些成果激励我继续开发这个项目,并将其分享给社区,以便初学者更容易学习一般的模式识别技术和在线手写识别技术。
演示程序不仅可以通过同时使用多神经网络来识别鼠标绘制的数字,还可以识别字母。
co
图 1a:孤立字符分割
图 1b:用于大写字母和数字识别的卷积网络
背景
这个库分为三个部分
第一部分:UNIPEN – 在线手写训练数据库库:包含用于处理 UNIPEN 数据库的几个类,UNIPEN 是全球最受欢迎的手写数据库之一。
第二部分:卷积神经网络库:该库基于神经网络对象进行组织,包括:网络、层、神经元、权重、连接、激活函数、前向传播、反向传播类。初学者可以轻松地创建传统神经网络和卷积网络,而且工作量最小。特别是,该库还支持在运行时创建网络。因此,我们可以在程序运行时创建或更改不同的网络。
第三部分:图像分割库:包含一些图像预处理和分割的函数。该部分正在开发中。
图 2:字符分割
这些技术已在之前的文章《UPV – UNIPEN 在线手写识别数据库查看器控件》和《C# 中的手写数字识别神经网络》中介绍过。然而,本文是对这些文章的综合,可以为手写识别系统提供更广阔的视野。在本文中,我将重点介绍一种用于将 UNIPEN 数据输入识别器的方法。还将描述一个用于识别大写字母和数字的卷积网络,以解释如何使用此库。
UNIPEN 及其格式
图 2:UNIPEN 数据浏览器,具有大写字母和数字识别功能。
通过一项大型的协作工作,众多研究机构和行业共同制定了 UNIPEN 标准和数据库。该数据最初由 NIST 托管,分为两个分发版本,称为训练集(trainset)和开发集(devset)。自 1999 年以来,国际 UNIPEN 基金会 (iUF) 一直负责托管数据,其目标是保障训练集的发布,并推广在线手写在研究和应用中的使用。近年来,已有数十名研究人员使用了训练集并报告了实验性能结果。许多研究人员报告了基于该数据的扎实研究和正确的识别率,但所有人都应用了特定的数据配置。在大多数情况下,数据被分解为训练、测试和验证三个子集,并采用了一些特定的过程。因此,尽管使用了相同的数据源,但由于采用了不同的分解技术,识别结果实际上无法进行比较。
在一段时间内,iUF 的目标是在剩余的开发集上组织一个基准测试。尽管开发集可供 UNIPEN 的一些原始贡献者使用,但它尚未正式发布给广大用户。我还没有机会使用它。
由于 UNIPEN 训练集是来自不同研究机构的特定数据集的集合,这些数据集是使用一些特定过程分解的。然而,我的方法略有不同;我试图在这些数据集的结构中找到一些共同点,从而创建一个能够正确分解训练集中的所有数据集的过程,并且在大多数情况下都能成功。
训练集组织如下:
cat nsegm nfiles
1a 15953 634 isolated digits
1b 28069 1423 isolated upper case
1c 61351 2145 isolated lower case
1d 17286 1222 isolated symbols (punctuations etc.)
2 122628 2735 isolated characters, mixed case
3 67352 1949 isolated characters in the context of words or texts
4 0 0 isolated printed words, not mixed with digits and symbols
5 0 0 isolated printed words, full character set
6 75529 3298 isolated cursive or mixed-style words (without digits and symbols)
7 85213 3393 isolated words, any style, full character set
8 14544 4563 text: (minimally two words of) free text, full character set
UNIPEN 格式在此处描述。该格式被视为一系列笔坐标,并带有各种信息的注释,包括分割和标签。笔迹轨迹被编码为 .PEN DOWN 和 .PEN UP 的一系列组件,包含笔坐标(例如,根据 .COORD 声明的 XY 或 XY T)。.DT 指令允许指定两个组件之间的时间间隔。数据库被划分为一个或多个数据集,以 .START SET 开始。在一个集合中,组件被隐式编号,从零开始。分割和标签由 .SEGMENT 指令提供。组件编号由 .SEGMENT 用于划分句子、单词、字符。通过 .HIERARCHY 声明分割层次结构(例如,句子-单词-字符)。由于组件通过集合名称和在该集合中的顺序号的唯一组合来引用,因此可以将 .SEGMENT 与数据本身分开。
总的来说,UNIPEN 数据文件的格式包含 KEYWORDS,这些 KEYWORDS 分为几组,例如:强制声明、数据文档、字母表、词汇表、数据布局、单位系统、笔迹轨迹、数据注释。为了获取信息并对这些关键字进行分类,我构建了一系列基于上述组的类,这些类可以帮助我从数据文件中获取和分类所有必要的信息。
尽管 UNIPEN 格式基于 KEYWORD,但它们的顺序并不固定。我创建了一个 DataSet 类,它就像一个存储架,当找到一个 KEYWORD 时,它会被分类并放入相应的架子。通常,每个 UNIPEN 文件包含一个或多个数据集。但在大多数情况下,一个文件中只有一个数据集。我的库目前只关注这种情况。
使用该库从训练集中获取训练模式(笔迹轨迹位图)非常简单,如下所示:
private void btnOpen_Click(object sender, EventArgs e)
{
if (dataProvider.IsDataStop == true)
{
try
{
FolderBrowserDialog fbd = new FolderBrowserDialog();
// Show the FolderBrowserDialog.
DialogResult result = fbd.ShowDialog();
if (result == DialogResult.OK)
{
bool fn = false;
string folderName = fbd.SelectedPath;
Task[] tasks = new Task[2];
isCancel = false;
tasks[0] = Task.Factory.StartNew(() =>
{
dataProvider.IsDataStop = false;
this.Invoke(DelegateAddObject, new object[] { 0, "Getting image training data, please be patient...." });
dataProvider.GetPatternsFromFiles(folderName); //get patterns with default parameters
dataProvider.IsDataStop = true;
if (!isCancel)
{
this.Invoke(DelegateAddObject, new object[] { 1, "Congatulation! Image training data loaded succesfully!" });
dataProvider.Folder.Dispose();
isDatabaseReady = true;
}
else
{
this.Invoke(DelegateAddObject, new object[] { 98, "Sorry! Image training data loaded fail!" });
}
fn = true;
});
tasks[1] = Task.Factory.StartNew(() =>
{
int i = 0;
while (!fn)
{
Thread.Sleep(100);
this.Invoke(DelegateAddObject, new object[] { 99, i });
i++;
if (i >= 100)
i = 0;
}
});
}
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
else
{
DialogResult result = MessageBox.Show("Do you really want to cancel this process?", "Cancel loadding Images", MessageBoxButtons.YesNo);
if (result == DialogResult.Yes)
{
dataProvider.IsDataStop = true;
isCancel = true;
}
}
}
之后,这些模式将成为神经网络的训练数据:
private void btTrain_Click(object sender, EventArgs e)
{
if (isDatabaseReady && !isTrainingRuning)
{
TrainingParametersForm form = new TrainingParametersForm();
form.Parameters = nnParameters;
DialogResult result = form.ShowDialog();
if (result == DialogResult.OK)
{
nnParameters = form.Parameters;
ByteImageData[] dt = new ByteImageData[dataProvider.ByteImagePatterns.Count];
dataProvider.ByteImagePatterns.CopyTo(dt);
nnParameters.RealPatternSize = dataProvider.PatternSize;
if (network == null)
{
CreateNetwork(); //create network for training
NetworkInformation();
}
var ntraining = new Neurons.NNTrainPatterns(network, dt, nnParameters, true, this);
tokenSource = new CancellationTokenSource();
token = tokenSource.Token;
this.btTrain.Image = global::NNControl.Properties.Resources.Stop_sign;
this.btLoad.Enabled = false;
this.btnOpen.Enabled = false;
maintask = Task.Factory.StartNew(() =>
{
if (stopwatch.IsRunning)
{
// Stop the timer; show the start and reset buttons.
stopwatch.Stop();
}
else
{
// Start the timer; show the stop and lap buttons.
stopwatch.Reset();
stopwatch.Start();
}
isTrainingRuning = true;
ntraining.BackpropagationThread(token);
if (token.IsCancellationRequested)
{
String s = String.Format("BackPropagation is canceled");
this.Invoke(this.DelegateAddObject, new Object[] { 4, s });
token.ThrowIfCancellationRequested();
}
},token);
}
}
else
{
tokenSource.Cancel();
}
}
卷积神经网络
卷积网络的理论已在我之前的文章以及 CodeProject 上的其他几篇文章中进行了描述。在本文中,我将仅关注该库相对于之前程序开发的方面。
该库已完全重写,以满足我当前的要求:易于初学者使用,他们不需要深入了解神经网络;简单地创建神经网络,无需更改代码即可更改网络参数,特别是能够在线交换不同的网络。
之前程序中的 CreateNetwork 函数
private bool CreateNNNetWork(NeuralNetwork network)
{
NNLayer pLayer;
int ii, jj, kk;
int icNeurons = 0;
int icWeights = 0;
double initWeight;
String sLabel;
var m_rdm = new Random();
// layer zero, the input layer.
// Create neurons: exactly the same number of neurons as the input
// vector of 29x29=841 pixels, and no weights/connections
pLayer = new NNLayer("Layer00", null);
network.m_Layers.Add(pLayer);
for (ii = 0; ii < 841; ii++)
{
sLabel = String.Format("Layer00_Neuro{0}_Num{1}", ii, icNeurons);
pLayer.m_Neurons.Add(new NNNeuron(sLabel));
icNeurons++;
}
//double UNIFORM_PLUS_MINUS_ONE= (double)(2.0 * m_rdm.Next())/Constants.RAND_MAX - 1.0 ;
// layer one:
// This layer is a convolutional layer that has 6 feature maps. Each feature
// map is 13x13, and each unit in the feature maps is a 5x5 convolutional kernel
// of the input layer.
// So, there are 13x13x6 = 1014 neurons, (5x5+1)x6 = 156 weights
pLayer = new NNLayer("Layer01", pLayer);
network.m_Layers.Add(pLayer);
for (ii = 0; ii < 1014; ii++)
{
sLabel = String.Format("Layer01_Neuron{0}_Num{1}", ii, icNeurons);
pLayer.m_Neurons.Add(new NNNeuron(sLabel));
icNeurons++;
}
for (ii = 0; ii < 156; ii++)
{
sLabel = String.Format("Layer01_Weigh{0}_Num{1}", ii, icWeights);
initWeight = 0.05 * (2.0 * m_rdm.NextDouble() - 1.0);
pLayer.m_Weights.Add(new NNWeight(sLabel, initWeight));
}
// interconnections with previous layer: this is difficult
// The previous layer is a top-down bitmap image that has been padded to size 29x29
// Each neuron in this layer is connected to a 5x5 kernel in its feature map, which
// is also a top-down bitmap of size 13x13. We move the kernel by TWO pixels, i.e., we
// skip every other pixel in the input image
int[] kernelTemplate = new int[25] {
29, 30, 31, 32, 33,
58, 59, 60, 61, 62,
87, 88, 89, 90, 91,
116,117,118,119,120 };
0, 1, 2, 3, 4,
int iNumWeight;
int fm;
for (fm = 0; fm < 6; fm++)
{
for (ii = 0; ii < 13; ii++)
{
for (jj = 0; jj < 13; jj++)
{
iNumWeight = fm * 26; // 26 is the number of weights per feature map
NNNeuron n = pLayer.m_Neurons[jj + ii * 13 + fm * 169];
n.AddConnection((uint)MyDefinations.ULONG_MAX, (uint)iNumWeight++); // bias weight
for (kk = 0; kk < 25; kk++)
{
// note: max val of index == 840, corresponding to 841 neurons in prev layer
n.AddConnection((uint)(2 * jj + 58 * ii + kernelTemplate[kk]), (uint)iNumWeight++);
}
}
}
}
// layer two:
// This layer is a convolutional layer that has 50 feature maps. Each feature
// map is 5x5, and each unit in the feature maps is a 5x5 convolutional kernel
// of corresponding areas of all 6 of the previous layers, each of which is a 13x13 feature map
// So, there are 5x5x50 = 1250 neurons, (5x5+1)x6x50 = 7800 weights
pLayer = new NNLayer("Layer02", pLayer);
network.m_Layers.Add(pLayer);
for (ii = 0; ii < 1250; ii++)
{
sLabel = String.Format("Layer02_Neuron{0}_Num{1}", ii, icNeurons);
pLayer.m_Neurons.Add(new NNNeuron(sLabel));
icNeurons++;
}
for (ii = 0; ii < 7800; ii++)
{
sLabel = String.Format("Layer02_Weight{0}_Num{1}", ii, icWeights);
initWeight = 0.05 * (2.0 * m_rdm.NextDouble() - 1.0);
pLayer.m_Weights.Add(new NNWeight(sLabel, initWeight));
}
// Interconnections with previous layer: this is difficult
// Each feature map in the previous layer is a top-down bitmap image whose size
// is 13x13, and there are 6 such feature maps. Each neuron in one 5x5 feature map of this
// layer is connected to a 5x5 kernel positioned correspondingly in all 6 parent
// feature maps, and there are individual weights for the six different 5x5 kernels. As
// before, we move the kernel by TWO pixels, i.e., we
// skip every other pixel in the input image. The result is 50 different 5x5 top-down bitmap
// feature maps
int[] kernelTemplate2 = new int[25]{
0, 1, 2, 3, 4,
13, 14, 15, 16, 17,
26, 27, 28, 29, 30,
39, 40, 41, 42, 43,
52, 53, 54, 55, 56 };
for (fm = 0; fm < 50; fm++)
{
for (ii = 0; ii < 5; ii++)
{
for (jj = 0; jj < 5; jj++)
{
iNumWeight = fm * 156; // 26 is the number of weights per feature map
NNNeuron n = pLayer.m_Neurons[jj + ii * 5 + fm * 25];
n.AddConnection((uint)MyDefinations.ULONG_MAX, (uint)iNumWeight++); // bias weight
for (kk = 0; kk < 25; kk++)
{
// note: max val of index == 1013, corresponding to 1014 neurons in prev layer
n.AddConnection((uint)(2 * jj + 26 * ii + kernelTemplate2[kk]), (uint)iNumWeight++);
n.AddConnection((uint)(169 + 2 * jj + 26 * ii + kernelTemplate2[kk]), (uint)iNumWeight++);
n.AddConnection((uint)(338 + 2 * jj + 26 * ii + kernelTemplate2[kk]), (uint)iNumWeight++);
n.AddConnection((uint)(507 + 2 * jj + 26 * ii + kernelTemplate2[kk]), (uint)iNumWeight++);
n.AddConnection((uint)(676 + 2 * jj + 26 * ii + kernelTemplate2[kk]), (uint)iNumWeight++);
n.AddConnection((uint)(845 + 2 * jj + 26 * ii + kernelTemplate2[kk]), (uint)iNumWeight++);
}
}
}
}
// layer three:
// This layer is a fully-connected layer with 100 units. Since it is fully-connected,
// each of the 100 neurons in the layer is connected to all 1250 neurons in
// the previous layer.
// So, there are 100 neurons and 100*(1250+1)=125100 weights
pLayer = new NNLayer("Layer03", pLayer);
network.m_Layers.Add(pLayer);
for (ii = 0; ii < 100; ii++)
{
sLabel = String.Format("Layer03_Neuron{0}_Num{1}", ii, icNeurons);
pLayer.m_Neurons.Add(new NNNeuron(sLabel));
icNeurons++;
}
for (ii = 0; ii < 125100; ii++)
{
sLabel = String.Format("Layer03_Weight{0}_Num{1}", ii, icWeights);
initWeight = 0.05 * (2.0 * m_rdm.NextDouble() - 1.0);
pLayer.m_Weights.Add(new NNWeight(sLabel, initWeight));
}
// Interconnections with previous layer: fully-connected
iNumWeight = 0; // weights are not shared in this layer
for (fm = 0; fm < 100; fm++)
{
NNNeuron n = pLayer.m_Neurons[fm];
n.AddConnection((uint)MyDefinations.ULONG_MAX, (uint)iNumWeight++); // bias weight
for (ii = 0; ii < 1250; ii++)
{
n.AddConnection((uint)ii, (uint)iNumWeight++);
}
}
// layer four, the final (output) layer:
// This layer is a fully-connected layer with 10 units. Since it is fully-connected,
// each of the 10 neurons in the layer is connected to all 100 neurons in
// the previous layer.
// So, there are 10 neurons and 10*(100+1)=1010 weights
pLayer = new NNLayer("Layer04", pLayer);
network.m_Layers.Add(pLayer);
for (ii = 0; ii < 10; ii++)
{
sLabel = String.Format("Layer04_Neuron{0}_Num{1}", ii, icNeurons);
pLayer.m_Neurons.Add(new NNNeuron(sLabel));
icNeurons++;
}
for (ii = 0; ii < 1010; ii++)
{
sLabel = String.Format("Layer04_Weight{0}_Num{1}", ii, icWeights);
initWeight = 0.05 * (2.0 * m_rdm.NextDouble() - 1.0);
pLayer.m_Weights.Add(new NNWeight(sLabel, initWeight));
}
// Interconnections with previous layer: fully-connected
iNumWeight = 0; // weights are not shared in this layer
for (fm = 0; fm < 10; fm++)
{
var n = pLayer.m_Neurons[fm];
n.AddConnection((uint)MyDefinations.ULONG_MAX, (uint)iNumWeight++); // bias weight
for (ii = 0; ii < 100; ii++)
{
n.AddConnection((uint)ii, (uint)iNumWeight++);
}
}
return true;
}
当前演示中使用此库的 CreateNetwork 函数
private List<Char> Letters2 = new List<Char>(36) { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
private List<Char> Letters = new List<Char>(62) { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r',
's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
private List<Char> Letters1 = new List<Char>(10) { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
void CreateNetwork1()
{
network = new ConvolutionNetwork();
//layer 0: inputlayer
network.Layers = new NNLayer[5];
network.LayerCount = 5;
NNLayer layer = new NNLayer("00-Layer Input", null, new Size(29, 29), 1, 5);
network.InputDesignedPatternSize = new Size(29, 29);
layer.Initialize();
network.Layers[0] = layer;
layer = new NNLayer("01-Layer ConvolutionalSubsampling", layer, new Size(13, 13), 6, 5);
layer.Initialize();
network.Layers[1] = layer;
layer = new NNLayer("02-Layer ConvolutionalSubsampling", layer, new Size(5, 5), 50, 5);
layer.Initialize();
network.Layers[2] = layer;
layer = new NNLayer("03-Layer FullConnected", layer, new Size(1, 100), 1, 5);
layer.Initialize();
network.Layers[3] = layer;
layer = new NNLayer("04-Layer FullConnected", layer, new Size(1, Letters1.Count), 1, 5);
layer.Initialize();
network.Layers[4] = layer;
network.TagetOutputs = Letters1;
}
在当前版本中,如果我想创建一个不仅能识别 10 个数字,还能识别字母(共 62 个输出)的网络,只需添加一些其他层并更改一些参数,如下所示:
void CreateNetwork()
{
network = new ConvolutionNetwork();
//layer 0: inputlayer
network.Layers = new NNLayer[6];
network.LayerCount = 6;
NNLayer layer = new NNLayer("00-Layer Input", null, new Size(29, 29), 1, 5);
network.InputDesignedPatternSize = new Size(29, 29);
layer.Initialize();
network.Layers[0] = layer;
layer = new NNLayer("01-Layer ConvolutionalSubsampling", layer, new Size(13, 13), 10, 5);
layer.Initialize();
network.Layers[1] = layer;
layer = new NNLayer("02-Layer ConvolutionalSubsampling", layer, new Size(5, 5), 60, 5);
layer.Initialize();
network.Layers[2] = layer;
layer = new NNLayer("03-Layer FullConnected", layer, new Size(1, 300), 1, 5);
layer.Initialize();
network.Layers[3] = layer;
layer = new NNLayer("04-Layer FullConnected", layer, new Size(1, 200), 1, 5);
layer.Initialize();
network.Layers[4] = layer;
layer = new NNLayer("05-Layer FullConnected", layer, new Size(1, Letters.Count), 1, 5);
layer.Initialize();
network.Layers[5] = layer;
network.TagetOutputs = Letters;
}
我们可以更改所有网络参数,例如:层数、输入模式大小、特征图数量、卷积网络中的卷积核大小、层中的神经元数量、输出数量……等,以获得最适合我们的网络。更改网络不会影响前向传播或反向传播类。
使用该库进行实验
演示程序提供了该库的两个主要功能:UNIPEN 数据浏览器以及卷积神经网络的训练和测试。当然,输入数据是 UNIPEN 训练集,可以在网站上下载:http://unipen.nici.kun.nl/。为了使演示程序能够正确运行,必须将 trainset 文件夹重命名为 **UnipenData**。
图 4:UNIPEN 数据浏览器
我们可以简单地选择 UnipenData 中的 Data 文件夹来浏览所有数据。通过加载网络参数文件可以激活识别功能。根据网络文件,程序可以仅识别数字,或者识别所有大写字母和数字。
图 5:卷积网络训练
默认的卷积网络是 62 个输出的网络。您可以加载附带的网络参数文件来更改网络。为了获得正确的训练数据,例如针对 36 个输出的网络(用于大写字母和数字的网络),您应该删除 Data 文件夹中的所有文件夹,只留下 1a、1b(数字和大写字母的文件夹)。
在我的实验中,结果相当不错,大写字母和数字集合的准确率为 88%,仅数字的准确率为 97%。我无法对 62 个输出的网络进行实验,因为我的笔记本电脑在训练网络时几乎要烧毁了。
兴趣点
就像人类大脑一样,人工智能系统无法创建一个拥有数十亿神经元来解决不同问题的唯一神经网络。它将包含几个可以解决独立问题的小型网络。我的库具备此功能。因此,我希望它不仅能应用于我女儿的程序,而且有一天也能应用于实际系统。
目前,该项目由我的大学资助作为年度小型研究项目。我正在寻找捐款或奖学金以继续这项研究。如果您对该项目感兴趣并能帮助其进一步发展,我将不胜感激。
欢迎对我的文章进行投票和评论……
历史
库版本:1.0 初始代码
版本 1.01:修复错误(Unipen 库可以正确读取 NicIcon、UJI-Penchar 文件),向 Unipen 库添加字符分割功能,修复 neuron 库中的错误。以前的网络参数与当前版本不兼容。如果您下载了 1.0 版演示,请重新下载所有文件。
版本 2.0(可以在鼠标绘制控件上识别 62 个字符,如图 1 所示)将在即将发布的文章《使用多神经网络的大型模式识别系统》中发布。