首页 > 人工智能 > 人工智能 > 利用浅层神经网络识别图片中的英文
如果你(自认为)是一个程序员(非机器学习),看完本教程后,你将变得与其他只会手工编写程序的程序员不同。这篇文章我们会按照下面的顺序展开:
一、“浅层”与“深度”的区别:
这里我们假设大家对于神经网络的结构都是了解的,于是我们就有一个问题需要引起重视:神经网络是万能的吗?或者说,对于神经网络来说,会不会存在其无法表示的问题?这个问题不是很好回答,但可以告诉大家的一点是,数学上可以证明,满足一定条件的神经网络,可以以任意精度逼近任何函数。对浅层与深层神经网络的区分,我们基本是以隐藏层的层数来划分的。对于隐层大于1的神经网络是深层神经网络,对于隐层等于1的网络我们称之为浅层神经网络。在通常意义上说,深层的神经网络比浅层的神经网络要好的多,这里面的原因有很多,其中最重要的一点是,深度神经网络可以利用 “层次化” 的信息表达减少网络中的参数数量,而且能够提高模型的表达能力,即靠后的网络层可以利用靠前的网络层中提取的较低层次的信息组合成更高层次或者更加抽象的信息。但是为了方便讲解,我们本次实验会选择相对容易实现的浅层神经网络来进行。
二、准备数据:
为了完成我们的实验,我们需要准备足够的训练数据d 构建一个浅层神经网络模型, 并且使用梯度下降算法去优化我们的模型。 我们先来解决训练数据的问题,我已经事先准备好了一些带有标签 (label,代表图片上的字母是什么,0 代表 A,1 代表 B, 依次类推) 的训练图片。
我们训练数据整理在一个文件夹下一共有 60000 张图片,每张图片的尺寸为 17*17,包含一个不等宽的大写英文字母。train.txt文件有 40000 行,每行的格式为 "图片路径 标签",代表一张有标签训练图片,validate.txt和test.txt文件格式与train.txt类似,且都包含 10000 行。train.txt、validate.txt和test.txt分别对应是我们本次实验的训练集,验证集和测试集。在实际研究中,我们也必须是这样划分数据集的。
我们通过梯度下降算法利用训练集来对模型中的参数进行优化,为了检验这些参数是否足够 “好”,可以通过观察训练过程中的损失函数值来判断,但通过损失函数值来判断有一个问题,就是我们的模型可能只是“记住” 了所有的训练数据,而不是真正的学会了训练数据中所包含的问题本身的性质。就像是如果我们考试时总是出原题,那笨学生只要把所有题目都记住也一样可以取得高分。所以为了检验我们的模型是在 “学习” 而不是在“死记硬背”,我们再使用与训练集不同的验证集对模型进行测试,当模型对验证集的分类准确率也比较高时,就可以认为我们的模型是真正的在 “学习”,此时我们称我们的模型拥有较好的泛化性能(generalization)-- 能够正确的对未曾见过的测试样例做出正确的预测。然而这里还是有一个问题,别忘了除了模型里的参数,我们还手动设置了超参数,我们的超参数也有可能只能适应一部分数据,所以为了避免这种情况,需要再设置一个与训练集和验证集都不同的测试集,测试在当前超参数的设置下,我们的模型具有良好的泛化性能。
三、处理数据:
对于图片数据,我们首先需要将它们转换成输入向量的形式,并且由于我们是有监督学习,每张图片的标签也必须与对应的图片向量一一对应。 编写数据预处理如下:
我们的预处理脚本接收两个参数,第一个参数src对应之前我们提到的train.txt、validate.txt和test.txt,我们从src中读取图片的路径和它的标签。第二个参数dst代表我们将预处理好的图片数据保存到哪里,我们直接使用 np.save() 函数将数组保存到npy文件。
注意原始图片中只有 0 和 255 两种灰度值,我们的代码对图片灰度值除以了 255,将图片矩阵转换成了只包含 0-1 值的矩阵。同时我们将图片矩阵转换成了列向量,注意这里的列向量的尺寸是 img.sizex1 而不是 img.size,即我们其实是使用矩阵的形式表示向量,这样可以方便我们之后的运算。
我们可以使用以下命令将图片转换成 npy 文件:
main('train.txt','train.npy')
main('test.txt','test.npy')
main('validate.txt','validate.npy')
预处理好了训练数据之后,我们还需要将数据读入我们的神经网络,为了一致性,我们将读入数据的操作放到一个数据层里面,数据层代码如下:
这里我们使用的是随机梯度下降算法(stochastic gradient descent)。在实际的深度学习训练过程当中,我们每次计算梯度并更新参数值时,总是一次性计算多个输入数据的梯度,并将这些梯度求平均值,再使用这个平均值对参数进行更新。这样做可以利用并行计算来提高训练速度。我们将一次性一起计算的一组数据称为一个batch。同时,我们称所有训练图片都已参与一遍训练的一个周期称为一个epoch。每个epoch结束时,我们会将训练数据重新打乱,这样可以获得更好的训练效果。我们通常会训练多个epoch。
在上次实验中,我们实现了一个全连接 FullyConnect 层,但是那段代码只能处理输出是一个标量的情况,对于输出是多个节点的情况无法处理。而且当一个 batch 中包含多个训练图片数据时,那段代码更是无法正常工作。
所以我们需要重新编写我们的全连接层,由于 batch 的引入,这时的全连接层要难了很多
为了理解上面的代码,我们以一个包含 100 个训练输入数据的 batch 为例,分析一下具体执行流程: 我们的 l_x 为输入单个数据向量的长度,在这里是 17* 17=289,l_y 代表全连接层输出的节点数量,由于大写英文字母有 26 个,所以这里的 l_y=26。 所以,我们的 self.weights 的尺寸为 26*289, self.bias 的尺寸为 26* 1(self.bias 也是通过矩阵形式表示的向量)。forward() 函数的输入 x 在这里的尺寸就是 100*289* 1(batch_size 向量长度 1)。backward() 函数的输入 d 代表从前面的网络层反向传递回来的 “部分梯度值”,其尺寸为 100*26* 1(batch_size 输出层节点数 l_y*1)。
forward() 函数里的代码比较好理解,由于这里的 x 包含了多组数据,所以要对每组数据分别进行计算。
backward() 函数里的代码就不太好理解了,ddw 保存的是对于每组输入数据,损失函数对于参数的梯度。由于这里的参数是一个 26* 289 的矩阵,所以,我们需要求损失函数对矩阵的导数。(对矩阵求导可能大部分本科生都不会。但其实也不难,如果你线性代数功底可以,可以尝试推导矩阵求导公式。)不过这里有一个简便的方法去推断对矩阵求导时应该如何计算:由于这里的参数矩阵本身是 26*289 的,那损失函数对于它的梯度(即损失函数对参数矩阵求导的结果)的尺寸也一定是 26* 289 的。而这里每组输入数据的尺寸是 289*1,每组数据对应的部分梯度尺寸为 26* 1, 要得到一个 26*289 尺寸的梯度矩阵,就只能是一个 26* 1 尺寸的矩阵乘以一个 1*289 尺寸的矩阵,需要对输入数据进行转置。所以这里计算的是np.dot(dd,xx.T)。 对一个 batch 里的数据分别求得梯度之后,按照随机梯度下降算法的要求,我们需要对所有梯度求平均值,得到 self.dw, 其尺寸为 26*289,刚好与我们的 self.weights 匹配。
由于全连接层对 bias 的部分导数为 1,所以这里对于 bias 的梯度 self.bias 就直接等于从之前的层反向传回来的梯度的平均值。 损失函数对于输入 x 的梯度值 self.dx 的求解与 self.dw 类似。由于输入数据 self.x 中的一个数据的尺寸为 289* 1,self.weights 的尺寸为 26*289, dd 的尺寸为 26* 1, 所以需要对 self.weights 进行转置。即 “289*1=(289* 26)(26*1)”。
最后是使用梯度更新参数,注意这里的 self.lr 即为前面我们提到过的学习速率alpha,它是一个需要我们手工设定的超参数。
这里的矩阵求导确实不太好处理,容易出错,请你仔细分析每一个变量代表的含义,如果对一个地方不清楚,请回到前面看看相关的概念是如何定义的。
四、梯度下降算法:
接下来编写的是激活函数:
sigmoid 函数将输出限制在 0 到 1 之间,刚好可以作为概率看待。这里我们有 26 个输入节点,经过 sigmoid 层计算之后,哪个输出节点的数值最大,就认为图片上最有可能是该节点代表的字母。比如如果输出层第 0 个节点值最大,就认为图片上的字母是 “A”, 如果第 25 个节点的值最大,就认为图片上的字母是 “Z”。
注意一般在计算神经网络的深度时我们一般不把激活层算进去,但这里为了编程方便,也将激活函数视为单独的一层。
之前我们讲解过二次损失函数quadratic loss的定义,这里我们来实现它:
在随机梯度下降算法里,每次前向计算和反向传播都会计算包含多个输入数据的一个 batch。所以损失函数值在随后也要除以 batch 中包含的数据数量, 即self.x.shape[0],同时这里除以了 2, 这个地方的 2 可以和对二次损失函数求导后多出来的系数 2 抵消掉。所以,我们的损失函数变成了一种非常规的形式。
前面我们提到过,为了判断经过训练的模型是否具有良好的泛化性能,需要使用验证集和测试集对模型的效果进行检验。所以我们还需要一个计算准确率的层:
如果我们的神经网络的输出层中,概率最大的节点的下标与实际的标签 label 相等,则预测正确。预测正确的数量除以总的数量,就得到了正确率
五、构建神经网络训练测试
我们已经写好了所有必须的网络层, 接下来我们要使用这些层构建出一个完整的神经网络,方法很简单,按顺序把它们 “堆叠” 起来就可以了,就像搭积木一样:
示例结果:
epochs: 0
loss: 0.5475572337223819
accuracy: 0.292
epochs: 1
loss: 0.3357296762021519
accuracy: 0.3904
epochs: 2
loss: 0.2720450330419767
accuracy: 0.5501
epochs: 3
loss: 0.22451741831232722
accuracy: 0.6553
epochs: 4
loss: 0.1959364738089137
accuracy: 0.6832
……
由于 FullyConnect 层和 Sigmoid 层在网络中的调用方式一模一样,所以把它们存到一个列表里,使用循环的方式调用。同时由于 Sigmoid 层一般不计入神经网络的深度,所以我们将这个列表命名为inner_layers而不是hidden_layers以免混淆。
datalayer1数据层用来输出训练集数据,datalayer2数据层用来输出验证集数据。accuracy层用来在每个 epoch 结束时计算验证集上的准确率。
这里设置学习速率为 1000(实际当中很少看到大于 1 的学习速率,下次实验我们会解释为什么这里的学习速率需要这么大), 你可以尝试将学习速率改变成其他的值,观察损失函数值和准确率的变化情况。
我们看到每个 epoch 结束时,会先输出在训练集上的损失函数值,再输出在验证集上的准确率。
20 个 epoch 结束时,准确率大概会在 0.9 左右 (为了节省时间这里只训练了 20 个 epoch, 你可以加大 epochs 的数值,看看最高能到多少,我这里测试大概是在 0.93),这非常令人振奋不是吗!一个原本通过手工编程不可解的图片分类问题,(几乎)被我们解决了,0.9 的准确率已经可以应用在一些实际的项目中了(比如这里),而且我们模型中的参数都是自动设定的,我们只是编写了模型和训练算法部分的代码。 而且,我们的代码具有很好的可扩展性,一方面我们可以很方便的向神经网络中添加更多的网络层使之成为真真的 “深度神经网络”,另一方面我们也可以很方便的将我们的模型运用到其他图片分类问题当中,我们只编写了一次代码,就有可能能够解决多种问题!
不过,我要告诉你的是,我们的神经网络的性能还没有被完全发掘出来,我们的准确率还可以更高!这次实验的最开始我们提到过,深度神经网络会比浅层神经网络拥有更好的性能,下次实验,我们会尝试使用深度神经网络来提高我们的模型性能,进行真正的深度学习!
https://www.toutiao.com/a6728574262006776334/
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69946223/viewspace-2654849/ ,如需转载,请注明出处,否则将追究法律责任。