Java-神经网络编程第二版-全-
Java 神经网络编程第二版(全)
原文:
zh.annas-archive.org/md5/fd578b058ad8c92db30ac728e6ba28ae译者:飞龙
前言
程序员的生活可以描述为一条持续不断的、永无止境的学习之路。程序员总是面临新技术或新方法带来的挑战。通常,在我们的一生中,尽管我们习惯了重复的事物,但我们总是要面对新事物。学习的过程是科学中最有趣的话题之一,有许多尝试去描述或重现人类的学习过程。
本书写作的指导思想是面对新内容并掌握它所带来的挑战。虽然“神经网络”这个名字可能听起来很陌生,甚至可能让人联想到这本书是关于神经学的,但我们努力通过关注您购买这本书的原因来简化这些细微差别。我们旨在构建一个框架,向您展示神经网络实际上很简单,很容易理解,并且完全不需要对该主题有任何先前的知识,才能完全理解我们在这里提出的概念。
因此,我们鼓励您充分利用本书的内容,在解决大问题时领略神经网络的强大力量,但始终以初学者的视角来看待。本书中涉及的所有概念都使用通俗易懂的语言进行解释,并附带技术背景。本书的使命是让您深入了解可以使用简单语言编写的智能应用。
本书涵盖内容
第1章, 神经网络入门,介绍了神经网络的概念,并展示了最基本的神经元结构(单层感知器、Adaline),激活函数、权重和学习算法。此外,本章还展示了从零开始到完成创建基本神经网络的Java实现过程。
第2章, 让神经网络学习,详细介绍了神经网络的学习过程。介绍了有用的概念,如训练、测试和验证。我们展示了如何实现训练和验证算法。本章还展示了错误评估的方法。
第3章, 感知器和监督学习,让您熟悉感知器和监督学习特性。我们展示了这些类型神经网络的训练算法。读者将学习如何在Java中实现这些特性。
第4章, 自组织映射,介绍了使用自组织映射的无监督学习,即Kohonen神经网络,以及它们的应用,特别是在分类和聚类问题上。
第5章,天气预报,涵盖了一个使用神经网络的实用问题,即天气预报。你将得到来自不同地区的历史天气记录的时间序列数据集,并学习在将它们呈现给神经网络之前如何进行预处理。
第6章,疾病诊断分类,介绍了一个分类问题,它也包含在监督学习中。使用患者记录数据,构建了一个神经网络作为专家系统,能够根据患者和症状给出诊断。
第7章,客户画像聚类,将教你如何使用神经网络进行聚类,以及应用无监督学习算法来实现这一目标。
第8章,文本识别,介绍了另一个涉及神经网络的常见任务,即光学字符识别(OCR),这是一个非常有用且令人印象深刻的任务,真正展示了神经网络强大的学习能力。
第9章,优化和调整神经网络,展示了帮助优化神经网络的技巧,例如输入选择、更好地将数据集分离为训练、验证和测试,以及数据过滤和选择隐藏神经元数量的选择。
第10章,神经网络当前趋势,将让你了解神经网络领域的当前前沿状态,使你能够理解和设计新的策略来解决更复杂的问题。
附录A,设置Netbeans开发环境,本附录展示了读者如何逐步设置Netbeans IDE的开发环境的步骤。
附录B,设置Eclipse开发环境,本附录展示了读者如果想要使用Eclipse IDE,如何设置开发环境的逐步过程。
这些附录在书中没有提供,但可以从以下链接下载:https://www.packtpub.com/sites/default/files/downloads/Neural_Network_Programming_with_Java_SecondEdition_Appendices.pdf
你需要这本书的什么
你需要Netbeans (www.netbeans.org) 或 Eclipse (www.eclipse.org)。这两个都是免费的,可以从它们的网站上下载。
这本书是为谁而写的
本书是为想要了解如何利用神经网络的力量开发更智能应用程序的Java开发者而编写的。那些处理大量复杂数据并希望在日常应用程序中有效使用这些数据的人会发现这本书非常有用。预期您有一些统计计算的基本经验。
惯例
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟URL、用户输入和Twitter昵称如下所示:"层被初始化和计算,以及神经元;它们还实现了init()和calc()方法"。
代码块设置如下:
public abstract class NeuralLayer {
protected int numberOfNeuronsInLayer;
private ArrayList<Neuron> neuron;
protected IActivationFunction activationFnc;
protected NeuralLayer previousLayer;
protected NeuralLayer nextLayer;
protected ArrayList<Double> input;
protected ArrayList<Double> output;
protected int numberOfInputs;
}
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"选择参数后,通过点击开始训练按钮开始训练"。
注意
警告或重要注意事项以如下框中的形式出现。
小贴士
小技巧和技巧以如下形式出现。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。
要向我们发送一般反馈,请简单地发送电子邮件至<[feedback@packtpub.com](mailto:feedback@packtpub.com)>,并在邮件主题中提及书籍标题。
如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在,您已经是Packt书籍的骄傲拥有者,我们有一些事情可以帮助您从购买中获得最大收益。
下载示例代码
您可以从您的账户中下载本书的示例代码文件http://www.packtpub.com。如果您在其他地方购买了这本书,您可以访问http://www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
使用您的电子邮件地址和密码登录或注册我们的网站。
-
将鼠标指针悬停在顶部的支持选项卡上。
-
点击代码下载与勘误。
-
在搜索框中输入书籍名称。
-
选择您想要下载代码文件的书籍。
-
从下拉菜单中选择您购买此书籍的地方。
-
点击代码下载。
您也可以通过点击Packt Publishing网站书籍网页上的代码文件按钮下载代码文件。您可以通过在搜索框中输入书籍名称来访问此页面。请注意,您需要登录到您的Packt账户。
文件下载完成后,请确保您使用最新版本解压缩或提取文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
本书代码包也托管在GitHub上,网址为 https://github.com/PacktPublishing/Neural-Network-Programming-with-Java-SecondEdition。我们还有其他来自我们丰富图书和视频目录的代码包,可在 https://github.com/PacktPublishing/ 找到。查看它们吧!
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这个问题,我们将不胜感激。这样做可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 http://www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误表,请访问 https://www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <[copyright@packtpub.com](mailto:copyright@packtpub.com)> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。
问题
如果您在这本书的任何方面遇到问题,您可以通过 <[questions@packtpub.com](mailto:questions@packtpub.com)> 联系我们,我们将尽力解决问题。
第一章:第1章. 神经网络入门
在本章中,我们将介绍神经网络及其设计目的。本章作为后续章节的基础层,同时介绍了神经网络的基本概念。在本章中,我们将涵盖以下内容:
-
人工神经元
-
权重和偏差
-
激活函数
-
神经元层
-
Java中的神经网络实现
发现神经网络
当我们听到神经网络这个词时,我们会在脑海中自然地浮现出一个大脑的图像,确实如此,如果我们把大脑看作是一个庞大而自然的神经网络的话。然而,关于人工神经网络(ANNs)又是怎样的呢?好吧,这里有一个与自然相对的词,鉴于“人工”这个词,我们首先想到的可能是人工大脑或机器人的形象。在这种情况下,我们也在处理创建一个类似于并受人类大脑启发的结构;因此,这可以被称为人工智能。
现在对于刚刚接触ANN的读者来说,可能会想这本书是教我们如何构建智能系统,包括一个能够使用Java代码模拟人类思维的的人工大脑,不是吗?令人惊讶的答案是肯定的,但当然,我们不会涵盖创造像《黑客帝国》三部曲电影中的人工思考机器这样的东西;读者将获得设计人工神经网络解决方案的流程指导,这些解决方案能够从原始数据中抽象知识,利用整个Java编程语言框架。
为什么是人工神经网络?
我们在谈论神经网络之前,不能不了解它们的起源,包括这个术语。尽管NN(神经网络)比ANN(人工神经网络)更通用,涵盖了自然神经网络,但在这本书中,NN和ANN被用作同义词。那么,实际上ANN究竟是什么呢?让我们探索一下这个术语的历史。
在20世纪40年代,神经生理学家Warren McCulloch和数学家Walter Pitts设计了第一个人工神经元的数学实现,将神经科学基础与数学运算相结合。当时,人类大脑主要被研究以了解其隐藏和神秘的行为,尤其是在神经科学领域。已知的自然神经元结构包括一个细胞核,树突接收来自其他神经元的传入信号,以及轴突激活信号到其他神经元,如下面的图所示:

McCulloch和Pitts的创新之处在于神经元模型中包含的数学成分,假设神经元是一个简单的处理器,将所有传入的信号求和,并激活一个新信号到其他神经元:

此外,考虑到大脑由数十亿个神经元组成,每个神经元与其他数万个神经元相互连接,从而形成数万亿个连接,我们正在谈论一个巨大的网络结构。基于这一事实,麦卡洛克和皮茨设计了一个简单的神经元模型,最初是为了模拟人类视觉。当时可用的计算器或计算机非常罕见,但能够很好地处理数学运算;另一方面,即使像视觉和声音识别这样的任务,如果没有使用特殊框架,也难以编程,这与数学运算和函数不同。尽管如此,人脑在声音和图像识别方面的效率比复杂的数学计算要高,这一事实确实让科学家和研究人员感到好奇。
然而,一个已知的事实是,人类大脑执行的所有复杂活动都是基于学习知识,因此为了克服传统算法方法在处理人类容易解决的问题时遇到的困难,人工神经网络被设计成具有通过其刺激(数据)学习如何自行解决某些任务的能力:
| 人类快速可解任务 | 计算机快速可解任务 |
|---|---|
| 图像分类语音识别面部识别基于经验预测事件 | 复杂计算语法错误纠正信号处理操作系统管理 |
神经网络的排列方式
考虑到人脑的特点,可以说人工神经网络(ANN)是一种受自然界启发的途径,其结构也是如此。一个神经元连接到许多其他神经元,这些神经元又连接到其他神经元,因此形成一个高度互联的结构。本书后面将展示,这种神经元之间的连接性是学习能力的来源,因为每个连接都可以根据刺激和期望的目标进行配置。
最基本的元素——人工神经元
让我们探索最基本的人工神经网络元素——人工神经元。自然神经元已被证明是信号处理器,因为它们在树突中接收微弱的信号,这些信号可以根据其强度或大小在轴突中触发信号。因此,我们可以将神经元视为在输入端有一个信号收集器,在输出端有一个激活单元,可以触发将被转发到其他神经元的信号,如图所示:

小贴士
在自然神经元中,存在一个阈值电位,当达到这个电位时,轴突会放电并将信号传播到其他神经元。这种放电行为通过激活函数来模拟,这已被证明在表示神经元中的非线性行为方面是有用的。
为神经元注入生命——激活函数
这个激活函数是根据所有输入信号的求和来触发神经元的输出的。从数学上讲,它为神经网络处理添加了非线性,从而为人工神经元提供了非线性行为,这在模拟自然神经元的非线性特性时非常有用。激活函数通常在输出端被限制在两个值之间,因此是一个非线性函数,但在某些特殊情况下,它也可以是一个线性函数。
虽然任何函数都可以用作激活函数,但让我们集中关注常用的那些:
| 函数 | 公式 | 图表 |
|---|---|---|
| Sigmoid | ![]() |
![]() |
| 双曲正切 | ![]() |
![]() |
| 硬限制阈值 | ![]() |
![]() |
| 线性 | ![]() |
![]() |
在这些方程和图表中,系数a可以作为激活函数的设置。
灵活的值 – 权重
当神经网络结构可以固定时,权重代表神经元之间的连接,并且它们有能力放大或衰减传入的神经信号,从而修改它们,并具有影响神经元输出的能力。因此,神经元的激活不仅依赖于输入,还依赖于权重。假设输入来自其他神经元或外部世界(刺激),权重被认为是神经网络神经元之间建立的联系。由于权重是神经网络的一个内部组件,并影响其输出,因此它们可以被认为是神经网络的知识,前提是改变权重将改变神经网络的输出,即对外部刺激的回答。
一个额外的参数 – 偏置
对于人工神经元来说,拥有一个独立的组件向激活函数添加额外信号是有用的:偏置。这个参数的作用像一个输入,除了它是由一个固定的值(通常是1)刺激,并且这个值乘以一个相关的权重。这个特性有助于神经网络知识表示作为一个更纯粹的非线性系统,前提是当所有输入都为零时,那个神经元不一定会产生零的输出,相反,它可以根据相关的偏置权重产生不同的值。
构成整体的各个部分 – 层
为了抽象出与我们的思维相似的处理层次,神经元被组织成层次。输入层接收来自外部世界的直接刺激,输出层触发将直接影响外部世界的动作。在这些层次之间,存在多个隐藏层,从外部世界的角度来看,它们是不可见的(隐藏的)。在人工神经网络中,一个层次的所有组成神经元具有相同的输入和激活函数,如图所示:

神经网络可以由几个相互连接的层次组成,形成所谓的多层网络。神经网络层可以被分类为输入、隐藏或输出。
在实践中,增加一个额外的神经网络层次可以增强神经网络表示更复杂知识的能力。
小贴士
任何神经网络至少都有一个输入/输出层,无论层数多少。在多层网络的情况下,输入和输出之间的层被称为隐藏层
了解神经网络架构
神经网络可以有不同的布局,这取决于神经元或层次如何相互连接。每个神经网络架构都是为了特定的目标而设计的。神经网络可以应用于许多问题,并且根据问题的性质,神经网络应该被设计得更加高效地解决这个问题。
神经网络架构的分类有两方面:
-
神经元连接
-
单层网络
-
多层网络
-
信号流
-
前馈网络
-
反馈网络
单层网络
在这种架构中,所有神经元都布局在同一级别,形成一个单独的层次,如图所示:

神经网络接收输入信号并将它们输入到神经元中,神经元随后产生输出信号。神经元可以高度相互连接,有或没有循环。这些架构的例子包括单层感知器、Adaline、自组织映射、Elman和Hopfield神经网络。
多层网络
在这个类别中,神经元被分为多个层次,每个层次对应于一个共享相同输入数据的神经元并行布局,如图所示:

径向基函数和多层感知器是这个架构的好例子。这类网络特别适用于将实际数据逼近到专门设计用来表示该数据的函数。此外,由于它们具有多个处理层次,这些网络适合于从非线性数据中学习,能够更容易地分离它或确定再现或识别这些数据的知识。
前馈网络
神经网络中信号的流动可以是单向的,也可以是循环的。在前一种情况下,我们称神经网络架构为前馈,因为输入信号被输入层接收;然后,在处理之后,它们被转发到下一层,正如多层部分中的图所示。多层感知器和径向基函数也是前馈网络的良好例子。
反馈网络
当神经网络具有某种内部循环时,这意味着信号被反馈到一个已经接收并处理过该信号的神经元或层,这种网络是反馈类型的。参见以下反馈网络图:

在网络中添加循环的特殊原因是为了产生动态行为,尤其是在网络处理涉及时间序列或模式识别的问题时,这些问题需要内部记忆来强化学习过程。然而,这种网络特别难以训练,因为在训练过程中最终会出现递归行为(例如,一个其输出被反馈到其输入的神经元),除了为训练安排数据之外。大多数反馈网络是单层,例如Elman和Hopfield网络,但也可以构建循环多层网络,例如回声和循环多层感知器网络。
从无知到知识——学习过程
神经网络通过调整神经元之间的连接来学习,即权重。如神经结构部分所述,权重代表神经网络知识。不同的权重会导致网络对相同的输入产生不同的结果。因此,神经网络可以通过根据学习规则调整其权重来提高其结果。学习的一般方案如图所示:

上图所示的过程被称为监督学习,因为存在一个期望的输出,但神经网络也可以通过输入数据来学习,而不需要任何期望的输出(监督)。在第2章《让神经网络学习》中,我们将更深入地探讨神经网络的学习过程。
让编码开始!神经网络在实践中的应用
在本书中,我们将使用Java编程语言来实现神经网络的全过程。Java是一种在20世纪90年代由Sun Microsystems的一小群工程师创建的面向对象编程语言,后来在2010年代被Oracle收购。如今,Java存在于我们日常生活中的许多设备中。
在面向对象的语言,如Java中,我们处理类和对象。类是现实世界中某物的蓝图,而对象是这个蓝图的实例,就像一辆车(类指的是所有和任何车辆)以及我的车(对象指的是特定的车辆——我的车)。Java类通常由属性和方法(或函数)组成,这些方法包括面向对象编程(OOP)概念。我们将简要回顾所有这些概念,而不会深入探讨它们,因为这本书的目标只是从实际的角度设计和创建神经网络。以下四个概念在此过程中相关且需要考虑:
-
抽象:将现实世界问题或规则转录到计算机编程领域,只考虑其相关特征,忽略通常阻碍发展的细节。
-
封装:类似于产品的封装,通过这种方式,一些相关特性被公开披露(公共方法),而其他特性则在其领域内保持隐藏(私有或受保护),从而避免误用或信息过多。
-
继承:在现实世界中,多个类对象以分层的方式共享属性和方法;例如,车辆可以是汽车和卡车的超类。因此,在OOP中,这个概念允许一个类从另一个类继承所有功能,从而避免代码的重写。
-
多态性:几乎与继承相同,但不同之处在于具有相同签名的方 法在不同的类上表现出不同的行为。
使用本章中介绍的神经网络概念和OOP概念,我们现在将设计实现神经网络的第一个类集。如所见,神经网络由层、神经元、权重、激活函数和偏差组成。关于层,有三种类型:输入、隐藏和输出。每一层可能有一个或多个神经元。每个神经元要么连接到神经输入/输出,要么连接到另一个神经元,这些连接被称为权重。
需要强调的是,神经网络可能有许多隐藏层或没有隐藏层,因为每层的神经元数量可能不同。然而,输入和输出层的神经元数量与神经输入/输出的数量相同。
因此,让我们开始实施。最初,我们将定义以下类:
-
神经元:定义人工神经元
-
神经网络层:抽象类,定义神经元层
-
输入层:定义神经网络的输入层
-
隐藏层:定义输入和输出之间的层
-
输出层:定义神经网络的输出层
-
输入神经元:定义神经网络输入处的神经元
-
神经网络:将所有前面的类组合成一个ANN结构
除了这些类之外,我们还应该为激活函数定义一个 IActivationFunction 接口。这是必要的,因为 Activation 函数将像方法一样行为,但它们需要作为神经元属性被分配。因此,我们将定义实现此接口的激活函数类:
-
Linear
-
Sigmoid
-
Step
-
HyperTan
我们的第一章编码几乎完成了。我们需要定义两个额外的类。一个用于处理可能抛出的异常(NeuralException),另一个用于生成随机数(RandomNumberGenerator)。最后,我们将这些类分开到两个包中:
-
edu.packt.neuralnet:对于与神经网络相关的类(NeuralNet、Neuron、NeuralLayer等) -
edu.packt.neuralnet.math:对于与数学相关的类(IActivationFunction、Linear等)
为了节省空间,我们不会写出每个类的完整描述,而是将重点放在最重要的类的关键特性上。然而,读者可以查看代码的 Javadoc 文档,以获取更多关于实现的细节。
神经元类
这是本章代码的非常基础类。根据理论,人工神经元有以下属性:
-
输入
-
权重
-
偏置
-
激活函数
-
输出
同样重要的是定义一个将在未来的示例中很有用的属性,即激活函数之前的输出。然后我们有以下属性的实现:
public class Neuron {
protected ArrayList<Double> weight;
private ArrayList<Double> input;
private Double output;
private Double outputBeforeActivation;
private int numberOfInputs = 0;
protected Double bias = 1.0;
private IActivationFunction activationFunction;
…
}
当实例化一个神经元时,我们需要指定将要为其提供值的输入数量,以及它应该使用的激活函数。因此,让我们看看构造函数:
public Neuron(int numberofinputs,IActivationFunction iaf){
numberOfInputs=numberofinputs;
weight=new ArrayList<>(numberofinputs+1);
input=new ArrayList<>(numberofinputs);
activationFunction=iaf;
}
注意,我们为偏置定义了一个额外的权重。一个重要的步骤是神经元的初始化,即权重如何获得它们的第一个值。这通过 init() 方法定义,其中权重通过 RandomNumberGenerator static 类随机生成值。注意需要防止尝试设置超出权重数组范围的值:
public void init(){
for(int i=0;i<=numberOfInputs;i++){
double newWeight = RandomNumberGenerator.GenerateNext();
try{
this.weight.set(i, newWeight);
}
catch(IndexOutOfBoundsException iobe){
this.weight.add(newWeight);
}
}
}
最后,让我们看看在 calc() 方法中如何计算输出值:
public void calc(){
outputBeforeActivation=0.0;
if(numberOfInputs>0){
if(input!=null && weight!=null){
for(int i=0;i<=numberOfInputs;i++){
outputBeforeActivation+=(i==numberOfInputs?bias:input.get(i))*weight.get(i);
}
}
}
output=activationFunction.calc(outputBeforeActivation);
}
注意,首先,所有输入和权重的乘积(偏置乘以最后一个权重 – i==numberOfInputs)被求和,这个值被保存在 outputBeforeActivation 属性中。激活函数使用这个值来计算神经元的输出。
神经层类
在这个类中,我们将把在同一层中排列的神经元分组。此外,还需要定义层之间的链接,因为一个层将值传递给另一个层。因此,该类将具有以下属性:
public abstract class NeuralLayer {
protected int numberOfNeuronsInLayer;
private ArrayList<Neuron> neuron;
protected IActivationFunction activationFnc;
protected NeuralLayer previousLayer;
protected NeuralLayer nextLayer;
protected ArrayList<Double> input;
protected ArrayList<Double> output;
protected int numberOfInputs;
…
}
注意,这个类是抽象的,可以实例化的层类有 InputLayer、HiddenLayer 和 OutputLayer。为了创建一个层,必须使用这些类中的一个构造函数,它们的工作方式相当相似:
public InputLayer(int numberofinputs);
public HiddenLayer(int numberofneurons,IActivationFunction iaf,
int numberofinputs);
public OutputLayer(int numberofneurons,IActivationFunction iaf,
int numberofinputs);
层以及神经元一样被初始化和计算,它们也实现了init()和calc()方法。受保护的签名保证只有子类可以调用或覆盖这些方法:
protected void init(){
for(int i=0;i<numberOfNeuronsInLayer;i++){
try{
neuron.get(i).setActivationFunction(activationFnc);
neuron.get(i).init();
}
catch(IndexOutOfBoundsException iobe){
neuron.add(new Neuron(numberOfInputs,activationFnc));
neuron.get(i).init();
}
}
}
protected void calc(){
for(int i=0;i<numberOfNeuronsInLayer;i++){
neuron.get(i).setInputs(this.input);
neuron.get(i).calc();
try{
output.set(i,neuron.get(i).getOutput());
}
catch(IndexOutOfBoundsException iobe){
output.add(neuron.get(i).getOutput());
}
}
}
激活函数接口
在我们定义NeuralNetwork类之前,让我们看看一个带有接口的Java代码示例:
public interface IActivationFunction {
double calc(double x);
public enum ActivationFunctionENUM {
STEP, LINEAR, SIGMOID, HYPERTAN
}
}
calc()签名方法由实现此接口的特定激活函数使用,例如Sigmoid函数:
public class Sigmoid implements IActivationFunction {
private double a=1.0;
public Sigmoid(double _a){this.a=_a;}
@Override
public double calc(double x){
return 1.0/(1.0+Math.exp(-a*x));
}
}
这是多态的一个例子,其中类或方法可以呈现不同的行为,但仍然在相同的签名下,允许灵活的应用。
神经网络类
最后,让我们定义神经网络类。到目前为止,我们知道神经网络将神经元组织成层,每个神经网络至少有两个层,一个用于收集输入,一个用于处理输出,以及可变数量的隐藏层。因此,我们的NeuralNet类将具有这些属性,以及其他与神经元和NeuralLayer类类似的属性,例如numberOfInputs、numberOfOutputs等:
public class NeuralNet {
private InputLayer inputLayer;
private ArrayList<HiddenLayer> hiddenLayer;
private OutputLayer outputLayer;
private int numberOfHiddenLayers;
private int numberOfInputs;
private int numberOfOutputs;
private ArrayList<Double> input;
private ArrayList<Double> output;
…
}
这个类的构造函数比之前的类有更多的参数:
public NeuralNet(int numberofinputs,int numberofoutputs,
int [] numberofhiddenneurons,IActivationFunction[] hiddenAcFnc,
IActivationFunction outputAcFnc)
假设隐藏层的数量是可变的,我们应该考虑到可能存在许多隐藏层或没有隐藏层,在每个隐藏层中将有可变数量的隐藏神经元。因此,处理这种可变性的最佳方式是将每个隐藏层中的神经元数量表示为整数向量(参数numberofhiddenlayers)。此外,还需要为每个隐藏层以及输出层定义激活函数,为此提供参数hiddenActivationFnc和outputAcFnc。
为了在本章中节省空间,我们不会展示这个构造函数的完整实现,但我们可以展示定义层及其之间链接的示例。首先,根据输入的数量定义输入层:
input=new ArrayList<>(numberofinputs);
inputLayer=new InputLayer(numberofinputs);
隐藏层的定义将取决于其位置,如果它紧接在输入层之后,其定义如下:
hiddenLayer.set(i,new HiddenLayer(numberofhiddenneurons[i], hiddenAcFnc[i],
inputLayer.getNumberOfNeuronsInLayer()));
inputLayer.setNextLayer(hiddenLayer.get(i));
否则,它将获取前一个隐藏层的引用:
hiddenLayer.set(i, new HiddenLayer(numberofhiddenneurons[i], hiddenAcFnc[i],hiddenLayer.get(i-1).getNumberOfNeuronsInLayer()));
hiddenLayer.get(i-1).setNextLayer(hiddenLayer.get(i));
对于输出层,其定义与后一种情况非常相似,只是涉及到OutputLayer类,以及可能不存在隐藏层的事实:
if(numberOfHiddenLayers>0){
outputLayer=new OutputLayer(numberofoutputs,outputAcFnc,
hiddenLayer.get(numberOfHiddenLayers-1).getNumberOfNeuronsInLayer() );
hiddenLayer.get(numberOfHiddenLayers-1).setNextLayer(outputLayer);
}else{
outputLayer=new OutputLayer(numberofinputs, outputAcFnc, numberofoutputs);
inputLayer.setNextLayer(outputLayer);
}
calc()方法执行从输入到输出端的信号前向流动:
public void calc(){
inputLayer.setInputs(input);
inputLayer.calc();
for(int i=0;i<numberOfHiddenLayers;i++){
HiddenLayer hl = hiddenLayer.get(i);
hl.setInputs(hl.getPreviousLayer().getOutputs());
hl.calc();
}
outputLayer.setInputs(outputLayer.getPreviousLayer().getOutputs());
outputLayer.calc();
this.output=outputLayer.getOutputs();
}
在附录C中,我们向读者展示了类的完整文档,包括它们的UML类图和包图,这无疑将有助于作为本书的参考。
是时候玩耍了!
现在,让我们应用这些类并获取一些结果。以下代码有一个test类,一个包含NeuralNet类对象nn的main方法。我们将定义一个简单的神经网络,包含两个输入,一个输出,以及一个包含三个神经元的隐藏层:
public class NeuralNetConsoleTest {
public static void main(String[] args) {
RandomNumberGenerator.seed=0;
int numberOfInputs=2;
int numberOfOutputs=1;
int[] numberOfHiddenNeurons= { 3 };
IActivationFunction[] hiddenAcFnc = { new Sigmoid(1.0) } ;
Linear outputAcFnc = new Linear(1.0);
System.out.println("Creating Neural Network...");
NeuralNet nn = new NeuralNet(numberOfInputs,numberOfOutputs,
numberOfHiddenNeurons,hiddenAcFnc,outputAcFnc);
System.out.println("Neural Network created!");
nn.print();
…
}
仍然在这个代码中,让我们给神经网络输入两组数据,看看它会产生什么输出:
double [] neuralInput = { 1.5 , 0.5 };
double [] neuralOutput;
System.out.println("Feeding the values ["+String.valueOf(neuralInput[0])+" ; "+
String.valueOf(neuralInput[1])+"] to the neural network");
nn.setInputs(neuralInput);
nn.calc();
neuralOutput=nn.getOutputs();
neuralInput[0] = 1.0;
neuralInput[1] = 2.1;
...
nn.setInputs(neuralInput);
nn.calc();
neuralOutput=nn.getOutputs();
这段代码给出了以下输出:

需要记住的是,每次代码运行时,它都会生成新的伪随机权重值,除非你使用相同的种子值。如果你按照这里提供的方式准确运行代码,控制台将显示相同的值:
摘要
在本章中,我们介绍了神经网络的基本概念,包括它们是什么,它们用于什么,以及它们的基本概念。我们还看到了用Java编程语言实现的一个非常基础的神经网络实例,其中我们通过编码每个神经网络元素来将理论神经网络概念应用于实践。在继续学习高级概念之前,理解基本概念非常重要。这同样适用于用Java实现的代码。
在下一章中,我们将深入探讨神经网络的学习过程,并通过简单的例子来探索不同的学习类型。
第二章:让神经网络学习
现在你已经了解了神经网络,是时候学习它们的 学习过程了。在本章中,我们将探讨神经网络学习所涉及的概念,以及它们在Java中的实现。我们将回顾神经网络学习过程的基础和灵感,这将指导我们在Java中实现学习算法,并将其应用于我们的神经网络代码。总的来说,本章涉及以下概念:
-
学习能力
-
学习如何帮助
-
学习范式
-
监督学习
-
无监督学习
-
学习过程
-
优化基础
-
成本函数
-
错误度量
-
学习算法
-
Δ规则
-
赫布规则
-
Adaline/感知器
-
训练、测试和验证
-
数据集拆分
-
过拟合和过训练
-
泛化
神经网络的学习能力
神经网络中真正令人惊叹的是它们从环境中学习的能力,就像天赋异禀的生物能够做到的那样。作为人类,我们通过观察和重复来体验学习过程,直到某个任务或概念完全掌握。从生理学的角度来看,人脑中的学习过程是节点(神经元)之间神经连接的重新配置,这导致了一种新的思维结构。
虽然神经网络连接的本质将学习过程分布在整个结构中,但这一特性使得该结构足够灵活,可以学习各种知识。与只能执行它们被编程执行的任务的普通数字计算机不同,神经网络能够根据某些满意标准改进和执行新的活动。换句话说,神经网络不需要被编程;它们通过自己学习程序。
学习如何帮助解决问题
考虑到每个要解决的问题可能有大量的理论上的可能解决方案,学习过程旨在找到一种最优解,以产生令人满意的结果。由于它们能够通过接收输入刺激(即与任务/问题相关的数据)严格地获取任何类型的知识,因此鼓励使用诸如人工神经网络(ANN)之类的结构。最初,ANN将产生一个随机结果和一个错误,然后基于这个错误,ANN参数进行调整。
小贴士
我们可以将ANN参数(权重)视为解决方案的组成部分。让我们想象每个权重对应一个维度,一个单一的解决方案代表了解决超空间中的一个点。对于每个单一的解决方案,都有一个误差度量,它告知该解决方案距离满意标准的距离。学习算法随后迭代地寻找一个更接近满意标准的解决方案。
学习范式
神经网络基本上有两种学习类型,即监督学习和无监督学习。例如,人类大脑中的学习也是这样进行的。我们能够从观察中构建知识,而不需要目标(无监督)或者我们可以有一个老师向我们展示正确的模式来遵循(监督)。这两种范式之间的区别主要在于目标模式的相关性,并且因问题而异。
监督学习
这种学习类型处理的是xs(独立值)和ys(依赖值)的成对,目标是将它们映射到一个函数中。在这里,Y数据是监督者,即期望的输出目标,而X是生成Y数据的源独立数据。这类似于一个正在教授某人执行特定任务的老师:

这种学习范式的一个特定特征是存在一个直接的错误参考,即目标与当前实际结果之间的比较。网络参数被输入到一个成本函数中,该函数量化了期望输出和实际输出之间的不匹配。
提示
成本函数只是优化问题中要最小化的一个度量。这意味着一个人寻求找到驱动成本函数达到最低可能值的参数。
成本函数将在本章后面详细介绍
监督学习适用于具有定义模式要重现的任务。一些例子包括图像分类、语音识别、函数逼近和预测。请注意,神经网络应该提供输入独立值(X)和输出依赖值(Y)的先验知识。存在依赖输出值是学习成为监督的必要条件。
无监督学习
在无监督学习中,我们只处理没有标签或分类的数据。相反,一个人试图通过只考虑独立数据X来做出推断和提取知识:

这类似于自我学习,当某人考虑自己的经验和一组支持标准时。在无监督学习中,我们没有定义的期望模式;相反,我们使用提供的数据来推断依赖输出Y,而不需要任何监督。
提示
在无监督学习中,独立数据越接近,生成的输出应该越相似,这应该在成本函数中考虑,与监督范式相反。
无监督学习可以应用于的任务示例包括聚类、数据压缩、统计建模和语言建模。这种学习范式将在第4章自组织映射中更详细地介绍。
学习过程
到目前为止,我们已经在理论上定义了学习过程及其执行方式。但在实践中,我们必须对数学逻辑进行更深入的探讨,以便实现学习算法本身。为了简化,在本章中,我们基本上涵盖了监督学习的情况;然而,我们在这里将介绍一个无监督学习更新权重的规则。学习算法是驱动神经网络学习过程的程序,它强烈依赖于神经网络架构。从数学的角度来看,人们希望找到最优权重 W,以驱动成本函数 C(X, Y) 达到尽可能低的值。然而,有时学习过程可能找不到一组能够满足接受标准的良好权重,但必须设置一个停止条件,以防止神经网络永远学习,从而造成 Java 程序冻结。
通常,这个过程按照以下流程图所示的方式进行:

寻找成本函数下降到最优路径
现在,让我们详细探讨成本函数所起的作用。让我们将成本函数想象成一个由超曲面表示的双变量函数的形状。为了简化,我们目前只考虑两个权重(二维空间加上表示成本函数的高度)。假设我们的成本函数具有以下形状:

从视觉上看,我们可以看到存在一个最优解,通过这个最优解,成本函数大致接近零。但我们如何程序化地实现这一点呢?答案在于数学优化,其中成本函数被定义为优化问题:

通过回忆费马优化定理,最优解位于所有维度上表面斜率应为零的位置,即偏导数应为零,并且应该是凸的(对于最小值情况)。考虑到从一个任意解 W 开始,寻找最优解应考虑表面高度下降的方向。这就是所谓的梯度法。
学习进行中 - 权重更新
根据使用的成本函数,更新规则将决定权重、神经网络可变参数应该如何改变,以便在新的权重下成本函数的值更低:

这里,k 表示第 k 次迭代,W(k) 表示第 k 次迭代的神经网络权重,随后 k+1 表示下一次迭代。
权重更新操作可以在在线或批量模式下执行。这里的“在线”意味着在数据集的每一条记录之后更新权重。批量更新意味着首先将数据集中的所有记录呈现给神经网络,然后它开始更新其权重。这将在本章末尾的代码中详细探讨。
计算成本函数
当神经网络学习时,它从环境中接收数据并根据目标调整其权重。这些数据被称为训练数据集,包含多个样本。单词“训练”背后的理念在于调整神经网络权重的过程,就像它们在“训练”一样,以便在神经网络中给出期望的响应。当神经网络仍在学习时,在监督情况下,目标输出(Y)和神经网络输出()之间存在误差:

小贴士
一些关于神经网络文献中用字母T表示目标变量,将神经网络输出表示为Y,而在这本书中,我们将用Y来表示它,为了避免读者混淆,因为它最初被表示为Y。
好吧,鉴于训练数据集具有多个值,对于每一条记录都会有N个误差值。那么,如何得到总体误差呢?一个直观的方法是取所有误差的平均值,但这可能会误导。误差向量可以取正值和负值,因此所有误差值的平均值很可能接近零,无论误差测量值有多大。使用绝对值来生成平均值似乎是一个更明智的方法,但这个函数在原点处有一个不连续性,这在计算其导数时是尴尬的:

因此,我们合理的选项是使用误差的二次和的平均值,也称为均方误差(MSE):

一般误差和总体误差
在进一步讨论之前,我们需要明确一点。由于神经网络是一个多输出结构,我们必须处理多输出情况,即当我们有一个误差矩阵而不是误差向量时:

好吧,在这种情况下,可能需要处理大量的错误,无论是关于一个特定的输出、特定的记录还是整个数据集。为了便于理解,让我们将特定到记录的误差称为一般误差,通过它为所有输出误差提供一个标量值,用于一般输出误差;而指整个数据的误差称为总体误差。
对于单输出网络,一般误差仅仅是目标值和输出值之间的差异,但在多输出情况下,它需要由每个输出误差组成。正如我们所见,平方误差是总结误差测量的合适方法,因此一般误差可以通过每个输出误差的平方来计算:

至于整体误差,它实际上考虑的是一般误差,但针对数据集中的所有记录。由于数据集可能非常大,因此最好使用二次一般误差的均方误差(MSE)来计算整体误差。
神经网络能否永远学习?何时停止学习是合适的?
随着学习过程的进行,神经网络必须给出越来越接近预期的结果,直到最终达到接受标准或学习迭代中的一个限制,我们称之为迭代次数。当满足以下条件之一时,学习过程被认为是完成的:
-
满意标准:根据学习范式,最小整体误差或最小权重距离
-
最大迭代次数
学习算法的示例
现在让我们将到目前为止所提出的理论内容合并到学习算法的简单示例中。在本章中,我们将探讨单层神经网络中的几个学习算法;多层将在下一章中介绍。
在Java代码中,我们将在新的包edu.packt.neural.learn中创建一个新的超类LearningAlgorithm。另一个有用的包名为edu.packt.neural.data,它将被创建来处理神经网络将处理的数据集,即NeuralInputData和NeuralOutputData这两个类,它们都由NeuralDataSet类引用。我们建议读者浏览一下代码文档,以了解这些类的组织结构,以节省这里的文本空间。
LearningAlgorithm类具有以下属性和方法:
public abstract class LearningAlgorithm {
protected NeuralNet neuralNet;
public enum LearningMode {ONLINE,BATCH};
protected enum LearningParadigm {SUPERVISED,UNSUPERVISED};
//…
protected int MaxEpochs=100;
protected int epoch=0;
protected double MinOverallError=0.001;
protected double LearningRate=0.1;
protected NeuralDataSet trainingDataSet;
protected NeuralDataSet testingDataSet;
protected NeuralDataSet validatingDataSet;
public boolean printTraining=false;
public abstract void train() throws NeuralException;
public abstract void forward() throws NeuralException;
public abstract void forward(int i) throws NeuralException;
public abstract Double calcNewWeight(int layer,int input,int neuron) throws NeuralException;
public abstract Double calcNewWeight(int layer,int input,int neuron,double error) throws NeuralException;
//…
}
neuralNet对象是这个学习算法将要训练的神经网络的引用。enums定义了学习模式和学习的范式。学习执行参数被定义(MaxEpochs,MinOverallError,LearningRate),以及在学习过程中将考虑的数据集。
方法train( )应该由每个学习算法实现重写。所有的训练过程都将在这个方法中发生。方法forward( )和forward(int k)分别处理包含所有输入数据的神经网络和第k个输入数据记录。最后,方法calcNewWeight( )将对连接输入到特定层中神经元的权重进行更新。calcNewWeight( )方法的变化允许在更新操作中提供一个特定的误差。
delta规则
此算法根据成本函数更新权重。遵循梯度方法,人们想知道哪些权重可以驱动成本函数达到更低的值。请注意,我们可以通过计算成本函数对每个权重的偏导数来找到方向。为了帮助理解,让我们考虑一个只有一个神经元、一个权重和一个偏差、因此只有一个输入的简单方法。输出将如下所示:

在这里,g是激活函数,X是包含x值的向量,Y是由神经网络生成的输出向量。第k个样本的一般误差相当简单:

然而,可以将这个误差定义为平方误差、N次方误差或均方误差。但为了简单起见,让我们考虑一般误差的简单误差差。现在,整体误差,也就是成本函数,应该按照以下方式计算:

权重和偏差根据delta规则进行更新,该规则考虑了权重和偏差的偏导数
。对于批量训练模式,X和E是向量:

如果训练模式是在线的,我们不需要执行点积:

学习率
注意在先前的方程中存在表示学习率的项α。它在权重更新中起着重要作用,因为它可以更快或更慢地达到最小成本值。让我们看看与两个权重相关的成本误差表面:

实现delta规则
我们将在名为DeltaRule的类中实现delta规则,该类将扩展LearningAlgorithm类:
public class DeltaRule extends LearningAlgorithm {
public ArrayList<ArrayList<Double>> error;
public ArrayList<Double> generalError;
public ArrayList<Double> overallError;
public double overallGeneralError;
public double degreeGeneralError=2.0;
public double degreeOverallError=0.0;
public enum ErrorMeasurement {SimpleError, SquareError,NDegreeError,MSE}
public ErrorMeasurement generalErrorMeasurement=ErrorMeasurement.SquareError;
public ErrorMeasurement overallErrorMeasurement=ErrorMeasurement.MSE;
private int currentRecord=0;
private ArrayList<ArrayList<ArrayList<Double>>> newWeights;
//…
}
在误差测量部分(一般误差和整体误差)中讨论的误差在DeltaRule类中实现,因为delta规则学习算法在训练期间考虑了这些误差。它们是数组,因为每个数据集记录都会有一般误差,每个输出也会有整体误差。一个名为overallGeneralError的属性承担成本函数的结果,即所有输出和记录的整体误差。一个名为error的矩阵存储每个输出记录组合的误差。
此类还允许以多种方式计算整体和一般误差。generalErrorMeasurement和overallErrorMeasurement属性可以接受简单误差、平方误差计算、N次方误差(立方、四次方等)或均方误差的输入值之一。默认情况下,一般误差将使用简单误差,整体误差将使用均方误差。
在此代码中,有两个重要的属性值得注意:currentRecord指的是在训练过程中被输入到神经网络的记录的索引,而newWeights立方矩阵是所有将要更新的神经网络新权重的集合。currentRecord属性在在线训练中很有用,而newWeights矩阵帮助神经网络在其所有原始权重计算完成之前保持所有原始权重,防止在正向处理阶段更新新权重,这可能会严重影响训练质量。
梯度规则学习的核心 - 训练和calcNewWeight方法
为了节省空间,我们在此不详细说明前向方法的实现。如前所述,前向意味着应该将神经网络数据集记录输入到神经网络中,然后计算误差值:
@Override
public void train() throws NeuralException{
//…
switch(learningMode){
case BATCH: //this is the batch training mode
epoch=0;
forward(); //all data are presented to the neural network
while(epoch<MaxEpochs && overallGeneralError>MinOverallError){ //continue condition
epoch++; //new epoch
for(int j=0;j<neuralNet.getNumberOfOutputs();j++){
for(int i=0;i<=neuralNet.getNumberOfInputs();i++){
//here the new weights are calculated
newWeights.get(0).get(j).set(i,calcNewWeight(0,i,j));
}
}
//only after all weights are calculated, they are applied
applyNewWeights();
// the errors are updated with the new weights
forward();
}
break;
case ONLINE://this is the online training
epoch=0;
int k=0;
currentRecord=0; //this attribute is used in weight update
forward(k); //only the k-th record is presented
while(epoch<MaxEpochs && overallGeneralError>MinOverallError){
for(int j=0;j<neuralNet.getNumberOfOutputs();j++){
for(int i=0;i<=neuralNet.getNumberOfInputs();i++){
newWeights.get(0).get(j).set(i,calcNewWeight(0,i,j));
}
}
//the new weights will be considered for the next record
applyNewWeights();
currentRecord=++k;
if(k>=trainingDataSet.numberOfRecords){
k=0; //if it was the last record, again the first
currentRecord=0;
epoch++; //epoch completes after presenting all records
}
forward(k); //presenting the next record
}
break;
}
}
我们注意到在train( )方法中,有一个带有继续训练条件的循环。这意味着当这个条件不再成立时,训练将停止。该条件检查epoch数量和总体误差。当epoch数量达到最大值或误差达到最小值时,训练完成。然而,在某些情况下,总体误差未能达到最小要求,神经网络需要停止训练。
新的权重是通过calcNewWeight( )方法计算的:
@Override
public Double calcNewWeight(int layer,int input,int neuron)
throws NeuralException{
//…
Double deltaWeight=LearningRate;
Neuron currNeuron=neuralNet.getOutputLayer().getNeuron(neuron);
switch(learningMode){
case BATCH: //Batch mode
ArrayList<Double> derivativeResult=currNeuron.derivativeBatch(trainingDataSet.getArrayInputData());
ArrayList<Double> _ithInput;
if(input<currNeuron.getNumberOfInputs()){ // weights
_ithInput=trainingDataSet.getIthInputArrayList(input);
}
else{ // bias
_ithInput=new ArrayList<>();
for(int i=0;i<trainingDataSet.numberOfRecords;i++){
_ithInput.add(1.0);
}
}
Double multDerivResultIthInput=0.0; // dot product
for(int i=0;i<trainingDataSet.numberOfRecords;i++){
multDerivResultIthInput+=error.get(i).get(neuron)*derivativeResult.get(i)*_ithInput.get(i);
}
deltaWeight*=multDerivResultIthInput;
break;
case ONLINE:
deltaWeight*=error.get(currentRecord).get(neuron);
deltaWeight*=currNeuron.derivative(neuralNet.getInputs());
if(input<currNeuron.getNumberOfInputs()){
deltaWeight*=neuralNet.getInput(input);
}
break;
}
return currNeuron.getWeight(input)+deltaWeight;
//…
}
注意,在权重更新中,有一个调用给定神经元的激活函数的导数。这是满足梯度规则所需的。在激活函数接口中,我们添加了此derivative( )方法,以便在每个实现类中重写。
注意
注意:对于批量模式,调用derivativeBatch( )时,接收并返回一个值数组,而不是单个标量。
在train( )方法中,我们看到了新权重被存储在newWeights属性中,以避免影响当前的学习过程,并且仅在训练迭代完成后才应用。
另一种学习算法 - 海布学习算法
在20世纪40年代,神经心理学家唐纳德·海布提出了这样的假设:同时激活或放电的神经元之间的连接,或者用他的话说,反复或持续地,应该被增强。这是无监督学习的一种方法,因为海布学习没有指定目标输出:

总结来说,海布学习算法的权重更新规则只考虑神经元的输入和输出。给定一个要更新的神经元j,其与神经元i(权重ij)的连接,更新由以下方程给出:

在这里,α是学习率,oj是神经元j的输出,oi是神经元i的输出,也是神经元j的输入i。对于批量训练案例,oi和oj将是向量,我们需要执行点积。
由于我们没有在Hebbian学习中包含错误测量,可以通过最大epoch数或神经网络输出的整体平均值的增加来确定停止条件。给定N条记录,我们计算神经网络产生的所有输出的期望值或平均值。当这个平均值超过一定水平时,就是停止训练的时候了,以防止神经输出的爆炸。
我们将开发一个新的类用于Hebbian学习,它也继承自LearningAlgorithm:
public class Hebbian extends LearningAlgorithm {
//…
private ArrayList<ArrayList<ArrayList<Double>>> newWeights;
private ArrayList<Double> currentOutputMean;
private ArrayList<Double> lastOutputMean;
}
除了缺失的错误测量和新的平均测量之外,所有参数都与DeltaRule类相同。方法相当相似,除了calcNewWeight( ):
@Override
public Double calcNewWeight(int layer,int input,int neuron)
throws NeuralException{
//…
Double deltaWeight=LearningRate;
Neuron currNeuron=neuralNet.getOutputLayer().getNeuron(neuron);
switch(learningMode){
case BATCH:
//…
//the batch case is analogous to the implementation in Delta Rule
//but with the neuron's output instead of the error
//we're suppressing here to save space
break;
case ONLINE:
deltaWeight*=currNeuron.getOutput();
if(input<currNeuron.getNumberOfInputs()){
deltaWeight*=neuralNet.getInput(input);
}
break;
}
return currNeuron.getWeight(input)+deltaWeight;
}
Adaline
Adaline是一种代表自适应线性神经元的架构,由Bernard Widrow和Ted Hoff开发,基于McCulloch和Pitts神经元。它只有一层神经元,可以类似于delta规则进行训练。主要区别在于更新规则是由输入加权和偏差与目标输出的误差给出,而不是基于激活函数后的神经元输出进行更新。当想要对分类问题进行连续学习时,这可能是所希望的,因为分类问题倾向于使用离散值而不是连续值。
下图说明了Adaline是如何学习的:

因此,权重是通过以下方程更新的:

为了实现Adaline,我们创建了一个名为Adaline的类,其中包含以下重写的calcNewWeight方法。为了节省空间,我们只展示在线案例:
@Override
public Double calcNewWeight(int layer,int input,int neuron)
throws NeuralException{
//…
Double deltaWeight=LearningRate;
Neuron currNeuron=neuralNet.getOutputLayer().getNeuron(neuron);
switch(learningMode){
case BATCH:
//…
break;
case ONLINE:
deltaWeight*=error.get(currentRecord).get(neuron)
*currNeuron.getOutputBeforeActivation();
if(input<currNeuron.getNumberOfInputs()){
deltaWeight*=neuralNet.getInput(input);
}
break;
}
return currNeuron.getWeight(input)+deltaWeight;
}
注意我们在上一章中提到的getOutputBeforeActivation( )方法;这个属性在将来会很有用。
现在是时候看到学习在实际中的应用了!
让我们处理一个非常简单但具有说明性的例子。假设你想要一个单神经元神经网络学习如何拟合以下简单的线性函数:
![]() |
![]() |
|---|
即使对数学背景薄弱的人来说也很简单,所以猜猜看?这是我们最简单的神经网络证明其学习能力的一个好起点!
教授神经网络 – 训练数据集
我们将使用以下代码来结构化神经网络的学习数据集,你可以在文件NeuralNetDeltaRuleTest的主方法中找到它:
Double[][] _neuralDataSet = {
{1.2 , fncTest(1.2)}
, {0.3 , fncTest(0.3)}
, {-0.5 , fncTest(-0.5)}
, {-2.3 , fncTest(-2.3)}
, {1.7 , fncTest(1.7)}
, {-0.1 , fncTest(-0.1)}
, {-2.7 , fncTest(-2.7)} };
int[] inputColumns = {0};
int[] outputColumns = {1};
NeuralDataSet neuralDataSet = newNeuralDataSet(_neuralDataSet,inputColumns,outputColumns);
funcTest函数定义为我们在上一章中提到的函数:
public static double fncTest(double x){
return 0.11*x;
}
注意,我们正在使用NeuralDataSet类来以正确的方式组织所有这些数据。现在让我们将这个数据集链接到神经网络。记住,这个网络在输出端只有一个神经元。让我们使用一个非线性激活函数,比如输出端的双曲正切函数,系数为0.85:
int numberOfInputs=1;
int numberOfOutputs=1;
HyperTan htAcFnc = new HyperTan(0.85);
NeuralNet nn = new NeuralNet(numberOfInputs,numberOfOutputs,
htAcFnc);
现在我们实例化DeltaRule对象并将其链接到创建的神经网络。然后我们将设置学习参数,如学习率、最小总体误差和最大迭代次数:
DeltaRule deltaRule=new DeltaRule(nn,neuralDataSet.LearningAlgorithm.LearningMode.ONLINE);
deltaRule.printTraining=true;
deltaRule.setLearningRate(0.3);
deltaRule.setMaxEpochs(1000);
deltaRule.setMinOverallError(0.00001);
现在我们来看看未训练的神经网络的第一批神经输出,这是在调用deltaRule对象的forward()方法之后:
deltaRule.forward();
neuralDataSet.printNeuralOutput();

绘制图表,我们发现神经网络生成的输出略有不同:

我们将开始以在线模式训练神经网络。我们已经将printTraining属性设置为true,因此我们将在屏幕上收到更新。以下代码将生成后续的截图:
System.out.println("Beginning training");
deltaRule.train();
System.out.println("End of training");
if(deltaRule.getMinOverallError()>=deltaRule.getOverallGeneralError()){
System.out.println("Training succesful!");
}
else{
System.out.println("Training was unsuccesful");
}

训练开始,并在每次权重更新后更新总体错误信息。注意错误正在减少:

经过五个迭代周期后,错误达到最小值;现在让我们看看神经输出和图表:
![]() |
![]() |
|---|
非常令人惊讶,不是吗?目标和神经输出几乎相同。现在让我们看看最终的权重和偏差:
weight = nn.getOutputLayer().getWeight(0, 0);
bias = nn.getOutputLayer().getWeight(1, 0);
System.out.println("Weight found:"+String.valueOf(weight));
System.out.println("Bias found:"+String.valueOf(bias));
//Weight found:0.2668421011698528
//Bias found:0.0011258204676042108
惊人的是,它已经学习到了!或者,它真的学习到了吗?下一步——测试
好吧,我们现在可能会问:那么神经网络已经从数据中学习到了;我们如何证明它已经有效地学习了呢?就像学生在考试中会遇到的,我们需要检查训练后的网络响应。但是等等!你认为老师会在考试中出与课堂上所讲相同的问题吗?用已知例子来评估某人的学习是没有意义的,或者怀疑老师可能会得出学生可能只是记住了内容而没有真正学习的结论。
好的,现在让我们解释这部分。我们在这里讨论的是测试。我们之前介绍的学习过程被称为训练。训练神经网络后,我们应该测试它是否真的学到了东西。为了测试,我们必须向神经网络展示来自它学习过的相同环境的另一部分数据。这是必要的,因为,就像学生一样,神经网络只能正确响应它接触过的数据点;这被称为过训练。为了检查神经网络是否没有过度训练,我们必须检查它对其他数据点的响应。
下图说明了过训练问题。想象一下,我们的网络被设计用来逼近某个函数 f(x),其定义是未知的。神经网络被喂食了该函数的一些数据,并在下图的左侧产生了结果。但当扩展到更广泛的领域时,例如,添加测试数据集,我们注意到神经网络的响应没有遵循数据(在图的右侧):

在这种情况下,我们看到神经网络未能学习整个环境(函数 f(x))。这发生是因为以下多个原因:
-
神经网络没有从环境中获得足够的信息
-
来自环境的数据是非确定性的
-
训练和测试数据集定义得不好
-
神经网络在训练数据上学习得太多,以至于它已经“忘记”了测试数据
在整本书中,我们将介绍防止这种情况以及其他在训练过程中可能出现的问题的过程。
过拟合和过训练
在我们之前的例子中,神经网络似乎学得非常好。然而,存在过拟合和过训练的风险。这两个概念之间的区别非常微妙。过拟合发生在神经网络记住问题的行为,因此它只能在训练点上提供良好的值,从而失去了泛化能力。过训练,这可能是过拟合的原因之一,发生在训练误差远小于测试误差的情况下,或者实际上,随着神经网络继续(过度)训练,测试误差开始增加:

防止过训练和过拟合的一种方法是在训练过程中检查测试误差。当测试误差开始增加时,就是停止的时候了。这将在下一章中更详细地介绍。
现在,让我们看看在我们的例子中是否存在这种情况。现在让我们添加一些更多数据并对其进行测试:
Double[][] _testDataSet ={
{-1.7 , fncTest(-1.7) }
, {-1.0 , fncTest(-1.0) }
, {0.0 , fncTest(0.0) }
, {0.8 , fncTest(0.8) }
, {2.0 , fncTest(2.0) }
};
NeuralDataSet testDataSet = new NeuralDataSet(_testDataSet, ....inputColumns, outputColumns);
deltaRule.setTestingDataSet(testDataSet);
deltaRule.test();
testDataSet.printNeuralOutput();
![]() |
![]() |
|---|
如所见,在这种情况下,神经网络表现出了一般化能力。尽管这个例子很简单,我们仍然可以看到神经网络的学习能力。
摘要
本章向读者展示了神经网络能够实现的整体学习过程。我们介绍了学习的基础知识,这些知识灵感来源于人类自身的学习。为了在实践中说明这一过程,我们已经在Java中实现了两种学习算法,并在两个示例中应用了它们。通过这种方式,读者可以对神经网络的学习方式有一个基本但有用的理解,甚至可以系统地描述学习过程。这将是下一章的基础,下一章将展示更复杂的示例。
第三章:第3章. 感知器和监督学习
在本章中,我们将更详细地探讨监督学习,这在寻找两个数据集之间的关系时非常有用。此外,我们介绍了感知器,这是一种非常流行的神经网络架构,它实现了监督学习。本章还介绍了它们的扩展广义版本,即所谓的多层感知器,以及它们的特性、学习算法和参数。读者还将学习如何在Java中实现它们以及如何使用它们解决一些基本问题。本章将涵盖以下主题:
-
监督学习
-
回归任务
-
分类任务
-
感知器
-
线性分离
-
局限性:XOR问题
-
多层感知器
-
广义delta规则 – 反向传播算法
-
Levenberg–Marquardt算法
-
单隐藏层神经网络
-
极端学习机
监督学习 – 教导神经网络
在上一章中,我们介绍了适用于神经网络的认知模式,其中监督学习意味着有一个目标或一个定义的目标要达到。在实践中,我们提供一组输入数据X和一个期望的输出数据YT,然后评估一个成本函数,其目的是减少神经网络输出Y与目标输出YT之间的误差。
在监督学习中,涉及两个主要类别的任务,具体如下:分类和回归。
分类 – 寻找合适的类别
神经网络也处理分类数据。给定一个类别列表和一个数据集,人们希望根据包含记录及其相应类别的历史数据集对它们进行分类。以下表格显示了该数据集的一个示例,考虑了主题的平均成绩在0到10之间:
| 学生ID | 主题 | 职业 |
|---|---|---|
| 英语 | 数学 | 物理 |
| --- | --- | --- |
| 89543 | 7.82 | 8.82 |
| 93201 | 8.33 | 6.75 |
| 95481 | 7.76 | 7.17 |
| 94105 | 8.25 | 7.54 |
| 96305 | 8.05 | 6.75 |
| 92904 | 6.95 | 8.85 |
一个例子是根据学术成绩预测职业。让我们考虑一组现在工作的前学生的数据集。我们编制了一个包含每个学生在每门课程上的平均成绩以及他/她的当前职业的数据集。请注意,输出将是职业名称,神经网络无法直接给出。相反,我们需要为每个已知职业创建一列(一个输出)。如果该学生选择了一个特定的职业,则对应该职业的列将具有值一,否则为零:

现在我们想要找到一个基于神经网络来预测学生将可能选择哪个职业的模型,基于他/她的成绩。为此,我们构建了一个神经网络,其中包含学术科目的数量作为输入,已知职业的数量作为输出,以及隐藏层中的任意数量的隐藏神经元:

对于分类问题,通常每个数据点只有一个类别。因此,在输出层,神经元被激活以产生零或一,最好使用输出值介于这两个值之间的激活函数。然而,我们必须考虑这种情况,即多个神经元被激活,给一个记录分配两个类别。有许多机制可以防止这种情况,例如 softmax 函数或全胜算法,例如。这些机制将在第 6 章[分类疾病诊断]的实践应用中详细说明,分类疾病诊断。
经过训练后,神经网络已经学会了给定学生的成绩,最有可能的职业是什么。
回归 – 将实际输入映射到输出
回归包括找到一些函数,这些函数将一组输入映射到一组输出。以下表格显示了包含 k 条记录的 m 个独立输入 X 的数据集如何与 n 个相关输出绑定:
| 输入独立数据 | 输出相关数据 |
|---|---|
| X1 | X2 |
| x1[0] | x2[0] |
| x1[1] | x2[1] |
| … | … |
| x1[k] | x2[k] |
前面的表格可以编译成矩阵格式:

与分类不同,输出值是数值而不是标签或类别。还有一个包含我们希望神经网络学习的某些行为记录的历史数据库。一个例子是预测两个城市之间的公交车票价。在这个例子中,我们从一系列城市和从一地出发到达另一地的公交车当前票价中收集信息。我们将城市特征以及它们之间的距离和/或时间作为输入,将公交车票价作为输出:

| 出发城市特征 | 目的地城市特征 | 路线特征 | 票价 |
|---|---|---|---|
| 人口 | GDP | 路线 | 人口 |
| 500,000 | 4.5 | 6 | 45,000 |
| 120,000 | 2.6 | 4 | 500,000 |
| 30,000 | 0.8 | 3 | 65,000 |
| 35,000 | 1.4 | 3 | 45,000 |
| … | |||
| 120,000 | 2.6 | 4 | 12,000 |
在结构化数据集后,我们定义了一个包含确切数量的特征(由于两个城市而乘以二)加上路线特征的输入,一个输出,以及隐藏层中的任意数量的神经元。在前面表格中展示的案例中,将有九个输入。由于输出是数值的,因此不需要转换输出数据。
这个神经网络将给出两个城市之间路线的估计价格,目前还没有任何公交运输公司提供服务。
一个基本的神经网络架构 – 感知器
感知器是最简单的神经网络架构。由 Frank Rosenblatt 在 1957 年提出,它只有一个神经元层,接收一组输入并产生另一组输出。这是神经网络首次获得关注的代表之一,尤其是由于它们的简单性:

在我们的 Java 实现中,这通过一个神经网络层(输出层)来展示。以下代码创建了一个具有三个输入和两个输出,输出层具有线性函数的感知器:
int numberOfInputs=3;
int numberOfOutputs=2;
Linear outputAcFnc = new Linear(1.0);
NeuralNet perceptron = new NeuralNet(numberOfInputs,numberOfOutputs,
outputAcFnc);
应用和限制
然而,科学家们并没有花很长时间就得出结论,感知器神经网络只能应用于简单任务,根据这种简单性。当时,神经网络被用于简单的分类问题,但感知器通常在面对更复杂的数据集时失败。让我们用一个非常基本的例子(一个 AND 函数)来更好地说明这个问题。
线性分离
该示例由一个接受两个输入,x1 和 x2 的 AND 函数组成。该函数可以绘制在以下二维图表中:

现在让我们考察神经网络如何使用感知器规则进行训练,考虑一对权重,w1 和 w2,初始值为 0.5,以及偏置值为 0.5。假设学习率 η 等于 0.2:
| 周期 | x1 | x2 | w1 | w2 | b | y | t | E | Δw1 | Δw2 | Δb |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 0.5 | 0.5 | 0.5 | 0.5 | 0 | -0.5 | 0 | 0 | -0.1 |
| 1 | 0 | 1 | 0.5 | 0.5 | 0.4 | 0.9 | 0 | -0.9 | 0 | -0.18 | -0.18 |
| 1 | 1 | 0 | 0.5 | 0.32 | 0.22 | 0.72 | 0 | -0.72 | -0.144 | 0 | -0.144 |
| 1 | 1 | 1 | 0.356 | 0.32 | 0.076 | 0.752 | 1 | 0.248 | 0.0496 | 0.0496 | 0.0496 |
| 2 | 0 | 0 | 0.406 | 0.370 | 0.126 | 0.126 | 0 | -0.126 | 0.000 | 0.000 | -0.025 |
| 2 | 0 | 1 | 0.406 | 0.370 | 0.100 | 0.470 | 0 | -0.470 | 0.000 | -0.094 | -0.094 |
| 2 | 1 | 0 | 0.406 | 0.276 | 0.006 | 0.412 | 0 | -0.412 | -0.082 | 0.000 | -0.082 |
| 2 | 1 | 1 | 0.323 | 0.276 | -0.076 | 0.523 | 1 | 0.477 | 0.095 | 0.095 | 0.095 |
| … | … | ||||||||||
| 89 | 0 | 0 | 0.625 | 0.562 | -0.312 | -0.312 | 0 | 0.312 | 0 | 0 | 0.062 |
| 89 | 0 | 1 | 0.625 | 0.562 | -0.25 | 0.313 | 0 | -0.313 | 0 | -0.063 | -0.063 |
| 89 | 1 | 0 | 0.625 | 0.500 | -0.312 | 0.313 | 0 | -0.313 | -0.063 | 0 | -0.063 |
| 89 | 1 | 1 | 0.562 | 0.500 | -0.375 | 0.687 | 1 | 0.313 | 0.063 | 0.063 | 0.063 |
经过 89 个周期后,我们发现网络产生的值接近期望的输出。由于在这个例子中,输出是二进制的(零或一),我们可以假设网络产生的任何低于 0.5 的值被认为是 0,任何高于 0.5 的值被认为是 1。因此,我们可以绘制一个函数
,其中包含学习算法找到的最终权重和偏置 w1=0.562,w2=0.5 和 b=-0.375,定义图表中的线性边界:

这个边界是网络给出的所有分类的定义。你可以看到,由于函数也是线性的,边界是线性的。因此,感知器网络非常适合那些模式线性可分的问题。
XOR 情况
现在我们来分析 XOR 情况:

我们看到,在二维空间中,不可能画出一条线来分离这两种模式。如果我们尝试训练一个单层感知器来学习这个函数,会发生什么?假设我们尝试了,让我们看看以下表格中的结果:
| 周期 | x1 | x2 | w1 | w2 | b | y | t | E | Δw1 | Δw2 | Δb |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 0.5 | 0.5 | 0.5 | 0.5 | 0 | -0.5 | 0 | 0 | -0.1 |
| 1 | 0 | 1 | 0.5 | 0.5 | 0.4 | 0.9 | 1 | 0.1 | 0 | 0.02 | 0.02 |
| 1 | 1 | 0 | 0.5 | 0.52 | 0.42 | 0.92 | 1 | 0.08 | 0.016 | 0 | 0.016 |
| 1 | 1 | 1 | 0.516 | 0.52 | 0.436 | 1.472 | 0 | -1.472 | -0.294 | -0.294 | -0.294 |
| 2 | 0 | 0 | 0.222 | 0.226 | 0.142 | 0.142 | 0 | -0.142 | 0.000 | 0.000 | -0.028 |
| 2 | 0 | 1 | 0.222 | 0.226 | 0.113 | 0.339 | 1 | 0.661 | 0.000 | 0.132 | 0.132 |
| 2 | 1 | 0 | 0.222 | 0.358 | 0.246 | 0.467 | 1 | 0.533 | 0.107 | 0.000 | 0.107 |
| 2 | 1 | 1 | 0.328 | 0.358 | 0.352 | 1.038 | 0 | -1.038 | -0.208 | -0.208 | -0.208 |
| … | … | ||||||||||
| 127 | 0 | 0 | -0.250 | -0.125 | 0.625 | 0.625 | 0 | -0.625 | 0.000 | 0.000 | -0.125 |
| 127 | 0 | 1 | -0.250 | -0.125 | 0.500 | 0.375 | 1 | 0.625 | 0.000 | 0.125 | 0.125 |
| 127 | 1 | 0 | -0.250 | 0.000 | 0.625 | 0.375 | 1 | 0.625 | 0.125 | 0.000 | 0.125 |
| 127 | 1 | 1 | -0.125 | 0.000 | 0.750 | 0.625 | 0 | -0.625 | -0.125 | -0.125 | -0.125 |
感知机无法找到任何一对权重来驱动以下误差 0.625。这可以从我们已从图表中感知到的数学上解释,即这个函数在二维空间中无法线性可分。那么如果我们增加另一个维度会怎样呢?让我们看看三维空间的图表:

在三维空间中,如果这个额外的维度能够正确地转换输入数据,就有可能画出一个平面来分离模式。好吧,但现在有一个额外的问题:我们只有两个输入变量,我们如何推导出这个额外的维度呢?一个明显但也是权宜之计的答案是从两个原始变量中添加一个第三个变量作为导数。而这个第三个变量是一个(导数),我们的神经网络可能具有以下形状:

好吧,现在感知机有三个输入,其中一个是其他输入的组合。这也引出了一个新的问题:这个组合应该如何处理?我们可以看到这个组件可以作为一个神经元,因此给神经网络一个嵌套架构。如果是这样,那么就会有另一个新的问题:由于错误在输出神经元上,我们如何训练这个新神经元的权重?
多层感知机
正如我们所见,一个简单的例子,其中模式无法线性可分,这导致我们使用感知机架构时遇到了越来越多的问题。这种需求导致了多层感知机的应用。在第 1 章 开始使用神经网络 中,我们处理了这样一个事实,即自然神经网络的结构也是分层的,每一层都从特定的环境中捕获信息片段。在人工神经网络中,神经元层以这种方式行动,通过从数据中提取和抽象信息,将它们转换成另一个维度或形状。
在XOR示例中,我们找到了解决方案是添加一个第三组件,使其能够进行线性分离。但关于如何计算这个第三组件仍有一些疑问。现在让我们将相同的解决方案视为一个双层感知器:

现在我们有三个神经元而不是只有一个,但在输出层,前一层的传递信息被转换成另一个维度或形状,理论上可以在这些数据点上建立线性边界。然而,关于如何找到第一层的权重的疑问仍未得到解答,或者我们是否可以将相同的训练规则应用于输出层以外的神经元?我们将在广义delta规则部分处理这个问题。
MLP属性
多层感知器可以具有任意数量的层,以及每一层任意数量的神经元。激活函数可以在任何层上不同。一个MLP网络通常由至少两层组成,一层用于输出,一层为隐藏层。
小贴士
也有一些参考资料将输入层视为收集输入数据的节点;因此,对于这些情况,MLP被认为至少有三级。为了本书的目的,让我们将输入层视为一种特殊的层,它没有权重,并且作为有效层,即那些能够被训练的层,我们将考虑隐藏层和输出层。
一个隐藏层之所以被称为隐藏层,是因为它实际上隐藏了其输出对外部世界。隐藏层可以以任意数量串联连接,从而形成一个深度神经网络。然而,神经网络层数越多,训练和运行的速度就越慢,根据数学基础,一个最多只有一两个隐藏层的神经网络可能学习效果与拥有数十个隐藏层的深度神经网络相当。但这取决于几个因素。
小贴士
真的强烈建议在隐藏层中使用非线性激活函数,尤其是在输出层激活函数是线性的情况下。根据线性代数,如果所有层的激活函数都是线性的,那么这相当于只有一个输出层,前提是层引入的额外变量将是前一层或输入的线性组合。通常,使用如双曲正切或Sigmoid这样的激活函数,因为它们是可导的。
MLP权重
在MLP前馈网络中,一个特定的神经元i从前一层的神经元j接收数据,并将其输出转发到下一层的神经元k:

神经网络的数学描述是递归的:

在这里,yo 是网络输出(如果我们有多个输出,我们可以用 Y 代替 yo,表示一个向量);fo 是输出层的激活函数;l 是隐藏层的数量;nhi 是隐藏层 i 中的神经元数量;wi 是连接最后一个隐藏层第 i 个神经元到输出的权重;fi 是神经元 i 的激活函数;bi 是神经元 i 的偏置。可以看出,随着层数的增加,这个方程会变得更大。在最后的求和操作中,会有输入 xi。
循环MLP
MLP中的神经元不仅可以向下一层的神经元(前馈网络)发送信号,还可以向同一层或上一层的神经元(反馈或循环)发送信号。这种行为允许神经网络在某个数据序列上维持状态,当处理时间序列或手写识别时,这个特性尤其被利用。循环网络通常更难训练,并且在执行时计算机可能会耗尽内存。此外,还有一些比MLP更好的循环网络架构,如Elman、Hopfield、Echo state、双向RNN(循环神经网络)。但我们不会深入探讨这些架构,因为这本书专注于对编程经验最少的人的最简单应用。然而,我们推荐那些对循环网络感兴趣的人阅读有关循环网络的良好文献。
编码MLP
将这些概念引入面向对象的观点,我们可以回顾到目前为止已经设计的类:

可以看到,神经网络结构是分层的。神经网络由层组成,而层由神经元组成。在MLP架构中,有三种类型的层:输入、隐藏和输出。所以假设在Java中,我们想要定义一个由三个输入、一个输出(线性激活函数)和一个包含五个神经元的隐藏层(sigmoid函数)组成的神经网络。生成的代码如下:
int numberOfInputs=3;
int numberOfOutputs=1;
int[] numberOfHiddenNeurons={5};
Linear outputAcFnc = new Linear(1.0);
Sigmoid hiddenAcFnc = new Sigmoid(1.0);
NeuralNet neuralnet = new NeuralNet(numberOfInputs, numberOfOutputs, numberOfHiddenNeurons, hiddenAcFnc, outputAcFnc);
MLP中的学习
多层感知器网络基于Delta规则进行学习,该规则也受到梯度下降优化方法的启发。梯度方法广泛应用于寻找给定函数的最小值或最大值:

此方法应用于行走函数输出更高或更低的方向,具体取决于标准。这个概念在Delta规则中得到了探索:

Delta 规则想要最小化的函数是神经网络输出和目标输出之间的误差,要找到的参数是神经权重。与感知器规则相比,这是一个增强的学习算法,因为它考虑了激活函数导数 g'(h),这在数学上表示函数减少最快的方向。
反向传播算法
虽然 Delta 规则对于只有输出层和输入层的神经网络效果很好,但对于 MLP 网络,由于隐藏层神经元的存在,纯 Delta 规则不能应用。为了克服这个问题,在 20 世纪 80 年代,Rummelhart 和其他人提出了一种新的算法,该算法也受到梯度方法的启发,称为反向传播。
这个算法实际上是 Delta 规则对于 MLP 的一般化。拥有额外的层来从环境中抽象更多数据的优势激励了开发一个能够正确调整隐藏层权重的训练算法。基于梯度方法,输出层的误差会(反向)传播到前面的层,因此可以使用与 Delta 规则相同的方程进行权重更新。
算法运行如下:

第二步是反向传播本身。它所做的就是根据梯度找到权重变化,这是 Delta 规则的基础:

这里,E 是误差,wji 是神经元 i 和 j 之间的权重,oi 是第 i 个神经元的输出,而 hi 是该神经元输入在传递到激活函数之前的加权和。记住,oi=f(hi),其中 f 是激活函数。
对于隐藏层的更新,由于我们将误差视为要更新的权重和输出之间所有神经元的函数,因此它稍微复杂一些。为了便于这个过程,我们应该计算敏感性或反向传播误差:

权重更新如下:

反向传播误差的计算对于输出层和隐藏层是不同的:
-
输出层的反向传播:
![反向传播算法]()
- 在这里,oi 是第 i 个输出,ti 是期望的第 i 个输出,f'(hi) 是输出激活函数的导数,而 hi 是第 i 个神经元输入的加权和
-
隐藏层的反向传播:
![反向传播算法]()
- 这里,l 是前一个层的神经元,wil 是连接当前神经元到前一个层第 l 个神经元的权重。
为了简化起见,我们没有完全展示如何发展反向传播方程。无论如何,如果你对细节感兴趣,你可以查阅Simon Haykin的《神经网络——全面基础》这本书。
动量
如同任何基于梯度的方法一样,存在陷入局部最小值的风险。为了减轻这种风险,我们可以在权重更新规则中添加另一个称为动量的项,它考虑了权重的最后一个变化:

在这里,μ是动量率,
是最后一个delta权重。这为更新提供了一个额外的步骤,因此减弱了误差超空间中的振荡。
编码反向传播
让我们在edu.packt.neural.learn包中定义backpropagation类。由于这个学习算法是DeltaRule的泛化,这个类可能继承并覆盖Delta规则中已经定义的特性。这个类包含的三个附加属性是动量率、delta神经元和最后一个delta权重数组:
public class Backpropagation extends DeltaRule {
private double MomentumRate=0.7;
public ArrayList<ArrayList<Double>> deltaNeuron;
public ArrayList<ArrayList<ArrayList<Double>>> lastDeltaWeights;
…
}
构造函数将具有与DeltaRule类相同的参数,并添加对deltaNeuron和lastDeltaWeights数组初始化方法的调用:
public Backpropagation(NeuralNet _neuralNet, NeuralDataSet _trainDataSet, DeltaRule.LearningMode _learningMode){
super(_neuralNet,_trainDataSet,_learningMode);
initializeDeltaNeuron();
initializeLastDeltaWeights();
}
train()方法将以与DeltaRule类类似的方式工作;额外的组件是反向步骤,其中错误在整个神经网络层中反向传播到输入:
@Override
public void train() throws NeuralException{
neuralNet.setNeuralNetMode(NeuralNet.NeuralNetMode.TRAINING);
epoch=0; // initialization of epoch
int k=0; // first training record
currentRecord=0; // this attribute keeps track of which record
// is currently under processing in the training
forward(); // initial forward step to determine the error
forward(k); // forward for backpropagation of first record error
while(epoch<MaxEpochs && overallGeneralError>MinOverallError){
backward(); // backward step
switch(learningMode){
case BATCH:
if(k==trainingDataSet.numberOfRecords-1)
applyNewWeights(); // batch update
break;
case ONLINE:
applyNewWeights(); //online update
}
currentRecord=++k; // moving on to the next record
if(k>=trainingDataSet.numberOfRecords){ //if it was the last
k=0;
currentRecord=0; // reset to the first
epoch++; // and increase the epoch
}
forward(k); // forward the next record
}
neuralNet.setNeuralNetMode(NeuralNet.NeuralNetMode.RUN);
}
向后步骤的作用是通过误差反向传播来确定delta权重,从输出层到第一隐藏层:
public void backward(){
int numberOfLayers=neuralNet.getNumberOfHiddenLayers();
for(int l=numberOfLayers;l>=0;l--){
int numberOfNeuronsInLayer=deltaNeuron.get(l).size();
for(int j=0;j<numberOfNeuronsInLayer;j++){
for(int i=0;i<newWeights.get(l).get(j).size();i++){
// get the current weight of the neuron
double currNewWeight = this.newWeights.get(l).get(j).get(i);
//if it is the first epoch, get directly from the neuron
if(currNewWeight==0.0 && epoch==0.0)
if(l==numberOfLayers)
currNewWeight=neuralNet.getOutputLayer().getWeight(i, j);
else
currNewWeight=neuralNet.getHiddenLayer(l).
getWeight(i, j);
// calculate the delta weight
double deltaWeight=calcDeltaWeight(l, i, j);
// store the new calculated weight for subsequent update
newWeights.get(l).get(j).set(i,currNewWeight+deltaWeight);
}
}
}
}
反向传播步骤是在calcDeltaWeight()方法中执行的。动量只会在更新权重之前添加,因为它应该调用确定的最后一个delta权重:
public Double calcDeltaWeight(int layer,int input,int neuron) {
Double deltaWeight=1.0;
NeuralLayer currLayer;
Neuron currNeuron;
double _deltaNeuron;
if(layer==neuralNet.getNumberOfHiddenLayers()){ //output layer
currLayer=neuralNet.getOutputLayer();
currNeuron=currLayer.getNeuron(neuron);
_deltaNeuron=error.get(currentRecord).get(neuron)
*currNeuron.derivative(currLayer.getInputs());
}
else{ //hidden layer
currLayer=neuralNet.getHiddenLayer(layer);
currNeuron=currLayer.getNeuron(neuron);
double sumDeltaNextLayer=0;
NeuralLayer nextLayer=currLayer.getNextLayer();
for(int k=0;k<nextLayer.getNumberOfNeuronsInLayer();k++){
sumDeltaNextLayer+=nextLayer.getWeight(neuron, k)
*deltaNeuron.get(layer+1).get(k);
}
_deltaNeuron=sumDeltaNextLayer*
currNeuron.derivative(currLayer.getInputs());
}
deltaNeuron.get(layer).set(neuron, _deltaNeuron);
deltaWeight*=_deltaNeuron;
if(input<currNeuron.getNumberOfInputs()){
deltaWeight*=currNeuron.getInput(input);
}
return deltaWeight;
}
注意到对于输出层和隐藏层,_deltaNeuron的计算是不同的,但它们都使用了导数。为了便于这项任务,我们在Neuron类中添加了derivative()方法。详细信息可以在附录III文档中找到。最后,将对应于权重的输入乘以计算出的delta权重。
权重更新是通过applyNewWeights()方法执行的。为了节省空间,我们不会在这里写出整个方法体,而只写出执行权重更新的核心部分:
HiddenLayer hl = this.neuralNet.getHiddenLayer(l);
Double lastDeltaWeight=lastDeltaWeights.get(l).get(j).get(i);
// determine the momentum
double momentum=MomentumRate*lastDeltaWeight;
//the momentum is then added to the new weight
double newWeight=this.newWeights.get(l).get(j).get(i)
-momentum;
this.newWeights.get(l).get(j).set(i,newWeight);
Neuron n=hl.getNeuron(j);
// save the current delta weight for the next step
double deltaWeight=(newWeight-n.getWeight(i));
lastDeltaWeights.get(l).get(j).set(i,(double)deltaWeight);
// finally the weight is updated
hl.getNeuron(j).updateWeight(i, newWeight);
在代码列表中,l代表层,j代表神经元,i代表输入到权重的输入。对于输出层,l将等于隐藏层的数量(超出Java数组限制),因此调用的NeuralLayer如下:
OutputLayer ol = this.neuralNet.getOutputLayer();
Neuron n=ol.getNeuron(j);
ol.getNeuron(j).updateWeight(i, newWeight);
这个类可以像DeltaRule一样使用:
int numberOfInputs=2;
int numberOfOutputs=1;
int[] numberOfHiddenNeurons={2};
Linear outputAcFnc = new Linear(1.0);
Sigmoid hdAcFnc = new Sigmoid(1.0);
IActivationFunction[] hiddenAcFnc={hdAcFnc };
NeuralNet mlp = new NeuralNet(numberOfInputs,numberOfOutputs
,numberOfHiddenNeurons,hiddenAcFnc,outputAcFnc);
Backpropagation backprop = new Backpropagation(mlp,neuralDataSet
,LearningAlgorithm.LearningMode.ONLINE);
在本章结束时,我们将对感知器的Delta规则与多层感知器的反向传播进行比较,尝试解决XOR问题。
Levenberg-Marquardt算法
反向传播算法,像所有基于梯度的方法一样,通常收敛速度较慢,尤其是在它陷入曲折情况时,当权重每两次迭代都改变到几乎相同的值时。这种缺点在1944年由Kenneth Levenberg在曲线拟合插值问题中进行了研究,后来在1963年由Donald Marquart进行了研究,他开发了一种基于高斯-牛顿算法和梯度下降法寻找系数的方法,因此算法得名。
LM 算法处理一些超出本书范围的优化项,但在参考文献部分,读者将找到学习这些概念的好资源,因此我们将以更简单的方式介绍这种方法。假设我们有一个输入 x 和输出 t 的列表:

我们已经看到,神经网络具有将输入映射到输出的特性,就像具有系数 W(权重和偏置)的非线性函数 f 一样:

非线性函数将产生与输出 T 不同的值;这是因为我们在方程中标记了变量 Y。Levenberg-Marquardt 算法在一个雅可比矩阵上工作,这是一个关于每个数据行中每个权重和偏置的所有偏导数的矩阵。因此,雅可比矩阵具有以下格式:

这里,k 是数据点的总数,p 是权重和偏置的总数。在雅可比矩阵中,所有权重和偏置都按顺序存储在单行中。雅可比矩阵的元素是从梯度计算出来的:

在反向传播算法中,计算了误差 E 对每个权重的偏导数,因此这个算法将运行反向传播步骤。
在每一个优化问题中,人们都希望最小化总误差:

在这里,W(在神经网络情况下为权重和偏置)是需要优化的变量。优化算法通过添加 ΔW 来更新 W。通过应用一些代数运算,最后一个方程可以扩展为以下形式:

转换为向量和符号:

最后,通过将误差 E 设置为零,经过一些操作后,我们得到 Levenberg-Marquardt 方程:

这是权重更新规则。如所见,它涉及到矩阵运算,如转置和求逆。希腊字母 λ 是阻尼因子,是学习率的等效物。
使用矩阵代数编码 Levenberg-Marquardt
为了有效地实现LM算法,与矩阵代数一起工作非常有用。为了解决这个问题,我们在edu.packt.neuralnet.math包中定义了一个名为Matrix的类,包括所有矩阵运算,如乘法、逆运算和LU分解等。读者可以参考文档以了解更多关于这个类的信息。
Levenberg-Marquardt算法使用了反向传播算法的许多特性;这就是为什么我们从这个类继承。包含了一些新的属性:
-
雅可比矩阵:这是一个包含所有训练记录对每个权重的偏导数的矩阵
-
阻尼因子
-
误差反向传播:这个数组与
deltaNeuron具有相同的功能,但它的计算对每个神经网络输出略有不同;这就是为什么我们将其定义为单独的属性 -
误差LMA:矩阵形式的误差:
public class LevenbergMarquardt extends Backpropagation { private Matrix jacobian = null; private double damping=0.1; private ArrayList<ArrayList<ArrayList<Double>>> errorBackpropagation; private Matrix errorLMA; public ArrayList<ArrayList<ArrayList<Double>>> lastWeights; }
基本上,训练函数与反向传播相同,除了以下雅可比矩阵和误差矩阵的计算以及阻尼更新:
@Override
public void train() throws NeuralException{
neuralNet.setNeuralNetMode(NeuralNet.NeuralNetMode.TRAINING);
forward();
double currentOverallError=overallGeneralError;
buildErrorVector(); // copy the error values to the error matrix
while(epoch<MaxEpochs && overallGeneralError>MinOverallError
&& damping<=10000000000.0){ // to prevent the damping from growing up to infinity
backward(); // to determine the error backpropgation
calculateJacobian(); // copies the derivatives to the jacobian matrix
applyNewWeights(); //update the weights
forward(); //forward all records to evaluate new overall error
if(overallGeneralError<currentOverallError){
if the new error is less than current
damping/=10.0; // the damping factor reduces
currentOverallError=overallGeneralError;
}
else{ // otherwise, the damping factor grows
damping*=10.0;
restoreLastWeights(); // the last weights are recovered
forward();
}
buildErrorVector(); reevaluate the error matrix
}
neuralNet.setNeuralNetMode(NeuralNet.NeuralNetMode.RUN);
}
遍历训练数据集的循环调用calculateJacobian方法。该方法在backward方法中评估误差反向传播:
double input;
if(p==numberOfInputs)
input=1.0;
else
input = n.getInput(p);
double deltaBackprop = errorBackpropagation.get(m).get(l).get(k);
jacobian.setValue(i, j++, deltaBackprop*input);
在代码列表中,p是连接到神经元的输入(当它等于神经元输入的数量时,它代表偏置),k是神经元,l是层,m是神经网络输出,i是记录的顺序索引,j是权重或偏置的顺序索引,根据其在层和神经元中的位置。请注意,在雅可比矩阵中设置值后,j会增加。
权重更新是通过确定deltaWeight矩阵来完成的:
Matrix jacob=jacobian.subMatrix(rowi, rowe, 0, numberOfWeights-1);
Matrix errorVec = errorLMA.subMatrix(rowi, rowe, 0, 0);
Matrix pseudoHessian=jacob.transpose().multiply(jacob);
Matrix miIdent = new IdentityMatrix(numberOfWeights)
.multiply(damping);
Matrix inverseHessianMi = pseudoHessian.add(miIdent).inverse();
Matrix deltaWeight = inverseHessianMi.multiply(jacob.transpose())
.multiply(errorVec);
之前的代码引用了算法展示部分中所示的矩阵代数。deltaWeight矩阵包含神经网络中每个权重的步骤。在以下代码中,k是神经元,j是输入,l是层:
Neuron n=nl.getNeuron(k);
double currWeight=n.getWeight(j);
double newWeight=currWeight+deltaWeight.getValue(i++,0);
newWeights.get(l).get(k).set(j,newWeight);
lastWeights.get(l).get(k).set(j,currWeight);
n.updateWeight(j, newWeight);
注意,权重保存在lastWeights数组中,因此如果误差变差,可以恢复它们。
极端学习机
利用矩阵代数,极端学习机(ELMs)能够非常快地收敛学习。这个学习算法有一个限制,因为它只应用于包含单个隐藏层的神经网络。在实践中,一个隐藏层对大多数应用来说都相当不错。
用矩阵代数表示神经网络,对于以下神经网络:

我们有相应的方程:

这里,H是隐藏层的输出,g()是隐藏层的激活函数,Xi是第i个输入记录,Wj是第j个隐藏神经元的权重向量,bj是第j个隐藏神经元的偏置,βp是输出p的权重向量,Y是神经网络生成的输出。
在ELM算法中,隐藏层权重是随机生成的,而输出权重则根据最小二乘法进行调整:


这里,T是目标输出训练数据集。
此算法在名为ELM的类中实现,该类与其他训练算法位于同一包中。这个类将继承自DeltaRule,它具有所有监督学习算法的基本属性:
public class ELM extends DeltaRule {
private Matrix H;
private Matrix T;
public ELM(NeuralNet _neuralNet,NeuralDataSet _trainDataSet){
super(_neuralNet,_trainDataSet);
learningMode=LearningMode.BATCH;
initializeMatrices();
}
}
在这个类中,我们定义了H和T矩阵,这些矩阵将用于后续的输出权重计算。构造函数与其他训练算法类似,只是这个算法只工作在批处理模式下。
由于这个训练算法只需要一个迭代周期,train方法将所有训练记录转发以构建H矩阵。然后,它计算输出权重:
@Override
public void train() throws NeuralException{
if(neuralNet.getNumberOfHiddenLayers()!=1)
throw new NeuralException("The ELM learning algorithm can be performed only on Single Hidden Layer Neural Network");
neuralNet.setNeuralNetMode(NeuralNet.NeuralNetMode.TRAINING);
int k=0;
int N=trainingDataSet.numberOfRecords;
currentRecord=0;
forward();
double currentOverallError=overallGeneralError;
while(k<N){
forward(k);
buildMatrices();
currentRecord=++k;
}
applyNewWeights();
forward();
currentOverallError=overallGeneralError;
neuralNet.setNeuralNetMode(NeuralNet.NeuralNetMode.RUN);
}
buildMatrices方法只将隐藏层的输出放置在H矩阵的对应行中。输出权重在applyNewWeights方法中调整:
@Override
public void applyNewWeights(){
Matrix Ht = H.transpose();
Matrix HtH = Ht.multiply(H);
Matrix invH = HtH.inverse();
Matrix invHt = invH.multiply(Ht);
Matrix beta = invHt.multiply(T);
OutputLayer ol = this.neuralNet.getOutputLayer();
HiddenLayer hl = (HiddenLayer)ol.getPreviousLayer();
int h = hl.getNumberOfNeuronsInLayer();
int n = ol.getNumberOfNeuronsInLayer();
for(int i=0;i<=h;i++){
for(int j=0;j<n;j++){
if(i<h || outputBiasActive)
ol.getNeuron(j).updateWeight(i, beta.getValue(i, j));
}
}
}
实际示例 1 – 使用delta规则和反向传播的XOR情况
现在让我们看看多层感知器的作用。我们编写了示例XORTest.java,它基本上创建了两个具有以下特征的神经网络:
| 神经网络 | 感知器 | 多层感知器 |
|---|---|---|
| 输入 | 2 | 2 |
| 输出 | 1 | 1 |
| 隐藏层 | 0 | 1 |
| 每层的隐藏神经元 | 0 | 2 |
| 隐藏层激活函数 | 非 | Sigmoid |
| 输出层激活函数 | 线性 | 线性 |
| 训练算法 | Delta规则 | 反向传播 |
| 学习率 | 0.1 | 0.3 动量 0.6 |
| 最大迭代次数 | 4000 | 4000 |
| 总体最小误差 | 0.1 | 0.01 |
在Java中,这可以这样编码:
public class XORTest {
public static void main(String[] args){
RandomNumberGenerator.seed=0;
int numberOfInputs=2;
int numberOfOutputs=1;
int[] numberOfHiddenNeurons={2};
Linear outputAcFnc = new Linear(1.0);
Sigmoid hdAcFnc = new Sigmoid(1.0);
IActivationFunction[] hiddenAcFnc={hdAcFnc};
NeuralNet perceptron = new NeuralNet(numberOfInputs,
numberOfOutputs,outputAcFnc);
NeuralNet mlp = new NeuralNet(numberOfInputs,numberOfOutputs
,numberOfHiddenNeurons,hiddenAcFnc,outputAcFnc);
}
}
然后,我们定义数据集和学习算法:
Double[][] _neuralDataSet = {
{0.0 , 0.0 , 1.0 }
, {0.0 , 1.0 , 0.0 }
, {1.0 , 0.0 , 0.0 }
, {1.0 , 1.0 , 1.0 }
};
int[] inputColumns = {0,1};
int[] outputColumns = {2};
NeuralDataSet neuralDataSet = new NeuralDataSet(_neuralDataSet,inputColumns,outputColumns);
DeltaRule deltaRule=new DeltaRule(perceptron,neuralDataSet
,LearningAlgorithm.LearningMode.ONLINE);
deltaRule.printTraining=true;
deltaRule.setLearningRate(0.1);
deltaRule.setMaxEpochs(4000);
deltaRule.setMinOverallError(0.1);
Backpropagation backprop = new Backpropagation(mlp,neuralDataSet
,LearningAlgorithm.LearningMode.ONLINE);
backprop.printTraining=true;
backprop.setLearningRate(0.3);
backprop.setMaxEpochs(4000);
backprop.setMinOverallError(0.01);
backprop.setMomentumRate(0.6);
然后对两种算法进行了训练。正如预期的那样,XOR情况不能由单个感知器层线性分离。神经网络运行了训练但未能成功:
deltaRule.train();

但多层感知器的反向传播算法在39个迭代周期后成功学习了XOR函数:
backprop.train();

实际示例 2 – 预测注册状态
在巴西,一个人进入大学的一种方式是通过参加考试,如果他/她达到了所申请课程的最低分数,那么他/她就可以注册入学。为了演示反向传播算法,让我们考虑这个场景。表中显示的数据是从大学数据库中收集的。第二列代表个人的性别(1表示女性,0表示男性);第三列是按100分缩放的分数,最后一列由两个神经元组成(1,0表示完成注册,0,1表示放弃注册):
| 样本 | 性别 | 分数 | 注册状态 |
|---|---|---|---|
| 1 | 1 | 0.73 | 1 0 |
| 2 | 1 | 0.81 | 1 0 |
| 3 | 1 | 0.86 | 1 0 |
| 4 | 0 | 0.65 | 1 0 |
| 5 | 0 | 0.45 | 1 0 |
| 6 | 1 | 0.70 | 0 1 |
| 7 | 0 | 0.51 | 0 1 |
| 8 | 1 | 0.89 | 0 1 |
| 9 | 1 | 0.79 | 0 1 |
| 10 | 0 | 0.54 | 0 1 |
Double[][] _neuralDataSet = {
{1.0, 0.73, 1.0, -1.0}
, {1.0, 0.81, 1.0, -1.0}
, {1.0, 0.86, 1.0, -1.0}
, {0.0, 0.65, 1.0, -1.0}
, {0.0, 0.45, 1.0, -1.0}
, {1.0, 0.70, -1.0, 1.0}
, {0.0, 0.51, -1.0, 1.0}
, {1.0, 0.89, -1.0, 1.0}
, {1.0, 0.79, -1.0, 1.0}
, {0.0, 0.54, -1.0, 1.0}
};
int[] inputColumns = {0,1};
int[] outputColumns = {2,3};
NeuralDataSet neuralDataSet = new NeuralDataSet(_neuralDataSet,inputColumns,outputColumns);
我们创建了一个包含三个隐藏层神经元的神经网络,如图所示:

int numberOfInputs = 2;
int numberOfOutputs = 2;
int[] numberOfHiddenNeurons={5};
Linear outputAcFnc = new Linear(1.0);
Sigmoid hdAcFnc = new Sigmoid(1.0);
IActivationFunction[] hiddenAcFnc={hdAcFnc };
NeuralNet nnlm = new NeuralNet(numberOfInputs,numberOfOutputs
,numberOfHiddenNeurons,hiddenAcFnc,outputAcFnc);
NeuralNet nnelm = new NeuralNet(numberOfInputs,numberOfOutputs
,numberOfHiddenNeurons,hiddenAcFnc,outputAcFnc);
我们还设置了学习算法Levenberg-Marquardt和极端学习机:
LevenbergMarquardt lma = new LevenbergMarquardt(nnlm,
neuralDataSet,
LearningAlgorithm.LearningMode.BATCH);
lma.setDamping(0.001);
lma.setMaxEpochs(100);
lma.setMinOverallError(0.0001);
ELM elm = new ELM(nnelm,neuralDataSet);
elm.setMinOverallError(0.0001);
elm.printTraining=true;
运行训练后,我们发现训练成功。对于Levenberg-Marquardt算法,在九个epoch后找到了满足最小误差:

极端学习机发现了一个接近零的错误:

摘要
在本章中,我们看到了感知器如何应用于解决线性分离问题,但也看到了它们在分类非线性数据时的局限性。为了抑制这些局限性,我们介绍了多层感知器(MLPs)和新的训练算法:反向传播、Levenberg-Marquardt和极端学习机。我们还看到了一些问题类别,MLPs可以应用于,例如分类和回归。Java实现探讨了反向传播算法在更新输出层和隐藏层权重方面的能力。展示了两个实际应用,以演示使用三种学习算法解决问题的MLPs。
第四章:第4章。自组织映射
在本章中,我们介绍了一种适合无监督学习的神经网络架构:自组织映射,也称为Kohonen网络。这种特定类型的神经网络能够对数据记录进行分类,而不需要任何目标输出或找到数据在较小维度上的表示。在本章中,我们将探讨如何实现这一点,以及证明其能力的示例。本章的子主题如下:
-
神经网络无监督学习
-
竞争学习
-
Kohonen 自组织映射
-
一维SOMs
-
二维SOMs
-
无监督学习解决的问题
-
Java 实现
-
数据可视化
-
实际问题
神经网络无监督学习
在第2章中,我们在《让神经网络学会学习》中已经了解了无监督学习,现在我们将更详细地探讨这种学习范式的特征。无监督学习算法的使命是在数据集中找到模式,其中参数(在神经网络的情况下为权重)在没有误差度量(没有目标值)的情况下进行调整。
虽然监督算法提供与所提供数据集相当的输出,但无监督算法不需要知道输出值。无监督学习的基础是受神经学事实的启发,即,在神经学中,相似的刺激产生相似的响应。因此,将此应用于人工神经网络,我们可以这样说,相似的数据产生相似的结果,因此这些结果可以被分组或聚类。
尽管这种学习可以应用于其他数学领域,例如统计学,但其核心功能旨在为机器学习问题,如数据挖掘、模式识别等设计。神经网络是机器学习学科的一个子领域,只要它们的结构允许迭代学习,它们就提供了一个很好的框架来应用这一概念。
大多数无监督学习应用都旨在聚类任务,这意味着相似的数据点将被聚在一起,而不同的数据点将形成不同的簇。此外,无监督学习适合的一个应用是降维或数据压缩,只要在大量数据集中可以找到更简单、更小的数据表示。
无监督学习算法
无监督算法不仅限于神经网络,K-means、期望最大化以及矩方法也是无监督学习算法的例子。所有学习算法的一个共同特征是当前数据集中变量之间没有映射;相反,人们希望找到这些数据的不同含义,这就是任何无监督学习算法的目标。
在监督学习算法中,我们通常有较少的输出,而对于无监督学习,需要产生一个抽象的数据表示,这可能需要大量的输出,但除了分类任务外,它们的含义与监督学习中的含义完全不同。通常,每个输出神经元负责表示输入数据中存在的特征或类别。在大多数架构中,不是所有输出神经元都需要同时激活;只有一组受限的输出神经元可能会被激活,这意味着该神经元能够更好地表示被馈送到神经网络输入的大部分信息。
小贴士
无监督学习相对于监督学习的一个优点是,它对学习大量数据集所需的计算能力较低。时间消耗呈线性增长,而监督学习的时间消耗呈指数增长。
在本章中,我们将探讨两种无监督学习算法:竞争学习和Kohonen自组织映射。
竞争学习
如其名所示,竞争学习处理输出神经元之间的竞争,以确定哪个是胜者。在竞争学习中,胜者神经元通常是通过比较权重与输入(它们具有相同的维度)来确定的。为了便于理解,假设我们想要训练一个具有两个输入和四个输出的单层神经网络:

每个输出神经元都与这两个输入相连,因此对于每个神经元都有两个权重。
小贴士
对于这种学习,从神经元中移除了偏差,因此神经元将只处理加权的输入。
竞争是在数据被神经元处理之后开始的。胜者神经元将是其权重与输入值接近的那个。与监督学习算法相比的一个额外区别是,只有胜者神经元可以更新其权重,而其他神经元保持不变。这就是所谓的胜者全得规则。这种意图是将神经元更靠近导致其赢得竞争的输入值。
考虑到每个输入神经元i通过权重wij与所有输出神经元j相连,在我们的情况下,我们会有一组权重:

假设每个神经元的权重与输入数据的维度相同,让我们在一张图中考虑所有输入数据点以及每个神经元的权重:

在这张图表中,让我们将圆圈视为数据点,将正方形视为神经元权重。我们可以看到,某些数据点与某些权重更接近,而其他数据点则更远,但更接近其他权重。神经网络在输入和权重之间的距离上执行计算:

这个方程式的结果将决定一个神经元相对于其竞争对手有多强。权重距离输入较小的神经元被认为是赢家。经过多次迭代后,权重被驱动到足够接近数据点,使得相应的神经元更有可能获胜,以至于变化要么太小,要么权重处于锯齿形设置中。最后,当网络已经训练好时,图表将呈现出另一种形状:

如所见,神经元围绕能够使相应神经元比竞争对手更强的点形成中心。
在无监督神经网络中,输出的数量完全是任意的。有时只有一些神经元能够改变它们的权重,而在其他情况下,所有神经元可能对相同的输入有不同的反应,导致神经网络无法学习。在这些情况下,建议审查输出神经元的数量,或者考虑另一种无监督学习类型。
在竞争学习中,有两个停止条件是可取的:
-
预定义的周期数:这防止我们的算法在没有收敛的情况下运行时间过长
-
权重更新的最小值:防止算法运行时间超过必要
竞争层
这种类型的神经网络层是特定的,因为输出不一定与神经元的输出相同。一次只有一个神经元被激活,因此需要特殊的规则来计算输出。因此,让我们创建一个名为 CompetitiveLayer 的新类,它将继承自 OutputLayer 并从两个新属性开始:winnerNeuron 和 winnerIndex:
public class CompetitiveLayer extends OutputLayer {
public Neuron winnerNeuron;
public int[] winnerIndex;
//…
}
这种新的神经网络层将覆盖 calc() 方法并添加一些特定的新方法来获取权重:
@Override
public void calc(){
if(input!=null && neuron!=null){
double[] result = new double[numberOfNeuronsInLayer];
for(int i=0;i<numberOfNeuronsInLayer;i++){
neuron.get(i).setInputs(this.input);
//perform the normal calculation
neuron.get(i).calc();
//calculate the distance and store in a vector
result[i]=getWeightDistance(i);
//sets all outputs to zero
try{
output.set(i,0.0);
}
catch(IndexOutOfBoundsException iobe){
output.add(0.0);
}
}
//determine the index and the neuron that was the winner
winnerIndex[0]=ArrayOperations.indexmin(result);
winnerNeuron=neuron.get(winnerIndex[0]);
// sets the output of this particular neuron to 1.0
output.set(winnerIndex[0], 1.0);
}
}
在接下来的章节中,我们将定义 Kohonen 类,用于 Kohonen 神经网络。在这个类中,将有一个名为 distanceCalculation 的 enum,它将包含不同的距离计算方法。在本章(和本书)中,我们将坚持使用欧几里得距离。
小贴士
创建了一个名为 ArrayOperations 的新类,以提供便于数组操作的函数。例如,获取最大值或最小值的索引或获取数组的一个子集等功能都实现在这个类中。
特定神经元的权重与输入之间的距离是通过 getWeightDistance( ) 方法计算的,该方法在 calc 方法内部被调用:
public double getWeightDistance(int neuron){
double[] inputs = this.getInputs();
double[] weights = this.getNeuronWeights(neuron);
int n=this.numberOfInputs;
double result=0.0;
switch(distanceCalculation){
case EUCLIDIAN:
//for simplicity, let's consider only the euclidian distance
default:
for(int i=0;i<n;i++){
result+=Math.pow(inputs[i]-weights[i],2);
}
result=Math.sqrt(result);
}
return result;
}
getNeuronWeights( ) 方法返回与数组中传入的索引对应的神经元的权重。由于它很简单,并且为了节省空间,我们邀请读者查看代码以检查其实现。
Kohonen 自组织映射
这种网络架构是由芬兰教授 Teuvo Kohonen 在 80 年代初创建的。它由一个单层神经网络组成,能够在一维或二维中提供数据的 可视化。
在这本书中,我们还将使用 Kohonen 网络作为没有神经元之间链接的基本竞争层。在这种情况下,我们将将其视为零维(0-D)。
理论上,Kohonen 网络能够提供数据的 3-D(甚至更多维度)表示;然而,在像这本书这样的印刷材料中,不重叠数据就无法展示 3-D 图表,因此在这本书中,我们将只处理 0-D、1-D 和 2-D Kohonen 网络。
Kohonen 自组织映射(SOM),除了传统的单层竞争神经网络(在本书中,0-D Kohonen 网络)外,还增加了邻域神经元的概念。一维 SOM 考虑到竞争层中神经元的索引,让神经元在学习阶段发挥相关的作用。
SOM 有两种工作模式:映射和学习。在映射模式下,输入数据被分类到最合适的神经元中,而在学习模式下,输入数据帮助学习算法构建 映射。这个映射可以解释为从某个数据集的降维表示。
扩展神经网络代码到 Kohonen
在我们的代码中,让我们创建一个新的类,它继承自 NeuralNet,因为它将是一种特定的神经网络类型。这个类将被命名为 Kohonen,它将使用 CompetitiveLayer 类作为输出层。以下类图显示了这些新类的排列方式:

本章涵盖了三种类型的 SOM:零维、一维和二维。这些配置在 enum MapDimension 中定义:
public enum MapDimension {ZERO,ONE_DIMENSION,TWO_DIMENSION};
Kohonen 构造函数定义了 Kohonen 神经网络的维度:
public Kohonen(int numberofinputs, int numberofoutputs, WeightInitialization _weightInitialization, int dim){
weightInitialization=_weightInitialization;
activeBias=false;
numberOfHiddenLayers=0; //no hidden layers
//…
numberOfInputs=numberofinputs;
numberOfOutputs=numberofoutputs;
input=new ArrayList<>(numberofinputs);
inputLayer=new InputLayer(this,numberofinputs);
// the competitive layer will be defined according to the dimension passed in the argument dim
outputLayer=new CompetitiveLayer(this,numberofoutputs, numberofinputs,dim);
inputLayer.setNextLayer(outputLayer);
setNeuralNetMode(NeuralNetMode.RUN);
deactivateBias();
}
零维 SOM
这是一个纯竞争层,其中神经元的顺序无关紧要。如邻域函数等特征不被考虑。在学习阶段,只有获胜神经元的权重受到影响。映射将仅由未连接的点组成。
以下代码片段定义了一个零维 SOM:
int numberOfInputs=2;
int numberOfNeurons=10;
Kohonen kn0 = new Kohonen(numberOfInputs,numberOfNeurons,new UniformInitialization(-1.0,1.0),0);
注意传递给参数 dim 的值 0(构造函数的最后一个参数)。
一维 SOM
这种架构与上一节中介绍的 竞争学习 网络类似,增加了输出神经元之间的邻域关系:

注意,输出层上的每个神经元都有一个或两个邻居。同样,触发最大值的神经元更新其权重,但在 SOM 中,邻域神经元也会以较小的速率更新其权重。
邻域效应将激活区域扩展到地图的更宽区域,前提是所有输出神经元都必须观察到一种组织,或者在一维情况下,观察到一条路径。邻域函数还允许更好地探索输入空间的特点,因为它迫使神经网络保持神经元之间的连接,因此除了形成的聚类之外,还会产生更多信息。
在输入数据点和神经权重的关系图中,我们可以看到由神经元形成的路径:

在这里展示的图表中,为了简单起见,我们只绘制了输出权重来展示地图是如何在一个(在这种情况下)2-D空间中设计的。经过多次迭代训练后,神经网络收敛到一个最终形状,代表所有数据点。有了这个结构,一组数据可能会使Kohonen网络在空间中设计出另一种形状。这是一个很好的降维例子,因为当多维数据集被展示给自组织映射时,能够产生一条单线(在1-D SOM中)来总结整个数据集。
要定义一维SOM,我们需要将值1作为参数dim传递:
Kohonen kn1 = new Kohonen(numberOfInputs,numberOfNeurons,new UniformInitialization(-1.0,1.0),1);
二维SOM
这是最常用的架构,以直观的方式展示Kohonen神经网络的强大功能。输出层是一个包含M x N神经元的矩阵,像网格一样相互连接:

在2-D SOM中,每个神经元现在最多有四个邻居(在正方形配置中),尽管在某些表示中,对角线神经元也可能被考虑,从而最多有八个邻居。六边形表示也很有用。让我们看看一个3x3 SOM图在2-D图表中的样子(考虑两个输入变量):

最初,未经训练的Kohonen网络显示出非常奇怪和扭曲的形状。权重的塑造将完全取决于将要馈送到SOM的输入数据。让我们看看地图开始组织自己的一个例子:
-
假设我们有一个如以下图表所示的密集数据集:
![二维SOM]()
-
应用SOM后,2-D形状逐渐变化,直到达到最终配置:
![二维SOM]()
2-D SOM的最终形状可能不总是完美的正方形;相反,它将类似于可以从数据集中绘制出的形状。邻域函数是学习过程中的一个重要组成部分,因为它近似了图中的邻近神经元,并将结构移动到一个更有组织的配置。
小贴士
图表上的网格只是更常用和更具教育意义的。还有其他方式来展示SOM图,比如U矩阵和聚类边界。
二维竞争层
为了更好地在网格形式中表示二维竞争层的神经元,我们正在创建 CompetitiveLayer2D 类,该类继承自 CompetitiveLayer。在这个类中,我们可以以 M x N 神经元网格的形式定义神经元的数量:
public class CompetitiveLayer2D extends CompetitiveLayer {
protected int sizeMapX; // neurons in dimension X
protected int sizeMapY; // neurons in dimension Y
protected int[] winner2DIndex;// position of the neuron in grid
public CompetitiveLayer2D(NeuralNet _neuralNet,int numberOfNeuronsX,int numberOfNeuronsY,int numberOfInputs){
super(_neuralNet,numberOfNeuronsX*numberOfNeuronsY,
numberOfInputs);
this.dimension=Kohonen.MapDimension.TWO_DIMENSION;
this.winnerIndex=new int[1];
this.winner2DIndex=new int[2];
this.coordNeuron=new int[numberOfNeuronsX*numberOfNeuronsY][2];
this.sizeMapX=numberOfNeuronsX;
this.sizeMapY=numberOfNeuronsY;
//each neuron is assigned a coordinate in the grid
for(int i=0;i<numberOfNeuronsY;i++){
for(int j=0;j<numberOfNeuronsX;j++){
coordNeuron[i*numberOfNeuronsX+j][0]=i;
coordNeuron[i*numberOfNeuronsX+j][1]=j;
}
}
}
2D 竞争层的坐标系类似于笛卡尔坐标系。每个神经元在网格中都有一个位置,索引从 0 开始:

在上面的插图上,12 个神经元被排列在一个 3 x 4 的网格中。在这个类中添加的另一个特性是按网格中的位置索引神经元。这允许我们获取神经元的子集(和权重),例如整个特定的行或列:
public double[] getNeuronWeights(int x, int y){
double[] nweights = neuron.get(x*sizeMapX+y).getWeights();
double[] result = new double[nweights.length-1];
for(int i=0;i<result.length;i++){
result[i]=nweights[i];
}
return result;
}
public double[][] getNeuronWeightsColumnGrid(int y){
double[][] result = new double[sizeMapY][numberOfInputs];
for(int i=0;i<sizeMapY;i++){
result[i]=getNeuronWeights(i,y);
}
return result;
}
public double[][] getNeuronWeightsRowGrid(int x){
double[][] result = new double[sizeMapX][numberOfInputs];
for(int i=0;i<sizeMapX;i++){
result[i]=getNeuronWeights(x,i);
}
return result;
}
SOM 学习算法
自组织图旨在通过聚类触发相同响应的输出数据点来对输入数据进行分类。最初,未训练的网络将产生随机输出,但随着更多示例的呈现,神经网络会识别哪些神经元被激活得更频繁,然后改变它们在 SOM 输出空间中的 位置。此算法基于竞争学习,这意味着获胜神经元(也称为最佳匹配单元或 BMU)将更新其权重及其邻居的权重。
下面的流程图说明了 SOM 网络的学习过程:

学习过程有点类似于第 2 章“让神经网络学习”和第 3 章“感知器和监督学习”中提到的算法。三个主要区别是 BMU 的确定基于距离、权重更新规则以及没有错误度量。距离意味着更近的点应该产生相似的输出,因此,确定最低 BMU 的标准是距离某些数据点较近的神经元。通常使用欧几里得距离,本书中我们将为了简单起见应用它:

权重到输入距离是通过 CompetitiveLayer 类的 getWeightDistance( ) 方法计算的,针对特定的神经元 i(参数神经元)。该方法已在上面描述。
邻近神经元的影响 – 邻域函数
权重更新规则使用一个邻域函数 Θ(u,v,s,t),该函数说明了邻居神经元 u(BMU 单元)与神经元 v 的接近程度。记住,在多维 SOM 中,BMU 神经元与其邻居神经元一起更新。这种更新还依赖于一个邻域半径,该半径考虑了 epoch 的数量 s 和参考 epoch t:

在这里,du,v 是网格中神经元 u 和 v 之间的神经元距离。半径的计算方法如下:

这里,是初始半径。epoch 数(s)和参考 epoch (t) 的影响是减小邻域半径,从而减小邻域的影响。这很有用,因为在训练的初期,权重需要更频繁地更新,因为它们通常随机初始化。随着训练过程的继续,更新需要变得较弱,否则神经网络将永远改变其权重,永远不会收敛。

邻域函数和神经元距离在 CompetitiveLayer 类中实现,CompetitiveLayer2D 类有重载版本:
| 竞争层 | 竞争层2D |
|---|
|
public double neighborhood(int u, int v, int s,int t){
double result;
switch(dimension){
case ZERO:
if(u==v) result=1.0;
else result=0.0;
break;
case ONE_DIMENSION:
default:
double exponent=-(neuronDistance(u,v)
/neighborhoodRadius(s,t));
result=Math.exp(exponent);
}
return result;
}
|
@Override
public double neighborhood(int u, int v, int s,int t){
double result;
double exponent=-(neuronDistance(u,v)
/neighborhoodRadius(s,t));
result=Math.exp(exponent);
return result;
}
|
|
public double neuronDistance(int u,int v){
return Math.abs(coordNeuron[u][0]-coordNeuron[v][0]);
}
|
@Override
public double neuronDistance(int u,int v){
double distance=
Math.pow(coordNeuron[u][0]
-coordNeuron[v][0],2);
distance+=
Math.pow(coordNeuron[u][1]-coordNeuron[v][1],2);
return Math.sqrt(distance);
}
|
邻域半径函数对两个类都是相同的:
public double neighborhoodRadius(int s,int t){
return this.initialRadius*Math.exp(-((double)s/(double)t));
}
学习率
随着训练的进行,学习率也会变得较弱:


参数是初始学习率。最后,考虑到邻域函数和学习率,权重更新规则如下:


在这里,X [k] 是第 k 个输入,而 W [kj] 是连接第 k 个输入到第 j 个输出的权重。
竞争学习的新类
现在我们有了竞争层、Kohonen 神经网络,并定义了邻域函数的方法,让我们创建一个新的竞争学习类。这个类将继承自 LearningAlgorithm,并将接收 Kohonen 对象进行学习:

如 第2章 所见,让神经网络学习 一个 LearningAlgorithm 对象接收一个用于训练的神经网络数据集。此属性由 CompetitiveLearning 对象继承,它实现了新的方法和属性以实现竞争学习过程:
public class CompetitiveLearning extends LearningAlgorithm {
// indicates the index of the current record of the training dataset
private int currentRecord=0;
//stores the new weights until they will be applied
private ArrayList<ArrayList<Double>> newWeights;
//saves the current weights for update
private ArrayList<ArrayList<Double>> currWeights;
// initial learning rate
private double initialLearningRate = 0.3;
//default reference epoch
private int referenceEpoch = 30;
//saves the index of winner neurons for each training record
private int[] indexWinnerNeuronTrain;
//…
}
与之前的算法不同,学习率现在会在训练过程中变化,并且将通过 getLearningRate( ) 方法返回:
public double getLearningRate(int epoch){
double exponent=(double)(epoch)/(double)(referenceEpoch);
return initialLearningRate*Math.exp(-exponent);
}
此方法用于 calcWeightUpdate( ):
@Override
public double calcNewWeight(int layer,int input,int neuron)
throws NeuralException{
//…
Double deltaWeight=getLearningRate(epoch);
double xi=neuralNet.getInput(input);
double wi=neuralNet.getOutputLayer().getWeight(input, neuron);
int wn = indexWinnerNeuronTrain[currentRecord];
CompetitiveLayer cl = ((CompetitiveLayer)(((Kohonen)(neuralNet))
.getOutputLayer()));
switch(learningMode){
case BATCH:
case ONLINE: //The same rule for batch and online modes
deltaWeight*=cl.neighborhood(wn, neuron, epoch, referenceEpoch) *(xi-wi);
break;
}
return deltaWeight;
}
train( ) 方法也针对竞争学习进行了调整:
@Override
public void train() throws NeuralException{
//…
epoch=0;
int k=0;
forward();
//…
currentRecord=0;
forward(currentRecord);
while(!stopCriteria()){
// first it calculates the new weights for each neuron and input
for(int j=0;j<neuralNet.getNumberOfOutputs();j++){
for(int i=0;i<neuralNet.getNumberOfInputs();i++){
double newWeight=newWeights.get(j).get(i);
newWeights.get(j).set(i,newWeight+calcNewWeight(0,i,j));
}
}
//the weights are promptly updated in the online mode
switch(learningMode){
case BATCH:
break;
case ONLINE:
default:
applyNewWeights();
}
currentRecord=++k;
if(k>=trainingDataSet.numberOfRecords){
//for the batch mode, the new weights are applied once an epoch
if(learningMode==LearningAlgorithm.LearningMode.BATCH){
applyNewWeights();
}
k=0;
currentRecord=0;
epoch++;
forward(k);
//…
}
}
}
方法 appliedNewWeights( ) 的实现与上一章中介绍的方法类似,只是没有偏差,只有一个输出层。
游戏时间:SOM应用实战。现在是时候动手实现Java中的Kohonen神经网络了。自组织映射有许多应用,其中大多数应用在聚类、数据抽象和降维领域。但聚类应用最有趣,因为它们有许多可能的应用。聚类的真正优势在于无需担心输入/输出关系,解决问题者可以专注于输入数据。一个聚类应用的例子将在第7章中探讨,即聚类客户档案。
可视化SOMs
在本节中,我们将介绍绘图功能。在Java中,可以通过使用免费提供的包JFreeChart(可以从http://www.jfree.org/jfreechart/下载)来绘制图表。此包附在本章的源代码中。因此,我们设计了一个名为Chart的类:
public class Chart {
//title of the chart
private String chartTitle;
//datasets to be rendered in the chart
private ArrayList<XYDataset> dataset = new ArrayList<XYDataset>();
//the chart object
private JFreeChart jfChart;
//colors of each dataseries
private ArrayList<Paint> seriesColor = new ArrayList<>();
//types of series (dots or lines for now)
public enum SeriesType {DOTS,LINES};
//collections of types for each series
public ArrayList<SeriesType> seriesTypes = new ArrayList<SeriesType>();
//…
}
本类实现的方法用于绘制线图和散点图。它们之间的主要区别在于线图在一个x轴(通常是时间轴)上绘制所有数据系列(每个数据系列是一条线);而散点图则在二维平面上显示点,表示其相对于每个轴的位置。下面的图表图形地显示了它们之间的区别以及生成它们的代码:

int numberOfPoints=10;
double[][] dataSet = {
{1.0, 1.0},{2.0,2.0}, {3.0,4.0}, {4+.0, 8.0},{5.0,16.0}, {6.0,32.0},
{7.0,64.0},{8.0,128.0}};
String[] seriesNames = {"Line Plot"};
Paint[] seriesColor = {Color.BLACK};
Chart chart = new Chart("Line Plot", dataSet, seriesNames, 0, seriesColor, Chart.SeriesType.LINE);
ChartFrame frame = new ChartFrame("Line Plot", chart.linePlot("X Axis", "Y Axis"));
frame.pack();
frame.setVisibile(true);

int numberOfInputs=2;
int numberOfPoints=100;
double[][] rndDataSet =
RandomNumberGenerator
.GenerateMatrixBetween
(numberOfPoints
, numberOfInputs, -10.0, 10.0);
String[] seriesNames = {"Scatter Plot"};
Paint[] seriesColor = {Color.WHITE};
Chart chart = new Chart("Scatter Plot", rndDataSet, seriesNames, 0, seriesColor, Chart.SeriesType.DOTS);
ChartFrame frame = new ChartFrame("Scatter Plot", chart.scatterPlot("X Axis", "Y Axis"));
frame.pack();
我们省略了图表生成代码(方法linePlot( )和scatterPlot( ));然而,在Chart.java文件中,读者可以找到它们的实现。
绘制二维训练数据集和神经元权重
现在我们有了绘制图表的方法,让我们绘制训练数据集和神经元权重。任何二维数据集都可以以与上一节图示相同的方式绘制。要绘制权重,我们需要使用以下代码获取Kohonen神经网络的权重:
CompetitiveLayer cl = ((CompetitiveLayer)(neuralNet.getOutputLayer()));
double[][] neuronsWeights = cl.getWeights();
在竞争学习中,我们可以通过视觉检查权重如何在数据集空间中移动。因此,我们将添加一个方法(showPlot2DData( ))来绘制数据集和权重,一个属性(plot2DData)来保存对ChartFrame的引用,以及一个标志(show2DData)来决定是否在每个epoch显示绘图:
protected ChartFrame plot2DData;
public boolean show2DData=false;
public void showPlot2DData(){
double[][] data= ArrayOperations. arrayListToDoubleMatrix( trainingDataSet.inputData.data);
String[] seriesNames = {"Training Data"};
Paint[] seriesColor = {Color.WHITE};
Chart chart = new Chart("Training epoch n°"+String.valueOf(epoch)+" ",data,seriesNames,0,seriesColor,Chart.SeriesType.DOTS);
if(plot2DData ==null){
plot2DData = new ChartFrame("Training",chart.scatterPlot("X","Y"));
}
Paint[] newColor={Color.BLUE};
String[] neuronsNames={""};
CompetitiveLayer cl = ((CompetitiveLayer)(neuralNet.getOutputLayer()));
double[][] neuronsWeights = cl.getWeights();
switch(cl.dimension){
case TWO_DIMENSION:
ArrayList<double[][]> gridWeights = ((CompetitiveLayer2D)(cl)). getGridWeights();
for(int i=0;i<gridWeights.size();i++){
chart.addSeries(gridWeights.get(i),neuronsNames, 0,newColor, Chart.SeriesType.LINES);
}
break;
case ONE_DIMENSION:
neuronsNames[0]="Neurons Weights";
chart.addSeries(neuronsWeights, neuronsNames, 0, newColor, Chart.SeriesType.LINES);
break;
case ZERO:
neuronsNames[0]="Neurons Weights";
default:
chart.addSeries(neuronsWeights, neuronsNames, 0,newColor, Chart.SeriesType.DOTS);
}
plot2DData.getChartPanel().setChart(chart.scatterPlot("X", "Y"));
}
此方法将在每个epoch结束时从train方法中调用。一个名为sleep的属性将决定图表显示多少毫秒,直到下一个epoch的图表替换它:
if(show2DData){
showPlot2DData();
if(sleep!=-1)
try{ Thread.sleep(sleep); }
catch(Exception e){}
}
测试Kohonen学习
现在让我们定义一个Kohonen网络并看看它是如何工作的。首先,我们创建一个零维度的Kohonen:
RandomNumberGenerator.seed=0;
int numberOfInputs=2;
int numberOfNeurons=10;
int numberOfPoints=100;
// create a random dataset between -10.0 and 10.0
double[][] rndDataSet = RandomNumberGenerator. GenerateMatrixBetween(numberOfPoints, numberOfInputs, -10.0, 10.0);
// create the Kohonen with uniform initialization of weights
Kohonen kn0 = new Kohonen(numberOfInputs,numberOfNeurons,new UniformInitialization(-1.0,1.0),0);
//add the dataset to the neural dataset
NeuralDataSet neuralDataSet = new NeuralDataSet(rndDataSet,2);
//create an instance of competitive learning in the online mode
CompetitiveLearning complrn=new CompetitiveLearning(kn0,neuralDataSet, LearningAlgorithm.LearningMode.ONLINE);
//sets the flag to show the plot
complrn.show2DData=true;
try{
// give names and colors for the dataset
String[] seriesNames = {"Training Data"};
Paint[] seriesColor = {Color.WHITE};
//this instance will create the plot with the random series
Chart chart = new Chart("Training",rndDataSet,seriesNames,0, seriesColor);
ChartFrame frame = new ChartFrame("Training", chart.scatterPlot("X", "Y"));
frame.pack();
frame.setVisible(true);
// we pass the reference of the frame to the complrn object
complrn.setPlot2DFrame(frame);
// show the first epoch
complrn.showPlot2DData();
//wait for the user to hit an enter
System.in.read();
//training begins, and for each epoch a new plot will be shown
complrn.train();
}
catch(Exception ne){
}
运行此代码后,我们得到第一个绘图:

随着训练的开始,权重开始分布在输入数据空间中,直到最终通过在输入数据空间中均匀分布而收敛:

对于一维,让我们尝试一些更有趣的东西。让我们创建一个基于余弦函数并带有随机噪声的数据集:
int numberOfPoints=1000;
int numberOfInputs=2;
int numberOfNeurons=20;
double[][] rndDataSet;
for (int i=0;i<numberOfPoints;i++){
rndDataSet[i][0]=i;
rndDataSet[i][0]+=RandomNumberGenerator.GenerateNext();
rndDataSet[i][1]=Math.cos(i/100.0)*1000;
rndDataSet[i][1]+=RandomNumberGenerator.GenerateNext()*400;
}
Kohonen kn1 = new Kohonen(numberOfInputs,numberOfNeurons,new UniformInitialization(0.0,1000.0),1);
通过运行相同的先前代码并将对象更改为kn1,我们得到一条连接所有权重点的线:

随着训练的继续,线条倾向于沿着数据波组织:

如果你想更改初始学习率、最大时期数和其他参数,请查看Kohonen1DTest.java文件。
最后,让我们看看二维的Kohonen图。代码将略有不同,因为现在,我们不是给出神经元的数量,而是要通知Kohonen构建者我们神经网络网格的维度。
这里使用的数据集将是一个带有随机噪声的圆:
int numberOfPoints=1000;
for (int i=0;i<numberOfPoints;i++){
rndDataSet[i][0]*=Math.sin(i);
rndDataSet[i][0]+=RandomNumberGenerator.GenerateNext()*50;
rndDataSet[i][1]*=Math.cos(i);
rndDataSet[i][1]+=RandomNumberGenerator.GenerateNext()*50;
}
现在我们来构建二维的Kohonen:
int numberOfInputs=2;
int neuronsGridX=12;
int neuronsGridY=12;
Kohonen kn2 = new Kohonen(numberOfInputs,neuronsGridX,neuronsGridY,new GaussianInitialization(500.0,20.0));
注意,我们使用的是均值为500.0和标准差为20.0的GaussianInitialization,这意味着权重将在位置(500.0,500.0)生成,而数据则围绕(50.0,50.0)中心:

现在我们来训练神经网络。在最初的几个时期,神经元权重迅速移动到圆圈中:

到了训练结束时,大多数权重将分布在圆周上,而在中心将有一个空隙,因为网格将被完全拉伸开:

摘要
在本章中,我们看到了如何在神经网络上应用无监督学习算法。我们介绍了一种新的、适合该目的的架构,即Kohonen的自组织图。无监督学习已被证明与监督学习方法一样强大,因为它们只关注输入数据,无需建立输入输出映射。我们通过图形展示了训练算法如何能够将权重驱动到输入数据附近,从而在聚类和降维中发挥作用。除了这些例子之外,Kohonen SOMs还能够对数据簇进行分类,因为每个神经元将针对特定的输入集提供更好的响应。
第五章:第5章:预测天气
本章介绍了一个在日常生活中神经网络可以完美应用的知名应用:天气预报。我们将详细介绍设计神经网络解决方案的整个过程:如何选择神经网络架构和神经元数量,以及选择和预处理数据。然后,读者将了解处理时间序列数据集的技术,我们的神经网络将使用Java编程语言对这些天气变量进行预测。本章涵盖的主题如下:
-
用于回归问题的神经网络
-
加载/选择数据
-
输入/输出变量
-
选择输入
-
预处理
-
归一化
-
神经网络的经验设计
用于回归问题的神经网络
到目前为止,读者已经接触到了许多神经网络实现和架构,现在是时候进入更复杂的情况了。神经网络在预测方面的力量确实令人惊叹,因为它们可以从历史数据中学习,使神经网络连接适应以产生相同的结果,根据某些输入数据。例如,对于给定的情况(原因),有一个结果(后果),这被表示为数据;神经网络能够学习将情况映射到后果(或原因映射到结果)的非线性函数。
预测和回归问题是应用神经网络的一个有趣类别。让我们看看一个包含天气数据的样本表格:
| 日期 | 平均温度 | 压力 | 湿度 | 降水量 | 风速 |
|---|---|---|---|---|---|
| 7月31日 | 23 ºC | 880 mbar | 66% | 16 mm | 5 m/s |
| 8月1日 | 22 ºC | 881 mbar | 78% | 3 mm | 3 m/s |
| 8月2日 | 25 ºC | 884 mbar | 65% | 0 mm | 4 m/s |
| 8月3日 | 27 ºC | 882 mbar | 53% | 0 mm | 3 m/s |
| … | |||||
| 12月11日 | 32 ºC | 890 mbar | 64% | 0 mm | 2 m/s |
上表描述了包含从假设城市收集的假设天气数据的五个变量,仅用于本例。现在假设每个变量都包含一个随时间顺序取值的值列表。我们可以将每个列表视为一个时间序列。在时间序列图表上,可以看到它们随时间的变化:

这些时间序列之间的关系表示了某个城市天气的动态表示,如上图所示。我们希望神经网络学习这种动态表示;然而,我们需要以神经网络可以处理的方式结构化这些数据,即识别哪些数据序列(变量)是原因,哪些是结果。动态系统具有其值依赖于过去值的变量,因此神经网络应用不仅依赖于当前情况,还可以依赖于过去。这一点非常重要,因为历史事件影响现在和未来。
只有在结构化数据之后,我们才能结构化神经网络,即输入、输出和隐藏节点的数量。然而,还有许多其他架构可能适合预测问题,例如径向基函数和反馈网络等。在本章中,我们处理的是具有反向传播学习算法的前馈多层感知器,以展示如何简单地利用这种架构来预测天气变量;此外,这种架构在选择了良好的数据时表现出非常好的泛化结果,并且在设计过程中涉及到的复杂性很小。
设计用于预测过程神经网络的总体过程如图所示:

如果神经网络未能通过验证(步骤5),通常定义一个新的结构(步骤3),尽管有时可能需要重复步骤1和步骤2。图中的每个步骤将在本章的下一节中详细说明。
加载/选择数据
首先,我们需要将原始数据加载到我们的Java环境中。数据可以存储在多种数据源中,从文本文件到结构化数据库系统。一种基本且简单类型是CSV(逗号分隔值),它简单且普遍使用。此外,我们还需要在将数据呈现给神经网络之前对其进行转换和选择。
构建辅助类
为了处理这些任务,我们需要在edu.packt.neuralnet.data包中的一些辅助类。第一个将是LoadCsv,用于读取CSV文件:
public class LoadCsv {
//Path and file name separated for compatibility
private String PATH;
private String FILE_NAME;
private double[][] dataMatrix;
private boolean columnsInFirstRow=false;
private String separator = ",";
private String fullFilePath;
private String[] columnNames;
final double missingValue=Double.NaN;
//Constructors
public LoadCsv(String path,String fileName)
//…
public LoadCsv(String fileName,boolean _columnsInFirstRow,String _separator)
//…
//Method to load data from file returning a matrix
public double[][] getDataMatrix(String fullPath,boolean _columnsInFirstRow,String _separator)
//…
//Static method for calls without instantiating LoadCsv object
public static double[][] getData(String fullPath,boolean _columnsInFirstRow,String _separator)
//…
Method for saving data into csv file
public void save()
//…
//…
}
小贴士
为了节省空间,我们这里不展示完整的代码。更多细节和完整的方法列表,请参阅附录C中的代码和文档。
我们还在创建一个类,用于将从CSV加载的原始数据存储到一个结构中,这个结构不仅包含数据,还包含关于这些数据的信息,例如列名。这个类将被称为DataSet,位于同一个包中:
public class DataSet {
//column names list
public ArrayList<String> columns;
//data matrix
public ArrayList<ArrayList<Double>> data;
public int numberOfColumns;
public int numberOfRecords;
//creating from Java matrix
public DataSet(double[][] _data,String[] _columns){
numberOfRecords=_data.length;
numberOfColumns=_data[0].length;
columns = new ArrayList<>();
for(int i=0;i<numberOfColumns;i++){
//…
columns.add(_columns[i]);
//…
}
data = new ArrayList<>();
for(int i=0;i<numberOfRecords;i++){
data.add(new ArrayList<Double>());
for(int j=0;j<numberOfColumns;j++){
data.get(i).add(_data[i][j]);
}
}
}
//creating from csv file
public DataSet(String filename,boolean columnsInFirstRow,String separator){
LoadCsv lcsv = new LoadCsv(filename,columnsInFirstRow,separator);
double[][] _data= lcsv.getDataMatrix(filename, columnsInFirstRow, separator);
numberOfRecords=_data.length;
numberOfColumns=_data[0].length;
columns = new ArrayList<>();
if(columnsInFirstRow){
String[] columnNames = lcsv.getColumnNames();
for(int i=0;i<numberOfColumns;i++){
columns.add(columnNames[i]);
}
}
else{ //default column names: Column0, Column1, etc.
for(int i=0;i<numberOfColumns;i++){
columns.add("Column"+String.valueOf(i));
}
}
data = new ArrayList<>();
for(int i=0;i<numberOfRecords;i++){
data.add(new ArrayList<Double>());
for(int j=0;j<numberOfColumns;j++){
data.get(i).add(_data[i][j]);
}
}
}
//…
//method for adding new column
public void addColumn(double[] _data,String name)
//…
//method for appending new data, number of columns must correspond
public void appendData(double[][] _data)
//…
//getting all data
public double[][] getData(){
return ArrayOperations.arrayListToDoubleMatrix(data);
}
//getting data from specific columns
public double[][] getData(int[] columns){
return ArrayOperations.getMultipleColumns(getData(), columns);
}
//getting data from one column
public double[] getData(int col){
return ArrayOperations.getColumn(getData(), col);
}
//method for saving the data in a csv file
public void save(String filename,String separator)
//…
}
在第4章中,我们已经在edu.packt.neuralnet.math包中创建了一个名为ArrayOperations的类来处理涉及数据数组操作。这个类有大量的静态方法,在这里描述所有这些方法是不切实际的;然而,相关信息可以在附录C中找到。
从CSV文件获取数据集
为了使任务更容易,我们在LoadCsv类中实现了一个静态方法来加载CSV文件并将其自动转换为DataSet对象的结构:
public static DataSet getDataSet(String fullPath,boolean _columnsInFirstRow, String _separator){
LoadCsv lcsv = new LoadCsv(fullPath,_columnsInFirstRow,_separator);
lcsv.columnsInFirstRow=_columnsInFirstRow;
lcsv.separator=_separator;
try{
lcsv.dataMatrix=lcsv.csvData2Matrix(fullPath);
System.out.println("File "+fullPath+" loaded!");
}
catch(IOException ioe){
System.err.println("Error while loading CSV file. Details: " + ioe.getMessage());
}
return new DataSet(lcsv.dataMatrix, lcsv.columnNames);
}
构建时间序列
时间序列结构对于所有涉及时间维度或域的问题都是必不可少的,例如预测和预测。一个名为TimeSeries的类实现了某些时间相关属性,如时间列和延迟。让我们看看这个类的结构:
public class TimeSeries extends DataSet {
//index of the column containing time information
private int indexTimeColumn;
public TimeSeries(double[][] _data,String[] _columns){
super(_data,_columns); //just a call to superclass constructor
}
public TimeSeries(String path, String filename){
super(path,filename);
}
public TimeSeries(DataSet ds){
super(ds.getData(),ds.getColumns());
}
public void setIndexColumn(int col){
this.indexTimeColumn=col;
this.sortBy(indexTimeColumn);
}
//…
}
在时间序列中,一个常见的操作是值的延迟移动。例如,我们希望在处理中包含的不是当前值而是前两天的日温度值。将温度视为包含时间列(日期)的时间序列,我们必须将值移动到所需的周期数(在这个例子中是一和二):

小贴士
我们已经使用Microsoft Excel®将datetime值转换为实数值。与处理日期或类别等结构相比,始终更喜欢处理数值。因此,在本章中,我们使用数值来表示日期。
在处理时间序列时,应注意以下两点:
-
在特定时间点可能存在缺失值或没有测量值;这可以在Java矩阵中生成NaN值。
-
将一列移动一个时间周期,例如,并不等同于获取上一行的值。这就是为什么选择一个列作为时间参考很重要。
在ArrayOperations类中,我们实现了一个名为shiftColumn的方法,该方法考虑时间列来移动矩阵的列。这个方法是从TimeSeries类中同名的方法中调用的,然后用于移动方法:
public double[] shiftColumn(int col,int shift){
double[][] _data = ArrayOperations.arrayListToDoubleMatrix(data);
return ArrayOperations.shiftColumn(_data, indexTimeColumn, shift, col);
}
public void shift(int col,int shift){
String colName = columns.get(col);
if(shift>0) colName=colName+"_"+String.valueOf(shift);
else colName=colName+"__"+String.valueOf(-shift);
addColumn(shiftColumn(col,shift),colName);
}
删除NaN值
NaN值是在加载数据或转换数据后经常出现的不希望有的值。它们是不希望有的,因为我们无法对它们进行操作。如果我们向神经网络输入一个NaN值,输出肯定会是NaN,这只会消耗更多的计算能力。这就是为什么最好将它们删除。在DataSet类中,我们实现了两种删除NaN的方法:一种是用一个值替换它们,另一种是如果整行至少有一个缺失值,就删除整行,如图所示:

// dropping with a substituting value
public void dropNaN(double substvalue)
//…
// dropping the entire row
public void dropNaN()
//…
获取天气数据
现在我们有了获取数据的工具,让我们在网上寻找一些数据集。在本章中,我们将使用巴西气象研究所(INMET:http://www.inmet.gov.br/,葡萄牙语)的数据,这些数据在互联网上免费提供;我们有权在本书中使用它。然而,读者在开发应用程序时可以使用互联网上的任何免费天气数据库。
以下是从英语语言来源中列出的一些示例:
-
Wunderground (http://wunderground.com/)
-
Open Weather Map (http://openweathermap.org/api)
-
Yahoo 天气 API (https://developer.yahoo.com/weather/)
-
美国国家气候数据中心 (http://www.ncdc.noaa.gov/)
天气变量
任何天气数据库几乎都会有相同的变量:
-
温度 (ºC)
-
湿度 (%)
-
压力 (mbar)
-
风速 (m/s)
-
风向 (º )
-
降水量 (mm)
-
日照小时数 (h)
-
太阳能量 (W/m2)
这些数据通常是从气象站、卫星或雷达每小时或每天收集的。
小贴士
根据收集频率,某些变量可能通过平均值、最小值或最大值进行汇总。
数据单位可能因来源而异;这就是为什么始终应该观察单位。
选择输入和输出变量
选择满足系统动力学大部分需求的数据需要谨慎进行。我们希望神经网络能够根据当前和过去的天气数据预测未来天气,但我们应该选择哪些变量呢?对此问题获得专家意见可以非常有帮助,以理解变量之间的关系。
小贴士
关于时间序列变量,可以通过应用历史数据来导出新的变量。这意味着,给定一个特定的日期,可以考虑该日期的值以及从过去日期收集(和/或汇总)的数据,从而增加变量的数量。
在定义使用神经网络解决的问题时,通常有一个或多个预定义的目标变量:预测温度、预报降水、测量日照等。但是,在某些情况下,人们希望模拟所有变量,因此需要找到它们之间的因果关系。因果关系可以通过统计工具识别,其中皮尔逊交叉相关是最常用的:

在这里,E[X.Y] 表示变量 X 和 Y 相乘的平均值;E[X] 和 E[Y] 分别是 X 和 Y 的平均值;σX 和 σY 分别是 X 和 Y 的标准差;最后 Cx,y 是与 Y 相关的 X 的皮尔逊相关系数,其值介于 -1 和 1 之间。这个系数显示了变量 X 与变量 Y 之间的相关程度。接近 0 的值表示弱相关或没有相关性,而接近 -1 或 1 的值分别表示负相关或正相关。从图形上看,可以通过散点图来观察,如下所示:

在左边的图表中,最后一天的降水量(标记为 [-1])与最高温度的相关性为 -0.202,这是一个负相关度较弱的值。在中间的图表中,日照与最高温度的相关性为 0.376,这是一个相当的相关性,但并不非常显著;可以看到一个轻微的正趋势。右边的图表显示了强正相关的例子,这是最后一天的最高温度与当天的最高温度之间的相关性。这个相关性为 0.793,我们可以看到表示趋势的点云较薄。
我们将使用相关性来选择神经网络最合适的输入。首先,我们需要在 DataSet 类中编写一个名为 correlation 的方法。请注意,均值和标准差等操作是在我们的 ArrayOperations 类中实现的:
public double correlation(int colx,int coly){
double[] arrx = ArrayOperations.getColumn(data,colx);
double[] arry = ArrayOperations.getColumn(data,coly);
double[] arrxy = ArrayOperations.elementWiseProduct(arrx, arry);
double meanxy = ArrayOperations.mean(arrxy);
double meanx = ArrayOperations.mean(arrx);
double meany = ArrayOperations.mean(arry);
double stdx = ArrayOperations.stdev(arrx);
double stdy = ArrayOperations.stdev(arry);
return (meanxy*meanx*meany)/(stdx*stdy);
}
在这本书中,我们不会深入探讨统计学,因此如果读者对这一主题的更多细节感兴趣,我们推荐一些参考文献。
预处理
从数据源收集的原始数据通常具有不同的特性,如数据范围、采样和类别。一些变量是测量结果,而另一些则是汇总或甚至计算得出的。预处理意味着将这些变量的值调整到神经网络可以适当处理的范围。
关于天气变量,让我们看看它们的范围、采样和类型:
| 变量 | 单位 | 范围 | 采样 | 类型 |
|---|---|---|---|---|
| 平均温度 | º C | 10.86 – 29.25 | 每小时 | 每小时测量值的平均值 |
| 降水量 | mm | 0 – 161.20 | 每日 | 每日降雨量的累积 |
| 日照 | 小时 | 0 – 10.40 | 每日 | 接收太阳辐射的小时数计数 |
| 平均湿度 | % | 45.00 – 96.00 | 每小时 | 每小时测量值的平均值 |
| 平均风速 | km/h | 0.00 – 3.27 | 每小时 | 每小时测量值的平均值 |
除了日照和降水之外,变量都是测量并共享相同的采样,但如果我们想使用,例如,每小时的数据集,我们就必须预处理所有变量以使用相同的采样率。其中三个变量使用每日平均值进行了汇总,但如果我们想的话,我们也可以使用小时数据测量。然而,范围肯定更大。
规范化
规范化是将所有变量转换到相同数据范围的过程,通常范围较小,在0到1或-1到1之间。这有助于神经网络在激活函数(如sigmoid或双曲正切)中呈现变量区内的值:

过高或过低的值可能会使神经元产生过高或过低的激活函数值,从而导致这些神经元的导数太小,接近零。在这本书中,我们实现了两种规范化模式:最小-最大和z分数。
最小-最大规范化应考虑数据集预定义的范围。它立即执行:

在这里,Nmin和Nmax分别是归一化的最小和最大限制,Xmin和Xmax分别是变量X的最小和最大限制,X是原始值,Xnorm是归一化值。如果我们想将规范化限制在0到1之间,例如,方程可以简化为以下形式:

通过应用规范化,生成一个新的规范化数据集,并将其输入到神经网络中。还应考虑到,用规范化值输入的神经网络将被训练以在输出上产生规范化值,因此逆(逆规范化)过程也变得必要:

或者

对于0到1之间的规范化。
另一种规范化模式是z分数,它考虑了均值和标准差:

在这里,S是一个缩放常数,E[X]是E的均值,σX是X的标准差。这种规范化模式的主要区别是,变量范围将没有定义的限制;然而,变量将具有与缩放常数S相等的标准差,并围绕零中心化的相同范围的值。
下图显示了两种规范化模式对数据做了什么:

实现了一个名为DataNormalization的类来处理数据的规范化。由于规范化考虑了数据的统计特性,我们需要在DataNormalization对象中存储这些统计信息:
public class DataNormalization {
//ENUM normalization types
public enum NormalizationTypes { MIN_MAX, ZSCORE }
// normalization type
public NormalizationTypes TYPE;
//statistical properties of the data
private double[] minValues;
private double[] maxValues;
private double[] meanValues;
private double[] stdValues;
//normalization properties
private double scaleNorm=1.0;
private double minNorm=-1.0;
//…
//constructor for min-max norm
public DataNormalization(double[][] data,double _minNorm, double _maxNorm){
this.TYPE=NormalizationTypes.MIN_MAX;
this.minNorm=_minNorm;
this.scaleNorm=_maxNorm-_minNorm;
calculateReference(data);
}
//constructor for z-score norm
public DataNormalization(double[][] data,double _zscale){
this.TYPE=NormalizationTypes.ZSCORE;
this.scaleNorm=_zscale;
calculateReference(data);
}
//calculation of statistical properties
private void calculateReference(double[][] data){
minValues=ArrayOperations.min(data);
maxValues=ArrayOperations.max(data);
meanValues=ArrayOperations.mean(data);
stdValues=ArrayOperations.stdev(data);
}
//…
}
规范化过程是在一个名为normalize的方法上执行的,它有一个逆规范化对应的方法称为denormalize:
public double[][] normalize( double[][] data ) {
int rows = data.length;
int cols = data[0].length;
//…
double[][] normalizedData = new double[rows][cols];
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
switch (TYPE){
case MIN_MAX:
normalizedData[i][j]=(minNorm) + ((data[i][j] - minValues[j]) / ( maxValues[j] - minValues[j] )) * (scaleNorm);
break;
case ZSCORE:
normalizedData[i][j]=scaleNorm * (data[i][j] - meanValues[j]) / stdValues[j];
break;
}
}
}
return normalizedData;
}
将NeuralDataSet调整为处理规范化
已经实现的NeuralDataSet、NeuralInputData和NeuralOutputData现在将具有DataNormalization对象来处理归一化操作。在NeuralDataSet类中,我们已添加了输入和输出数据归一化的对象:
public DataNormalization inputNorm;
public DataNormalization outputNorm;
//zscore normalization
public void setNormalization(double _scaleNorm){
inputNorm = new DataNormalization(_scaleNorm);
inputData.setNormalization(inputNorm);
outputNorm = new DataNormalization(_scaleNorm);
outputData.setNormalization(outputNorm);
}
//min-max normalization
public void setNormalization(double _minNorm,double _maxNorm){
inputNorm = new DataNormalization(_minNorm,_maxNorm);
inputData.setNormalization(inputNorm);
outputNorm = new DataNormalization(_minNorm,_maxNorm);
outputData.setNormalization(outputNorm);
}
NeuralInputData和NeuralOutputData现在将具有normdata属性来存储归一化数据。从这些类检索数据的方法将有一个布尔参数isNorm,以指示要检索的值是否应该归一化。
考虑到NeuralInputData将为神经网络提供输入数据,这个类将仅在将数据馈送到神经网络之前执行归一化。为此,该类实现了setNormalization方法:
public ArrayList<ArrayList<Double>> normdata;
public DataNormalization norm;
public void setNormalization(DataNormalization dn){
//getting the original data into java matrix
double[][] origData = ArrayOperations.arrayListToDoubleMatrix(data);
//perform normalization
double[][] normData = dn.normalize(origData);
normdata=new ArrayList<>();
//store the normalized values into ArrayList normdata
for(int i=0;i<normData.length;i++){
normdata.add(new ArrayList<Double>());
for(int j=0;j<normData[0].length;j++){
normdata.get(i).add(normData[i][j]);
}
}
}
在NeuralOutputData中,有两个数据集,一个用于目标,一个用于神经网络输出。目标数据集被归一化,以向训练算法提供归一化值。然而,神经网络输出数据集是神经网络的输出,即它将首先被归一化。在设置神经网络输出数据集后,我们需要执行去归一化:
public ArrayList<ArrayList<Double>> normTargetData;
public ArrayList<ArrayList<Double>> normNeuralData;
public void setNeuralData(double[][] _data,boolean isNorm){
if(isNorm){ //if is normalized
this.normNeuralData=new ArrayList<>();
for(int i=0;i<numberOfRecords;i++){
this.normNeuralData.add(new ArrayList<Double>());
//… save in the normNeuralData
for(int j=0;j<numberOfOutputs;j++){
this.normNeuralData.get(i).add(_data[i][j]);
}
}
double[][] deNorm = norm.denormalize(_data);
for(int i=0;i<numberOfRecords;i++)
for(int j=0;j<numberOfOutputs;j++) //then in neuralData
this.neuralData.get(i).set(j,deNorm[i][j]);
}
else setNeuralData(_data);
}
将学习算法适应归一化
最后,LearningAlgorithm类需要包括归一化属性:
protected boolean normalization=false;
在训练期间,每次调用NeuralDataSet方法检索或写入数据时,应在参数isNorm中传递归一化属性,就像在Backpropagation类的方法forward中一样:
@Override
public void forward(){
for(int i=0;i<trainingDataSet.numberOfRecords;i++){
neuralNet.setInputs(trainingDataSet.
getInputRecord(i,normalization));
neuralNet.calc();
trainingDataSet.setNeuralOutput(i, neuralNet.getOutputs(), normalization);
//…
}
}
天气预报的Java实现
在Java中,我们将使用包edu.packt.neuralnet.chart来绘制一些图表并可视化数据。我们还从巴西气象研究所INMET下载了历史气象数据。我们已从几个城市下载了数据,因此我们的天气预报案例中可以包含各种气候。
提示
为了快速运行训练,我们选择了一个较短的时间段(5年),它包含超过2000个样本。
收集天气数据
在这个例子中,我们想要从不同地方收集各种数据,以证明神经网络预测其能力。由于我们从仅覆盖巴西领土的INMET网站下载了它,因此只覆盖了巴西城市。然而,这是一个非常广阔的领土,气候种类繁多。以下是收集数据的地方列表:
| # | City Name | Latitude | Longitude | Altitude | Climate Type |
|---|---|---|---|---|---|
| 1 | Cruzeiro do Sul | 7º37'S | 72º40'W | 170 m | 热带雨林 |
| 2 | Picos | 7º04'S | 41º28'W | 208 m | 半干旱 |
| 3 | Campos do Jordão | 22º45'S | 45º36'W | 1642 m | 亚热带高山 |
| 4 | Porto Alegre | 30º01'S | 51º13'W | 48 m | 亚热带湿润 |
这些四个城市的地理位置在下面的地图上标出:

来源:维基百科,用户NordNordWest使用美国国家影像和测绘局数据,世界数据库II数据
收集的天气数据是从2010年1月到2016年11月,并保存在数据文件夹中,文件名与城市对应。
从INMET网站收集的数据包括这些变量:
-
降水量(mm)
-
最大温度(ºC)
-
最小温度(ºC)
-
日照时数(晴朗小时数)
-
蒸发量(mm)
-
平均温度(ºC)
-
平均湿度(%)
-
平均风速(mph)
-
日期(转换为Excel数字格式)
-
电站位置(纬度、经度和海拔)
对于每个城市,我们打算基于过去的数据构建一个神经网络来预测天气。但首先,我们需要指出两个重要的事实:
-
位于高纬度的城市由于季节变化而经历高天气变化;也就是说,天气将取决于日期
-
天气是一个非常动态的系统,其变量受过去值的影响
为了克服第一个问题,我们可以从日期派生一个新列来指示太阳中午角,这是太阳光线在最高点(中午)达到城市表面的角度。这个角度越大,太阳辐射就越强烈、越温暖;另一方面,当这个角度较小时,表面将接收到较少的太阳辐射:

太阳中午角是通过以下公式和WeatherExample类中的Java实现计算的,该类将在本章中使用:

public double calcSolarNoonAngle(double date,double latitude){
return 90-Math.abs(-23.44*Math.cos((2*Math.PI/365.25)*(date+8.5))-latitude);
}
public void addSolarNoonAngle(TimeSeries ts,double latitude){// to add column
double[] sna = new double[ts.numberOfRecords];
for(int i=0;i<ts.numberOfRecords;i++)
sna[i]=calcSolarNoonAngle(
ts.data.get(i).get(ts.getIndexColumn()), latitude);
ts.addColumn(sna, "NoonAngle");
}
延迟变量
在WeatherExample类中,让我们放置一个名为makeDelays的方法,稍后它将从主方法中调用。延迟将在给定的TimeSeries上执行,并针对时间序列的所有列(除了索引列)达到给定的数量:
public void makeDelays(TimeSeries ts,int maxdelays){
for(int i=0;i<ts.numberOfColumns;i++)
if(i!=ts.getIndexColumn())
for(int j=1;j<=maxdelays;j++)
ts.shift(i, -j);
}
提示
注意不要多次调用此方法;它可能会反复延迟同一列。
加载数据并开始播放!
在WeatherExample类中,我们将为每种情况添加四个TimeSeries属性和四个NeuralNet属性:
public class WeatherExample {
TimeSeries cruzeirodosul;
TimeSeries picos;
TimeSeries camposdojordao;
TimeSeries portoalegre;
NeuralNet nncruzeirosul;
NeuralNet nnpicos;
NeuralNet nncamposjordao;
NeuralNet nnportoalegre;
//…
}
在main方法中,我们将数据加载到每个对象中,并在三天前延迟列:
public static void main(String[] args) {
WeatherExample we = new WeatherExample();
//load weather data
we.cruzeirodosul = new TimeSeries(LoadCsv.getDataSet("data", "cruzeirodosul2010daily.txt", true, ";"));
we.cruzeirodosul.setIndexColumn(0);
we.makeDelays(we.cruzeirodosul, 3);
we.picos = new TimeSeries(LoadCsv.getDataSet("data", "picos2010daily.txt", true, ";"));
we.picos.setIndexColumn(0);
we.makeDelays(we.picos, 3);
we.camposdojordao = new TimeSeries(LoadCsv.getDataSet("data", "camposdojordao2010daily.txt", true, ";"));
we.camposdojordao.setIndexColumn(0);
we.makeDelays(we.camposdojordao, 3);
we.portoalegre = new TimeSeries(LoadCsv.getDataSet("data", "portoalegre2010daily.txt", true, ";"));
we.portoalegre.setIndexColumn(0);
we.makeDelays(we.portoalegre, 3);
//…
提示
由于每个文件可能有超过2,000行,这段代码可能需要几分钟才能执行。
加载后,我们需要删除NaN值,因此我们调用每个时间序列对象的dropNaN方法:
//…
we.cruzeirodosul.dropNaN();
we.camposdojordao.dropNaN();
we.picos.dropNaN();
we.portoalegre.dropNaN();
//…
为了节省时间和精力,以便未来的执行,让我们保存这些数据集:
we.cruzeirodosul.save("data","cruzeirodosul2010daily_delays_clean.txt",";");
//…
we.portoalegre.save("data","portoalegre2010daily_delays_clean.txt",";");
现在,对于所有时间序列,每一列有三个延迟,我们希望神经网络预测下一天的最高和最低温度。我们可以通过只考虑现在和过去来预测未来,因此对于输入,我们必须依赖于延迟数据(从-1天到-3天前的数据),而对于输出,我们可以考虑当前的温度值。时间序列数据集中的每一列都由一个索引表示,其中零是日期的索引。由于某些数据集在某些列上存在缺失数据,因此列的索引可能不同。然而,输出变量的索引在所有数据集中都是相同的(索引2和3)。
让我们进行相关性分析
我们对在延迟数据和当前最高和最低温度之间找到模式感兴趣。因此,我们执行一个结合所有输出和潜在输入变量的交叉相关性分析,并选择至少达到最小绝对相关性的变量作为阈值。因此,我们编写了一个名为correlationAnalysis的方法,将最小绝对相关性作为参数。为了节省空间,我们在这里省略了代码:
public void correlationAnalysis(double minAbsCorr){
//indexes of output variables (max. and min. temperature)
int[][] outputs = {
{2,3}, //cruzeiro do sul
{2,3}, //picos
{2,3}, //campos do jordao
{2,3}}; //porto alegre
int[][] potentialInputs = { //indexes of input variables (delayed)
{10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,38,39,40}, //cruzeiro do sul
//… and all others
};
ArrayList<ArrayList<ArrayList<Double>>> chosenInputs = new ArrayList<>();
TimeSeries[] tscollect = {this.cruzeirodosul,this.picos,this.camposdojordao,this.portoalegre};
double[][][] correlation = new double[4][][];
for(int i=0;i<4;i++){
chosenInputs.add(new ArrayList<ArrayList<Double>>());
correlation[i]=new double[outputs[i].length][potentialInputs[i].length];
for(int j=0;j<outputs[i].length;j++){
chosenInputs.get(i).add(new ArrayList<Double>());
for(int k=0;k<potentialInputs[i].length;k++){
correlation[i][j][k]=tscollect[i].correlation(outputs[i][j], potentialInputs[i][k]);
//if the absolute correlation is above the threshold
if(Math.abs(correlation[i][j][k])>minAbsCorr){
//it is added to the chosen inputs
chosenInputs.get(i).get(j).add(correlation[i][j][k]);
//and we see the plot
tscollect[i].getScatterChart("Correlation "+String.valueOf(correlation[i][j][k]), outputs[i][j], potentialInputs[i][k], Color.BLACK).setVisible(true);
}
}
}
}
}
通过运行此分析,我们得到了以下结果(粗体列被选为神经网络输入):
| 克鲁塞罗多斯尔时间序列的相关性分析 | |
|---|---|
| 与输出变量的相关性:MaxTempNoonAngle:0.0312808Precipitation__1:-0.115547Precipitation__2:-0.038969Precipitation__3:-0.062173MaxTemp__1:0.497057MaxTemp__2:0.252831MaxTemp__3:0.159098MinTemp__1:-0.033339MinTemp__2:-0.123063MinTemp__3:-0.125282Insolation__1:0.395741Insolation__2:0.197949Insolation__3:0.134345Evaporation__1:0.21548Evaporation__2:0.161384Evaporation__3:0.199385AvgTemp__1:0.432280AvgTemp__2:0.152103AvgTemp__3:0.060368AvgHumidity__1:-0.415812AvgHumidity__2:-0.265189AvgHumidity__3:-0.214624WindSpeed__1:-0.166418WindSpeed__2:-0.056825WindSpeed__3:-0.001660NoonAngle__1:0.0284473NoonAngle__2:0.0256710NoonAngle__3:0.0227864 | 与输出变量的相关性:MinTempNoonAngle:0.346545Precipitation__1:0.012696Precipitation__2:0.063303Precipitation__3:0.112842MaxTemp__1:0.311005MaxTemp__2:0.244364MaxTemp__3:0.123838MinTemp__1:0.757647MinTemp__2:0.567563MinTemp__3:0.429669Insolation__1:-0.10192Insolation__2:-0.101146Insolation__3:-0.151896Evaporation__1:-0.115236Evaporation__2:-0.160718Evaporation__3:-0.160536AvgTemp__1:0.633741AvgTemp__2:0.487609AvgTemp__3:0.312645AvgHumidity__1:0.151009AvgHumidity__2:0.155019AvgHumidity__3:0.177833WindSpeed__1:-0.198555WindSpeed__2:-0.227227WindSpeed__3:-0.185377NoonAngle__1:0.353834NoonAngle__2:0.360943NoonAngle__3:0.367953 |
散点图显示了这些数据之间的关系:

在左侧,前一天的最高温度与当前温度之间存在相当的相关性;在中间,前一天最低温度与当前温度之间存在强烈的相关性;在右侧,三天前的NoonAngle与当前最低温度之间存在较弱的相关性。通过为其他所有城市运行此分析,我们确定其他神经网络的输入:
| 克鲁泽伊罗·多·苏尔 | 皮科斯 | 卡姆波斯多·若达奥 | 波尔图·阿雷格里 | |
| --- | --- | --- | --- |
| 中午角度最大温度__1分钟温度__1分钟温度__2分钟温度__3日照__1平均温度__1平均温度__2平均湿度__1中午角度__1中午角度__2中午角度__3 | 最大温度最大温度__1最大温度__2最大温度__3分钟温度__1分钟温度__2分钟温度__3日照__1日照__2蒸发__1蒸发__2蒸发__3平均温度__1平均温度__2平均温度__3平均湿度__1平均湿度__2平均湿度__3 | 中午角度最大温度__1最大温度__2最大温度__3分钟温度__1分钟温度__2分钟温度__3蒸发__1平均温度__1平均温度__2平均温度__3平均湿度__1中午角度__1中午角度__2中午角度__3 | 最大温度中午角度最大温度__1最大温度__2最大温度__3分钟温度__1分钟温度__2分钟温度__3日照__1日照__2日照__3蒸发__1蒸发__2蒸发__3平均温度__1平均温度__2平均温度__3平均湿度__1平均湿度__2中午角度__1中午角度__2中午角度__3 |
创建神经网络
我们正在使用四个神经网络来预测最低和最高温度。最初,它们将有两个隐藏层,每个层有20和10个神经元,并使用双曲正切和sigmoid激活函数。我们将应用最小-最大归一化。WeatherExample类中的以下方法创建具有此配置的神经网络:
public void createNNs(){
//fill a vector with the indexes of input and output columns
int[] inputColumnsCS = {10,14,17,18,19,20,26,27,29,38,39,40};
int[] outputColumnsCS = {2,3};
//this static method hashes the dataset
NeuralDataSet[] nnttCS = NeuralDataSet.randomSeparateTrainTest(this.cruzeirodosul, inputColumnsCS, outputColumnsCS, 0.7);
//setting normalization
DataNormalization.setNormalization(nnttCS, -1.0, 1.0);
this.trainDataCS = nnttCS[0]; // 70% for training
this.testDataCS = nnttCS[1]; // rest for test
//setup neural net parameters:
this.nncruzeirosul = new NeuralNet( inputColumnsCS.length, outputColumnsCS.length, new int[]{20,10}
, new IActivationFunction[] {new HyperTan(1.0),new Sigmoid(1.0)}
, new Linear()
, new UniformInitialization(-1.0, 1.0) );
//…
}
训练和测试
在第二章让神经网络学习中,我们了解到神经网络应该经过测试来验证其学习效果,因此我们将数据集划分为训练集和测试集。通常,大约50-80%的原始过滤数据集用于训练,剩余的部分用于测试。
NeuralDataSet类中的静态方法randomSeparateTrainTest将数据集划分为这两个子集。为了确保最大泛化,该数据集的记录被哈希处理,如图所示:

记录可能最初是顺序的,如天气时间序列;如果我们随机位置哈希它们,训练集和测试集将包含所有时期的记录。
训练神经网络
神经网络将使用基本的反向传播算法进行训练。以下是对Cruzeiro do Sul数据集的代码示例:
Backpropagation bpCS = new Backpropagation(we.nncruzeirosul
,we.trainDataCS
,LearningAlgorithm.LearningMode.BATCH);
bpCS.setTestingDataSet(we.testDataCS);
bpCS.setLearningRate(0.3);
bpCS.setMaxEpochs(1000);
bpCS.setMinOverallError(0.01); //normalized error
bpCS.printTraining = true;
bpCS.setMomentumRate( 0.3 );
try{
bpCS.forward();
bpCS.train();
System.out.println("Overall Error:" + String.valueOf(bpCS.getOverallGeneralError()));
System.out.println("Testing Error:" + String.valueOf(bpCS.getTestingOverallGeneralError()));
System.out.println("Min Overall Error:" + String.valueOf(bpCS.getMinOverallError()));
System.out.println("Epochs of training:" + String.valueOf(bpCS.getEpoch()));
}
catch(NeuralException ne){ }
绘制错误图
使用JFreeCharts框架,我们可以绘制训练集和测试集的错误演变图。在LearningAlgorithm类中有一个名为showErrorEvolution的新方法,该方法由BackPropagation继承并覆盖。要查看图表,可以像以下示例中那样调用:
//plot list of errors by epoch
bpCS.showErrorEvolution();
这将显示如下图中所示的图表:

查看神经网络输出
使用此相同设施,很容易看到并比较神经网络输出。首先,让我们将神经网络输出转换为向量形式,并使用 addColumn 方法将其添加到我们的数据集中。让我们将其命名为 NeuralMinTemp 和 NeuralMaxTemp:
String[] neuralOutputs = { "NeuralMaxTemp", "NeuralMinTemp"};
we.cruzeirodosul.addColumn(we.fullDataCS.getIthNeuralOutput(0), neuralOutputs[0]);
we.cruzeirodosul.addColumn(we.fullDataCS.getIthNeuralOutput(1), neuralOutputs[1]);
String[] comparison = {"MaxTemp","NeuralMaxTemp"};
Paint[] comp_color = {Color.BLUE, Color.RED};
final double minDate = 41200.0;
final double maxDate = 41300.0;
类 TimeSeries 有一个名为 getTimePlot 的方法,用于在指定范围内绘制变量:
ChartFrame viewChart = we.cruzeirodosul.getTimePlot("Comparison", comparison, comp_color, minDate, maxDate);

神经网络的经验设计
在使用神经网络进行回归问题(包括预测)时,没有固定的隐藏神经元数量,因此通常求解器会选择一个任意数量的神经元,然后根据创建的网络的输出结果进行变化。此过程可能需要重复多次,直到找到一个令人满意的准则的网络。
设计实验
可以在相同的训练和测试数据集上进行实验,同时改变其他网络参数,如学习率、归一化和隐藏单元的数量。目标是选择在实验中表现最佳的神经网络。最佳性能分配给具有较低MSE错误的网络,但使用测试数据进行泛化分析也很有用。
小贴士
在设计实验时,始终从较少的隐藏神经元数量开始,因为希望有较低的计算处理消耗。
下表显示了为所有城市运行的所有实验:

结果和模拟
为了便于执行实验,我们设计了一个Java Swing 图形用户界面(GUI),通过它可以选择用于训练的神经网络参数和数据集。
小贴士
此接口仅涵盖只有一个隐藏层的神经网络;然而,由于代码是开放的,建议将具有多个隐藏层的多层感知器实现作为练习,以及选择其他训练算法。
图表仅显示预测的最大温度;因此,建议实现一个显示最低温度的选项。
在选择参数后,点击 开始训练 按钮开始训练:

在运行了12次实验后,我们找到了每个数据集的以下MSE训练误差:
| 实验 | 克鲁泽伊罗·多·苏尔 | 皮科斯 | 贾尔多安 | 波尔图·阿雷格里 |
|---|---|---|---|---|
| #1 | 0.130156 | 0.147111 | 0.300437 | 0.323342 |
| #2 | 0.512389 | 0.572588 | 0.428692 | 0.478379 |
| #3 | 0.08659 | 0.094822 | 0.124752 | 0.114486 |
| #4 | 0.360728 | 0.258596 | 0.168351 | 0.192012 |
| #5 | 0.076476 | 0.074777 | 0.108991 | 0.085029 |
| #6 | 0.328493 | 0.186793 | 0.152499 | 0.151248 |
| #7 | 0.146801 | 0.130004 | 0.277765 | 0.19076 |
| #8 | 0.431811 | 0.29629 | 0.364418 | 0.278864 |
| #9 | 0.071135 | 0.081159 | 0.117634 | 0.091174 |
| #10 | 0.332534 | 0.210107 | 0.170179 | 0.164179 |
| #11 | 0.07247 | 0.089069 | 0.102137 | 0.076578 |
| #12 | 0.33342 | 0.19835 | 0.155036 | 0.145843 |
MSE误差信息仅给我们一个大致的想法,即神经网络输出在整体背景下与真实数据匹配的程度。这种性能可以通过查看时间序列比较和散点图来验证:

这些图表显示,尽管在许多情况下温度无法准确预测,但总体上正遵循一个趋势。这一点可以通过散点图中可见的相关性得到证实。表中最后一行显示了对于具有亚热带气候和高温变化的Porto Alegre的预测,即使在极端温度变化的情况下,也显示出良好的预测效果。然而,我们提醒读者,预测天气需要考虑许多额外的变量,这些变量由于可用性限制而无法包含在本例中。无论如何,结果表明我们在寻找一个能够超越这些发现的神经网络配置方面已经迈出了良好的开端。
摘要
在本章中,我们看到了神经网络应用的一个有趣的实际案例。天气预报一直是研究丰富的领域,实际上神经网络被广泛用于此目的。在本章中,读者还学习了如何为预测问题准备类似的实验。正确应用数据选择和预处理的技术可以在设计神经网络时节省大量时间。本章也为下一章奠定了基础,因为所有下一章都将关注实际案例;因此,在本章学到的概念将在本书的其余部分得到广泛探索。
第六章:第6章. 疾病诊断分类
到目前为止,我们一直在使用监督学习来预测数值;然而,在现实世界中,数字只是数据的一部分。真实变量还包含分类值,这些值不是纯数值,但描述了影响神经网络应用解决的问题的重要特征。在本章中,读者将了解到一个涉及分类值和分类的非常具有教育意义且有趣的应用:疾病诊断。本章深入探讨了分类问题以及如何表示分类数据,以及如何使用神经网络设计分类算法。本章涵盖的主题如下:
-
分类问题的基础
-
分类数据
-
逻辑回归
-
混淆矩阵
-
敏感性和特异性
-
用于分类的神经网络
-
使用神经网络进行疾病诊断
-
癌症诊断
-
糖尿病诊断
分类问题的基础
神经网络真正擅长的一件事是分类记录。非常简单的感知器网络绘制决策边界,定义数据点属于哪个区域,而区域表示一个类别。让我们通过一个x-y散点图来直观地看一下:

虚线明确地将点分为不同的类别。这些点代表原本具有相应类别标签的数据记录。这意味着它们的类别已经已知,因此这个分类任务属于监督学习类别。
一个分类算法试图在数据超空间中的类别之间找到边界。一旦定义了分类边界,一个未知类别的新的数据点就会根据分类算法定义的边界获得一个类别标签。下面的图示显示了如何对一条新记录进行分类:

根据当前的类别配置,新记录的类别是第三个类别。
分类数据
应用通常从以下图示中展示的数据类型开始:

数据可以是数值型或分类型,简单来说,就是数字或文字。数值型数据由一个数值表示,它可以连续或离散。到目前为止,这本书的应用中已经使用了这种数据类型。分类型数据是一个更广泛的数据类别,包括文字、字母,甚至数字,但具有完全不同的含义。虽然数值型数据可以支持算术运算,但分类型数据仅是描述性的,不能像数字那样处理,即使其值是数字。一个例子是疾病严重程度的等级(例如,从零到五)。分类型数据的另一个特性是,某个变量具有有限个值;换句话说,只能将定义好的值集分配给分类型变量。分类型数据内部的一个子类是顺序数据。这个类别特别之处在于定义的值可以按预定义的顺序排序。一个例子是表示某物状态或质量的形容词(差、一般、好、优秀):
| 数值型 | 分类型 |
|---|---|
| 仅数字 | 数字、文字、字母、符号 |
| 支持算术运算 | 不支持算术运算 |
| 无限或未定义的值域 | 有限或定义好的值集 |
| 连续 | 离散 |
| 实数值 | 整数、小数 |
| 任何可能的值 | 预定义的区间 |
提示
注意,这里我们只讨论结构化数据。在现实世界中,大多数数据是非结构化的,包括文本和多媒体内容。尽管这些类型的数据在数据学习应用中也被处理,但神经网络需要将它们转换成结构化数据类型。
处理分类数据
结构化数据文件,如CSV或Excel中使用的,通常包含数值型和分类型数据的列。在第5章 预测天气 中,我们创建了LoadCsv类(用于加载csv文件)和DataSet类(用于存储csv数据),但这些类仅适用于处理数值数据。表示分类值的最简单方法是将每个可能的值转换为二进制列,其中如果原始列中包含给定值,则相应的二进制列将有一个作为转换值,否则为0:

顺序列可以假设定义的值作为同一列中的数值;然而,如果原始值是字母或文字,则需要通过Java字典将它们转换为数字。
上述策略可以通过练习由你实现。否则,你将不得不手动处理。在这种情况下,根据数据行数,可能会很耗时。
逻辑回归
我们已经讨论了神经网络可以通过在超空间中的数据上建立决策边界来作为数据分类器工作。这个边界可以是线性的,例如在感知器的情况下,或者非线性的,例如在其他神经网络架构中,如MLPs、Kohonen或Adaline。线性情况基于线性回归,分类边界实际上是一条线,如前图所示。如果数据的散点图看起来像以下图中的那样,则需要非线性分类边界:

神经网络实际上是一个非常好的非线性分类器,这是通过使用非线性激活函数实现的。一个实际上对非线性分类效果很好的非线性函数是Sigmoid函数,而使用此函数进行分类的过程称为逻辑回归:

此函数返回介于零和一之间的值。在此函数中,α参数表示从零到一的转换有多硬。以下图表显示了差异:

注意,alpha参数越高,逻辑函数就越接近硬限定的阈值函数,也称为阶梯函数。
多分类与二分类
分类问题通常处理多分类的情况,其中每个类别都被分配一个标签。然而,二分类方案在神经网络中很有用。这是因为输出层具有逻辑函数的神经网络只能产生介于0和1之间的值,这意味着它属于(1)或不属于(0)某个类别。
然而,对于多分类,有一种使用二进制函数的方法。假设有一个用于分类疾病的网络;每个神经元的输出将代表一个应用于某些症状的疾病:

小贴士
注意,在那个配置中,可能会有多个疾病具有相同的症状,这种情况可能发生。然而,如果只希望选择一个类别,那么竞争学习算法的方案在这种情况下可能更适合。
混淆矩阵
没有完美的分类器算法;它们都存在错误和偏差;然而,预期一个分类算法可以正确分类70-90%的记录。
小贴士
非常高的正确分类率并不总是理想的,因为输入数据中可能存在的偏差可能会影响分类任务,并且也存在过拟合的风险,当只有训练数据被正确分类时。
混淆矩阵显示了给定类别的记录中有多少被正确分类,以及有多少被错误分类。以下表格描述了混淆矩阵可能的样子:

注意,主对角线应期望有更高的值,因为分类算法总是会尝试从输入数据集中提取有意义的信息。所有行的总和必须等于100%,因为给定类别的所有元素都应该被分类到可用的类别之一。注意,某些类别可能会收到比预期更多的分类。
混淆矩阵越像单位矩阵,分类算法就越好。
敏感性和特异性
当分类是二进制时,混淆矩阵被发现是一个简单的2x2矩阵,因此其位置被特别命名:
| 实际类别 | 推断类别 |
|---|---|
| 正面(1) | 负面(0) |
| 正面(1) | 真阳性 |
| 负面(0) | 假阳性 |
在本章的主题——疾病诊断中,二进制混淆矩阵的概念被应用,即错误诊断可能是假阳性或假阴性。可以通过敏感性和特异性指数来衡量错误结果的比率。
敏感性意味着真阳性率;它衡量有多少记录被正确地分类为正面:

特异性,反过来,意味着真正的负率;它表示负记录识别的比例:

希望敏感性和特异性都高;然而,根据应用领域,敏感性可能更有意义。
实现混淆矩阵
在我们的代码中,让我们在NeuralOutputData类中实现混淆矩阵。下面的calculateConfusionMatrix方法被编程为考虑输出层中的两个神经元。如果输出是10,那么它对混淆矩阵来说是是;如果输出是01,那么它是否定:
public double[][] calculateConfusionMatrix(double[][] dataOutputTestAdapted, double[][] dataOutputTargetTestAdapted) {
int TP = 0;
int TN = 0;
int FP = 0;
int FN = 0;
for (int m = 0; m < getTargetData().length; m++) {
if ( ( dataOutputTargetTestAdapted[m][0] == 1.0 && dataOutputTargetTestAdapted[m][1] == 0.0 )
&& ( dataOutputTestAdapted[m][0] == 1.0 && dataOutputTestAdapted[m][1] == 0.0 ) ) {
TP++;
} else if ( ( dataOutputTargetTestAdapted[m][0] == 0.0 && dataOutputTargetTestAdapted[m][1] == 1.0 )
&& ( dataOutputTestAdapted[m][0] == 0.0 && dataOutputTestAdapted[m][1] == 1.0 ) ) {
TN++;
} else if ( ( dataOutputTargetTestAdapted[m][0] == 1.0 && dataOutputTargetTestAdapted[m][1] == 0.0 )
&& ( dataOutputTestAdapted[m][0] == 0.0 && dataOutputTestAdapted[m][1] == 1.0 ) ) {
FP++;
} else if ( ( dataOutputTargetTestAdapted[m][0] == 0.0 && dataOutputTargetTestAdapted[m][1] == 1.0 )
&& ( dataOutputTestAdapted[m][0] == 1.0 && dataOutputTestAdapted[m][1] == 0.0 ) ) {
FN++;
}
}
return new double[][] {{TP,FN},{FP,TN}};
}
在NeuralOutputData类中实现的一种方法是calculatePerformanceMeasures。它接收混淆矩阵作为参数,并计算并打印以下分类的性能指标:
-
正类错误率
-
负类错误率
-
总错误率
-
总准确率
-
精确度
-
敏感性
-
特异性
以下展示了该方法:
public void calculatePerformanceMeasures(double[][] confMat) {
double errorRatePositive = confMat[0][1] / (confMat[0][0]+confMat[0][1]);
double errorRateNegative = confMat[1][0] / (confMat[1][0]+confMat[1][1]);
double totalErrorRate = (confMat[0][1] + confMat[1][0]) / (confMat[0][0] + confMat[0][1] + confMat[1][0] + confMat[1][1]);
double totalAccuracy = (confMat[0][0] + confMat[1][1]) / (confMat[0][0] + confMat[0][1] + confMat[1][0] + confMat[1][1]);
double precision = confMat[0][0] / (confMat[0][0]+confMat[1][0]);
double sensibility = confMat[0][0] / (confMat[0][0]+confMat[0][1]);
double specificity = confMat[1][1] / (confMat[1][0]+confMat[1][1]);
System.out.println("### PERFORMANCE MEASURES ###");
System.out.println("positive class error rate: "+(errorRatePositive*100.0)+"%");
System.out.println("negative class error rate: "+(errorRateNegative*100.0)+"%");
System.out.println("total error rate: "+(totalErrorRate*100.0)+"%");
System.out.println("total accuracy: "+(totalAccuracy*100.0)+"%");
System.out.println("precision: "+(precision*100.0)+"%");
System.out.println("sensibility: "+(sensibility*100.0)+"%");
System.out.println("specificity: "+(specificity*100.0)+"%");
}
分类用的神经网络
可以使用本书迄今为止所涵盖的任何监督神经网络来完成分类任务。然而,建议您使用更复杂的架构,如MLP。在本章中,我们将使用NeuralNet类构建一个具有一个隐藏层和输出处的sigmoid函数的MLP。每个输出神经元都代表一个类别。
用于实现示例的代码与测试类(BackpropagationTest)非常相似。然而,DiagnosisExample类会询问用户想要使用哪个数据集以及其他神经网络参数,例如迭代次数、隐藏层中的神经元数量和学习率。
使用神经网络进行疾病诊断
对于疾病诊断,我们将使用免费的proben1数据集,该数据集可在网络上找到(http://www.filewatcher.com/m/proben1.tar.gz.1782734-0.html)。Proben1是从不同领域收集的几个数据集的基准集。我们将使用癌症和糖尿病数据集。我们添加了一个类别来运行每个案例的实验:DiagnosisExample。
乳腺癌
乳腺癌数据集由10个变量组成,其中9个是输入变量,1个是二进制输出。数据集共有699条记录,但我们排除了其中16条不完整的记录,因此我们使用了683条记录来训练和测试神经网络。
提示
在实际实际问题中,常见有缺失或无效数据。理想情况下,分类算法必须处理这些记录,但有时建议排除它们,因为可能没有足够的信息来产生准确的结果。
下表显示了该数据集的配置:
| 变量名称 | 类型 | 最大值和最小值 |
|---|---|---|
| 诊断结果 | 输出 | [0; 1] |
| 腺体厚度 | 输入 #1 | [1; 10] |
| 细胞大小均匀性 | 输入 #2 | [1; 10] |
| 细胞形状均匀性 | 输入 #3 | [1; 10] |
| 边缘粘附 | 输入 #4 | [1; 10] |
| 单个上皮细胞大小 | 输入 #5 | [1; 10] |
| 裸核 | 输入 #6 | [1; 10] |
| 浅色染色质 | 输入 #7 | [1; 10] |
| 正常核仁 | 输入 #8 | [1; 10] |
| 有丝分裂 | 输入 #9 | [1; 10] |
因此,提出的神经网络拓扑结构将如下图的所示:

数据集的划分如下:
-
训练: 549条记录(80%);
-
测试: 134条记录(20%)
与之前的案例一样,我们进行了多次实验,试图找到最佳的神经网络来分类癌症是良性还是恶性。因此,我们进行了12个不同的实验(每个实验1,000个迭代),分析了均方误差和准确率值。然后,使用测试数据集生成了混淆矩阵、敏感度和特异性,并进行了分析。最后,进行了泛化分析。实验中涉及的神经网络如下表所示:
| 实验 | 隐藏层中的神经元数量 | 学习率 | 激活函数 |
|---|---|---|---|
| #1 | 3 | 0.1 | 隐藏层: SIGLOG 输出层: LINEAR |
| #2 | 隐藏层: HYPERTAN 输出层: LINEAR | ||
| #3 | 0.5 | 隐藏层: SIGLOG 输出层: LINEAR | |
| #4 | 隐藏层: HYPERTAN 输出层: LINEAR | ||
| #5 | 0.9 | 隐藏层: SIGLOG 输出层: LINEAR | |
| #6 | 隐藏层:HYPERTAN 输出层:LINEAR | ||
| #7 | 5 | 0.1 | 隐藏层:SIGLOG 输出层:LINEAR |
| #8 | 隐藏层:HYPERTAN 输出层:LINEAR | ||
| #9 | 0.5 | 隐藏层:SIGLOG 输出层:LINEAR | |
| #10 | 隐藏层:HYPERTAN 输出层:LINEAR | ||
| #11 | 0.9 | 隐藏层:SIGLOG 输出层:LINEAR | |
| #12 | 隐藏层:HYPERTAN 输出层:LINEAR |
每次实验后,我们收集了均方误差值(表X);实验#4、#8、#9、#10和#11是等效的,因为它们具有低均方误差值和相同的总准确度指标(92.25%)。因此,我们选择了实验#4和#11,因为它们在前面提到的五个实验中均方误差值最低:
| 实验 | 均方误差训练率 | 总准确率 |
|---|---|---|
| #1 | 0.01067 | 96.29% |
| #2 | 0.00443 | 98.50% |
| #3 | 9.99611E-4 | 97.77% |
| #4 | 9.99913E-4 | 99.25% |
| #5 | 9.99670E-4 | 96.26% |
| #6 | 9.92578E-4 | 97.03% |
| #7 | 0.01392 | 98.49% |
| #8 | 0.00367 | 99.25% |
| #9 | 9.99928E-4 | 99.25% |
| #10 | 9.99951E-4 | 99.25% |
| #11 | 9.99926E-4 | 99.25% |
| #12 | NaN | 3.44% |
从图表中可以看出,第四个实验的均方误差随时间快速演变,尽管我们使用了1,000个周期进行训练,但实验提前停止,因为达到了最小整体误差(0.001):

混淆矩阵以及两个实验的灵敏度和特异性在表中展示。可以检查出两个实验的指标是相同的:
| 实验 | 混淆矩阵 | 灵敏度 | 特异性 |
|---|---|---|---|
| #4 | [[34.0, 1.0][0.00, 99.0]] | 97.22% | 100.0% |
| #11 | [[34.0, 1.0][0.00, 99.0]] | 97.22% | 100.0% |
如果我们不得不在实验#4或#11生成的模型之间做出选择,我们建议选择#4,因为它比#11简单(隐藏层中的神经元更少)。
糖尿病
需要进一步探索的另一个例子是糖尿病的诊断。此数据集有八个输入和一个输出,如下表所示。共有768条记录,全部完整。然而,proben1表示存在几个无意义的零值,这可能是缺失数据的指示。我们仍然将这些数据当作真实数据来处理,从而在数据集中引入了一些错误(或噪声):
| 变量名称 | 类型 | 最大值和最小值 |
|---|---|---|
| 诊断结果 | 输出 | [0; 1] |
| 怀孕次数 | 输入 #1 | [0.0; 17] |
| 口服葡萄糖耐量试验中2小时血浆葡萄糖浓度 | 输入 #2 | [0.0; 199] |
| 舒张压(mm Hg) | 输入 #3 | [0.0; 122] |
| 三角肌皮肤褶皱厚度(mm) | 输入 #4 | [0.0; 99] |
| 2小时血清胰岛素(mu U/ml) | 输入 #5 | [0.0; 744] |
| 体质指数(体重(kg)/身高(m)^2) | 输入 #6 | [0.0; 67.1] |
| 糖尿病家系函数 | 输入 #7 | [0.078; 2420] |
| 年龄(年) | 输入 #8 | [21; 81] |
数据集划分如下:
-
训练: 617条记录(80%)
-
测试: 151条记录(20%)
为了发现用于分类糖尿病的最佳神经网络拓扑结构,我们使用了与上一节中描述相同的神经网络架构。然而,我们在输出层使用了多类分类:这个层将使用两个神经元,一个用于糖尿病的存在,另一个用于不存在。
因此,所提出的神经网络架构看起来如下图的所示:

下表显示了前六个实验和最后六个实验的均方误差(MSE)训练值和准确率:
| 实验 | MSE训练率 | 总准确率 |
|---|---|---|
| #1 | 0.00807 | 60.54% |
| #2 | 0.00590 | 71.03% |
| #3 | 9.99990E-4 | 75.49% |
| #4 | 9.98840E-4 | 74.17% |
| #5 | 0.00184 | 61.58% |
| #6 | 9.82774E-4 | 59.86% |
| #7 | 0.00706 | 63.57% |
| #8 | 0.00584 | 72.41% |
| #9 | 9.99994E-4 | 74.66% |
| #10 | 0.01047 | 72.14% |
| #11 | 0.00316 | 59.86% |
| #12 | 0.43464 | 40.13% |
在这两种情况下,MSE的下降都很快。然而,实验#9在初始值中产生了错误率的增加。如下图所示:

分析混淆矩阵,可以看出度量值非常相似:
| 实验 | 混淆矩阵 | 灵敏度 | 特异性 |
|---|---|---|---|
| #3 | [[35.0, 12.0][25.0, 79.0]] | 74.46% | 75.96% |
| #9 | [[34.0, 12.0][26.0, 78.0]] | 73.91% | 75.00% |
再次建议选择最简单的模型。在糖尿病的例子中,它是实验#3生成的神经网络。
提示
建议您探索类D iagnosisExample并创建一个GUI来轻松选择神经网络参数,就像在上一章中所做的那样。您应该尝试通过继承概念重用已编写的代码。
摘要
在本章中,我们看到了两个使用神经网络进行疾病诊断应用的例子。为了使本章探索的知识水平一致,简要回顾了分类问题的基本原理。分类任务属于机器学习/数据挖掘领域中应用最广泛的有监督任务类型之一,神经网络被证明非常适合应用于此类问题。读者还介绍了评估分类任务的几个概念,如灵敏度、特异性和混淆矩阵。这些符号对于所有分类任务都非常有用,包括那些使用除神经网络以外的其他算法处理的任务。下一章将探索类似类型的任务,但使用无监督学习——这意味着没有预期的输出数据——但本章中介绍的基本原理将有所帮助。
第七章:第7章. 聚类客户画像
神经网络在应用无监督学习时的一项令人惊叹的能力是它们发现隐藏模式的能力,即使专家也可能毫无头绪。在本章中,我们将通过一个实际应用来探索这一迷人的特性,该应用旨在通过交易数据库中提供的客户和产品聚类来寻找。我们将回顾无监督学习和聚类任务。为了演示这个应用,读者将获得一个关于客户画像及其在Java中实现的实际示例。本章的主题包括:
-
聚类任务
-
聚类分析
-
聚类评估
-
应用无监督学习
-
径向基函数神经网络
-
用于聚类的Kohonen网络
-
处理数据类型
-
客户画像
-
预处理
-
Java实现
-
信用分析和客户画像
聚类任务
聚类是数据分析中更广泛任务集的一部分,其目标是把看起来相似、彼此更相似的数据元素分组或归类。聚类任务完全基于无监督学习,因为不需要包含任何目标输出数据来找到聚类;相反,解决方案设计者可以选择他们想要将记录分组的聚类数量,并检查算法对此的反应。
小贴士
聚类任务似乎与分类任务重叠,但关键区别在于聚类中不需要在运行聚类算法之前有一个预定义的类别集合。
当几乎没有关于如何将数据分组的信息时,人们可能会希望应用聚类。在提供数据集的情况下,我们希望我们的神经网络能够识别出组和它们的成员。虽然这在二维数据集中看起来很容易且直观,如图所示,但随着维数的增加,这项任务就不再那么简单,需要算法解决方案:

在聚类中,聚类的数量不是由数据决定的,而是由希望聚类数据的分析师决定的。在这里,边界与分类任务的边界略有不同,因为它们主要取决于聚类的数量。
聚类分析
聚类任务中的一个困难,也是无监督学习任务中的一个困难,是对结果的准确解释。虽然在监督学习中有一个定义明确的目标,我们可以从中推导出误差度量或混淆矩阵,但在无监督学习中,质量的评估完全不同,并且完全依赖于数据本身。验证标准涉及指数,这些指数断言数据在聚类中的分布有多好,以及来自专家的外部意见,这些意见也是质量的一个衡量标准。
小贴士
为了说明一个例子,让我们假设一个基于植物特征(大小、叶色、结果实期等)进行聚类任务,而一个神经网络错误地将仙人掌和松树分到了同一个簇中。一个植物学家当然不会支持基于他们在该领域的专业知识,这种分组没有任何意义。
聚类过程中有两个主要问题。一个是某个神经网络的输出永远不会被激活,这意味着一个簇没有任何数据点与之关联。另一个是非线性或稀疏簇的情况,这些簇可能会错误地分成几个簇,而实际上可能只有一个。

聚类评估和验证
不幸的是,如果神经网络聚类效果不佳,就需要重新定义簇的数量或进行额外的数据预处理。为了评估聚类数据的好坏,可以应用 Davies-Bouldin 和 Dunn 指数。
Davies-Boudin 指数考虑簇的质心来寻找簇和簇成员之间的簇间和簇内距离:

其中 n 是簇的数量,ci 是簇 i 的质心,σi 是簇 i 中所有元素的平均距离,而 d(ci,cj) 是簇 i 和 j 之间的距离。DB 指数的值越小,神经网络在聚类方面的表现越好。
然而,对于密集和稀疏簇,DB 指数不会提供太多有用的信息。这种限制可以通过 Dunn 指数来克服:

其中 d(i,j) 是簇 i 和 j 之间的簇间距离,而 d'(k) 是簇 k 的簇内距离。在这里,Dunn 指数越高,聚类效果越好,因为尽管簇可能很稀疏,但它们仍然需要被分组在一起,高簇内距离将表示数据分组不良。
实现
在 CompetitiveLearning 类中,我们将实现以下指标:
public double DBIndex(){
int numberOfClusters = this.neuralNet.getNumberOfOutputs();
double sum=0.0;
for(int i=0;i<numberOfClusters;i++){
double[] index = new double[numberOfClusters];
for(int j=0;j<numberOfClusters;j++){
if(i!=j){
//calculate the average distance for cluster i
Double Sigmai=averageDistance(i,trainingDataSet);
Double Sigmaj=averageDistance(j,trainingDataSet);
Double[] Centeri=neuralNet.getOutputLayer().getNeuron(i).getWeights();
Double[] Centerj=neuralNet.getOutputLayer().getNeuron(j).getWeights();
Double distance = getDistance(Centeri,Centerj);
index[j]=((Sigmai+Sigmaj)/distance);
}
}
sum+=ArrayOperations.max(index);
}
return sum/numberOfClusters;
}
public double Dunn(){
int numberOfClusters = this.neuralNet.getNumberOfOutputs();
ArrayList<double> interclusterDistance;
for(int i=0;i<numberOfClusters;i++){
for(int j=i+1;j<numberOfClusters;j++){
interClusterDistance.add(minInterClusterDistance (i,j,trainingDataSet);
}
}
ArrayList<double> intraclusterDistance;
for(int k=0;k<numberOfClusters;k++){
intraclusterDistance.add(maxIntraClusterDistance(k, trainingDataSet);
}
return ArrayOperations.Min(interclusterDistance)/ ArrayOperations.Max(intraclusterDistance);
}
外部验证
在某些情况下,聚类已经有了预期的结果,例如植物聚类的例子。这被称为外部验证。可以将无监督学习的神经网络应用于已经分配了值的聚类数据。与分类的主要区别在于,目标输出不被考虑,因此算法本身预期仅基于数据绘制边界线。
应用无监督学习
在神经网络中,有许多架构实现了无监督学习;然而,本书的范围将仅涵盖在第 4 章([第 4 章](ch04.xhtml "第 4 章. 自组织映射")中开发的 Kohonen 神经网络,自组织映射。
Kohonen 神经网络
Kohonen网络,在第4章中有所介绍,自组织映射现在以修改后的方式被使用。Kohonen可以在一维或二维输出中产生一个形状,但在这里我们感兴趣的是聚类,这可以简化为一维。
小贴士
实际上,在这个框架中实现的Kohonen神经网络考虑了零维、一维和二维,其中零表示输出神经元之间没有连接,一表示它们形成一条线,二表示一个网格。对于本章的示例,我们需要一个没有连接输出神经元的Kohonen网络,因此维度将为零。
此外,聚类之间可能有关联也可能无关,因此在本章中可以暂时忽略神经元的邻近性,这意味着只有一个神经元会被激活,而其邻居将保持不变。因此,神经网络将调整其权重以匹配数据到一系列聚类:

训练算法将是竞争学习,其中输出最大的神经元将调整其权重。到训练结束时,神经网络的所有聚类都应被定义。请注意,输出神经元之间没有链接,这意味着输出只有一个活动输入。
配置文件
无监督学习中的一个有趣任务是信息的配置文件或聚类,在本章中是客户和产品。给定一个数据集,人们希望找到具有相似特征的记录组。例如,购买相同产品的客户或通常一起购买的产品。这项任务为商业主带来了许多好处,因为他们可以了解他们拥有的客户和产品组,从而能够更准确地针对他们。
预处理
如第6章中所示,疾病诊断分类事务数据库可以包含数值数据和分类数据。每当面对一个分类未缩放变量时,我们需要使用CategoricalDataSet类将其拆分为变量可能取的值的数量。例如,假设我们有以下客户购买事务列表:
| 交易ID | 客户ID | 产品 | 折扣 | 总计 |
|---|---|---|---|---|
| 1399 | 56 | 牛奶,面包,黄油 | 0.00 | 4.30 |
| 1400 | 991 | 干酪,牛奶 | 2.30 | 5.60 |
| 1401 | 406 | 面包,香肠 | 0.00 | 8.80 |
| 1402 | 239 | 奶椒酱,香料 | 0.00 | 6.70 |
| 1403 | 33 | 火鸡 | 0.00 | 4.50 |
| 1404 | 406 | 火鸡,黄油,香料 | 1.00 | 9.00 |
可以很容易地看出,产品是不加缩放的分类数据,并且对于每一笔交易,购买的产品数量是未定义的,客户可能购买一个或多个。为了将此数据集转换为数值数据集,需要进行预处理。对于每个产品,将在数据集中添加一个变量,结果如下:
| 客户 ID | 牛奶 | 面包 | 黄油 | 干酪 | 香肠 | 奇普otle酱 | 调味品 | 火鸡 |
|---|---|---|---|---|---|---|---|---|
| 56 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 |
| 991 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| 406 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 |
| 239 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 |
| 33 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
为了节省空间,我们忽略了数值变量,并考虑客户购买的产品存在与否作为 1 和 0。另一种预处理可能考虑值的出现次数,因此不再二进制,而是离散的。
Java实现
在本章中,我们将探讨将Kohonen神经网络应用于基于从Proben1(卡片数据集)收集的客户信息的客户聚类。
卡片 – 信用分析用于客户画像
卡片数据集总共由16个变量组成。其中15个是输入,1个是输出。出于安全考虑,所有变量名都已更改为无意义的符号。此数据集带来了各种变量类型的良好混合(连续型、具有少量值的分类型,以及具有更多值的分类型)。以下表格显示了数据的摘要:
| 变量 | 类型 | 值 |
|---|---|---|
| V1 | 输出 | 0; 1 |
| V2 | 输入 #1 | b, a |
| V3 | 输入 #2 | 连续 |
| V4 | 输入 #3 | 连续 |
| V5 | 输入 #4 | u, y, l, t. |
| V6 | 输入 #5 | g, p, gg |
| V7 | 输入 #6 | c, d, cc, i, j, k, m, r, q, w, x, e, aa, ff |
| V8 | 输入 #7 | v, h, bb, j, n, z, dd, ff, o |
| V9 | 输入 #8 | 连续 |
| V10 | 输入 #9 | t, f |
| V11 | 输入 #10 | t, f |
| V12 | 输入 #11 | 连续 |
| V13 | 输入 #12 | t, f |
| V14 | 输入 #13 | g, p, s |
| V15 | 输入 #14 | 连续 |
| V16 | 输入 #15 | 连续 |
为了简化,我们没有使用输入 v5-v8 和 v14,以避免过多地增加输入数量。我们应用了以下转换:
| 变量 | 类型 | 值 | 转换 |
|---|---|---|---|
| V1 | 输出 | 0; 1 | - |
| V2 | 输入 #1 | b, a | b = 1, a = 0 |
| V3 | 输入 #2 | 连续 | - |
| V4 | 输入 #3 | 连续 | - |
| V9 | 输入 #8 | 连续 | - |
| V10 | 输入 #9 | t, f | t = 1, f = 0 |
| V11 | 输入 #10 | t, f | t = 1, f = 0 |
| V12 | 输入 #11 | 连续 | - |
| V13 | 输入 #12 | t, f | t = 1, f = 0 |
| V15 | 输入 #14 | 连续 | - |
| V16 | 输入 #15 | 连续 | - |
提出的神经网络拓扑结构如下所示:

存储的示例数量为690个,但其中37个示例存在缺失值。这37条记录被丢弃。因此,使用了653个示例来训练和测试神经网络。数据集的划分如下:
-
训练: 583条记录
-
测试: 70条记录
用于聚类相似行为的Kohonen训练算法依赖于一些参数,例如:
-
归一化类型
-
学习率
需要考虑的是,Kohonen训练算法是无监督的。因此,当输出未知时使用此算法。在卡片示例中,数据集中有输出值,它们将在这里仅用于验证聚类。但在传统的聚类情况下,输出值是不可用的。
在这个特定案例中,因为输出是已知的,作为分类,聚类质量可以通过以下方式来验证:
-
敏感性(真阳性率)
-
特异性(真阴性率)
-
总准确率
在Java项目中,这些值的计算是通过一个名为NeuralOutputData的类来完成的,该类是在第6章,疾病诊断分类中之前开发的。
进行多次实验以尝试找到最佳的神经网络来聚类客户档案是一种好的做法。将生成10个不同的实验,并且每个实验都将使用之前提到的质量率进行分析。以下表格总结了将遵循的策略:
| 实验 | 学习率 | 归一化类型 |
|---|---|---|
| #1 | 0.1 | MIN_MAX |
| #2 | Z_SCORE | |
| #3 | 0.3 | MIN_MAX |
| #4 | Z_SCORE | |
| #5 | 0.5 | MIN_MAX |
| #6 | Z_SCORE | |
| #7 | 0.7 | MIN_MAX |
| #8 | Z_SCORE | |
| #9 | 0.9 | MIN_MAX |
| #10 | Z_SCORE |
创建ClusterExamples类是为了运行每个实验。除了在第4章中处理数据,自组织映射还解释了如何创建Kohonen网络以及如何通过欧几里得距离算法对其进行训练。
以下代码片段展示了其部分实现:
// enter neural net parameter via keyboard (omitted)
// load dataset from external file (omitted)
// data normalization (omitted)
// create ANN and define parameters to TRAIN:
CompetitiveLearning cl = new CompetitiveLearning(kn1, neuralDataSetToTrain, LearningAlgorithm.LearningMode.ONLINE);
cl.show2DData=false;
cl.printTraining=false;
cl.setLearningRate( typedLearningRate );
cl.setMaxEpochs( typedEpochs );
cl.setReferenceEpoch( 200 );
cl.setTestingDataSet(neuralDataSetToTest);
// train ANN
try {
System.out.println("Training neural net... Please, wait...");
cl.train();
System.out.println("Winner neurons (clustering result [TRAIN]):");
System.out.println( Arrays.toString( cl.getIndexWinnerNeuronTrain() ) );
} catch (NeuralException ne) {
ne.printStackTrace();
}
在使用ClusteringExamples类运行每个实验并保存混淆矩阵和总准确率后,可以观察到实验#4、#6、#8和#10具有相同的混淆矩阵和准确率。这些实验使用了z-score来归一化数据:
| 实验 | 混淆矩阵 | 总准确率 |
|---|---|---|
| #1 | [[14.0, 21.0][18.0, 17.0]] | 44.28% |
| #2 | [[11.0, 24.0][34.0, 1.0]] | 17.14% |
| #3 | [[21.0, 14.0][17.0, 18.0]] | 55.71% |
| #4 | [[24.0, 11.0][1.0, 34.0]] | 82.85% |
| #5 | [[21.0, 14.0][17.0, 18.0]] | 55.71% |
| #6 | [[24.0, 11.0][1.0, 34.0]] | 82.85% |
| #7 | [[8.0, 27.0][7.0, 28.0]] | 51.42% |
| #8 | [[24.0, 11.0][1.0, 34.0]] | 82.85% |
| #9 | [[27.0, 8.0][28.0, 7.0]] | 48.57% |
| #10 | [[24.0, 11.0][1.0, 34.0]] | 82.85% |
因此,实验#4、#6、#8或#10构建的神经网络可能被用来达到超过80%的准确率以进行客户财务聚类。
产品分析
使用代码提供的交易数据库,我们已将大约650笔购买交易编译成一个大的矩阵交易 x 产品,其中每个单元格中都有对应交易中购买的相应产品的数量:
| #Trns. | Prd.1 | Prd.2 | Prd.3 | Prd.4 | Prd.5 | Prd.6 | Prd.7 | … | Prd.N |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 56 | 0 | 0 | 3 | 2 | 0 | 0 | … | 0 |
| 2 | 0 | 0 | 40 | 0 | 7 | 0 | 19 | … | 0 |
| … | … | … | … | … | … | … | … | … | … |
| n | 0 | 0 | 0 | 0 | 0 | 0 | 0 | … | 1 |
让我们考虑这个矩阵是在一个 N 维超空间中的表示,每个产品作为一个维度,交易作为点。为了简单起见,让我们考虑一个三维的例子。一个给定的交易将每个产品的购买数量放置在每个维度的对应点上。

策略是聚类这些交易,以找到哪些产品通常一起购买。因此,我们将使用 Kohonen 神经网络来找到产品簇中心的定位。
我们的数据库由一家服装店和27个注册产品的样本组成:
| 1 长裙 A | 19 带拉链的整体 | 43 百慕大 M |
|---|---|---|
| 3 长裙 B | 22 肩部整体 | 48 条纹裙 |
| 7 短裙 A | 23 长印花裙 | 67 无袖衫肩带 |
| 8 印花裙 | 24 印花短裙 | 68 牛仔裤 M |
| 9 女士无袖衫 | 28 短裤 M | 69 XL 短裙 |
| 13 短裤 S | 31 无袖短裙 | 74 条纹无袖衫 S |
| 16 儿童整体 | 32 短裙肩部 | 75 条纹无袖衫 M |
| 17 短裤 | 34 短裙 B | 76 条纹无袖衫 L |
| 18 印花整体 | 42 两件套上衣 | 106 直筒裙 |
有多少个簇?
有时在聚类算法中确定要找到多少个簇可能很困难。确定最佳选择的某些方法包括信息准则,如 赤池信息准则 (AIC), 贝叶斯信息准则 (BIC), 以及从中心到数据的马氏距离。我们建议读者如果对这些准则的更多细节感兴趣,请查阅参考文献。
为了对产品进行测试,我们也应该使用 ClusteringExamples 类。为了简单起见,我们使用三个和五个簇进行测试。对于每个实验,epoch 数为 1000,学习率为 0.5,归一化类型为 MIN_MAX (-1; 1)。一些结果如下表所示:
| 簇数量 | 前十五个元素组成的簇 | 购买产品总和 |
|---|---|---|
| 3 | 0, 1, 2, 2, 2,2, 2, 2, 2, 2,2, 2, 0, 0, 2, | 973, 585, 11, 5, 2,4, 11, 6, 3, 2,2, 2, 669, 672, 7, |
| 5 | 0, 1, 4, 4, 4,4, 4, 4, 4, 4,4, 4, 0, 0, 4, | 973, 585, 11, 5, 2,4, 11, 6, 3, 2,2, 2, 669, 672, 7, |
观察前表,我们注意到当产品组合的总和超过600时,它们会聚集在一起。否则,当总和在500到599之间时,会形成另一个簇。最后,如果总和较低,会创建一个大簇,因为数据集由许多客户购买不超过20件商品的案例组成。
小贴士
如前一章所建议,我们建议您探索ClusteringExamples类,并创建一个GUI以轻松选择神经网络参数。您应该尝试通过继承概念重用代码。
另一个技巧是进一步探索产品特征示例:调整神经网络训练参数、簇的数量,或者开发其他分析聚类结果的方法。
摘要
在本章中,我们展示了使用Kohonen神经网络的客户特征应用。与分类任务不同,聚类任务不考虑对所需输出的先前知识;相反,希望神经网络找到簇。然而,我们已经看到验证技术可能包括外部验证,这可以与所谓的目标输出进行比较。客户特征分析很重要,因为它为业务所有者提供了关于客户更准确和干净的信息,没有人为干预指出哪些客户属于某个群体或另一个群体,正如在监督学习中发生的那样。这就是无监督学习的优势,使数据能够完全自行得出结果。
第八章:第8章:文本识别
我们都知道人类阅读和识别图像的速度比任何超级计算机都快。然而,到目前为止,我们已经看到神经网络在监督学习和非监督学习两种方式下都表现出惊人的学习能力。在本章中,我们提出了一个涉及光学字符识别示例的额外模式识别案例。神经网络可以被训练来严格识别图像文件中写下的数字。本章的主题包括:
-
模式识别
-
定义好的类
-
未定义的类
-
模式识别中的神经网络
-
MLP
-
OCR问题
-
预处理和类定义
-
Java实现
-
数字识别
模式识别
模式是一组看起来彼此相似的数据和元素,它们可以系统地发生并时不时地重复。这是一个可以通过聚类进行无监督学习解决的问题;然而,当存在标记数据或数据有定义好的类别时,这个问题可以通过监督方法解决。我们作为人类,比我们想象的更经常地执行这个任务。当我们看到物体并将它们识别为属于某个类别时,我们实际上是在识别一个模式。此外,当我们分析图表、离散事件和时间序列时,我们可能会发现某些事件序列在特定条件下系统性地重复的证据。总之,模式可以通过数据观察来学习。
模式识别任务的例子包括但不限于:
-
形状识别
-
物体分类
-
行为聚类
-
语音识别
-
OCR
-
化学反应分类法
定义好的类
在一个为特定领域预定义的类列表中,每个类都被视为一个模式;因此,每个数据记录或发生的事件都被分配了这些预定义类中的一个。
小贴士
类的预定义通常可以由专家执行或基于应用领域的先前知识。此外,当我们希望数据严格分类到预定义的类别之一时,我们希望应用定义好的类。
一个使用定义好的类的模式识别示例是图像中的动物识别,如图下所示。然而,模式识别器应该被训练来捕捉所有正式定义类的特征。在示例中,展示了八种动物图像,属于两个类别:哺乳动物和鸟类。由于这是一个监督学习模式,神经网络应该提供足够多的图像,以便它能够正确地分类新的图像。

当然,有时分类可能会失败,主要是因为图像中可能被神经网络捕捉到的相似隐藏模式,以及形状中存在的小细节。例如,海豚有鳍,但它仍然是一种哺乳动物。有时,为了获得更准确的分类,有必要应用预处理并确保神经网络将接收允许进行分类的适当数据。
未定义的类
当数据未标记且没有预定义的类别集时,这是一个无监督学习场景。形状识别是一个很好的例子,因为形状可能是灵活的,并且具有无限数量的边、顶点或绑定。

在上面的图像中,我们可以看到一些形状,我们想要将它们排列,以便相似的形状可以分组到同一个簇中。基于图像中存在的形状信息,模式识别器很可能会将矩形、正方形和三角形分类到同一个组。然而,如果信息以图形的形式呈现给模式识别器,而不是图像,而是带有边和顶点坐标的图形,分类可能会略有变化。
总结来说,模式识别任务可能使用监督学习和无监督学习模式,这基本上取决于识别的目标。
模式识别中的神经网络
对于模式识别,可以应用的神经网络架构有MLPs(监督学习)和Kohonen网络(无监督学习)。在第一种情况下,问题应设置为一个分类问题,即数据应转换为X-Y数据集,其中对于X中的每个数据记录,在Y中应有相应的类别。如第3章中所述,感知器和监督学习和第6章中所述,疾病诊断分类,对于分类问题,神经网络的输出应包含所有可能的类别,这可能需要预处理输出记录。
对于其他情况,无监督学习,不需要对输出应用标签,但输入数据应该适当结构化。为了提醒您,以下图中展示了两种神经网络的架构:

数据预处理
如前所述,在第6章中,疾病诊断分类和第7章中,客户档案聚类,我们必须处理所有可能类型的数据,即数值(连续和离散)和分类(有序或未缩放的)。
然而,在这里我们有在多媒体内容上执行模式识别的可能性,例如图像和视频。那么,多媒体能否被处理?这个问题的答案在于这些内容在文件中的存储方式。例如,图像是用称为像素的小彩色点表示的。每种颜色都可以用RGB表示法编码,其中红色、绿色和蓝色的强度定义了人眼能看到的所有颜色。因此,一个100x100像素的图像将会有10,000个像素,每个像素有红色、绿色和蓝色的三个值,总共30,000个点。这就是神经网络中图像处理的挑战。
一些方法,我们将在下一章中回顾,可以减少这个巨大的维度数。之后,图像可以被视为一个由数值连续值组成的大矩阵。
为了简化,我们将在本章中仅应用小尺寸的灰度图像。
文本识别(光学字符识别)
许多文档现在正被扫描并存储为图像,这使得将文档转换回文本成为必要,以便计算机应用编辑和文本处理。然而,这一功能涉及许多挑战:
-
文本字体的多样性
-
文本大小
-
图像噪声
-
手稿
尽管如此,人类可以轻松地解释和阅读即使在质量较差的图像中产生的文本。这可以解释为人类已经熟悉文本字符和他们的语言中的单词。某种方式,算法必须熟悉这些元素(字符、数字、信号等),以便在图像中成功识别文本。
数字识别
尽管市场上有很多OCR工具,但算法正确识别图像中的文本仍然是一个巨大的挑战。因此,我们将限制我们的应用在一个更小的领域,这样我们会面临更简单的问题。因此,在本章中,我们将实现一个神经网络来识别图像上表示的0到9的数字。此外,为了简化,图像将具有标准化的和小尺寸。
数字表示
我们在灰度图像中应用了标准的10x10(100像素)尺寸,每个图像有100个灰度值:

在前面的图像中,我们有一个表示左侧数字3的草图以及对应相同数字的灰度值矩阵,以灰度形式呈现。
我们应用这种预处理是为了在这个应用中表示所有十个数字。
Java实现
为了识别光学字符,我们产生了用于训练和测试神经网络的训练数据。在这个例子中,考虑了从0(超级黑色)到255(超级白色)的数字。根据像素分布,为每个数字数据创建了两个版本:一个用于训练,另一个用于测试。这里将使用第3章中介绍的分类技术,感知器和监督学习和第6章中介绍的分类技术,疾病诊断分类。
生成数据
使用Microsoft Paint®绘制了从零到九的数字。这些图像已被转换为矩阵,以下图像显示了其中的一些示例。所有介于零到九之间的像素值都是灰度值:

对于每个数字,我们生成了五个变体,其中一个是完美的数字,其余的包含噪声,无论是通过绘制还是通过图像质量。
将每个矩阵行合并成向量(D[train]和D[test]),形成用于训练和测试神经网络的模式。因此,神经网络的输入层将由101个神经元组成。
输出数据集由十个模式表示。每个模式都有一个更具有表达力的值(一个)其余的值都是零。因此,神经网络的输出层将有十个神经元。
神经网络架构
因此,在这个应用中,我们的神经网络将有100个输入(对于10x10像素大小的图像)和十个输出,隐藏神经元的数量不受限制。我们在包examples.chapter08中创建了一个名为DigitExample的类来处理这个应用。神经网络架构是根据以下参数选择的:
-
神经网络类型:MLP
-
训练算法:反向传播
-
隐藏层数量:1
-
隐藏层中的神经元数量:18
-
迭代次数:1000
-
最小总体误差:0.001

实验
现在,就像之前展示的其他情况一样,让我们找到最佳神经网络拓扑结构,通过训练多个网络来实现。实现这一目标的策略总结在下表中:
| 实验 | 学习率 | 激活函数 |
|---|---|---|
| #1 | 0.3 | 隐藏层:SIGLOG |
| 输出层:LINEAR | ||
| #2 | 0.5 | 隐藏层:SIGLOG |
| 输出层:LINEAR | ||
| #3 | 0.8 | 隐藏层:SIGLOG |
| 输出层:LINEAR | ||
| #4 | 0.3 | 隐藏层:HYPERTAN |
| 输出层:LINEAR | ||
| #5 | 0.5 | 隐藏层:SIGLOG |
| 输出层:LINEAR | ||
| #6 | 0.8 | 隐藏层:SIGLOG |
| 输出层:LINEAR | ||
| #7 | 0.3 | 隐藏层:HYPERTAN |
| 输出层:SIGLOG | ||
| #8 | 0.5 | 隐藏层:HYPERTAN |
| 输出层:SIGLOG | ||
| #9 | 0.8 | 隐藏层:HYPERTAN |
| 输出层:SIGLOG |
以下DigitExample类代码定义了如何创建一个从数字数据读取的神经网络:
// enter neural net parameter via keyboard (omitted)
// load dataset from external file (omitted)
// data normalization (omitted)
// create ANN and define parameters to TRAIN:
Backpropagation backprop = new Backpropagation(nn, neuralDataSetToTrain, LearningAlgorithm.LearningMode.BATCH);
backprop.setLearningRate( typedLearningRate );
backprop.setMaxEpochs( typedEpochs );
backprop.setGeneralErrorMeasurement(Backpropagation.ErrorMeasurement.SimpleError);
backprop.setOverallErrorMeasurement(Backpropagation.ErrorMeasurement.MSE);
backprop.setMinOverallError(0.001);
backprop.setMomentumRate(0.7);
backprop.setTestingDataSet(neuralDataSetToTest);
backprop.printTraining = true;
backprop.showPlotError = true;
// train ANN:
try {
backprop.forward();
//neuralDataSetToTrain.printNeuralOutput();
backprop.train();
System.out.println("End of training");
if (backprop.getMinOverallError() >= backprop.getOverallGeneralError()) {
System.out.println("Training successful!");
} else {
System.out.println("Training was unsuccessful");
}
System.out.println("Overall Error:" + String.valueOf(backprop.getOverallGeneralError()));
System.out.println("Min Overall Error:" + String.valueOf(backprop.getMinOverallError()));
System.out.println("Epochs of training:" + String.valueOf(backprop.getEpoch()));
} catch (NeuralException ne) {
ne.printStackTrace();
}
// test ANN (omitted)
结果
在使用DigitExample类运行每个实验后,除了使用测试数据(上表)排除训练和测试总体误差以及正确数字分类的数量外,可以观察到实验 #2 和 #4 具有最低的MSE值。这两个实验之间的区别在于输出层使用的学习率和激活函数。
| 实验 | 训练总体误差 | 测试总体误差 | 正确数字分类数量 |
|---|---|---|---|
| #1 | 9.99918E-4 | 0.01221 | 2 x 10 |
| #2 | 9.99384E-4 | 0.00140 | 5 x 10 |
| #3 | 9.85974E-4 | 0.00621 | 4 x 10 |
| #4 | 9.83387E-4 | 0.02491 | 3 x 10 |
| #5 | 9.99349E-4 | 0.00382 | 3 x 10 |
| #6 | 273.70 | 319.74 | 2 x 10 |
| #7 | 1.32070 | 6.35136 | 5 x 10 |
| #8 | 1.24012 | 4.87290 | 7 x 10 |
| #9 | 1.51045 | 4.35602 | 3 x 10 |
上图显示了通过实验 #2 图形化地展示了每个epoch的均方误差(MSE)演变(训练和测试)。值得注意的是,曲线在第30个epoch附近稳定:

对实验 #8 也进行了相同的图形分析。可以检查到MSE曲线在第200个epoch附近稳定。

如前所述,仅MSE值可能不足以证明神经网络的质量。因此,测试数据集已验证了神经网络的泛化能力。下表显示了实验 #2 和 #8 的实际输出(含噪声)与神经网络估计输出的比较。可以得出结论,实验 #8 的神经网络权重比 #2 的更能识别七个数字模式:
| 输出比较 |
|---|
| 实际输出(测试数据集) |
| 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 |
| 估计输出(测试数据集)- 实验 #2 |
| 0.20 0.26 0.09 -0.09 0.39 0.24 0.35 0.30 0.24 1.02 0.42 -0.23 0.39 0.06 0.11 0.16 0.43 0.25 0.17 -0.26 0.51 0.84 -0.17 0.02 0.16 0.27 -0.15 0.14 -0.34 -0.12 -0.20 -0.05 -0.58 0.20 -0.16 0.27 0.83 -0.56 0.42 0.35 0.24 0.05 0.72 -0.05 -0.25 -0.38 -0.33 0.66 0.05 -0.63 0.08 0.41 -0.21 0.41 0.59 -0.12 -0.54 0.27 0.38 0.00 -0.76 -0.35 -0.09 1.25 -0.78 0.55 -0.22 0.61 0.51 0.27 -0.15 0.11 0.54 -0.53 0.55 0.17 0.09 -0.72 0.03 0.12 0.03 0.41 0.49 -0.44 -0.01 0.05 -0.05 -0.03 -0.32 -0.30 0.63 -0.47 -0.15 0.17 0.38 -0.24 0.58 0.07 -0.16 0.54 |
| 估计输出(测试数据集)- 实验 #8 |
| 0.10 0.10 0.12 0.10 0.12 0.13 0.13 0.26 0.17 0.39 0.13 0.10 0.11 0.10 0.11 0.10 0.29 0.23 0.32 0.10 0.26 0.38 0.10 0.10 0.12 0.10 0.10 0.17 0.10 0.10 0.10 0.10 0.17 0.39 0.10 0.38 0.10 0.15 0.10 0.24 0.10 0.10 0.10 0.10 0.39 0.37 0.10 0.20 0.12 0.10 0.10 0.37 0.10 0.10 0.10 0.10 0.10 0.10 0.10 0.10 0.39 0.10 0.10 0.10 0.28 0.10 0.27 0.11 0.10 0.21 |
小贴士
本章中展示的实验已经考虑了10x10像素信息图像。我们建议你尝试使用20x20像素的数据集来构建一个能够分类这种尺寸数字图像的神经网络。
你还应该更改神经网络的训练参数以实现更好的分类。
摘要
在本章中,我们看到了神经网络在识别图像中0到9数字的强大能力。尽管在10x10的图像中数字的编码非常小,但在实践中理解这一概念是很重要的。神经网络能够从数据中学习,并且只要现实世界的表示可以转化为数据,考虑字符识别可以作为模式识别应用的一个非常好的例子是合理的。这里的应用可以扩展到任何类型的字符,前提是神经网络都应该展示预定义的字符。
第九章:第9章:优化和调整神经网络
在本章中,读者将了解帮助优化神经网络的技巧,以便获得最佳性能。例如,输入选择、数据集分离和过滤、选择隐藏神经元数量和交叉验证策略等任务都是可以调整以改善神经网络性能的例子。此外,本章重点介绍将神经网络适应实时数据的方法。这里将展示这些技术的两种实现。练习将选择应用问题。本章涉及以下主题:
-
输入选择
-
维度降低
-
数据过滤
-
结构选择
-
剪枝
-
验证策略
-
交叉验证
-
在线重新训练
-
随机在线学习
-
自适应神经网络
-
自适应共振理论
神经网络实现中的常见问题
在开发神经网络应用时,遇到关于结果准确性的问题是很常见的。这些问题可能来自多个方面:
-
输入选择不当
-
噪声数据
-
数据集太大
-
不合适的结构
-
隐藏神经元数量不足
-
学习率不足
-
缺乏停止条件
-
数据集分割不当
-
差的验证策略
神经网络应用的设计有时需要很多耐心和试错方法。没有具体说明应该使用多少隐藏单元和/或架构的方法论,但有一些关于如何正确选择这些参数的建议。程序员可能面临的另一个问题是训练时间过长,这通常会导致神经网络无法学习数据。无论训练运行多长时间,神经网络都不会收敛。
小贴士
设计神经网络需要程序员或设计师根据需要多次测试和重新设计神经网络结构,直到获得可接受的结果。
另一方面,神经网络解决方案的设计师可能希望改进结果。因为神经网络可以学习直到学习算法达到停止条件,比如迭代次数或均方误差,结果可能不够准确或不够泛化。这需要重新设计神经网络结构或选择新的数据集。
输入选择
设计神经网络应用的一个关键任务是选择合适的输入。对于无监督情况,希望只使用神经网络能找到模式的变量。对于监督情况,需要将输出映射到输入,因此需要选择对输出有一定影响的输入变量。
数据相关性
在监督学习案例中,选择良好输入的一种策略是数据系列之间的相关性,这在第5章 预测天气 中实现。数据系列之间的相关性是衡量一个数据序列如何反应或影响另一个数据序列的度量。假设我们有一个包含多个数据系列的数据集,从中选择一个作为输出。现在我们需要从剩余的变量中选择输入。
相关系数的取值范围从 -1 到 1,其中接近 +1 的值表示正相关,接近 -1 的值表示负相关,接近 0 的值表示没有相关性。
例如,让我们看看两个变量 X 和 Y 的三个图表:

在第一个图表中,左侧可以看到,当一个变量减少时,另一个变量的值增加(相关系数 -0.8)。中间的图表显示了两个变量在同一方向变化的情形,因此是正相关(相关系数 +0.7)。右侧的第三个图表显示了变量之间没有相关性的情形(相关系数 -0.1)。
没有关于应该考虑哪个相关性作为限制阈值的规则;这取决于应用。虽然绝对相关性值大于 0.5 可能适合一个应用,但在其他情况下,接近 0.2 的值可能贡献显著。
数据转换
当数据系列在假设上是线性的时,线性相关性在检测数据系列之间的行为方面非常好。然而,如果两个数据系列在绘制在一起时形成一个抛物线,线性相关性将无法识别任何关系。这就是为什么有时我们需要将数据转换为一个显示线性相关性的视角。
数据转换取决于面临的问题。它包括插入一个或多个数据系列处理后的附加数据系列。一个例子是包含一个或多个参数的方程(可能是非线性的)。在数据转换的视角下,一些行为更容易被检测到。
小贴士
数据转换还涉及一些关于问题的知识。然而,通过查看两个数据系列的散点图,选择要应用哪种转换变得更容易。
维度降低
另一个有趣的观点是关于去除冗余数据。在无监督学习和监督学习中,当有大量可用数据时,有时这可能是所需的。作为一个例子,让我们看看两个变量的图表:

可以看到 X 和 Y 变量具有相同的形状,这可以解释为冗余,因为这两个变量由于高度正相关而携带几乎相同的信息。因此,可以考虑一种称为 主成分分析 (PCA) 的技术,它为处理这些情况提供了一个良好的方法。
PCA的结果将是一个新的变量,它总结了前面的两个(或更多)变量。基本上,原始数据系列减去平均值,然后乘以协方差矩阵的转置特征向量:

在这里,SXY是变量X和Y之间的协方差。
导出的新数据将是:

让我们看看新的变量在图表中看起来会是什么样子,与原始变量相比:

在我们的框架中,我们将添加一个名为PCA的类,该类将在将数据应用于神经网络之前执行这种转换和预处理:
public class PCA {
DataSet originalDS;
int numberOfDimensions;
DataSet reducedDS;
DataNormalization normalization = new DataNormalization(DataNormalization.NormalizationTypes.ZSCORE);
public PCA(DataSet ds,int dimensions){
this.originalDS=ds;
this.numberOfDimensions=dimensions;
}
public DataSet reduceDS(){
//matrix algebra to calculate transformed data in lower dimension
…
}
public DataSet reduceDS(int numberOfDimensions){
this.numberOfDimensions = numberOfDimensions;
return reduceDS;
}
}
数据过滤
噪声数据和不良数据也是神经网络应用中问题的来源;这就是为什么我们需要过滤数据。一种常见的数据过滤技术可以通过排除超出常规范围的记录来实现。例如,温度值在-40到40之间,因此像50这样的值将被视为异常值并可能被移除。
3-σ规则是一种良好且有效的过滤措施。它包括过滤那些超出平均值三倍标准差之外的值:

让我们添加一个类来处理数据过滤:
public abstract class DataFiltering {
DataSet originalDS;
DataSet filteredDS;
}
public class ThreeSigmaRule extends DataFiltering {
double thresholdDistance = 3.0;
public ThreeSigmaRule(DataSet ds,double threshold){
this.originalDS=ds;
this.thresholdDistance=threshold;
}
public DataSet filterDS(){
//matrix algebra to calculate the distance of each point in each column
…
}
}
这些类可以通过以下方法在DataSet中调用,然后在其他地方用于过滤和降维:
public DataSet applyPCA(int dimensions){
PCA pca = new PCA(this,dimensions);
return pca.reduceDS();
}
public DataSet filter3Sigma(double threshold){
ThreeSigmaRule df = new ThreeSigmaRule(this,threshold);
return df.filterDS();
}
交叉验证
在许多验证神经网络的策略中,交叉验证是非常重要的一种。这种策略确保所有数据都作为训练数据和测试数据呈现给神经网络。数据集被划分为K组,其中一组用于测试,其余用于训练:

在我们的代码中,让我们创建一个名为CrossValidation的类来管理交叉验证:
public class CrossValidation {
NeuralDataSet dataSet;
int numberOfFolds;
public LearningAlgorithm la;
double[] errorsMSE;
public CrossValidation(LearningAlgorithm _la,NeuralDataSet _nds,int _folds){
this.dataSet=_nds;
this.la=_la;
this.numberOfFolds=_folds;
this.errorsMSE=new double[_folds];
}
public void performValidation() throws NeuralException{
//shuffle the dataset
NeuralDataSet shuffledDataSet = dataSet.shuffle();
int subSize = shuffledDataSet.numberOfRecords/numberOfFolds;
NeuralDataSet[] foldedDS = new NeuralDataSet[numberOfFolds];
for(int i=0;i<numberOfFolds;i++){
foldedDS[i]=shuffledDataSet.subDataSet(i*subSize,(i+1)*subSize-1);
}
//run the training
for(int i=0;i<numberOfFolds;i++){
NeuralDataSet test = foldedDS[i];
NeuralDataSet training = foldedDS[i==0?1:0];
for(int k=1;k<numberOfFolds;k++){
if((i>0)&&(k!=i)){
training.append(foldedDS[k]);
}
else if(k>1) training.append(foldedDS[k]);
}
la.setTrainingDataSet(training);
la.setTestingDataSet(test);
la.train();
errorsMSE[i]=la.getMinOverallError();
}
}
}
结构选择
选择一个适合神经网络的适当结构也是一个非常重要的步骤。然而,这通常是通过经验来做的,因为没有关于神经网络应该有多少隐藏单元的规则。衡量单元数量的唯一标准是神经网络的性能。人们假设如果总体误差足够低,那么结构就是合适的。尽管如此,可能存在一个更小的结构可以产生相同的结果。
在这个背景下,基本上有两种方法:构造性和剪枝。构造性方法是从只有输入和输出层开始,然后在隐藏层中添加新的神经元,直到获得良好的结果。破坏性方法,也称为剪枝,是在一个更大的结构上进行的,其中移除了对输出贡献很少的神经元。
构造性方法在以下图中表示:

剪枝是回归之路:当给定大量神经元时,人们希望剪除那些灵敏度非常低的神经元,即那些对误差贡献最小的神经元:

为了实现剪枝,我们在NeuralNet类中添加了以下属性:
public class NeuralNet{
//…
public Boolean pruning;
public double senstitityThreshold;
}
在NeuralLayer类中有一个名为removeNeuron的方法,实际上是将神经元的所有连接设置为0,禁用权重更新,并在神经元输出处仅产生0。如果将NeuralNet对象的剪枝属性设置为true,则调用此方法。灵敏度计算是根据链式法则进行的,如第3章中所示,感知器和监督学习,并在calcNewWeigth方法中实现:
@Override
public Double calcNewWeight(int layer,int input,int neuron){
Double deltaWeight=calcDeltaWeight(layer,input,neuron);
if(this.neuralNet.pruning){
if(deltaWeight<this.neuralNet.sensitivityThreshold)
neuralNet.getHiddenLayer(layer).remove(neuron);
}
return newWeights.get(layer).get(neuron).get(input)+deltaWeight;
}
在线重新训练
在学习过程中,设计如何进行训练非常重要。两种基本方法是批量学习和增量学习。
在批量学习中,所有记录都被输入到网络中,因此它可以评估误差,然后更新权重:

在增量学习中,更新是在将每条记录发送到网络后进行的:

这两种方法都工作得很好,并且各有优缺点。虽然批量学习可以用于较少但更直接的权重更新,但增量学习提供了一种微调权重调整的方法。在这种情况下,可以设计一种学习模式,使网络能够持续学习。
小贴士
作为一项建议的练习,读者可以从代码中可用的数据集中选择一个,并使用部分记录设计训练,然后在两种模式下(在线和批量)使用另一部分进行训练。有关详细信息,请参阅IncrementalLearning.java文件。
随机在线学习
离线学习意味着神经网络在不在操作状态下进行学习。每个神经网络应用都应在某个环境中工作,为了进入生产状态,它应该得到适当的训练。离线训练适合将网络投入运行,因为其输出可能在大范围内变化,如果它在运行中,这肯定会损害系统。但是,当涉及到在线学习时,存在限制。在离线学习中,可以使用交叉验证和自助法来预测误差,而在在线学习中,由于没有“训练数据集”,这不可能做到。然而,当需要提高神经网络性能时,就需要进行在线训练。
在进行在线学习时使用随机方法。这个用于改进神经网络训练的算法由两个主要特性组成:随机选择训练样本和在运行时(在线)变动学习率。当在目标函数中发现噪声时,使用这种方法进行训练。它有助于逃离局部最小值(最佳解决方案之一)并达到全局最小值(最佳解决方案):
伪算法如下(来源:ftp://ftp.sas.com/pub/neural/FAQ2.html#A_styles):
Initialize the weights.
Initialize the learning rate.
Repeat the following steps:
Randomly select one (or possibly more) case(s)
from the population.
Update the weights by subtracting the gradient
times the learning rate.
Reduce the learning rate according to an
appropriate schedule.
实现方式
Java项目在learn包内创建了名为BackpropagtionOnline的类。这个算法与经典反向传播算法的不同之处在于通过修改train()方法,添加了两个新方法:generateIndexRandomList()和reduceLearningRate()。第一个方法生成一个随机索引列表,用于训练步骤,第二个方法根据以下启发式方法在线执行学习率的变动:
private double reduceLearningRate(NeuralNet n, double percentage) {
double newLearningRate = n.getLearningRate() *
((100.0 - percentage) / 100.0);
if(newLearningRate < 0.1) {
newLearningRate = 1.0;
}
return newLearningRate;
}
此方法将在train()方法结束时被调用。
应用
已经使用前几章的数据来测试这种新的训练神经网络的方法。每个章节中定义的相同神经网络拓扑(第5章, 预测天气 和 第8章, 文本识别)已经用于训练本章的神经网络。第一个是天气预报问题,第二个是OCR。下表显示了结果的比较:

此外,已经绘制了MSE演变的图表,并在此处展示:


第一张图表(天气预报)中显示的曲线呈锯齿状,这是因为学习率的变动。此外,它非常类似于第5章中显示的曲线,预测天气。另一方面,第二张图表(OCR)显示训练过程更快,并在大约第900个epoch时停止,因为它达到了一个非常小的均方误差(MSE)错误。
还进行了其他实验:使用反向传播算法训练神经网络,并考虑在线方法找到的学习率。两个问题中的MSE值都降低了:

另一个重要的观察结果是,训练过程在约第3,000个epoch时几乎终止。因此,它比使用相同算法在第8章, 文本识别中看到的训练过程更快、更好。
自适应神经网络
与人类学习类似,神经网络也可能工作以避免忘记以前的知识。使用传统的神经网络学习方法,由于每次训练都意味着用新的连接替换已经建立的连接,因此几乎不可能忘记以前的知识。因此,需要使神经网络通过增加而不是替换当前知识来适应新知识。为了解决这个问题,我们将探讨一种称为自适应共振理论(ART)的方法。
自适应共振理论
驱动这一理论发展的疑问是:一个自适应系统如何对重要输入保持可塑性,同时对无关输入保持稳定性?换句话说:它如何在学习新信息的同时保留以前学习的信息?
我们已经看到,在无监督学习中,竞争性学习处理模式识别,其中相似的输入产生相似的输出或激活相同的神经元。在ART拓扑结构中,当从网络中检索信息时,通过提供竞争层和输入层的反馈,共振出现。因此,当网络接收数据以进行学习时,竞争层和输入层之间的反馈会产生振荡。当模式在神经网络内部完全发展时,这种振荡会稳定下来。然后,这种共振会加强存储的模式。
实现
在某个包中创建了一个名为ART的新类,它继承自CompetitiveLearning。除了其他小的贡献外,它的重大变化是警觉性测试:
public class ART extends CompetitiveLearning{
private boolean vigilanceTest(int row_i) {
double v1 = 0.0;
double v2 = 0.0;
for (int i = 0; i < neuralNet.getNumberOfInputs(); i++) {
double weightIn = neuralNet.getOutputLayer().getWeight(i);
double trainPattern = trainingDataSet.getIthInput(row_i)[i];
v1 = v1 + (weightIn * trainPattern);
v2 = v2 + (trainPattern * trainPattern);
}
double vigilanceValue = v1 / v2;
if(vigilanceValue > neuralNet.getMatchRate()){
return true;
} else {
return false;
}
}
}
训练方法如下所示。可以注意到,首先,全局变量和神经网络被初始化;之后,存储了训练集的数量和训练模式;然后开始训练过程。这个过程的第一步是计算获胜神经元的索引;第二步是对神经网络输出进行归因。接下来的步骤包括验证神经网络是否已经学习,是否已经学习到权重是固定的;如果没有,则向网络展示另一个训练样本:
epoch=0;
int k=0;
forward();
//...
currentRecord=0;
forward(currentRecord);
while(!stopCriteria()){
//...
boolean isMatched = this.vigilanceTest(currentRecord);
if ( isMatched ) {
applyNewWeights();
}
摘要
在本章中,我们看到了一些使神经网络工作得更好的主题,无论是通过提高其准确性还是通过扩展其知识。这些技术在设计使用人工神经网络的解决方案方面非常有帮助。读者可以自由地将这个框架应用于任何可以使用神经网络的期望任务,以探索这些结构可能具有的增强能力。甚至像选择输入数据这样的简单细节也可能影响整个学习过程,以及过滤不良数据或消除冗余变量。我们展示了两种实现,两种有助于提高神经网络性能的策略:随机在线学习和自适应共振理论。这些方法使网络能够扩展其知识,因此能够适应新的、不断变化的环境。
第十章:第10章:神经网络当前趋势
本章向读者展示了神经网络领域的最新趋势。尽管本书是入门级的,但了解最新的发展以及这一理论背后的科学走向总是很有用的。最新的进展之一是所谓的深度学习,这是许多数据科学家非常受欢迎的研究领域;这一类型的网络在本章中有所概述。卷积和认知架构也属于这一趋势,并在多媒体数据识别方面越来越受欢迎。结合不同架构的混合系统是解决更复杂问题以及涉及分析、数据可视化等应用的一种非常有趣的策略。由于更偏向理论,这些架构并没有实际的实现,尽管提供了一个混合系统实现的例子。本章涵盖的主题包括:
-
深度学习
-
卷积神经网络
-
长短期记忆网络
-
混合系统
-
神经模糊
-
神经遗传学
-
混合神经网络的实现
深度学习
神经网络领域最新的进展之一是所谓的深度学习。如今,如果不提及深度学习,几乎不可能谈论神经网络,因为最近在特征提取、数据表示和转换方面的研究已经发现,许多层的处理信息能够抽象并产生更好的数据表示以供学习。贯穿本书,我们看到了神经网络需要以数值形式输入数据,无论原始数据是分类的还是二元的,神经网络不能直接处理非数值数据。但现实世界中的大多数数据是非数值的,甚至是未结构化的,如图像、视频、音频、文本等。
在这种意义上,一个深度网络将会有许多层,这些层可以作为数据处理单元来转换这些数据,并将其提供给下一层以进行后续的数据处理。这与大脑中发生的过程类似,从神经末梢到认知核心;在这个漫长的过程中,信号被多层处理,最终产生控制人体的信号。目前,大多数深度学习研究集中在非结构化数据处理上,尤其是图像和声音识别以及自然语言处理。
小贴士
深度学习仍在发展中,自2012年以来已经发生了很大的变化。像谷歌和微软这样的大公司都有研究这个领域的团队,未来几年可能会有很多变化。
下一个图中展示了深度学习架构的方案:

另一方面,深度神经网络存在一些需要克服的问题。主要问题是过拟合。产生数据新表示的许多层对训练数据非常敏感,因为信号在神经网络层中的深度越深,对输入数据的转换就越具体。正则化方法和剪枝通常被用来防止过拟合。计算时间是训练深度网络时的另一个常见问题。标准的反向传播算法训练深度神经网络可能需要非常长的时间,尽管选择较小的训练数据集等策略可以加快训练时间。此外,为了训练深度神经网络,通常建议使用更快的机器并将训练尽可能并行化。
深度架构
虽然它们通常是前馈的,但存在各种具有前馈和反馈流的深度神经网络架构。主要架构包括但不限于:
卷积神经网络
在这个架构中,层可能具有多维组织。受到动物视觉皮层的启发,应用于层的典型维度是三维的。在卷积神经网络(CNNs)中,前一层的部分信号被输入到下一层中的一些神经元。这种架构是前馈的,非常适合图像和声音识别。这种架构与多层感知器的主要区别在于层之间的部分连接性。考虑到并非所有神经元都对下一层的某个神经元相关,连接性是局部的,并尊重神经元之间的相关性。这既防止了长时间训练,也防止了过拟合,因为当图像的维度增长时,完全连接的MLP会爆炸性地增加权重的数量。此外,层的神经元在维度上排列,通常是三维的,因此以宽度、高度和深度堆叠成数组。

在这种架构中,层可能具有多维组织。受动物视觉皮层的启发,应用于层的典型维度是三维的。在卷积神经网络(CNNs)中,前一层的部分信号被输入到下一层中的一些神经元。这种架构是前馈的,并且适用于图像和声音识别。这种架构与多层感知器的主要区别在于层之间的部分连接性。考虑到并非所有神经元都对下一层的某个神经元都相关,连接是局部的,并尊重神经元之间的相关性。这防止了长时间训练和过拟合,因为如果是一个全连接的MLP,随着图像维度的增长,权重数量会爆炸。此外,层的神经元在维度上排列,通常是三维的,因此以宽度、高度和深度堆叠在数组中。
长短期记忆:这是一种循环神经网络,它始终考虑隐藏层的最后一个值,就像一个隐藏马尔可夫模型(HMM)。长短期记忆网络(LSTM)使用LSTM单元而不是传统神经元,这些单元执行存储和忘记值等操作,以控制深度网络中的流动。这种架构由于能够保留长时间的信息,同时接收完全无结构的音频或文本文件等数据,因此在自然语言处理中得到了很好的应用。训练这种类型网络的一种方法是时间反向传播(BPTT)算法,但还有其他算法,如强化学习或进化策略。

深度信念网络:深度信念网络(DBN's)是一种概率模型,其中层被分类为可见和隐藏。这也是一种基于受限玻尔兹曼机(RBM)的循环神经网络。它通常用作深度神经网络(DNN)训练的第一步,然后通过其他监督算法如反向传播进一步训练。在这个架构中,每一层都像一个特征检测器,抽象出数据的新表示。可见层既作为输出也作为输入,最深层的隐藏层代表最高层次的抽象。这种架构的应用通常与卷积神经网络相同。

如何在Java中实现深度学习
由于这本书是入门级的,我们在这个章节中不会深入探讨深度学习的细节。然而,提供了一些关于深度架构的代码推荐。这里提供了一个关于卷积神经网络如何实现的例子。需要实现一个名为ConvolutionalLayer的类来表示多维层,以及一个名为CNN的类来表示卷积神经网络本身:
public class ConvolutionalLayer extends NeuralLayer{
int height,width, depth;
//…
ArrayList<ArrayList<ArrayList<Neuron>>> neurons;
Map<Neuron,Neuron> connections;
ConvolutionalLayer previousLayer;
//the method call should take into account the mapping
// between neurons from different layers
@Override
public void calc(){
ArrayList<ArrayList<ArrayList<double>>> inputs;
foreach(Neuron n:neurons){
foreach(Neuron m:connections.keySet()){
// here we get only the inputs that are connected to the neuron
}
}
}
}
public class CNN : NeuralNet{
int depth;
ArrayList<ConvolutionalLayer> layers;
//…
@Override
public void calc(){
//here we perform the calculation for each layer,
//taking into account the connections between layers
}
}
在这个类中,神经元按维度和组织,并使用剪枝方法来使层之间的连接。请参阅文件ConvolutionalLayer.java和CNN.java以获取更多详细信息。
由于其他架构是循环的,而本书没有涵盖循环神经网络(为了入门书籍的简洁性),它们仅提供供读者参考。我们建议读者查看提供的参考文献,以了解更多关于这些架构的信息。
混合系统
在机器学习或甚至人工智能领域,除了神经网络之外,还有许多其他算法和技术。每种技术都有其优势和劣势,这激励了许多研究人员将它们结合成一个单一的结构。神经网络是人工智能连接主义方法的一部分,其中操作是在数值和连续值上进行的;但还有其他方法,包括认知(基于规则的系统)和进化计算。
| 连接主义 | 认知 | 进化 |
|---|---|---|
| 数值处理 | 符号处理 | 数值和符号处理 |
| 大型网络结构 | 大型规则库和前提 | 大量解决方案 |
| 通过统计进行性能 | 通过专家/统计进行设计 | 每次迭代产生更好的解决方案 |
| 对数据高度敏感 | 对理论高度敏感 | 局部最小值证明 |
Neuro-fuzzy
模糊逻辑是一种基于规则的加工方式,其中每个变量都根据隶属函数转换为符号值,然后所有变量的组合将针对一个IF-THEN规则数据库进行查询。

隶属函数通常具有高斯钟形形状,这告诉我们给定值属于该类有多少程度。以温度为例,它可能具有三个不同的类别(冷、正常和暖)。隶属值将随着温度越接近钟形中心而越高。

此外,模糊处理找出每个输入记录触发的规则以及产生的输出值。神经模糊架构对每个输入的处理方式不同,因此第一个隐藏层有一组神经元对应于每个隶属函数:

提示
在这个架构中,训练仅找到规则处理和后续参数加权的最优权重,第一隐藏层没有可调整的权重。
在模糊逻辑架构中,专家定义了一个规则数据库,随着变量数量的增加,这个数据库可能会变得很大。神经模糊架构释放了设计者定义规则的需求,并让神经网络来完成这项任务。神经模糊的训练可以通过梯度类型算法,如反向传播或矩阵代数,如最小二乘法,在监督模式下进行。神经模糊系统适用于动态系统的控制和诊断。
神经遗传
在进化人工智能方法中,一种常见的策略是遗传算法。这个名字的灵感来源于自然进化,它表明更能适应环境的生物能够产生新一代更适应环境的生物。在计算智能领域,生物或个体是能够解决优化问题的候选解或假设。由于存在一个我们希望通过调整神经网络权重来最小化的错误度量,因此监督神经网络用于优化。虽然训练算法能够通过梯度方法找到更好的权重,但它们往往陷入局部最小值。尽管一些机制,如正则化和动量,可能会改善结果,但一旦权重陷入局部最小值,找到更好的权重的可能性非常小,在这种情况下,遗传算法在这方面非常擅长。
将神经网络权重想象成遗传代码(或DNA)。如果我们能够生成有限数量的随机生成的权重集,并评估哪些产生最佳结果(较小的错误或其他性能度量),我们将选择前N个最佳权重,然后对它们进行设置和应用遗传操作,如繁殖(权重交换)和变异(随机改变权重)。

这个过程会重复进行,直到找到某个可接受的解决方案。
另一种策略是对神经网络参数使用遗传操作,例如神经元数量、学习率、激活函数等。考虑到这一点,我们总是需要调整参数或多次训练以确保我们找到了一个好的解决方案。因此,可以将所有参数编码在遗传代码(参数集)中,并为每个参数集生成多个神经网络。
遗传算法的方案如下所示:

小贴士
遗传算法被广泛用于许多优化问题,但在这本书中,我们坚持使用这两类问题,即权重和参数优化。
实现混合神经网络
现在,让我们实现一个可以在神经模糊和神经遗传网络中使用的简单代码。首先,我们需要定义激活函数的高斯函数,这些函数将是隶属函数:
public class Gaussian implements ActivationFunction{
double A=1.0,B=0.0,C=1.0;
public Gaussian(double A){ ///…
}
public double calc(double x){
return this.A*Math.exp(-Math.pow(x-this.B,2.0) / 2*Math.pow(this.C,2.0));
}
}
模糊集和规则需要以一种神经网络可以理解和驱动执行的方式表示。这种表示包括每个输入的集合数量,因此包含有关神经元如何连接的信息;以及每个集合的隶属函数。表示数量的简单方法是一个数组。集合数组仅指示每个变量有多少个集合;规则数组是一个矩阵,其中每一行代表一个规则,每一列代表一个变量;每个集合可以在规则数组中分配一个数值整数作为参考。以下是一个包含三个变量,每个变量有三个集合的示例,以及相应的规则:
int[] setsPerVariable = {3,3,3};
int[][] rules = {{0,0,0},{0,1,0},{1,0,1},{1,1,0},{2,0,2},{2,1,1}, {2,2,2}};
隶属函数可以参考一个序列化数组:
ActivationFunction[] fuzzyMembership = {new Gausian(1.0),//…
}};
我们还需要创建神经模糊架构层的类,例如InputFuzzyLayer和RuleLayer。它们可以是NeuroFuzzyLayer超类的子类,该超类可以继承自NeuralLayer。这些类是必要的,因为它们的工作方式与已经定义的神经网络层不同:
public class NeuroFuzzyLayer extends NeuralLayer{
double[] inputs;
ArrayList<Neuron> neurons;
Double[] outputs;
NeuroFuzzyLayer previousLayer;
///…
}
public class InputFuzzyLayer extends NeuroFuzzyLayer{
int[] setsPerVariable;
ActivationFunction[] fuzzyMembership;
//…
}
public class RuleLayer extends NeuroFuzzyLayer{
int[][] rules;
//…
}
NeuroFuzzy类将继承自NeuralNet,并引用其他模糊层类。NeuroFuzzyLayer的calc()方法也将不同,考虑到隶属函数的中心:
public class NeuroFuzzy extends NeuralNet{
InputFuzzyLayer inputLayer;
RuleLayer ruleLayer;
NeuroFuzzyLayer outputLayer;
//…
}
更多细节,请参阅edu.packt.neuralnet.neurofuzzy包中的文件。
要为权重集编写神经遗传算法,需要定义遗传操作。让我们创建一个名为NeuroGenetic的类来实现繁殖和变异:
public class NeuroGenetic{
// each element ArrayList<double> is a solution, i.e.
// a set of weights
ArrayList<ArrayList<double>> population;
ArrayList<double> score;
NeuralNet neuralNet;
NeuralDataSet trainingDataSet;
NeuralDataSet testDataSet;
public ArrayList<ArrayList<double>> reproduction(ArrayList<ArrayList<double>> solutions){
// a set of weights is passed as an argument
// the weights are just swapped between them in groups of two
}
public ArrayList<ArrayList<double>> mutation(ArrayList<ArrayList<double>> solutions){
// a random weight can suddenly change its value
}
}
下一步是定义每次迭代的每个权重的评估:
public double evaluation(ArrayList<double> solution){
neuralNet.setAllWeights(solution);
LearningAlgorithm la = new LearningAlgorithm(neuralNet,trainingDataSet);
la.forward();
return la.getOverallGeneralError();
}
最后,我们可以通过以下代码调用神经遗传算法:
public void run{
generatePopulation();
int generation=0;
while(generation<MaxGenerations && bestMSError>MinMSError){
//evaluate all
foreach(ArrayList<double> solution:population){
score.set(i,evaluation(solution));
}
//make a rank
int[] rank = rankAll(score);
//check the best MSE
if(ArrayOperations.min(score)<bestMSError){
bestMSError = ArrayOperations.min(score);
bestSolution = population.get(ArrayOperations.indexMin(score));
}
//perform a selection for reproduction
ArrayList<ArrayList<double>> newSolutions = reproduction(
selectionForReproduction(rank,score,population));
//perform selection for mutation
ArrayList<ArrayList<double>> mutated = mutation(selectionForMutation(rank,score,population));
//perform selection for elimintation
if(generation>5)
eliminateWorst(rank,score,population);
//add the new elements
population.append(newSolutions);
population.append(mutated);
}
System.out.println("Best MSE found:"+bestMSError);
}
摘要
在这一章的最后,我们向读者展示了在这个领域下一步应该做什么。由于更偏向理论,这一章更多地关注功能和信息,而不是实际实现,因为这将对于一个入门书籍来说过于复杂。在每种情况下,都提供了一个简单的代码示例,以提示如何进一步实现深度神经网络。然后鼓励读者修改前几章的代码,将它们适应混合神经网络,并比较结果。作为一个非常动态和新兴的研究领域,在每一个时刻都有新的方法和算法在开发中,我们在参考文献中提供了一份出版物列表,以保持对这个主题的最新了解。
附录A.参考文献
这里有一些参考文献,供读者查阅,如果想要了解更多关于本书中涉及的具体主题。
第1章:神经网络入门
普里迪,凯文·L.,凯勒,保罗·E.,《人工神经网络:入门》,SPIE出版社,ISBN-13 9780819459879,1月1日,2005年。
利文尼克,J.,《简单Java:Java编程入门》,查尔斯河媒体;第1版,ISBN-13 9781584504269,9月8日,2005年。
第2章:让神经网络学习
塞约诺夫斯基,特伦斯·J.,《神经网络学习算法》,神经网络计算机第41卷,斯普林格学习版,第291-300页,1989年。
阮,德瑞克·H.,维德罗,伯纳德,《用于自学习控制系统的神经网络》,IEEE控制系统杂志,1990年4月。
第3章:感知器和监督学习
海金,西蒙·O.,《神经网络与学习机器》,普伦蒂斯·霍尔,第3版,ISBN-13 9780131471399,2008年11月28日。
鲁梅尔哈特,大卫·E.,辛顿,杰弗里·E.,威廉姆斯,罗纳德·J.,《通过反向传播错误学习表示”,自然第323卷(6088),第533-536页,1986年10月8日。
利文伯格,K.,《解决某些最小二乘法非线性问题的方法》,应用数学季刊,第2卷,第164-168页,1944年。
马尔夸尔特,D.,《非线性参数最小二乘估计的算法》,应用数学杂志,第11卷(2),第431-441页,1963年。
黄,广B.,朱,秦Y.,谢,奇K.,《极端学习机:前馈神经网络的新的学习方案》,IEEE国际神经网络联合会议论文集,2004年。
第4章:自组织映射
杜达,理查德·O,哈特,彼得·E.,斯托克,大卫·G.,《无监督学习和聚类:模式分类第2版》, Wiley,ISBN-10 0471056693,2001年。
万·胡尔,马克·M.,《自组织映射》,自然计算手册,第585-622页,ISBN-13 978-3-540-92910-9,2012年。
鲁梅尔哈特,大卫·E.,齐普泽,大卫,竞争学习中的特征发现,认知科学第9卷第1期,第75-112页,1985年。
科霍内,特乌沃,拓扑正确特征图的自组织形成,生物控制论,第43卷(1),第59-69页,1982年。
第5章:天气预报
多伊德,S.,韦尔登,S.,《研究中的统计学》,Wiley,ISBN-10 0471086029,第230页,1983年。
佩尔,朱迪亚,《因果关系:模型、推理和推断》,剑桥大学出版社,ISBN-10 0521773628,2000年。
佛图纳,路易吉,格拉齐亚尼,萨尔瓦托雷,里佐,亚历山德罗,西比利亚,玛丽亚·G.,《用于工业过程监控和控制的软传感器》,斯普林格工业控制进展,ISBN-13 9781846284793,2007年。
科恩,雅各布,科恩,帕特里夏,韦斯特,斯蒂芬·G.,艾肯,利奥娜·S.,《行为科学中的应用多重回归/相关分析》,劳特利奇,ISBN 9781134800940,2013年。
斯帕茨,克里斯,《基本统计学:分布的故事》,Cengage学习,ISBN 9780495383932,2007年。
第6章:疾病诊断分类
Altman, Edward I.,Marco, Giancarlo,Varetto, Franco,企业困境诊断:使用线性判别分析和神经网络的比较(意大利经验),银行与金融杂志第18卷,第505-529页,1994年。
Bishop, C.M. 神经网络模式识别,牛津大学出版社,ISBN-10 0198538499,1995年。
Al-Shayea, Qeethara K.,医学诊断中的人工神经网络,计算机科学问题国际杂志,第8卷第2期,第150-154页,2011年3月。
Freedman, David A.,统计模型:理论与实践,剑桥大学出版社,2009年。
Fawcett, Tom,ROC分析的介绍,模式识别信函,第27卷第8期,第861-874页,2006年。
第7章:聚类客户档案
Du, K.L.,聚类:神经网络方法,神经网络,第23卷第1期,第89-107页,2010年1月。
Park, J,Sandberg, I.W.,径向基函数网络的全局逼近,神经计算,第3卷第2期,第246-257页,1991年。
Wall, Michael E.,Rechtsteiner Andreas,Rocha, Luis M.,奇异值分解和主成分分析,微阵列数据分析实用方法,第91-109页,2003年。
Cross Glendon,Thompson Wayne,理解你的客户:电信行业获取客户洞察和预测风险的细分技术,SAS全球论坛,2008年。
Bozdogan, Hamparsun,赤池信息准则与信息复杂性的最新发展,数学心理学杂志,第44卷第1期,第62-91页,2000年3月。
第8章:文本识别
Basu, Jayanta K.,Bhattacharyya Debnath,Kim, Tai-hoon,在模式识别中使用人工神经网络,软件工程及其应用国际杂志,第4卷第2期,第2010年4月。
Shrivastava, Vivek,Sharma, Navdeep,基于人工神经网络的基于光学字符识别,信号与图像处理:国际杂志(SIPIJ),第3卷第5期,第2012年10月。
第9章:优化和调整神经网络
Utrans, J,Moody J.,Rehfuss, S.,Siegelmann, H.,神经网络输入变量选择:应用于预测美国商业周期,金融工程计算智能,IEEE/IAFE 1995年会议论文集。
Saxén, H.,Pettersson, F.,前馈神经网络输入选择和结构选择方法,计算机与化学工程,第30卷第6-7期,第1048-1045页,2006年5月15日。
Souza, Alan M.F.,Affonso, Carolina M.,Soares, Fábio M.,De Oliveira, Roberto C.L.,气体处理中心氟化铝推断的软传感器,智能数据工程与自动学习2012年,计算机科学讲义第7435卷,第294-302页,斯普林格柏林海德堡实验室,2012年。
Jollife, I.T. 主成分分析,第2版。斯普林格威利出版社,2002年。
Karmin E.D.,用于剪枝反向传播训练的神经网络的简单方法,IEEE神经网络交易,第239-242页,1990年6月。
吉尔,P.E,穆雷,W.赖特,M.H,实用优化,学术出版社:伦敦,1981年。
卡彭特,盖尔·A,格罗斯伯格,斯蒂芬,自适应共振理论,大脑理论与神经网络手册,第2版,第1-11页,2002年。
张国强,胡,迈克尔·Y,帕图沃,爱迪·B,因多,丹尼尔·C,破产预测中的人工神经网络:通用框架和交叉验证分析,欧洲运筹学杂志,第116卷,第1期,第16-32页,1999年7月。
第10章:神经网络当前趋势
施密德胡贝尔,于尔根,神经网络中的深度学习:概述,神经网络,爱思唯尔出版社,第61卷,第85-117页,2015年1月。
克里泽夫斯基,亚历克斯,苏茨克维尔,伊利亚,辛顿,杰弗里,使用深度卷积神经网络进行ImageNet分类,神经信息处理系统进展,第85-117页,2012年。
纳克,德特莱夫,克拉沃恩,弗兰克,克鲁斯,鲁道夫,神经模糊系统基础,约翰·威利和桑斯出版社,ISBN 047197150,1997年。
蒙大拿,大卫·J,戴维斯,劳伦斯,使用遗传算法训练前馈神经网络,IJCAI,第89卷,第762-767页,1989年。




















浙公网安备 33010602011771号