雷锋网按:本文是介绍用TensorFlow构建图像识别系统的第三部分。 在前两部分中,我们构建了一个softmax分类器来标记来自CIFAR-10数据集的图像,实现了约25-30%的精度。 因为有10个不同可能性的类别,所以我们预期的随机标记图像的精度为10%。25-30%的结果已经比随机标记的结果好多了,但仍有很大的改进空间。在这篇文章中,作者Wolfgang Beyer将介绍如何构建一个执行相同任务的神经网络。看看可以提高预测精度到多少!雷锋网(公众号:雷锋网)对全文进行编译,未经许可不得转载。
关于前两部分,可以参看《机器学习零基础?手把手教你用TensorFlow搭建图像识别系统》(一)和(二)。
神经网络
神经网络是基于生物大脑的工作原理设计的,由许多人工神经元组成,每个神经元处理多个输入信号并返回单个输出信号,然后输出信号可以用作其他神经元的输入信号。我们先来看看一个单独的神经元,大概长这样:
一个人工神经元:其输出是其输入加权和的ReLU函数值。
在单个神经元中发生的情况与在softmax分类器中发生的情况非常相似。一个神经元有一个输入值的向量和一个权重值的向量,权重值是神经元的内部参数。输入向量和权重值向量包含相同数量的值,因此可以使用它们来计算加权和。
WeightedSum=input1×w1+input2×w2+...
到目前为止,我们正在做与softmax分类器完全相同的计算,现在开始,我们要进行一些不同的处理:只要加权和的结果是正值,神经元的输出是这个值;但是如果加权和是负值,就忽略该负值,神经元产的输出为0。 此操作称为整流线性单元(ReLU)。
由 f(x) = max(0, x)定义的整流线性单元
使用ReLU的原因是其具备非线性特点,因而现在神经元的输出并不是严格的输入线性组合(也就是加权和)。当我们不再从单个神经元而是从整个网络来看时,会发现非线性很有用处。
人工神经网络中的神经元通常不是彼此随机连接的,大多数时候是分层排列的:
人工神经网络具有隐藏层和输出层2个层。
输入并不被当作一层,因为它只是将数据(不转换它)馈送到第一个合适的层。
输入图像的像素值是第1层网络中的神经元的输入。第1层中的神经元的输出是第2层网络的神经元的输入,后面的层之间以此类推。如果没有每层的ReLU,我们只是得到一个加权和的序列;并且堆积的加权和可以被合并成单个加权和,这样一来,多个层并没有比单层网络有任何改进之处。这就是为什么要具有非线性的重要原因。ReLU非线性解决了上述问题,它使每个附加层的确给网络添加了一些改进。
我们所关注的是图像类别的分数,它是网络的最后一层的输出。在这个网络架构中,每个神经元连接到前一层的所有神经元,因此这种网络被称为完全连接的网络。我们将会在本教程的第3部分中看到一些不同于此的其他情况。
对神经网络理论的简短介绍到此结束。 让我们开始建立一个真正的神经网络!
代码实战
此示例的完整代码在Github上提供。它需要TensorFlow和CIFAR-10数据集(雷锋网的此前文章有提及)。
如果你已经通过我以前的博客文章,你会看到神经网络分类器的代码非常类似于softmax分类器的代码。 除了切换出定义模型的代码部分之外,我还添加了一些小功能使TensorFlow可以做以下一些事情:
正则化:这是一种非常常见的技术,用于防止模型过拟合。它的工作原理是在优化过程中施加反作用力,其目的是保持模型简单
使用TensorBoard可视化模型:TensorBoard包含TensorFlow,允许您根据模型和模型生成的数据生成表格和图形。这有助于分析您的模型,并且对调试特别有用。
检查点:此功能允许您保存模型的当前状态以供以后使用。训练一个模型可能需要相当长的时间,所以它是必要的,当您想再次使用模型时不必从头开始。
这次代码被分成两个文件:定义模型two_layer_fc.py和运行模型run_fc_model.py(提示:'fc'代表完全连接的意思)。
两层全连接的神经网络
让我们先看看模型本身,然后进行一些运行和训练处理。two_layer_fc.py包含以下函数:
inference(),使我们从输入数据到类分数。
loss(),从类分数中计算损失值。
training(),执行单个训练步骤。
evaluation(),计算网络的精度。
生成类分数:inference()
inference()描述了通过网络的正向传递。那么,类分数是如何从输入图片开始被计算的呢?
参数images是包含实际图像数据的TensorFlow占位符。接下来的三个参数描述网络的形状或大小。 image_pixels是每个输入图像的像素数,classes是不同输出标签的数量,hidden_units是网络的第一个层或者隐藏层中的神经元数量。
每个神经元从上一层获取所有值作为输入,并生成单个输出值。因此,隐藏层中的每个神经元都具有image_pixels输入,并且该层作为整体生成hidden_units输出。然后将这些输入到输出层的类神经元中,生成类输出值,每个类一个分数。
reg_constant是正则化常数。TensorFlow允许我们非常容易地通过自动处理大部分计算来向网络添加正则化。 当使用到损失函数时,我会进一步讲述细节。
由于神经网络有2个相似的图层,因此将为每个层定义一个单独的范围。 这允许我们在每个作用域中重复使用变量名。变量biases以我们熟悉的tf.Variable()方式来定义。
此处会更多地涉及到weights变量的定义。tf.get_variable()允许我们添加正则化。weights是以hidden_units(输入向量大小乘以输出向量大小)为维度的image_pixels矩阵。initialier参数描述了weights变量的初始值。目前为止我们已经将weights变量初始化为0,但此处并不会起作用。关于单层中的神经元,它们都接收完全相同的输入值,如果它们都具有相同的内部参数,则它们将进行相同的计算并且输出相同的值。为了避免这种情况,需要随机化它们的初始权重。我们使用了一个通常可以很好运行的初始化方案,将weights初始化为正态分布值。丢弃与平均值相差超过2个标准偏差的值,并且将标准偏差设置为输入像素数量的平方根的倒数。幸运的是TensorFlow为我们处理了所有这些细节,我们只需要指定调用truncated_normal_initializer便可完成上述工作。
weights变量的最终参数是regularizer。现在要做的是告诉TensorFlow要为weights变量使用L2-正则化。我将在这里讨论正则化。
第一层的输出等于images矩阵乘以weights矩阵,再加上bisa变量。这与上一篇博文中的softmax分类器完全相同。然后应用tf.nn.relu(),取ReLU函数的值作为隐藏层的输出。
第2层与第1层非常相似,其输入值为hidden_units,输出值为classes,因此weights矩阵的维度是是[hidden_units,classes]。 由于这是我们网络的最后一层,所以不再需要ReLU。 通过将输入(hidden)互乘以weights,再加上bias就可得到类分数(logits)。
tf.histogram_summary()允许我们记录logits变量的值,以便以后用TensorBoard进行分析。这一点稍后会介绍。
总而言之,整个inference()函数接收输入图像并返回类分数。这是一个训练有素的分类器需要做的,但为了得到一个训练有素的分类器,首先需要测量这些类分数表现有多好,这是损失函数要做的工作。
计算损失: loss()
首先,我们计算logits(模型的输出)和labels(来自训练数据集的正确标签)之间的交叉熵,这已经是我们对softmax分类器的全部损失函数,但是这次我们想要使用正则化,所以必须给损失添加另一个项。
让我们先放一边吧,先看看通过使用正则化能实现什么。
过度拟合和正则化
当捕获数据中随机噪声的统计模型是被数据训练出来的而不是真实的数据基础关系时,就被称为过拟合。
红色和蓝色圆圈表示两个不同的类。绿线代表过拟合模型,而黑线代表具有良好拟合的模型。
在上面的图像中有两个不同的类,分别由蓝色和红色圆圈表示。绿线是过度拟合的分类器。它完全遵循训练数据,同时也严重依赖于训练数据,并且可能在处理未知数据时比代表正则化模型的黑线表现更差。因此,我们的正则化目标是得到一个简单的模型,不附带任何不必要的复杂。我们选择L2-正则化来实现这一点,L2正则化将网络中所有权重的平方和加到损失函数。如果模型使用大权重,则对应重罚分,并且如果模型使用小权重,则小罚分。
这就是为什么我们在定义权重时使用了regularizer参数,并为它分配了一个l2_regularizer。这告诉了TensorFlow要跟踪l2_regularizer这个变量的L2正则化项(并通过参数reg_constant对它们进行加权)。所有正则化项被添加到一个损失函数可以访问的集合——tf.GraphKeys.REGULARIZATION_LOSSES。将所有正则化损失的总和与先前计算的交叉熵相加,以得到我们的模型的总损失。
优化变量:training()
global_step是跟踪执行训练迭代次数的标量变量。当在我们的训练循环中重复运行模型时,我们已经知道这个值,它是循环的迭代变量。直接将这个值添加到TensorFlow图表的原因是想要能够拍摄模型的快照,这些快照应包括有关已执行了多少训练步骤的信息。
梯度下降优化器的定义很简单。我们提供学习速率并告诉优化器它应该最小化哪个变量。 此外,优化程序会在每次迭代时自动递增global_step参数。
测量性能: evaluation()
模型精度的计算与softmax情况相同:将模型的预测与真实标签进行比较,并计算正确预测的频率。 我们还对随着时间的推移精度如何演变感兴趣,因此添加了一个跟踪accuracy的汇总操作。 将在关于TensorBoard的部分中介绍这一点。
总结我们迄今为止做了什么,已经定义了使用4个函数的2层人工神经网络的行为:inference()构成通过网络的正向传递并返回类分数。loss()比较预测和真实的类分数并生成损失值。 training()执行训练步骤,并优化模型的内部参数。evaluation()测量模型的性能。
运行神经网络
现在神经网络已经定义完毕,让我们看看run_fc_model.py是如何运行、训练和评估模型的。
在强制导入之后,将模型参数定义为外部标志。 TensorFlow有自己的命令行参数模块,这是一个围绕Python argparse的小封装包。 在这里使用它是为了方便,但也可以直接使用argparse。
在代码开头两行中定义了命令行参数。每个标志的参数是标志的名称(其默认值和一个简短的描述)。 使用-h标志执行文件将显示这些描述。第二个代码块调用实际解析命令行参数的函数,然后将所有参数的值打印到屏幕上。
用常数定义每个图像的像素数(32 x 32 x 3)和不同图像类别的数量。
使用一个时钟来记录运行时间。
我们想记录关于训练过程的一些信息,并使用TensorBoard显示该信息。 TensorBoard要求每次运行的日志都位于单独的目录中,因此我们将日期和时间信息添加到日志目录的名称地址。
load_data()加载CIFAR-10数据,并返回包含独立训练和测试数据集的字典。
生成TensorFlow图
定义TensorFlow占位符。 当执行实际计算时,这些将被填充训练和测试数据。
images_placeholder将每张图片批处理成一定尺寸乘以像素的大小。 批处理大小设定为“None”允许运行图片时可随时设定大小(用于训练网络的批处理大小可以通过命令行参数设置,但是对于测试,我们将整个测试集作为一个批处理) 。
labels_placeholder是一个包含每张图片的正确类标签的整数值向量。
这里引用了我们之前在two_layer_fc.py中描述的函数。
inference()使我们从输入数据到类分数。
loss()从类分数中计算损失值。
training()执行单个训练步骤。
evaluation()计算网络的精度。
为TensorBoard定义一个summary操作函数 (更多介绍可参见前文).
生成一个保存对象以保存模型在检查点的状态(更多介绍可参见前文)。
开始TensorFlow会话并立即初始化所有变量。 然后我们创建一个汇总编辑器,使其定期将日志信息保存到磁盘。
这些行负责生成批输入数据。让我们假设我们有100个训练图像,批次大小为10.在softmax示例中,我们只为每次迭代选择了10个随机图像。这意味着,在10次迭代之后,每个图像将被平均选取一次。但事实上,一些图像将被选择多次,而一些图像不会被添加到任何一个批次。但只要重复的次数够频发,所有图片被随机分到不同批次的情况会有所改善。
这一次我们要改进抽样过程。要做的是首先对训练数据集的100个图像随机混洗。混洗之后的数据的前10个图像作为我们的第一个批次,接下来的10个图像是我们的第二批,后面的批次以此类推。 10批后,在数据集的末尾,再重复混洗过程,和开始步骤一致,依次取10张图像作为一批次。这保证没有任何图像比任何其它图像被更频繁地拾取,同时仍然确保图像被返回的顺序是随机的。
为了实现这一点,data_helpers()中的gen_batch()函数返回一个Python generator,它在每次评估时返回下一个批次。generator原理的细节超出了本文的范围(这里有一个很好的解释)。使用Python的内置zip()函数来生成一个来自[(image1,label1),(image2,label2),...]的元组列表,然后将其传递给生成函数。
next(batch)返回下一批数据。 因为它仍然是[(imageA,labelA),(imageB,labelB),...]的形式,需要先解压它以从标签中分离图像,然后填充feed_dict,字典包含用单批培训数据填充的TensorFlow占位符。
每100次迭代之后模型的当前精度会被评估并打印到屏幕上。此外,正在运行summary操作,其结果被添加到负责将摘要写入磁盘的summary_writer(看此章节)。
此行运行train_step操作(之前定义为调用two_layer_fc.training(),它包含用于优化变量的实际指令)。
当训练模型需要较长的时间,有一个简单的方法来保存你的进度的快照。 这允许您以后回来并恢复模型在完全相同的状态。 所有你需要做的是创建一个tf.train.Saver对象(我们之前做的),然后每次你想拍摄快照时调用它的save()方法。恢复模型也很简单,只需调用savever的restore()。 代码示例请看gitHub存储库中的restore_model.py文件。
在训练完成后,最终模型在测试集上进行评估(记住,测试集包含模型到目前为止还没有看到的数据,使我们能够判断模型是否能推广到新的数据)。
结果
让我们使用默认参数通过“python run_fc_model.py”运行模型。 我的输出如下所示:
可以看到训练的准确性开始于我们所期望到随机猜测水平(10级 - > 10%的机会选择到正确的)。 在第一次约1000次迭代中,精度增加到约50%,并且在接下来的1000次迭代中围绕该值波动。 46%的测试精度不低于训练精度。 这表明我们的模型没有显着过度拟合。 softmax分级器的性能约为30%,因此46%的改进约为50%。不错!
用TensorBoard可视化
TensorBoard允许您从不同方面可视化TensorFlow图形,并且对于调试和改进网络非常有用。 让我们看看TensorBoard相关的代码。
在 two_layer_fc.py 我可以看到以下代码:
这三行中的每一行都创建一个汇总操作。通过定义一个汇总操作告诉TensorFlow收集某些张量(在本例中logits,loss和accuracy)的摘要信息。汇总操作的其他参数就只是一些想要添加到总结的标签。
有不同种类的汇总操作。使用scalar_summary记录有关标量(非矢量)值以及histogram_summary收集有关的多个值分布信息(有关各种汇总运算更多信息可以在TensorFlow文档中找到)。
在 run_fc_model.py 是关于TensorBoard 可视化的一些代码:
TensorFlow中的一个操作本身不运行,您需要直接调用它或调用依赖于它的另一个操作。由于我们不想在每次要收集摘要信息时单独调用每个摘要操作,因此使用tf.merge_all_summaries创建一个运行所有摘要的单个操作。
在TensorFlow会话的初始化期间,创建一个摘要写入器,摘要编入器负责将摘要数据实际写入磁盘。在摘要写入器的构造函数中,logdir是日志的写入地址。可选的图形参数告诉TensorBoard渲染显示整个TensorFlow图形。每100次迭代,我们执行合并的汇总操作,并将结果馈送到汇总写入器,将它们写入磁盘。要查看结果,我们通过“tensorboard --logdir = tf_logs”运行TensorBoard,并在Web浏览器中打开localhost:6006。在“事件”标签中,我们可以看到网络的损失是如何减少的,以及其精度是如何随时间增加而增加的。
tensorboard图显示模型在训练中的损失和精度。
“Graphs”选项卡显示一个已经定义的可视化的tensorflow图,您可以交互式地重新排列直到你满意。我认为下面的图片显示了我们的网络结构非常好。
Tensorboard1以交互式可视化的方式显示Tensorboard图像
有关在“分布”和“直方图”标签的信息可以进一步了解tf.histogram_summary操作,这里不做进一步的细节分析,更多信息可在官方tensorflow文件相关部分。
后续改进
也许你正在想训练softmax分类器的计算时间比神经网络少了很多。事实确实如此,但即使把训练softmax分类器的时间增加到和神经网络来训练所用的时间一样长,前者也不会达到和神经网络相同的性能,前者训练时间再长,额外的收益和一定程度的性能改进几乎是微乎其微的。我们也已经在神经网络中也验证也这点,额外的训练时间不会显著提高准确性,但还有别的事情我们可以做。
已选的默认参数值表现是相当不错的,但还有一些改进的余地。通过改变参数,如隐藏层中的神经元的数目或学习率,应该能够提高模型的准确性,模型的进一步优化使测试精度很可能大于50%。如果这个模型可以调整到65%或更多,我也会相当惊喜。但还有另一种类型的网络结构能够比较轻易实现这一点:卷积神经网络,这是一类不完全连通的神经网络,相反,它们尝试在其输入中理解局部特征,这对于分析图像非常有用。它使得在解读图像获取空间信息的时候有非常直观的意义。在本系列的下一部分中,我们将看到卷积神经网络的工作原理,以及如何构建一个自己的神经网络.。
本文作者:陈鸣鸠
本文转自雷锋网禁止二次转载,原文链接