原文:
annas-archive.org/md5/d89c1af559e5f85f6b80756c31400c3f
译者:飞龙
前言
数据的持续增长以及对基于这些数据做出越来越复杂决策的需求,正在带来巨大的障碍,阻碍组织通过传统的分析方法及时获取见解。
为了寻找有意义的价值和见解,深度学习得以发展,深度学习是基于学习多个抽象层次的机器学习算法分支。神经网络作为深度学习的核心,被广泛应用于预测分析、计算机视觉、自然语言处理、时间序列预测以及执行大量其他复杂任务。
至今,大多数深度学习书籍都是以 Python 编写的。然而,本书是为开发者、数据科学家、机器学习从业者和深度学习爱好者设计的,旨在帮助他们利用 Deeplearning4j(一个基于 JVM 的深度学习框架)的强大功能构建强大、稳健和准确的预测模型,并结合其他开源 Java API。
在本书中,你将学习如何使用前馈神经网络、卷积神经网络、递归神经网络、自编码器和因子分解机开发实际的 AI 应用。此外,你还将学习如何在分布式环境下通过 GPU 进行深度学习编程。
完成本书后,你将熟悉机器学习技术,特别是使用 Java 进行深度学习,并能够在研究或商业项目中应用所学知识。总之,本书并非从头到尾逐章阅读。你可以跳到某一章,选择与你所要完成的任务相关的内容,或者是一个激发你兴趣的章节。
祝阅读愉快!
本书的读者群体
本书对于希望通过利用基于 JVM 的 Deeplearning4j (DL4J)、Spark、RankSys 以及其他开源库的强大功能来开发实际深度学习项目的开发者、数据科学家、机器学习从业者和深度学习爱好者非常有用。需要具备一定的 Java 基础知识。然而,如果你有一些关于 Spark、DL4J 和基于 Maven 的项目管理的基本经验,将有助于更快速地掌握这些概念。
本书内容
第一章,深度学习入门,解释了机器学习和人工神经网络作为深度学习核心的一些基本概念。然后简要讨论了现有的和新兴的神经网络架构。接着,介绍了深度学习框架和库的各种功能。随后,展示了如何使用基于 Spark 的多层感知器(MLP)解决泰坦尼克号生还预测问题。最后,讨论了与本项目及深度学习领域相关的一些常见问题。
第二章,使用递归类型网络进行癌症类型预测,展示了如何开发一个深度学习应用程序,用于从高维基因表达数据集中进行癌症类型分类。首先,它执行必要的特征工程,以便数据集能够输入到长短期记忆(LSTM)网络中。最后,讨论了一些与该项目和 DL4J 超参数/网络调整相关的常见问题。
第三章,使用卷积神经网络进行多标签图像分类,演示了如何在 DL4J 框架上,使用 CNN 处理多标签图像分类问题的端到端项目。它讨论了如何调整超参数以获得更好的分类结果。
第四章,使用 Word2Vec 和 LSTM 网络进行情感分析,展示了如何开发一个实际的深度学习项目,将评论文本分类为正面或负面情感。将使用大规模电影评论数据集来训练 LSTM 模型,并且 Word2Vec 将作为神经网络嵌入。最后,展示了其他评论数据集的示例预测。
第五章,图像分类的迁移学习,展示了如何开发一个端到端项目,利用预训练的 VGG-16 模型解决猫狗图像分类问题。我们将所有内容整合到一个 Java JFrame 和 JPanel 应用程序中,以便让整个流程更容易理解,进行示例的对象检测。
第六章,使用 YOLO、JavaCV 和 DL4J 进行实时对象检测,展示了如何开发一个端到端项目,在视频片段连续播放时,从视频帧中检测对象。预训练的 YOLO v2 模型将作为迁移学习使用,而 JavaCV API 将用于视频帧处理,基于 DL4J 进行开发。
第七章,使用 LSTM 网络进行股价预测,展示了如何开发一个实际的股票开盘、收盘、最低、最高价格或交易量预测项目,使用 LSTM 在 DL4J 框架上进行训练。将使用来自实际股市数据集的时间序列来训练 LSTM 模型,并且模型仅预测 1 天后的股价。
第八章,云端分布式深度学习——使用卷积 LSTM 网络进行视频分类,展示了如何开发一个端到端项目,使用结合 CNN 和 LSTM 网络在 DL4J 上准确分类大量视频片段(例如 UCF101)。训练将在 Amazon EC2 GPU 计算集群上进行。最终,这个端到端项目可以作为从视频中进行人体活动识别的入门项目。
第九章,使用深度强化学习玩GridWorld 游戏,专注于设计一个由批评和奖励驱动的机器学习系统。接着,展示了如何使用 DL4J、RL4J 和神经网络 Q 学习开发一个 GridWorld 游戏,Q 函数由该网络担任。
第十章,使用因式分解机开发电影推荐系统,介绍了使用因式分解机开发一个样例项目,用于预测电影的评分和排名。接着,讨论了基于矩阵因式分解和协同过滤的推荐系统的理论背景,然后深入讲解基于 RankSys 库的因式分解机的项目实现。
第十一章,讨论、当前趋势与展望,总结了所有内容,讨论了已完成的项目以及一些抽象的收获。然后提供了一些改进建议。此外,还涵盖了其他现实生活中的深度学习项目的扩展指南。
为了最大限度地发挥本书的价值
所有示例都已使用 Deeplearning4j 和一些 Java 开源库实现。具体来说,以下 API/工具是必需的:
-
Java/JDK 版本 1.8
-
Spark 版本 2.3.0
-
Spark csv_2.11 版本 1.3.0
-
ND4j 后端版本为 nd4j-cuda-9.0-platform(用于 GPU),否则为 nd4j-native
-
ND4j 版本 >=1.0.0-alpha
-
DL4j 版本 >=1.0.0-alpha
-
Datavec 版本 >=1.0.0-alpha
-
Arbiter 版本 >=1.0.0-alpha
-
Logback 版本 1.2.3
-
JavaCV 平台版本 1.4.1
-
HTTP 客户端版本 4.3.5
-
Jfreechart 1.0.13
-
Jcodec 0.2.3
-
Eclipse Mars 或 Luna(最新版本)或 IntelliJ IDEA
-
Maven Eclipse 插件(2.9 或更高版本)
-
Eclipse 的 Maven 编译插件(2.3.2 或更高版本)
-
Eclipse 的 Maven assembly 插件(2.4.1 或更高版本)
关于操作系统:推荐使用 Linux 发行版(包括 Debian、Ubuntu、Fedora、RHEL、CentOS)。具体来说,例如,对于 Ubuntu,建议安装 14.04(LTS)64 位(或更高版本)的完整安装,或使用 VMWare player 12 或 Virtual box。你也可以在 Windows(XP/7/8/10)或 Mac OS X(10.4.7 及以上版本)上运行 Spark 作业。
关于硬件配置:需要一台配备 Core i5 处理器、约 100GB 磁盘空间和至少 16GB 内存的机器或服务器。此外,如果你希望在 GPU 上进行训练,还需要安装 Nvidia GPU 驱动程序,并配置 CUDA 和 CuDNN。如果要运行大型作业,需要足够的存储空间(具体取决于你处理的数据集大小),最好至少有 50GB 的空闲磁盘存储(用于独立作业和 SQL 数据仓库)。
下载示例代码文件
你可以从你在www.packtpub.com的账户下载本书的示例代码文件。如果你在其他地方购买了本书,可以访问www.packtpub.com/support并注册以直接将文件通过电子邮件发送给你。
您可以按照以下步骤下载代码文件:
-
登录或注册 www.packtpub.com。
-
选择“支持”选项卡。
-
点击“代码下载和勘误”。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
文件下载完成后,请确保使用最新版本的工具解压或提取文件夹:
-
适用于 Windows 的 WinRAR/7-Zip
-
适用于 Mac 的 Zipeg/iZip/UnRarX
-
适用于 Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/Java-Deep-Learning-Projects
。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。
我们还提供了来自我们丰富书籍和视频目录的其他代码包,您可以在 github.com/PacktPublishing/
上查看。快去看看吧!
下载彩色图片
我们还提供了一个包含本书中使用的屏幕截图/图表的彩色图片的 PDF 文件。您可以在此处下载:www.packtpub.com/sites/default/files/downloads/JavaDeepLearningProjects_ColorImages.pdf
。
使用的约定
本书中使用了一些文本约定。
CodeInText
:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:“然后,我解压并将每个 .csv
文件复制到一个名为 label
的文件夹中。”
代码块如下所示:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
当我们希望引起您对代码块中特定部分的注意时,相关行或项将以粗体显示:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
粗体:表示新术语、重要词汇或在屏幕上看到的单词。例如,菜单或对话框中的词汇会以这种方式出现在文本中。示例如下:“我们随后读取并处理图像,生成 PhotoID | 向量地图。”
警告或重要说明将以如下形式显示。
提示和技巧会以如下形式出现。
与我们联系
我们始终欢迎读者的反馈。
一般反馈:请发送电子邮件至 feedback@packtpub.com
,并在邮件主题中注明书名。如果您对本书的任何方面有疑问,请通过 questions@packtpub.com
与我们联系。
勘误:虽然我们已尽一切努力确保内容的准确性,但仍可能会有错误。如果您在本书中发现错误,请向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入详细信息。
盗版:如果您在互联网上发现任何形式的非法复制品,我们将非常感激您提供该位置地址或网站名称。请通过 copyright@packtpub.com
与我们联系,并附上链接。
如果您有兴趣成为作者:如果您在某个领域具有专长,并且有兴趣写作或参与编写一本书,请访问 authors.packtpub.com。
评论
请留下您的评论。阅读并使用本书后,为什么不在您购买本书的网站上留下评论呢?潜在读者可以看到并参考您的公正意见来做出购买决策,我们 Packt 可以了解您对我们产品的看法,作者们也可以看到您对其书籍的反馈。谢谢!
欲了解有关 Packt 的更多信息,请访问 packtpub.com。
第一章:深度学习入门
在本章中,我们将解释一些基础的机器学习(ML)和**深度学习(DL)**概念,这些概念将在后续的所有章节中使用。我们将从简要介绍机器学习开始。接下来,我们将讲解深度学习,它是机器学习的一个新兴分支。
我们将简要讨论一些最著名和广泛使用的神经网络架构。接下来,我们将了解深度学习框架和库的各种特性。然后,我们将学习如何准备编程环境,在此基础上使用一些开源深度学习库,如**DeepLearning4J (DL4J)**进行编程。
然后我们将解决一个非常著名的机器学习问题:泰坦尼克号生存预测。为此,我们将使用基于 Apache Spark 的多层感知器(MLP)分类器来解决这个问题。最后,我们将看到一些常见问题解答,帮助我们将深度学习的基本理解推广到更广泛的应用。简而言之,以下主题将被覆盖:
-
机器学习的简单介绍
-
人工神经网络(ANNs)
-
深度神经网络架构
-
深度学习框架
-
从灾难中学习深度学习——使用 MLP 进行泰坦尼克号生存预测
-
常见问题解答(FAQ)
机器学习的简单介绍
机器学习方法基于一组统计和数学算法,以执行诸如分类、回归分析、概念学习、预测建模、聚类和挖掘有用模式等任务。因此,通过使用机器学习,我们旨在改善学习体验,使其变得自动化。结果,我们可能不需要完全的人类互动,或者至少我们可以尽可能减少这种互动的程度。
机器学习算法的工作原理
我们现在引用 Tom M. Mitchell 的经典机器学习定义(《机器学习,Tom Mitchell,McGraw Hill》),他从计算机科学的角度解释了学习真正意味着什么:
“如果一个计算机程序在经验 E 的基础上,在某些任务类别 T 和性能度量 P 的衡量下,其在任务 T 上的表现通过经验 E 得到提升,那么我们就说该程序从经验中学习。”
根据这个定义,我们可以得出结论:计算机程序或机器可以执行以下任务:
-
从数据和历史中学习
-
通过经验提升
-
迭代优化一个可以用于预测问题结果的模型
由于它们是预测分析的核心,几乎我们使用的每个机器学习算法都可以视为一个优化问题。这涉及到找到最小化目标函数的参数,例如,像成本函数和正则化这样的加权和。通常,一个目标函数有两个组成部分:
-
一个正则化器,用来控制模型的复杂性
-
损失,衡量模型在训练数据上的误差。
另一方面,正则化参数定义了在最小化训练误差与模型复杂度之间的权衡,以避免过拟合问题。如果这两个组件都是凸的,那么它们的和也是凸的;否则,它是非凸的。更详细地说,在使用机器学习(ML)算法时,目标是获取能够在预测时返回最小误差的函数的最佳超参数。因此,使用凸优化技术,我们可以最小化该函数,直到它收敛到最小误差。
由于问题是凸的,通常更容易分析算法的渐进行为,这展示了当模型观察到越来越多的训练数据时,它的收敛速度如何。机器学习的挑战在于使模型训练能够识别复杂模式,并且不仅能以自动化的方式做出决策,还能尽可能地智能地做出决策。整个学习过程需要输入数据集,这些数据集可以被拆分(或已经提供)为三种类型,具体如下:
-
训练集是来自历史或实时数据的知识库,用于拟合机器学习算法的参数。在训练阶段,机器学习模型利用训练集来找到网络的最佳权重,并通过最小化训练误差来达到目标函数。在这里,使用反向传播规则(或其他更高级的优化器与适当的更新器;稍后会讨论)来训练模型,但所有超参数必须在学习过程开始之前设置**。**
-
验证集是一组用于调整机器学习模型参数的示例。它确保模型经过良好的训练,并能很好地泛化,从而避免过拟合。一些机器学习实践者也将其称为开发集或dev 集。
-
测试集用于评估训练模型在未见过的数据上的表现。此步骤也称为模型推理。在对测试集中的最终模型进行评估后(即,当我们对模型的表现完全满意时),我们不需要进一步调整模型,训练好的模型可以部署到生产环境中。
一个常见的做法是将输入数据(在必要的预处理和特征工程后)拆分为 60%的训练数据,10%的验证数据和 20%的测试数据,但这实际上取决于具体使用案例。此外,有时我们需要根据数据集的可用性和质量对数据进行上采样或下采样。
此外,学习理论使用的是源自概率论和信息论的数学工具。将简要讨论三种学习范式:
-
有监督学习
-
无监督学习
-
强化学习
以下图表总结了三种学习类型及其所解决的问题:
学习类型及相关问题
监督学习
监督学习是最简单且最著名的自动学习任务。它基于一组预定义的示例,其中每个输入所属的类别已经知道。图 2展示了监督学习的典型工作流程。
一位参与者(例如,机器学习实践者、数据科学家、数据工程师、机器学习工程师等)执行提取转换加载(ETL)及必要的特征工程(包括特征提取、选择等),以获得具有特征和标签的适当数据。然后他执行以下操作:
-
将数据拆分为训练集、开发集和测试集
-
使用训练集训练机器学习模型
-
验证集用于验证训练是否过拟合以及正则化
-
然后,他在测试集上评估模型的表现(即未见过的数据)
-
如果性能不令人满意,他可以进行额外的调优,以通过超参数优化获得最佳模型
-
最后,他将最佳模型部署到生产环境中
监督学习的实际应用
在整个生命周期中,可能会有多个参与者参与(例如,数据工程师、数据科学家或机器学习工程师),他们独立或协作执行每个步骤。
监督学习的任务包括分类和回归;分类用于预测数据点属于哪个类别(离散值),而回归用于预测连续值。换句话说,分类任务用于预测类属性的标签,而回归任务则用于对类属性进行数值预测。
在监督学习的背景下,不平衡数据指的是分类问题,其中不同类别的实例数量不平衡。例如,如果我们有一个仅针对两个类别的分类任务,平衡数据意味着每个类别都有 50%的预先分类示例。
如果输入数据集稍微不平衡(例如,某一类占 60%,另一类占 40%),学习过程将要求将输入数据集随机拆分为三个子集,其中 50%用于训练集,20%用于验证集,剩余 30%用于测试集。
无监督学习
在无监督学习中,训练阶段将输入集提供给系统。与监督学习不同,输入对象没有被标记其类别。对于分类任务,我们假设给定一个正确标记的数据集。然而,在现实世界中收集数据时,我们不总是拥有这种优势。
例如,假设你在硬盘的一个拥挤且庞大的文件夹中有一大堆完全合法的、没有盗版的 MP3 文件。在这种情况下,如果我们无法直接访问它们的元数据,我们如何可能将歌曲归类呢?一种可能的方法是混合各种机器学习技术,但聚类往往是最好的解决方案。
那么,假设你能构建一个聚类预测模型,帮助自动将相似的歌曲分组,并将它们组织成你最喜欢的类别,如乡村、说唱、摇滚等。简而言之,无监督学习算法通常用于聚类问题。下图给我们展示了应用聚类技术解决此类问题的思路:
聚类技术 —— 一种无监督学习的例子
尽管数据点没有标签,但我们仍然可以进行必要的特征工程和对象分组,将属于同一组的对象(称为聚类)聚集在一起。这对于人类来说并不容易。标准方法是定义两个对象之间的相似度度量,然后寻找任何比其他聚类中的对象更相似的对象群集。一旦我们完成了数据点(即 MP3 文件)的聚类并完成验证,我们就能知道数据的模式(即,哪些类型的 MP3 文件属于哪个组)。
强化学习
强化学习是一种人工智能方法,专注于通过与环境的交互来学习系统。在强化学习中,系统的参数会根据从环境中获得的反馈进行调整,而环境又会对系统做出的决策提供反馈。下图展示了一个人在做决策,以便到达目的地。
让我们以你从家到工作地点的路线为例。在这种情况下,你每天都走相同的路线。然而,某天你突然好奇,决定尝试另一条路线,目的是寻找最短的路径。这个尝试新路线与坚持走最熟悉路线之间的两难困境,就是探索与利用的一个例子:
一个智能体始终尝试到达目的地
我们可以看一个更多的例子,假设有一个系统模拟一个棋手。为了提高其表现,系统利用先前动作的结果;这样的系统被称为强化学习系统。
将机器学习任务整合在一起
我们已经了解了机器学习算法的基本工作原理。接下来我们了解了基本的机器学习任务,以及它们如何形成特定领域的问题。现在让我们来看一下如何总结机器学习任务以及一些应用,如下图所示:
来自不同应用领域的机器学习任务及一些应用案例
然而,前面的图表只列出了使用不同机器学习任务的一些应用案例。在实践中,机器学习在许多应用场景中都有广泛应用。我们将在本书中尽量涵盖其中的一些案例。
深入了解深度学习
以往在常规数据分析中使用的简单机器学习方法已经不再有效,应该被更强大的机器学习方法所替代。尽管传统的机器学习技术允许研究人员识别相关变量的组或聚类,但随着大规模和高维数据集的增加,这些方法的准确性和有效性逐渐降低。
这里出现了深度学习,它是近年来人工智能领域最重要的进展之一。深度学习是机器学习的一个分支,基于一套算法,旨在尝试对数据中的高级抽象进行建模。
深度学习是如何将机器学习提升到一个新水平的?
简而言之,深度学习算法大多是一些人工神经网络(ANN),它们能够更好地表示大规模数据集,从而建立能够深入学习这些表示的模型。如今,这不仅仅局限于人工神经网络,实际上,理论上的进展以及软件和硬件的改进都是我们能够走到今天这一步的必要条件。在这方面,Ian Goodfellow 等人(《深度学习》,MIT 出版社,2016 年)将深度学习定义如下:
“深度学习是一种特定类型的机器学习,通过学习将世界表示为一个嵌套的概念层级结构,在这个层级中,每个概念都是相对于更简单的概念来定义的,更抽象的表示是通过较不抽象的表示来计算的,从而实现了巨大的能力和灵活性。”
让我们举个例子;假设我们想开发一个预测分析模型,比如一个动物识别器,在这种情况下,我们的系统需要解决两个问题:
-
用于判断一张图片是猫还是狗
-
用于将狗和猫的图片进行聚类。
如果我们使用典型的机器学习方法来解决第一个问题,我们必须定义面部特征(如耳朵、眼睛、胡须等),并编写一个方法来识别在分类特定动物时哪些特征(通常是非线性的)更为重要。
然而,与此同时,我们无法解决第二个问题,因为用于图像聚类的传统机器学习算法(例如k-means)无法处理非线性特征。深度学习算法将把这两个问题提升一个层次,最重要的特征将在确定哪些特征对分类或聚类最为重要后自动提取。
相比之下,当使用传统的机器学习算法时,我们必须手动提供这些特征。总结来说,深度学习的工作流程如下:
-
深度学习算法首先会识别在聚类猫或狗时最相关的边缘。接着,它会尝试以层级方式找到各种形状和边缘的组合。这个步骤叫做 ETL(提取、转换、加载)。
-
经过多次迭代后,复杂概念和特征的层级识别被执行。然后,基于已识别的特征,深度学习算法自动决定哪些特征在统计上对分类动物最为重要。这个步骤叫做特征提取。
-
最后,算法去掉标签列,使用自编码器(AEs)进行无监督训练,以提取潜在特征,再将这些特征分配给 k-means 进行聚类。
-
然后,聚类分配硬化损失(CAH 损失)和重建损失被联合优化,以实现最优的聚类分配。深度嵌入聚类(详见
arxiv.org/pdf/1511.06335.pdf
)就是这种方法的一个例子。我们将在第十一章中讨论基于深度学习的聚类方法,讨论、当前趋势与展望。
到目前为止,我们看到深度学习系统能够识别图像代表的是什么。计算机看图像的方式与我们不同,因为它只知道每个像素的位置和颜色。通过深度学习技术,图像被分解成多个分析层次。
在较低的层次上,软件分析例如一小块像素网格,任务是检测某种颜色或其不同的色调。如果它发现了什么,它会通知下一层,下一层会检查该颜色是否属于更大的形态,比如一条线。这个过程会一直持续到更高层次,直到你理解图像展示的内容。下图展示了我们在图像分类系统中讨论的内容:
在处理狗与猫分类问题时的深度学习系统工作原理
更准确地说,前述的图像分类器可以逐层构建,如下所示:
-
第一层:算法开始识别原始图像中的暗像素和亮像素。
-
第二层:算法接着识别边缘和形状。
-
第三层:接下来,算法识别更复杂的形状和物体。
-
第四层:算法接着学习哪些物体定义了人脸。
尽管这只是一个非常简单的分类器,但能够进行这些操作的软件如今已经非常普及,广泛应用于面部识别系统,或例如在 Google 中通过图像进行搜索的系统。这些软件是基于深度学习算法的。
相反,通过使用线性机器学习(ML)算法,我们无法构建这样的应用程序,因为这些算法无法处理非线性的图像特征。而且,使用机器学习方法时,我们通常只处理少数几个超参数。然而,当神经网络进入这个领域时,事情变得复杂了。在每一层中,都有数百万甚至数十亿个超参数需要调整,以至于成本函数变得非凸。
另一个原因是隐藏层中使用的激活函数是非线性的,因此成本是非凸的。我们将在后续章节中更详细地讨论这一现象,但先快速了解一下人工神经网络(ANNs)。
人工神经网络(ANNs)
人工神经网络(ANNs)基于深度学习的概念。它们通过多个神经元之间的相互通信,代表了人类神经系统的工作方式,这些神经元通过轴突相互联系。
生物神经元
人工神经网络(ANNs)的工作原理受到人脑工作的启发,如图 7所示。受体接收来自内部或外部世界的刺激;然后将信息传递给生物神经元进行进一步处理。除了另一个长的延伸部分称为轴突外,还有许多树突。
在其末端,有一些微小的结构称为突触末端,用于将一个神经元连接到其他神经元的树突。生物神经元接收来自其他神经元的短电流冲动,称为信号,并以此触发自己的信号:
生物神经元的工作原理
我们可以总结出,神经元由一个细胞体(也称为胞体)、一个或多个树突用于接收来自其他神经元的信号,以及一个轴突用于传递神经元产生的信号。
当神经元向其他神经元发送信号时,它处于激活状态。然而,当它接收来自其他神经元的信号时,它处于非激活状态。在空闲状态下,神经元会积累所有接收到的信号,直到达到一定的激活阈值。这一现象促使研究人员提出了人工神经网络(ANN)。
人工神经网络(ANNs)的简史
受到生物神经元工作原理的启发,沃伦·麦卡洛克和沃尔特·皮茨于 1943 年提出了第一个人工神经元模型,作为神经活动的计算模型。这个简单的生物神经元模型,也称为人工神经元(AN),有一个或多个二进制(开/关)输入,只有一个输出。
一个人工神经元只有在它的输入中有超过一定数量的输入处于活动状态时,才会激活它的输出。例如,在这里我们看到几个执行各种逻辑运算的人工神经网络(ANNs)。在这个例子中,我们假设一个神经元只有在至少有两个输入处于活动状态时才会被激活:
执行简单逻辑计算的人工神经网络(ANNs)
这个例子听起来过于简单,但即使是如此简化的模型,也能构建一个人工神经网络。然而,这些网络也可以组合在一起计算复杂的逻辑表达式。这个简化模型启发了约翰·冯·诺依曼、马文·明斯基、弗兰克·罗森布拉特等人在 1957 年提出了另一个模型——感知器。
感知器是过去 60 年里我们见过的最简单的人工神经网络架构之一。它基于一种略有不同的人工神经元——线性阈值单元(LTU)。唯一的区别是输入和输出现在是数字,而不是二进制的开/关值。每个输入连接都与一个权重相关联。LTU 计算其输入的加权和,然后对该和应用一个阶跃函数(类似于激活函数的作用),并输出结果:
左图表示一个线性阈值单元(LTU),右图显示一个感知器
感知器的一个缺点是其决策边界是线性的。因此,它们无法学习复杂的模式。它们也无法解决一些简单的问题,比如异或(XOR)。然而,后来通过堆叠多个感知器(称为 MLP),感知器的局限性在某种程度上得到了消除。
人工神经网络是如何学习的?
基于生物神经元的概念,人工神经元的术语和思想应运而生。与生物神经元类似,人工神经元由以下部分组成:
-
一个或多个输入连接,用于从神经元聚合信号
-
一个或多个输出连接,用于将信号传递到其他神经元
-
一个激活函数,用于确定输出信号的数值
神经网络的学习过程被配置为迭代过程,即对权重的优化(更多内容见下一节)。在每一轮训练中,权重都会被更新。一旦训练开始,目标是通过最小化损失函数来生成预测。然后,网络的性能将在测试集上进行评估。
现在我们知道了人工神经元的简单概念。然而,单单生成一些人工信号并不足以学习复杂的任务。尽管如此,一个常用的监督学习算法是反向传播算法,它被广泛用于训练复杂的人工神经网络。
人工神经网络和反向传播算法
反向传播算法旨在最小化当前输出与期望输出之间的误差。由于网络是前馈型的,激活流总是从输入单元向输出单元前进。
成本函数的梯度会反向传播,网络权重会被更新;该方法可以递归地应用于任何数量的隐藏层。在这种方法中,两种阶段之间的结合是非常重要的。简而言之,训练过程的基本步骤如下:
-
用一些随机(或更先进的 XAVIER)权重初始化网络
-
对于所有训练样本,按照接下来的步骤执行前向和后向传播
前向和后向传播
在前向传播中,会执行一系列操作来获得一些预测或评分。在这个操作中,创建一个图,将所有依赖操作按从上到下的方式连接起来。然后计算网络的误差,即预测输出与实际输出之间的差异。
另一方面,反向传播主要涉及数学运算,比如为所有微分操作(即自动微分方法)创建导数,从上到下(例如,测量损失函数以更新网络权重),对图中的所有操作进行处理,然后应用链式法则。
在这个过程中,对于所有层,从输出层到输入层,展示了网络层的输出与正确的输入(误差函数)。然后,调整当前层的权重以最小化误差函数。这是反向传播的优化步骤。顺便说一下,自动微分方法有两种类型:
-
反向模式:关于所有输入的单个输出的导数
-
前向模式:关于一个输入的所有输出的导数
反向传播算法以这样的方式处理信息,使得网络在学习迭代过程中减少全局误差;然而,这并不保证能够到达全局最小值。隐藏单元的存在和输出函数的非线性意味着误差的行为非常复杂,具有许多局部最小值。
这个反向传播步骤通常会执行成千上万次,使用许多训练批次,直到模型参数收敛到能最小化代价函数的值。当验证集上的误差开始增大时,训练过程结束,因为这可能标志着过拟合阶段的开始。
权重和偏置
除了神经元的状态外,还考虑突触权重,它影响网络中的连接。每个权重都有一个数值,表示为 W[ij],它是连接神经元 i 和神经元 j 的突触权重。
突触权重:这个概念源自生物学,指的是两个节点之间连接的强度或幅度,在生物学中对应于一个神经元的激活对另一个神经元的影响程度。
对于每个神经元(也叫单元) i,可以定义一个输入向量 x[i] = (x[1], x[2], … x[n]),并且可以定义一个权重向量 w[i] = (w[i1], w[i2], … w[in])。现在,根据神经元的位置,权重和输出函数决定了单个神经元的行为。然后,在前向传播过程中,隐藏层中的每个单元都会接收到以下信号:
然而,在权重中,还有一种特殊类型的权重叫做偏置单元b。从技术上讲,偏置单元并不与任何前一层连接,因此它没有真正的活动。但偏置b的值仍然可以让神经网络将激活函数向左或向右平移。现在,考虑到偏置单元后,修改后的网络输出可以表示如下:
上述方程表示,每个隐藏单元都会得到输入的总和乘以相应的权重——求和节点。然后,求和节点的结果通过激活函数,激活函数会压缩输出,如下图所示:
人工神经元模型
现在,有一个棘手的问题:我们该如何初始化权重?如果我们将所有的权重初始化为相同的值(例如 0 或 1),每个隐藏神经元都会得到完全相同的信号。让我们试着分析一下:
-
如果所有的权重都初始化为 1,那么每个单元将收到等于输入总和的信号
-
如果所有的权重都为 0,那么情况就更糟了,隐藏层中的每个神经元都会收到零信号
对于网络权重初始化,目前广泛使用 Xavier 初始化方法。它类似于随机初始化,但通常效果更好,因为它可以根据输入和输出神经元的数量自动确定初始化的规模。
有兴趣的读者可以参考这篇出版物以获取详细信息:Xavier Glorot 和 Yoshua Bengio,理解训练深度前馈神经网络的难度:2010 年第 13 届国际人工智能与统计学会议(AISTATS)论文集,地点:意大利撒丁岛 Chia Laguna 度假村;JMLR 卷 9:W&CP。
你可能会想,是否可以在训练普通的 DNN(例如 MLP 或 DBN)时摆脱随机初始化。最近,一些研究人员提到过随机正交矩阵初始化,这种方法比任何随机初始化更适合用于训练 DNN。
在初始化偏置时,我们可以将其初始化为零。但将所有偏置设置为一个小的常数值,例如 0.01,可以确保所有修正线性单元(ReLU)单元能够传播一些梯度。然而,这种方法并不表现良好,也没有展现出一致的改善。因此,推荐将其保持为零。
权重优化
在训练开始之前,网络的参数是随机设置的。然后,为了优化网络权重,使用一种叫做梯度下降法(GD)的迭代算法。通过 GD 优化,我们的网络根据训练集计算代价梯度。然后,通过迭代过程,计算误差函数E的梯度G。
在下图中,误差函数E的梯度G提供了当前值的误差函数最陡坡度的方向。由于最终目标是减少网络误差,梯度下降法沿着相反方向*-G前进,采取小步走。这一迭代过程执行多次,使得误差E逐渐下降,向全局最小值移动。这样,最终目标是达到G = 0的点,表示无法再进行优化:
在搜索误差函数 E 的最小值时,我们朝着误差函数 E 的梯度 G 最小的方向移动
缺点是收敛速度太慢,导致无法满足处理大规模训练数据的需求。因此,提出了一种更快的梯度下降法,称为随机梯度下降法(SGD),这也是深度神经网络(DNN)训练中广泛使用的优化器。在 SGD 中,每次迭代我们只使用来自训练集的一个训练样本来更新网络参数。
我并不是说 SGD 是唯一可用的优化算法,但现在有很多先进的优化器可供选择,例如 Adam、RMSProp、ADAGrad、Momentum 等。或多或少,它们大多数都是 SGD 的直接或间接优化版本。
顺便说一下,术语随机源于基于每次迭代中单个训练样本的梯度是对真实代价梯度的随机近似。
激活函数
为了让神经网络学习复杂的决策边界,我们在某些层上应用非线性激活函数。常用的函数包括 Tanh、ReLU、softmax 及其变种。从技术上讲,每个神经元接收的输入信号是与其连接的神经元的突触权重和激活值的加权和。为了实现这个目的,最广泛使用的函数之一是所谓的Sigmoid 函数。它是逻辑函数的一个特例,定义如下公式:
该函数的定义域包括所有实数,值域为(0, 1)。这意味着神经元输出的任何值(根据其激活状态的计算)都将始终在零和一之间。如下图所示,Sigmoid 函数提供了对神经元饱和度的解释,从不激活(= 0)到完全饱和,发生在预定的最大值(= 1)时。
另一方面,双曲正切(tanh)是另一种激活函数。Tanh 将实值数字压缩到范围*[-1, 1]*内。特别地,数学上,tanh 激活函数可以表示为以下公式:
上述公式可以通过以下图形表示:
Sigmoid 与 tanh 激活函数
通常,在**前馈神经网络(FFNN)**的最后一层,会应用 softmax 函数作为决策边界。这是一个常见的情况,尤其是在解决分类问题时。在概率论中,softmax 函数的输出会被压缩为 K 种不同可能结果的概率分布。然而,softmax 函数也被用于多类别分类方法中,使得网络的输出在各类别之间分布(即,类别的概率分布),并且具有 -1 到 1 或 0 到 1 之间的动态范围。
对于回归问题,我们不需要使用任何激活函数,因为网络生成的是连续值——概率。然而,近年来我看到一些人开始在回归问题中使用**恒等(IDENTITY)**激活函数。我们将在后续章节中讨论这一点。
总结来说,选择合适的激活函数和网络权重初始化是两个决定网络性能的重要问题,能帮助获得良好的训练效果。我们将在后续章节中进一步讨论,探讨如何选择合适的激活函数。
神经网络架构
神经网络有多种架构类型。我们可以将深度学习架构分为四大类:深度神经网络(DNN)、卷积神经网络(CNN)、递归神经网络(RNN)和涌现架构(EA)。
如今,基于这些架构,研究人员提出了许多针对特定领域的变种,以应对不同的研究问题。接下来的章节将简要介绍这些架构,更多的详细分析和应用实例将在本书的后续章节中讨论。
深度神经网络
DNN(深度神经网络)是一种具有复杂和更深层次架构的神经网络,每层包含大量的神经元,并且连接众多。每一层的计算会将后续层的表示转化为更抽象的表示。然而,我们将使用“DNN”这一术语专指多层感知机(MLP)、堆叠自编码器(SAE)和深度信念网络(DBN)。
SAE 和 DBN 使用 AE 和**受限玻尔兹曼机(RBM)**作为架构的构建模块。这些与 MLP 的主要区别在于,训练过程分为两个阶段:无监督预训练和有监督微调。
SAE 和 DBN 分别使用 AE 和 RBM
在无监督预训练中,如前图所示,各层被按顺序堆叠,并以逐层的方式进行训练,类似于使用无标记数据的 AE 或 RBM。之后,在有监督的微调过程中,堆叠一个输出分类层,并通过使用标记数据重新训练整个神经网络来进行优化。
多层感知机
如前所述,单一感知器甚至无法逼近 XOR 函数。为了克服这一限制,将多个感知器堆叠在一起形成 MLP,其中各层作为有向图连接。通过这种方式,信号沿着一个方向传播,从输入层到隐藏层再到输出层,如下图所示:
一个具有输入层、两个隐藏层和输出层的 MLP 架构
从根本上讲,MLP 是最简单的前馈神经网络(FFNN),至少有三层:输入层、隐藏层和输出层。MLP 最早在 1980 年代使用反向传播算法进行训练。
深度信念网络
为了克服 MLP 中的过拟合问题,Hinton 等人提出了 DBN。它使用贪心的逐层预训练算法,通过概率生成模型初始化网络权重。
DBN 由一个可见层和多个层—隐藏单元组成。顶部两层之间有无向对称连接,形成关联记忆,而较低层则从前一层接收自上而下的有向连接。DBN 的构建模块是 RBM,如下图所示,多个 RBM 一个接一个地堆叠在一起形成 DBN:
用于半监督学习的 DBN 配置
单个 RBM 由两层组成。第一层由可见神经元组成,第二层由隐藏神经元组成。图 16展示了一个简单 RBM 的结构,其中神经元按照对称的二分图排列:
RBM 架构
在 DBN 中,首先用输入数据训练 RBM,这被称为无监督预训练,隐藏层代表了通过称为监督微调的贪心学习方法学习到的特征。尽管 DBN 取得了众多成功,但它们正被 AE 所取代。
自动编码器
AE 是一个具有三层或更多层的网络,其中输入层和输出层的神经元数量相同,而中间的隐藏层神经元数量较少。网络被训练以在输出端重现每个输入数据的相同活动模式。
AE 的常见应用包括数据去噪和用于数据可视化的降维。下图展示了 AE 的典型工作原理。它通过两个阶段重建接收到的输入:编码阶段,通常对应于原始输入的降维;解码阶段,能够从编码(压缩)表示中重建原始输入:
自动编码器(AE)的编码和解码阶段
卷积神经网络
CNN 已在计算机视觉(例如,图像识别)领域取得了巨大成功,并被广泛采用。在 CNN 网络中,定义卷积层(conv)的连接方案与 MLP 或 DBN 有显著不同。
重要的是,DNN 对像素如何组织没有先验知识;它并不知道邻近的像素是接近的。CNN 的架构嵌入了这种先验知识。低层通常识别图像中小区域的特征,而高层则将低级特征组合成更大的特征。这在大多数自然图像中效果良好,使得 CNN 在 DNN 之前占据了决定性优势:
常规 DNN 与 CNN
仔细观察前面的图示;左侧是一个常规的三层神经网络,右侧是一个将神经元以三维(宽度、高度和深度)排列的 CNN。在 CNN 架构中,几个卷积层以级联方式连接,每一层后面跟着一个 ReLU 层,然后是池化层,再接几个卷积层(+ReLU),再接另一个池化层,依此类推。
每个卷积层的输出是一组由单个内核滤波器生成的特征图。这些特征图随后可以作为下一层的新输入。CNN 网络中的每个神经元都会产生一个输出,并跟随一个激活阈值,该阈值与输入成正比且没有限制。这种类型的层称为卷积层。以下图示为用于面部识别的 CNN 架构示意图:
用于面部识别的 CNN 架构示意图
递归神经网络
递归神经网络(RNN)是一类人工神经网络(ANN),其中单元之间的连接形成有向循环。RNN 架构最初由 Hochreiter 和 Schmidhuber 于 1997 年提出。RNN 架构有标准的 MLP,并且加上了循环(如下面的图所示),因此它们能够利用 MLP 强大的非线性映射能力;并且具有某种形式的记忆:
RNN 架构
上面的图像展示了一个非常基础的 RNN,包含输入层、两个递归层和一个输出层。然而,这个基础的 RNN 存在梯度消失和爆炸问题,无法建模长期依赖性。因此,设计了更先进的架构,利用输入数据的顺序信息,并在各个构建模块(如感知器)之间使用循环连接。这些架构包括长短期记忆网络(LSTM)、门控递归单元(GRUs)、双向 LSTM等变体。
因此,LSTM 和 GR 可以克服常规 RNN 的缺点:梯度消失/爆炸问题以及长期短期依赖问题。我们将在第二章中详细讨论这些架构。
新兴架构
许多其他新兴的深度学习架构已经被提出,例如 深度时空神经网络 (DST-NNs)、多维递归神经网络 (MD-RNNs),以及 卷积自编码器 (CAEs)。
然而,还有一些新兴的网络,如 CapsNets(CNN 的改进版本,旨在消除常规 CNN 的缺点)、用于图像识别的 RNN,以及用于简单图像生成的 生成对抗网络 (GANs)。除了这些,个性化的因式分解机和深度强化学习也被广泛应用。
残差神经网络
由于有时涉及数百万甚至数十亿个超参数和其他实际因素,训练更深的神经网络非常困难。为了解决这个问题,Kaiming He 等人(见 arxiv.org/abs/1512.03385v1
)提出了一种残差学习框架,简化了训练比以前更深的网络。
他们还明确地将层次结构重新定义为学习参考层输入的残差函数,而不是学习无参考的函数。通过这种方式,这些残差网络更容易优化,并且可以从显著增加的深度中获得更高的准确性。
不利的一面是,简单堆叠残差块来构建网络不可避免地会限制其优化能力。为了克服这一局限性,Ke Zhang 等人还提出了使用多层次残差网络(arxiv.org/abs/1608.02908
)。
生成对抗网络
GANs 是深度神经网络架构,由两个相互对抗的网络组成(因此得名“对抗”)。Ian Goodfellow 等人在一篇论文中介绍了 GANs(详情见 arxiv.org/abs/1406.2661v1
)。在 GANs 中,两个主要组件是 生成器 和 判别器。
生成对抗网络(GANs)的工作原理
生成器将尝试从特定的概率分布中生成数据样本,这些样本与实际对象非常相似。判别器则会判断其输入是来自原始训练集还是来自生成器部分。
胶囊网络
CNNs 在图像分类方面表现优异。然而,如果图像有旋转、倾斜或其他不同的方向,CNNs 的表现会相对较差。即使是 CNN 中的池化操作,也无法在位置不变性方面提供太多帮助。
CNN 中的这一问题促使了 CapsNet 的最新进展,相关论文为 胶囊之间的动态路由(详情见 arxiv.org/abs/1710.09829
),由 Geoffrey Hinton 等人提出。
与常规的 DNN 不同,在 CapsNets 中,核心思想是将更多的层添加到单一层内部。这样,CapsNet 就是一个嵌套的神经网络层集合。我们将在 第十一章 中详细讨论,讨论、当前趋势与展望。
深度学习框架和云平台
在本节中,我们将介绍一些最流行的深度学习框架。然后,我们将讨论一些可以部署/运行深度学习应用程序的云平台。简而言之,几乎所有的库都提供了使用图形处理器加速学习过程的可能性,都是开源发布的,并且是大学研究小组的成果。
深度学习框架
TensorFlow 是一个数学软件,也是一个用于机器智能的开源软件库。由 Google Brain 团队于 2011 年开发,并在 2015 年开源。TensorFlow 最新版本(本书写作时为 v1.8)提供的主要功能包括更快的计算速度、灵活性、可移植性、易于调试、统一的 API、透明的 GPU 计算支持、易于使用和可扩展性。一旦你构建了神经网络模型,并进行了必要的特征工程后,你可以通过绘图或 TensorBoard 轻松地进行交互式训练。
Keras 是一个深度学习库,位于 TensorFlow 和 Theano 之上,提供了一个直观的 API,灵感来源于 Torch。它可能是现存最好的 Python API。DeepLearning4J 将 Keras 作为其 Python API,并通过 Keras 从 Theano 和 TensorFlow 导入模型。
Theano 也是一个用 Python 编写的深度学习框架。它允许使用 GPU,速度是单个 CPU 的 24 倍。在 Theano 中,定义、优化和评估复杂的数学表达式非常直接。
Neon 是一个基于 Python 的深度学习框架,由 Nirvana 开发。Neon 的语法类似于 Theano 的高级框架(例如 Keras)。目前,Neon 被认为是基于 GPU 实现的最快工具,尤其适用于 CNN。但其基于 CPU 的实现相比大多数其他库较为逊色。
PyTorch 是一个庞大的机器学习生态系统,提供大量的算法和功能,包括深度学习和处理各种类型的多媒体数据,特别专注于并行计算。Torch 是一个高度可移植的框架,支持多种平台,包括 Windows、macOS、Linux 和 Android。
Caffe 由 伯克利视觉与学习中心(BVLC)主要开发,是一个因其表达性、速度和模块化而突出的框架。
MXNet *(mxnet.io/
)是一个支持多种语言的深度学习框架,诸如 R、Python、C++ 和 Julia。这个特点很有帮助,因为如果你掌握了这些语言中的任何一种,你将无需走出舒适区就能训练你的深度学习模型。它的后端使用 C++ 和 CUDA 编写,能够像 Theano 一样管理自己的内存。
Microsoft Cognitive Toolkit(CNTK)是微软研究院推出的统一深度学习工具包,旨在简化多 GPU 和服务器上的流行模型类型训练与组合。CNTK 实现了高效的 CNN 和 RNN 训练,适用于语音、图像和文本数据。它支持 cuDNN v5.1 进行 GPU 加速。
DeepLearning4J 是首批为 Java 和 Scala 编写的商业级开源分布式深度学习库之一。它还提供了对 Hadoop 和 Spark 的集成支持。DeepLearning4J 旨在用于分布式 GPU 和 CPU 环境中的企业应用。
DeepLearning4J 旨在成为前沿技术且即插即用,更注重约定而非配置,这使得非研究人员能够快速原型开发。以下库可以与 DeepLearning4J 集成,无论你是用 Java 还是 Scala 开发机器学习应用,都将让你的 JVM 使用体验更加便捷。
ND4J 就像是 JVM 上的 NumPy,提供了诸如矩阵创建、加法和乘法等线性代数的基本操作。而 ND4S 是一个用于线性代数和矩阵操作的科学计算库,支持 JVM 语言中的 n 维数组。
总结如下图所示,展示了过去一年来关于不同深度学习框架的 Google 趋势:
不同深度学习框架的趋势。TensorFlow 和 Keras 是最具主导性的框架,而 Theano 的人气正在下降。另一方面,DeepLearning4J 正在成为 JVM 上的新兴选择。
基于云平台的深度学习
除了前述的库之外,最近在云端也有一些深度学习的倡议。其理念是将深度学习能力引入到拥有数百万、数十亿数据点和高维数据的大数据中。例如,Amazon Web Services(AWS)、Microsoft Azure、Google Cloud Platform 和 NVIDIA GPU Cloud(NGC)都提供原生于其公共云平台的机器学习和深度学习服务。
2017 年 10 月,AWS 发布了针对 Amazon Elastic Compute Cloud(EC2)P3 实例的深度学习 Amazon Machine Images(AMIs)。这些 AMI 预装了深度学习框架,如 TensorFlow、Gluon 和 Apache MXNet,且已针对 Amazon EC2 P3 实例内的 NVIDIA Volta V100 GPU 进行了优化。
微软认知工具包是 Azure 的开源深度学习服务。与 AWS 提供的服务类似,它专注于可以帮助开发者构建和部署深度学习应用程序的工具。
另一方面,NGC 为 AI 科学家和研究人员提供了 GPU 加速的容器(见www.nvidia.com/en-us/data-center/gpu-cloud-computing/
)。NGC 提供了如 TensorFlow、PyTorch、MXNet 等容器化的深度学习框架,这些框架经过 NVIDIA 的调优、测试和认证,可以在最新的 NVIDIA GPU 上运行。
现在我们对可用的深度学习库、框架和云平台有了基本的了解,能够运行和部署我们的深度学习应用程序,我们可以开始编写代码了。首先,我们将通过解决著名的泰坦尼克号生存预测问题来入手。不过,我们不会使用之前列出的框架;我们将使用 Apache Spark ML 库。由于我们将结合其他深度学习库使用 Spark,了解一点 Spark 知识会帮助我们在接下来的章节中更好地掌握相关内容。
深度学习与灾难——泰坦尼克号生存预测
在本节中,我们将解决 Kaggle 上的著名泰坦尼克号生存预测问题(见www.kaggle.com/c/titanic/data
)。任务是使用机器学习算法完成对哪些人群可能生还的分析。
问题描述
在开始编写代码之前,让我们先看看问题的简短描述。以下段落直接引用自 Kaggle 泰坦尼克号生存预测页面:
“RMS 泰坦尼克号沉没事件是历史上最臭名昭著的海难之一。1912 年 4 月 15 日,泰坦尼克号在她的处女航中与冰山相撞沉没,造成 2224 名乘客和船员中 1502 人丧生。这场震惊国际社会的悲剧促使各国对船舶安全规定进行了改进。沉船导致如此大量的生命损失,其中一个原因是乘客和船员没有足够的救生艇。虽然幸存者在沉船过程中有一些运气成分,但某些群体比其他群体更有可能幸存,比如女性、儿童和上层阶级。在这个挑战中,我们要求你完成对哪些人群更可能生还的分析。特别是,我们要求你运用机器学习工具来预测哪些乘客在这场灾难中幸存下来。”
现在,在深入之前,我们需要了解泰坦尼克号灾难中乘客的数据,以便我们可以开发出可用于生存分析的预测模型。数据集可以从github.com/rezacsedu/TitanicSurvivalPredictionDataset
下载。数据集中有两个.csv
文件:
-
训练集 (
train.csv
): 可用于构建你的机器学习模型。此文件还包括每位乘客的标签,作为训练集的真实标签。 -
测试集 (
test.csv
): 可以用来查看你的模型在未见数据上的表现。然而,对于测试集,我们没有为每个乘客提供实际结果。
简而言之,对于测试集中的每个乘客,我们必须使用训练好的模型预测他们是否能在泰坦尼克号沉没中幸存。表 1 显示了训练集的元数据:
Variable | 定义 |
---|
| survival
| 两个标签:
-
0 = 否
-
1 = 是
|
pclass | 这是乘客的社会经济地位(SES)的代理,分为上层、中层和下层。具体来说,1 = 1^(st), 2 = 2^(nd), 3 = 3^(rd)。 |
---|---|
sex | 男性或女性。 |
Age | 年龄(单位:年)。 |
| sibsp
| 这表示家庭关系,如下所示:
-
Sibling = 兄弟,姐妹,继兄,继姐
-
Spouse = 丈夫,妻子(情妇和未婚夫未考虑在内)
|
| parch
| 在数据集中,家庭关系定义如下:
-
Parent = 母亲,父亲
-
Child = 女儿,儿子,继女,继子
有些孩子是单独和保姆一起旅行的,因此对于他们来说,parch=0。
ticket | 票号。 |
---|---|
fare | 乘客票价。 |
cabin | 舱号。 |
| embarked
| 三个港口:
-
C = 瑟堡
-
Q = 皇后镇
-
S = 南安普顿
|
现在问题是:使用这些标注数据,我们能否得出一些直接的结论?比如说,女性、头等舱以及儿童是能够提高乘客在这场灾难中幸存几率的因素。
为了解决这个问题,我们可以从基本的 MLP 开始,MLP 是最古老的深度学习算法之一。为此,我们使用基于 Spark 的 MultilayerPerceptronClassifier
。此时,你可能会想,既然 Spark 不是深度学习库,为什么我要讲 Spark?不过,Spark 有一个 MLP 实现,这足以满足我们的目标。
接下来的章节中,我们将逐步开始使用更强大的 DNN,通过使用 DeepLearning4J,一个基于 JVM 的深度学习应用开发框架。所以我们来看看如何配置我们的 Spark 环境。
配置编程环境
我假设你的机器上已经安装了 Java,且 JAVA_HOME
也已设置。还假设你的 IDE 安装了 Maven 插件。如果是这样,那么只需创建一个 Maven 项目,并按照以下方式添加项目属性:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<jdk.version>1.8</jdk.version>
<spark.version>2.3.0</spark.version>
</properties>
在前面的标签中,我指定了 Spark(即 2.3.0),但你可以进行调整。然后在 pom.xml
文件中添加以下依赖项:
<dependencies>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-sql_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-mllib_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-graphx_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-yarn_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-network-shuffle_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-flume_2.11</artifactId>
<version>${spark.version}</version>
</dependency>
<dependency>
<groupId>com.databricks</groupId>
<artifactId>spark-csv_2.11</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
然后如果一切顺利,所有 JAR 文件都会作为 Maven 依赖下载到项目目录下。好了!接下来我们可以开始编写代码了。
特征工程和输入数据集准备
在本小节中,我们将看到一些基本的特征工程和数据集准备,它们可以输入到 MLP 分类器中。那么让我们从创建 SparkSession
开始,它是访问 Spark 的门户:
SparkSession spark = SparkSession
.*builder*()
.master("local[*]")
.config("spark.sql.warehouse.dir", "/tmp/spark")
.appName("SurvivalPredictionMLP")
.getOrCreate();
然后让我们读取训练集并看一看它的概况:
Dataset<Row> df = spark.sqlContext()
.read()
.format("com.databricks.spark.csv")
.option("header", "true")
.option("inferSchema", "true")
.load("data/train.csv");
df.show();
数据集的快照如下所示:
泰坦尼克号生存数据集快照
现在我们可以看到,训练集同时包含了分类特征和数值特征。此外,一些特征并不重要,例如PassengerID
、Ticket
等。同样,Name
特征也不重要,除非我们手动基于标题创建一些特征。然而,我们还是保持简单。不过,一些列包含了空值,因此需要大量的考虑和清理。
我忽略了PassengerId
、Name
和Ticket
列。除此之外,Sex
列是分类变量,因此我根据male
和female
对乘客进行了编码。然后,Embarked
列也被编码了。我们可以将S
编码为0
,C
编码为1
,Q
编码为2
。
对于这一点,我们也可以编写名为normSex
和normEmbarked
的用户定义函数(UDF),分别对应Sex
和Embarked
。让我们看看它们的签名:
private static UDF1<String,Option<Integer>> *normEmbarked*=(String d) -> {
if (null == d)
return Option.*apply*(null);
else {
if (d.equals("S"))
return Some.apply(0);
else if (d.equals("C"))
return Some.apply(1);
else
return Some.apply(2);
}
};
因此,这个 UDF 接收一个String
类型,并将其编码为整数。normSex
UDF 的工作方式也类似:
private static UDF1<String, Option<Integer>> normSex = (String d) -> {
if (null == d)
return Option.apply(null);
else {
if (d.equals("male"))
return Some.apply(0);
else
return Some.apply(1);
}
};
所以我们现在可以只选择有用的列,但对于Sex
和Embarked
列,我们需要使用上述 UDF:
Dataset<Row> projection = df.select(
col("Survived"),
col("Fare"),
callUDF("normSex", col("Sex")).alias("Sex"),
col("Age"),
col("Pclass"),
col("Parch"),
col("SibSp"),
callUDF("normEmbarked",
col("Embarked")).alias("Embarked"));
projectin.show();
现在我们已经能够将一个分类列转换为数值型;然而,正如我们所看到的,仍然存在空值。那么,我们该怎么办呢?我们可以选择直接删除null
值,或者使用一些null
填补技术,用该列的均值进行填充。我认为第二种方法更好。
现在,再次针对这个空值填补,我们也可以编写用户定义函数(UDF)。不过,为此我们需要了解一些关于数值列的统计信息。不幸的是,我们不能对 DataFrame 执行汇总统计。因此,我们必须将 DataFrame 转换为JavaRDD<Vector>
。另外,我们在计算时也忽略了null
值:
JavaRDD<Vector> statsDf =projection.rdd().toJavaRDD().map(row -> Vectors.*dense*( row.<Double>getAs("Fare"),
row.isNullAt(3) ? 0d : row.Double>getAs("Age")
));
现在,让我们计算多变量统计summary
。summary
统计将进一步用于计算这两个特征对应的缺失值的meanAge
和meanFare
:
MultivariateStatisticalSummary summary = Statistics.*colStats*(statsRDD.rdd());
double meanFare = summary.mean().apply(0);
double meanAge = summary.mean().apply(1);
现在,让我们为Age
和Fare
列创建两个 UDF 来进行空值填补:
UDF1<String, Option<Double>> normFare = (String d) -> {
if (null == d) {
return Some.apply(meanFare);
}
else
return Some.apply(Double.parseDouble(d));
};
因此,我们定义了一个 UDF,如果数据没有条目,它会填充meanFare
值。现在,让我们为Age
列创建另一个 UDF:
UDF1<String, Option<Double>> normAge = (String d) -> {
if (null == d)
return Some.apply(meanAge);
else
return Some.apply(Double.parseDouble(d));
};
现在我们需要按照如下方式注册 UDF:
spark.sqlContext().udf().register("normFare", normFare, DataTypes.DoubleType);
spark.sqlContext().udf().register("normAge", normAge, DataTypes.DoubleType);
因此,让我们应用前面的 UDF 进行null
填补:
Dataset<Row> finalDF = projection.select(
*col*("Survived"),
*callUDF*("normFare",
*col*("Fare").cast("string")).alias("Fare"),
*col*("Sex"),
*callUDF*("normAge",
*col*("Age").cast("string")).alias("Age"),
*col*("Pclass"),
*col*("Parch"),
*col*("SibSp"),
*col*("Embarked"));
finalDF.show();
太棒了!我们现在可以看到,null
值已经被Age
和Fare
列的均值所替代。然而,数值仍然没有经过缩放。因此,最好对它们进行缩放。但是,为此,我们需要计算均值和方差,然后将它们存储为模型,以便以后进行缩放:
Vector stddev = Vectors.dense(Math.sqrt(summary.variance().apply(0)), Math.sqrt(summary.variance().apply(1)));
Vector mean = Vectors.dense(summary.mean().apply(0), summary.mean().apply(1));
StandardScalerModel scaler = new StandardScalerModel(stddev, mean);
然后,我们需要一个用于数值值的编码器(即Integer
;可以是BINARY
或Double
):
Encoder<Integer> integerEncoder = Encoders.INT();
Encoder<Double> doubleEncoder = Encoders.DOUBLE();
Encoders.BINARY();
Encoder<Vector> vectorEncoder = Encoders.kryo(Vector.class);
Encoders.tuple(integerEncoder, vectorEncoder);
Encoders.tuple(doubleEncoder, vectorEncoder);
然后我们可以创建一个VectorPair
,由标签(即Survived
)和特征组成。这里的编码基本上是创建一个缩放后的特征向量:
JavaRDD<VectorPair> scaledRDD = trainingDF.toJavaRDD().map(row -> {
VectorPair vectorPair = new VectorPair();
vectorPair.setLable(new
Double(row.<Integer> getAs("Survived")));
vectorPair.setFeatures(Util.*getScaledVector*(
row.<Double>getAs("Fare"),
row.<Double>getAs("Age"),
row.<Integer>getAs("Pclass"),
row.<Integer>getAs("Sex"),
row.isNullAt(7) ? 0d :
row.<Integer>getAs("Embarked"),
scaler));
return vectorPair;
});
在上面的代码块中,getScaledVector()
方法执行了缩放操作。该方法的函数签名如下所示:
public static org.apache.spark.mllib.linalg.Vector getScaledVector(double fare,
double age, double pclass, double sex, double embarked, StandardScalerModel scaler) {
org.apache.spark.mllib.linalg.Vector scaledContinous = scaler.transform(Vectors.dense(fare, age));
Tuple3<Double, Double, Double> pclassFlat = flattenPclass(pclass);
Tuple3<Double, Double, Double> embarkedFlat = flattenEmbarked(embarked);
Tuple2<Double, Double> sexFlat = flattenSex(sex);
return Vectors.dense(
scaledContinous.apply(0),
scaledContinous.apply(1),
sexFlat._1(),
sexFlat._2(),
pclassFlat._1(),
pclassFlat._2(),
pclassFlat._3(),
embarkedFlat._1(),
embarkedFlat._2(),
embarkedFlat._3());
}
由于我们计划使用基于 Spark ML 的分类器(即 MLP 实现),我们需要将这个 RDD 向量转换为 ML 向量:
Dataset<Row> scaledDF = spark.createDataFrame(scaledRDD, VectorPair.class);
最后,让我们看看结果 DataFrame 的样子:
scaledDF.show();
到目前为止,我们已经能够准备好特征了。不过,这仍然是一个基于 MLlib 的向量,因此我们需要进一步将其转换为 ML 向量:
Dataset<Row> scaledData2 = MLUtils.convertVectorColumnsToML(scaledDF);
太棒了!现在我们几乎完成了准备一个可以供 MLP 分类器使用的训练集。由于我们还需要评估模型的性能,因此可以随机拆分训练数据为训练集和测试集。我们将 80%分配给训练,20%分配给测试。它们将分别用于训练模型和评估模型:
Dataset<Row> data = scaledData2.toDF("features", "label");
Dataset<Row>[] datasets = data.randomSplit(new double[]{0.80, 0.20}, 12345L);
Dataset<Row> trainingData = datasets[0];
Dataset<Row> validationData = datasets[1];
好的。现在我们已经有了训练集,可以对 MLP 模型进行训练了。
训练 MLP 分类器
在 Spark 中,MLP 是一个包含多层的分类器。每一层都与网络中的下一层完全连接。输入层的节点表示输入数据,而其他节点通过线性组合输入、节点的权重和偏置,并应用激活函数,将输入映射到输出。
有兴趣的读者可以查看spark.apache.org/docs/latest/ml-classification-regression.html#multilayer-perceptron-classifier
。
那么让我们为 MLP 分类器创建层。对于这个示例,考虑到我们的数据集维度并不高,让我们构建一个浅层网络。
假设第一隐藏层只有 18 个神经元,第二隐藏层有8
个神经元就足够了。请注意,输入层有10
个输入,因此我们设置10
个神经元,输出层设置2
个神经元,因为我们的 MLP 只会预测2
个类别。有一件事非常重要——输入的数量必须等于特征向量的大小,输出的数量必须等于标签的总数:
int[] layers = new int[] {10, 8, 16, 2};
然后我们用训练器实例化模型,并设置其参数:
MultilayerPerceptronClassifier mlp = new MultilayerPerceptronClassifier()
.setLayers(layers)
.setBlockSize(128)
.setSeed(1234L)
.setTol(1E-8)
.setMaxIter(1000);
正如你所理解的,前面的MultilayerPerceptronClassifier()
是基于 MLP 的分类器训练器。除了输出层使用 softmax 激活函数外,每一层都使用 sigmoid 激活函数。需要注意的是,基于 Spark 的 MLP 实现仅支持小批量梯度下降(minibatch GD)和 LBFGS 优化器。
简而言之,我们不能在隐藏层使用其他激活函数,如 ReLU 或 tanh。除此之外,其他高级优化器也不被支持,批量归一化等也无法使用。这是该实现的一个严重限制。在下一章中,我们将尝试用 DL4J 克服这个问题。
我们还将迭代的收敛容差设置为非常小的值,这样可以通过更多的迭代获得更高的准确性。我们设置了块大小,以便在矩阵中堆叠输入数据,从而加速计算。
如果训练集的大小很大,那么数据会在分区内堆叠。如果块大小超过了分区中剩余的数据量,那么它会调整为该数据的大小。推荐的块大小在 10 到 1,000 之间,但默认的块大小为 128。
最后,我们计划将训练迭代 1,000 次。那么让我们开始使用训练集来训练模型:
MultilayerPerceptronClassificationModel model = mlp.fit(trainingData);
评估 MLP 分类器
当训练完成后,我们计算测试集上的预测结果,以评估模型的鲁棒性:
Dataset<Row> predictions = model.transform(validationData);
那么,如何看一些样本预测呢?让我们观察真实标签和预测标签:
predictions.show();
我们可以看到一些预测是正确的,但也有一些是错误的。然而,以这种方式很难猜测性能。因此,我们可以计算精确度、召回率和 F1 值等性能指标:
MulticlassClassificationEvaluator evaluator = new MulticlassClassificationEvaluator()
.setLabelCol("label")
.setPredictionCol("prediction");
MulticlassClassificationEvaluator evaluator1 = evaluator.setMetricName("accuracy");
MulticlassClassificationEvaluator evaluator2 = evaluator.setMetricName("weightedPrecision");
MulticlassClassificationEvaluator evaluator3 = evaluator.setMetricName("weightedRecall");
MulticlassClassificationEvaluator evaluator4 = evaluator.setMetricName("f1");
现在让我们计算分类的准确度
、精确度
、召回率
、F1
值以及测试数据上的错误率:
double accuracy = evaluator1.evaluate(predictions);
double precision = evaluator2.evaluate(predictions);
double recall = evaluator3.evaluate(predictions);
double f1 = evaluator4.evaluate(predictions);
// Print the performance metrics
System.*out*.println("Accuracy = " + accuracy);
System.*out*.println("Precision = " + precision);
System.*out*.println("Recall = " + recall);
System.*out*.println("F1 = " + f1);
System.*out*.println("Test Error = " + (1 - accuracy));
<q>>>></q> Accuracy = 0.7796476846282568
Precision = 0.7796476846282568
Recall = 0.7796476846282568
F1 = 0.7796476846282568
Test Error = 0.22035231537174316
做得很好!我们已经能够达到一个相当高的准确率,即 78%。不过,我们依然可以通过额外的特征工程进行改进。更多提示将在下一节给出!现在,在结束本章之前,让我们尝试利用训练好的模型对测试集进行预测。首先,我们读取测试集并创建 DataFrame:
Dataset<Row> testDF = Util.getTestDF();
然而,即使你查看测试集,你会发现其中有一些空值。所以我们需要对Age
和Fare
列进行空值填充。如果你不想使用 UDF,你可以创建一个 MAP,包含你的填充方案:
Map<String, Object> m = new HashMap<String, Object>();
m.put("Age", meanAge);
m.put("Fare", meanFare);
Dataset<Row> testDF2 = testDF.na().fill(m);
然后,我们再次创建一个包含特征和标签(目标列)的vectorPair
的 RDD:
JavaRDD<VectorPair> testRDD = testDF2.javaRDD().map(row -> {
VectorPair vectorPair = new VectorPair();
vectorPair.setLable(row.<Integer>getAs("PassengerId"));
vectorPair.setFeatures(Util.*getScaledVector*(
row.<Double>getAs("Fare"),
row.<Double>getAs("Age"),
row.<Integer>getAs("Pclass"),
row.<Integer>getAs("Sex"),
row.<Integer>getAs("Embarked"),
scaler));
return vectorPair;
});
然后我们创建一个 Spark DataFrame:
Dataset<Row> scaledTestDF = spark.createDataFrame(testRDD, VectorPair.class);
最后,让我们将 MLib 向量转换为基于 ML 的向量:
Dataset<Row> finalTestDF = MLUtils.convertVectorColumnsToML(scaledTestDF).toDF("features", "PassengerId");
现在,让我们执行模型推理,即为PassengerId
列创建预测并展示示例prediction
:
Dataset<Row> resultDF = model.transform(finalTestDF).select("PassengerId", "prediction");
resultDF.show();
最后,让我们将结果写入 CSV 文件:
resultDF.write().format("com.databricks.spark.csv").option("header", true).save("result/result.csv");
常见问题解答(FAQs)
现在,我们已经以可接受的准确率解决了泰坦尼克号生存预测问题,但这个问题以及深度学习现象中其他实际问题也需要考虑。在这一节中,我们将看到一些你可能已经在想的常见问题。答案可以在附录 A中找到。
-
使用原始人工神经元绘制一个计算 XOR 操作的人工神经网络:A⊕ B。将这个问题正式描述为一个分类问题。为什么简单的神经元无法解决这个问题?多层感知器(MLP)是如何通过堆叠多个感知器来解决这个问题的?
-
我们简要回顾了人工神经网络的历史。那么在深度学习的时代,最重要的里程碑是什么?我们能否用一张图来解释时间线?
-
我可以使用其他深度学习框架更灵活地解决这个泰坦尼克号生存预测问题吗?
-
我可以在代码中使用
Name
作为 MLP 中的一个特征吗? -
我理解输入层和输出层的神经元数量。那么我应该为隐藏层设置多少个神经元?
-
我们不能通过交叉验证和网格搜索技术来提高预测准确性吗?
总结
在本章中,我们介绍了一些深度学习的基本主题。我们从对机器学习的基本但全面的介绍开始。然后,我们逐步过渡到深度学习和不同的神经网络结构。接着,我们对最重要的深度学习框架进行了简要概述。最后,我们看了一些与深度学习和泰坦尼克号生存预测问题相关的常见问题。
在下一章中,我们将开始深入学习深度学习,通过使用多层感知器(MLP)解决泰坦尼克号生存预测问题。然后,我们将开始开发一个端到端的项目,用于使用循环 LSTM 网络进行癌症类型分类。我们将使用一个非常高维的基因表达数据集来训练和评估模型。
常见问题解答
问题 1 的答案:解决这个问题的方法有很多:
-
A ⊕ B= (A ∨ ¬ B)∨ (¬ A ∧ B)
-
A ⊕ B = (A ∨ B) ∧ ¬(A ∨ B)
-
A ⊕ B = (A ∨ B) ∧ (¬ A ∨ ∧ B),依此类推
如果我们采用第一种方法,得到的人工神经网络将如下所示:
现在,从计算机科学文献中,我们知道 XOR 操作仅与两个输入组合和一个输出相关联。对于输入(0, 0)或(1, 1),网络输出 0;对于输入(0, 1)或(1, 0),网络输出 1。因此,我们可以正式地将前述真值表表示如下:
X0 | X1 | Y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
在这里,每个模式被分类为两个类之一,这两个类可以通过一条单独的直线L分开。它们被称为线性可分模式,如下所示:
问题 2 的答案:人工神经网络和深度学习的最重要进展可以通过以下时间线描述。我们已经看到,人工神经元和感知器分别在 1943 年和 1958 年为基础提供了支持。然后,1969 年,Minsky 等人将 XOR 问题公式化为一个线性不可分的问题。但是,后来在 1974 年,Werbos 等人展示了用于训练感知器的反向传播算法。
然而,最重要的进展发生在 1980 年代,当时 John Hopfield 等人于 1982 年提出了 Hopfield 网络。然后,神经网络和深度学习的奠基人之一 Hinton 和他的团队于 1985 年提出了玻尔兹曼机。然而,可能最重要的进展发生在 1986 年,当时 Hinton 等人成功训练了 MLP,而 Jordan 等人提出了 RNN。同年,Smolensky 等人也提出了改进版的 RBM。
在 1990 年代,最重要的一年是 1997 年。Lecun 等人于 1990 年提出了 LeNet,而 Jordan 等人则在 1997 年提出了 RNN。同年,Schuster 等人提出了改进版的 LSTM 和改进版的原始 RNN,称为 双向 RNN。
尽管计算能力有了显著的进展,但从 1997 年到 2005 年,我们并没有经历太多的突破,直到 2006 年 Hinton 再次提出了 DBN——通过堆叠多个 RBM。然后在 2012 年,Hinton 又发明了 dropout,这大大改善了 DNN 中的正则化和过拟合问题。
之后,Ian Goodfellow 等人引入了 GAN,这是图像识别领域的一个重要里程碑。2017 年,Hinton 提出了 CapsNets 来克服常规 CNN 的局限性——迄今为止,这是最重要的里程碑之一。
问题 3 的答案:是的,你可以使用深度学习框架部分中描述的其他深度学习框架。然而,由于本书是关于使用 Java 进行深度学习的,我建议使用 DeepLearning4J。我们将在下一章中看到如何灵活地通过堆叠输入层、隐藏层和输出层来创建网络,使用 DeepLearning4J。
问题 4 的答案:是的,你可以,因为乘客的名字包含不同的称呼(例如,先生、夫人、小姐、少爷等等)也可能很重要。例如,我们可以想象,作为女性(即夫人)和作为一个年轻人(例如,少爷)可能有更高的生存机会。
甚至,在看完著名电影《泰坦尼克号》(1997)后,我们可以想象,如果一个女孩处于一段关系中,她可能有更好的生存机会,因为她的男朋友会尝试救她!不过,这只是想象而已,所以不要太当真。现在,我们可以编写一个用户定义的函数,使用 Apache Spark 来编码这个过程。让我们来看一下以下的 Java 中的 UDF:
private static final UDF1<String, Option<String>> getTitle = (String name) -> {
if(name.contains("Mr.")) { // If it has Mr.
return Some.apply("Mr.");
} else if(name.contains("Mrs.")) { // Or if has Mrs.
return Some.apply("Mrs.");
} else if(name.contains("Miss.")) { // Or if has Miss.
return Some.apply("Miss.");
} else if(name.contains("Master.")) { // Or if has Master.
return Some.apply("Master.");
} else{ // Not any.
return Some.apply("Untitled");
}
};
接下来,我们可以注册 UDF。然后我必须按如下方式注册前面的 UDF:
spark.sqlContext().udf().register("getTitle", getTitle, DataTypes.StringType);
Dataset<Row> categoricalDF = df.select(callUDF("getTitle", col("Name")).alias("Name"), col("Sex"),
col("Ticket"), col("Cabin"), col("Embarked"));
categoricalDF.show();
结果列看起来如下所示:
问题 5 的答案:对于许多问题,你可以从只有一到两个隐藏层开始。使用两个隐藏层(具有相同总神经元数量,稍后阅读时你会了解神经元数量)并且训练时间大致相同,这个设置就能很好地工作。现在让我们来看看关于设置隐藏层数量的一些简单估算:
-
0:只能表示线性可分函数
-
1:可以近似任何包含从一个有限空间到另一个有限空间的连续映射的函数
-
2:可以以任意精度表示任意的决策边界
然而,对于更复杂的问题,你可以逐渐增加隐藏层的数量,直到开始过拟合训练集。不过,你也可以尝试逐步增加神经元的数量,直到网络开始过拟合。这意味着不会导致过拟合的隐藏神经元的上限是:
在上述方程中:
-
N[i] = 输入神经元的数量
-
N[o] = 输出神经元的数量
-
N[s] = 训练数据集中的样本数量
-
α = 任意的缩放因子,通常为2-10
请注意,上述方程并非来源于任何研究,而是来自我个人的工作经验。
问题 6 的答案:当然可以。我们可以对训练进行交叉验证,并创建网格搜索技术来寻找最佳超参数。让我们试试看。
首先,我们定义了各层。不幸的是,我们无法对各层进行交叉验证。这可能是一个 bug,或者是 Spark 团队故意为之。所以我们坚持使用单层结构:
int[] layers = new int[] {10, 16, 16, 2};
然后我们创建训练器,并只设置层和种子参数:
MultilayerPerceptronClassifier mlp = new MultilayerPerceptronClassifier()
.setLayers(layers)
.setSeed(1234L);
我们在 MLP 的不同超参数中搜索最佳模型:
ParamMap[] paramGrid = new ParamGridBuilder()
.addGrid(mlp.blockSize(), new int[] {32, 64, 128})
.addGrid(mlp.maxIter(), new int[] {10, 50})
.addGrid(mlp.tol(), new double[] {1E-2, 1E-4, 1E-6})
.build();
MulticlassClassificationEvaluator evaluator = new MulticlassClassificationEvaluator()
.setLabelCol("label")
.setPredictionCol("prediction");
接着,我们设置交叉验证器,并执行 10 折交叉验证:
int numFolds = 10;
CrossValidator crossval = new CrossValidator()
.setEstimator(mlp)
.setEvaluator(evaluator)
.setEstimatorParamMaps(paramGrid)
.setNumFolds(numFolds);
然后,我们使用交叉验证后的模型进行训练:
CrossValidatorModel cvModel = crossval.fit(trainingData);
最后,我们对测试集上的交叉验证模型进行评估,如下所示:
Dataset<Row> predictions = cvModel.transform(validationData);
现在我们可以计算并显示性能指标,类似于我们之前的示例:
double accuracy = evaluator1.evaluate(predictions);
double precision = evaluator2.evaluate(predictions);
double recall = evaluator3.evaluate(predictions);
double f1 = evaluator4.evaluate(predictions);
// Print the performance metrics
System.out.println("Accuracy = " + accuracy);
System.out.println("Precision = " + precision);
System.out.println("Recall = " + recall);
System.out.println("F1 = " + f1);
System.out.println("Test Error = " + (1 - accuracy));
>>>Accuracy = 0.7810132575757576
Precision = 0.7810132575757576
Recall = 0.7810132575757576
F1 = 0.7810132575757576
Test Error = 0.21898674242424243
第二章:使用递归类型网络进行癌症类型预测
大规模的癌症基因组学数据通常以多平台和异构形式存在。这些数据集在生物信息学方法和计算算法方面带来了巨大的挑战。许多研究人员提出利用这些数据克服多个挑战,将经典的机器学习算法作为癌症诊断和预后的主要方法或辅助元素。
本章中,我们将使用一些深度学习架构来进行癌症类型分类,数据来自 The Cancer Genome Atlas(TCGA)中整理的高维数据集。首先,我们将描述该数据集并进行一些预处理,使得数据集可以输入到我们的网络中。然后,我们将学习如何准备编程环境,接下来使用一个开源深度学习库Deeplearning4j(DL4J)进行编码。首先,我们将再次回顾 Titanic 生存预测问题,并使用 DL4J 中的多层感知器(MLP)实现。
然后,我们将使用一种改进的递归神经网络(RNN)架构,称为长短期记忆(LSTM),进行癌症类型预测。最后,我们将了解一些与此项目及 DL4J 超参数/网络调优相关的常见问题。
简而言之,本章将学习以下内容:
-
癌症基因组学中的深度学习
-
癌症基因组学数据集描述
-
开始使用 Deeplearning4j
-
使用 LSTM-RNN 开发癌症类型预测模型
-
常见问题
癌症基因组学中的深度学习
生物医学信息学包括与生物系统研究相关的数据分析、数学建模和计算仿真技术的开发。近年来,我们见证了生物计算的巨大飞跃,结果是大量信息丰富的资源已可供我们使用。这些资源涵盖了诸如解剖学、建模(3D 打印机)、基因组学和药理学等多个领域。
生物医学信息学最著名的成功案例之一来自基因组学领域。人类基因组计划(HGP)是一个国际研究项目,旨在确定人类 DNA 的完整序列。这个项目是计算生物学中最重要的里程碑之一,并为其他项目提供了基础,包括致力于对人类大脑进行基因组测序的人类大脑计划。本文所使用的数据也是 HGP 的间接成果。
大数据时代大约从过去十年开始,标志着数字信息的激增,相比其模拟对手。仅在 2016 年,16.1 泽字节的数字数据被生成,预计到 2025 年将达到每年 163 泽字节。虽然这是一则好消息,但仍然存在一些问题,尤其是在数据存储和分析方面。对于后者,简单的机器学习方法在常规数据分析中的应用已不再有效,应被深度神经网络学习方法所取代。深度学习通常被认为能非常有效地处理这些类型的大型复杂数据集。
与其他重要领域一样,生物医学领域也受到了大数据现象的影响。最主要的大型数据来源之一是诸如基因组学、代谢组学和蛋白质组学等 omics 数据。生物医学技术和设备的创新,如 DNA 测序和质谱分析,导致了 -omics 数据的巨大积累。
通常,-omics 数据充满了真实性、变异性和高维度性。这些数据集来源于多个,甚至有时是不兼容的数据平台。这些特性使得这些类型的数据适合应用深度学习方法。对 -omics 数据的深度学习分析是生物医学领域的主要任务之一,因为它有可能成为个性化医疗的领导者。通过获取一个人 omics 数据的信息,可以更好地应对疾病,治疗可以集中于预防措施。
癌症通常被认为是世界上最致命的疾病之一,主要是由于其诊断和治疗的复杂性。它是一种涉及多种基因突变的遗传性疾病。随着癌症治疗中遗传学知识重要性的逐渐受到重视,最近出现了多个记录癌症患者遗传数据的项目。其中最著名的项目之一是癌症基因组图谱(TCGA)项目,该项目可在 TCGA 研究网络上找到:cancergenome.nih.gov/
。
如前所述,生物医学领域,包括癌症研究,已经有许多深度学习应用。在癌症研究中,大多数研究者通常使用 -omics 或医学影像数据作为输入。多个研究工作聚焦于癌症分析。其中一些使用组织病理图像或 PET 图像作为数据来源。大多数研究集中于基于这些图像数据的分类,采用卷积神经网络(CNNs)。
然而,许多研究使用-omics 数据作为其数据来源。Fakoor 等人使用患者的基因表达数据对各种类型的癌症进行了分类。由于每种癌症类型的数据维度不同,他们首先使用主成分分析(PCA)来减少微阵列基因表达数据的维度。
主成分分析(PCA)是一种统计技术,用于强调数据的变化并提取数据集中最显著的模式;主成分是基于真实特征向量的最简单的多元分析方法。PCA 通常用于使数据探索更易于可视化。因此,PCA 是数据探索分析和构建预测模型中最常用的算法之一。
然后,他们应用稀疏和堆叠自编码器对多种癌症进行分类,包括急性髓性白血病、乳腺癌和卵巢癌。
有关详细信息,请参阅以下文献:《使用深度学习增强癌症诊断与分类》,作者:R. Fakoor 等人,发表于 2013 年国际机器学习会议论文集中。
另一方面,Ibrahim 等人使用了来自六种癌症的基因/miRNA 特征选择的 miRNA 表达数据。他们提出了一种新的多级特征选择方法,名为MLFS(多级基因/miRNA 特征选择的简称),该方法基于**深度置信网络(DBN)**和无监督主动学习。
您可以在以下文献中阅读更多内容:《使用深度置信网络和主动学习的多级基因/miRNA 特征选择》,作者:R. Ibrahim 等人,发表于 2014 年 36 届国际工程医学生物学学会年会(EMBC)论文集,页 3957-3960,IEEE,2014。
最后,Liang 等人使用多平台基因组学和临床数据对卵巢癌和乳腺癌患者进行了聚类。卵巢癌数据集包含 385 名患者的基因表达、DNA 甲基化和 miRNA 表达数据,这些数据从**癌症基因组图谱(TCGA)**下载。
您可以在以下文献中阅读更多内容:《多平台癌症数据的集成数据分析与多模态深度学习方法》,作者:M. Liang 等人,发表于《分子药学》期刊,卷 12,页 928-937,IEEE/ACM 计算生物学与生物信息学学报,2015。
乳腺癌数据集包括基因表达数据和相应的临床信息,如生存时间和复发时间数据,这些数据由荷兰癌症研究所收集。为了处理这些多平台数据,他们使用了多模态深度置信网络(mDBN)。
首先,他们为每种数据实现了一个深度置信网络(DBN)以获取其潜在特征。然后,另一个用于执行聚类的深度置信网络使用这些潜在特征作为输入。除了这些研究人员外,还有大量研究正在进行,旨在为癌症基因组学、识别和治疗提供重要推动。
癌症基因组学数据集描述
基因组学数据涵盖与生物体 DNA 相关的所有数据。尽管在本论文中我们还将使用其他类型的数据,如转录组数据(RNA 和 miRNA),但为了方便起见,所有数据将统称为基因组数据。人类基因组学的研究在最近几年取得了巨大的突破,这得益于 HGP(1984-2000)在测序人类 DNA 全序列方面的成功。
受此影响最大的领域之一是与遗传学相关的所有疾病的研究,包括癌症。通过对 DNA 进行各种生物医学分析,出现了各种类型的-组学或基因组数据。以下是一些对癌症分析至关重要的-组学数据类型:
-
原始测序数据:这对应于整个染色体的 DNA 编码。一般来说,每个人体内的每个细胞都有 24 种染色体,每条染色体由 4.6 亿至 2.47 亿个碱基对组成。每个碱基对可以用四种不同的类型进行编码,分别是腺嘌呤(A)、胞嘧啶(C)、鸟嘌呤(G)和胸腺嘧啶(T)。因此,原始测序数据由数十亿个碱基对数据组成,每个碱基对都用这四种类型之一进行编码。
-
单核苷酸多态性(SNP)数据:每个人都有不同的原始序列,这会导致基因突变。基因突变可能导致实际的疾病,或者仅仅是外貌上的差异(如发色),也可能什么都不发生。当这种突变仅发生在单个碱基对上,而不是一段碱基对序列时,这被称为单核苷酸多态性(SNP)。
-
拷贝数变异(CNV)数据:这对应于发生在碱基对序列中的基因突变。突变可以有多种类型,包括碱基对序列的缺失、碱基对序列的倍增以及碱基对序列在染色体其他部位的重排。
-
DNA 甲基化数据:这对应于染色体上某些区域发生的甲基化量(甲基基团连接到碱基对上)。基因启动子区域的甲基化量过多可能会导致基因沉默。DNA 甲基化是我们每个器官表现出不同功能的原因,尽管它们的 DNA 序列是相同的。在癌症中,这种 DNA 甲基化被破坏。
-
基因表达数据:这对应于某一时刻从基因中表达的蛋白质数量。癌症的发生通常是由于致癌基因(即引发肿瘤的基因)表达过高、抑癌基因(即防止肿瘤的基因)表达过低,或两者兼有。因此,基因表达数据的分析有助于发现癌症中的蛋白质生物标志物。我们将在本项目中使用这种数据。
-
miRNA 表达数据:对应于在特定时间内表达的微小 RNA 的数量。miRNA 在 mRNA 阶段起到蛋白质沉默的作用。因此,基因表达数据的分析有助于发现癌症中的 miRNA 生物标志物。
有多个基因组数据集的数据库,其中可以找到上述数据。它们中的一些专注于癌症患者的基因组数据。这些数据库包括:
-
癌症基因组图谱(TCGA):
cancergenome.nih.gov/
-
国际癌症基因组联盟(ICGC):
icgc.org/
-
癌症体细胞突变目录(COSMIC):
cancer.sanger.ac.uk/cosmic
这些基因组数据通常伴随着患者的临床数据。临床数据可以包括一般的临床信息(例如,年龄或性别)以及他们的癌症状态(例如,癌症的位置或癌症的分期)。所有这些基因组数据本身具有高维度的特点。例如,每个患者的基因表达数据是基于基因 ID 构建的,达到约 60,000 种类型。
此外,一些数据本身来自多个格式。例如,70%的 DNA 甲基化数据来自乳腺癌患者,剩余 30%则是来自不同平台的整理数据。因此,这个数据集有两种不同的结构。因此,为了分析基因组数据并处理其异质性,研究人员通常采用强大的机器学习技术,甚至是深度神经网络。
现在让我们看看一个可以用于我们目的的实际数据集。我们将使用从 UCI 机器学习库下载的基因表达癌症 RNA-Seq 数据集(有关更多信息,请参见archive.ics.uci.edu/ml/datasets/gene+expression+cancer+RNA-Seq#
)。
泛癌症分析项目的数据收集流程(来源:“Weinstein, John N., et al. ‘The cancer genome atlas pan-cancer analysis project.’ Nature Genetics 45.10 (2013): 1113-1120”)
这个数据集是从以下论文中报告的另一个数据集的随机子集:Weinstein, John N., et al. The cancer genome atlas pan-cancer analysis project. Nature Genetics 45.10 (2013): 1113-1120。前面的图示展示了泛癌症分析项目的数据收集流程。
项目的名称是泛癌症分析项目。该项目汇集了来自不同部位发生原发肿瘤的数千名患者的数据。它涵盖了 12 种肿瘤类型(见前面图示的左上角面板),包括:
-
胶质母细胞瘤(GBM)
-
急性淋巴细胞白血病(AML)
-
头颈部鳞状细胞癌(HNSC)
-
肺腺癌(LUAD)
-
肺鳞状细胞癌 (LUSC)
-
乳腺癌 (BRCA)
-
肾脏透明细胞癌 (KIRC)
-
卵巢癌 (OV)
-
膀胱癌 (BLCA)
-
结肠腺癌 (COAD)
-
子宫颈和子宫内膜癌 (UCEC)
-
直肠腺癌 (READ)
这组数据是 RNA-Seq (HiSeq) PANCAN 数据集的一部分。它是从患有不同类型肿瘤的患者(如 BRCA、KIRC、COAD、LUAD 和 PRAD)中随机提取的基因表达数据。
该数据集是从 801 名癌症患者中随机收集的,每名患者有 20,531 个属性。样本(实例)按行存储。每个样本的变量(属性)是通过 illumina HiSeq 平台测量的 RNA-Seq 基因表达水平。每个属性都被赋予一个虚拟名称(gene_XX
)。属性的顺序与原始提交一致。例如,sample_0
上的gene_1
的基因表达水平显著且有差异,数值为2.01720929003
。
当你下载数据集时,你会看到有两个 CSV 文件:
-
data.csv
: 包含每个样本的基因表达数据 -
labels.csv
: 与每个样本相关的标签
让我们来看一下处理过的数据集。请注意,由于高维度性,我们只会看到一些选择的特征,以下截图中第一列表示样本 ID(即匿名患者 ID)。其余列表示某些基因在患者肿瘤样本中的表达情况:
样本基因表达数据集
现在看一下图 3中的标签。在这里,id
包含样本 ID,Class
表示癌症标签:
样本被分类为不同的癌症类型
现在你可以理解为什么我选择了这个数据集。尽管我们没有太多样本,但这个数据集仍然是非常高维的。此外,这种高维数据集非常适合应用深度学习算法。
好的。那么,如果给定了特征和标签,我们能否根据特征和真实标签对这些样本进行分类呢?为什么不呢?我们将尝试使用 DL4J 库解决这个问题。首先,我们需要配置我们的编程环境,以便开始编写代码。
准备编程环境
在本节中,我们将讨论如何在开始编写代码之前配置 DL4J、ND4s、Spark 和 ND4J。使用 DL4J 时需要的前提条件如下:
-
Java 1.8+(仅限 64 位)
-
用于自动构建和依赖管理的 Apache Maven
-
IntelliJ IDEA 或 Eclipse IDE
-
用于版本控制和 CI/CD 的 Git
以下库可以与 DJ4J 集成,以增强你在开发机器学习应用时的 JVM 体验:
-
DL4J:核心神经网络框架,提供了许多深度学习架构和底层功能。
-
ND4J:可以被认为是 JVM 的 NumPy。它包括一些基本的线性代数操作,例如矩阵创建、加法和乘法。
-
DataVec:这个库在执行特征工程的同时,支持 ETL 操作。
-
JavaCPP:这个库充当 Java 与本地 C++之间的桥梁。
-
Arbiter:这个库为深度学习算法提供基本的评估功能。
-
RL4J:为 JVM 提供深度强化学习。
-
ND4S:这是一个科学计算库,并且它也支持 JVM 语言中的 n 维数组。
如果你在你喜欢的 IDE 中使用 Maven,我们可以在pom.xml
文件中定义项目属性来指定版本:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<nd4j.version>1.0.0-alpha</nd4j.version>
<dl4j.version>1.0.0-alpha</dl4j.version>
<datavec.version>1.0.0-alpha</datavec.version>
<arbiter.version>1.0.0-alpha</arbiter.version>
<logback.version>1.2.3</logback.version>
<dl4j.spark.version>1.0.0-alpha_spark_2</dl4j.spark.version>
</properties>
然后使用以下依赖项,这些依赖项是 DL4J、ND4S、ND4J 等所需要的:
<dependencies>
<dependency>
<groupId>org.nd4j</groupId>
<artifactId>nd4j-native</artifactId>
<version>${nd4j.version}</version>
</dependency>
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>dl4j-spark_2.11</artifactId>
<version>1.0.0-alpha_spark_2</version>
</dependency>
<dependency>
<groupId>org.nd4j</groupId>
<artifactId>nd4j-native</artifactId>
<version>1.0.0-alpha</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>deeplearning4j-core</artifactId>
<version>${dl4j.version}</version>
</dependency>
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>deeplearning4j-nlp</artifactId>
<version>${dl4j.version}</version>
</dependency>
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>deeplearning4j-zoo</artifactId>
<version>${dl4j.version}</version>
</dependency>
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>arbiter-deeplearning4j</artifactId>
<version>${arbiter.version}</version>
</dependency>
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>arbiter-ui_2.11</artifactId>
<version>${arbiter.version}</version>
</dependency>
<dependency>
<artifactId>datavec-data-codec</artifactId>
<groupId>org.datavec</groupId>
<version>${datavec.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.5</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
</dependencies>
顺便说一下,DL4J 随 Spark 2.1.0 一起提供。此外,如果你的机器上没有配置本地系统 BLAS,ND4J 的性能会降低。当你执行 Scala 编写的简单代码时,你将看到以下警告:
****************************************************************
WARNING: COULD NOT LOAD NATIVE SYSTEM BLAS
ND4J performance WILL be reduced
****************************************************************
然而,安装和配置 BLAS(如OpenBLAS
或IntelMKL
)并不难;你可以花些时间去完成它。更多细节可以参考以下网址:nd4j.org/getstarted.html#open
。
干得好!我们的编程环境已经准备好用于简单的深度学习应用开发。现在是时候动手写一些示例代码了。
使用 DL4J 重新审视泰坦尼克号生存预测
在前一章中,我们使用基于 Spark 的 MLP 解决了泰坦尼克号生存预测问题。我们还看到,通过使用基于 Spark 的 MLP,用户几乎无法了解层次结构的使用情况。此外,超参数等的定义也不够明确。
因此,我所做的是使用训练数据集,并进行了预处理和特征工程。然后,我将预处理后的数据集随机分为训练集和测试集(具体来说,70%用于训练,30%用于测试)。首先,我们按照如下方式创建 Spark 会话:
SparkSession spark = SparkSession.builder()
.master("local[*]")
.config("spark.sql.warehouse.dir", "temp/")// change accordingly
.appName("TitanicSurvivalPrediction")
.getOrCreate();
在本章中,我们看到有两个 CSV 文件。然而,test.csv
没有提供任何实际的标签。因此,我决定只使用training.csv
文件,以便我们可以比较模型的性能。所以我们通过 Spark 的read()
API 读取训练数据集:
Dataset<Row> df = spark.sqlContext()
.read()
.format("com.databricks.spark.csv")
.option("header", "true") // Use first line of all files as header
.option("inferSchema", "true") // Automatically infer data types
.load("data/train.csv");
我们在第一章《深度学习入门》中看到,Age
和Fare
列有许多空值。因此,在这里,我直接用这些列的均值来替换缺失值,而不是为每一列编写UDF
:
Map<String, Object> m = new HashMap<String, Object>();
m.put("Age", 30);
m.put("Fare", 32.2);
Dataset<Row> trainingDF1 = df2.na().fill(m);
要深入了解如何处理缺失/空值和机器学习,感兴趣的读者可以阅读 Boyan Angelov 的博客,链接如下:towardsdatascience.com/working-with-missing-data-in-machine-learning-9c0a430df4ce
。
为了简化,我们还可以删除一些列,例如“PassengerId
”、“Name
”、“Ticket
”和“Cabin
”:
Dataset<Row> trainingDF2 = trainingDF1.drop("PassengerId", "Name", "Ticket", "Cabin");
现在,进入难点了。类似于基于 Spark ML 的估计器,基于 DL4J 的网络也需要数字形式的训练数据。因此,我们现在必须将类别特征转换为数值。为此,我们可以使用StringIndexer()
转换器。我们要做的是为“Sex
”和“Embarked
”列创建两个StringIndexer
:
StringIndexer sexIndexer = new StringIndexer()
.setInputCol("Sex")
.setOutputCol("sexIndex")
.setHandleInvalid("skip");//// we skip column having nulls
StringIndexer embarkedIndexer = new StringIndexer()
.setInputCol("Embarked")
.setOutputCol("embarkedIndex")
.setHandleInvalid("skip");//// we skip column having nulls
然后我们将它们串联成一个管道。接下来,我们将执行转换操作:
Pipeline pipeline = new Pipeline().setStages(new PipelineStage[] {sexIndexer, embarkedIndexer});
接着,我们将拟合管道,转换数据,并删除“Sex
”和“Embarked
”列,以获取转换后的数据集:
Dataset<Row> trainingDF3 = pipeline.fit(trainingDF2).transform(trainingDF2).drop("Sex", "Embarked");
然后,我们的最终预处理数据集将只包含数值特征。请注意,DL4J 将最后一列视为标签列。这意味着 DL4J 会将“Pclass
”、“Age
”、“SibSp
”、“Parch
”、“Fare
”、“sexIndex
”和“embarkedIndex
”视为特征。因此,我将“Survived
”列放在了最后:
Dataset<Row> finalDF = trainingDF3.select("Pclass", "Age", "SibSp","Parch", "Fare",
"sexIndex","embarkedIndex", "Survived");
finalDF.show();
然后,我们将数据集随机拆分为 70%训练集和 30%测试集。即,我们使用 70%数据进行训练,剩余的 30%用于评估模型:
Dataset<Row>[] splits = finalDF.randomSplit(new double[] {0.7, 0.3});
Dataset<Row> trainingData = splits[0];
Dataset<Row> testData = splits[1];
最后,我们将两个 DataFrame 分别保存为 CSV 文件,供 DL4J 使用:
trainingData
.coalesce(1)// coalesce(1) writes DF in a single CSV
.write()
.format("com.databricks.spark.csv")
.option("header", "false") // don't write the header
.option("delimiter", ",") // comma separated
.save("data/Titanic_Train.csv"); // save location
testData
.coalesce(1)// coalesce(1) writes DF in a single CSV
.write()
.format("com.databricks.spark.csv")
.option("header", "false") // don't write the header
.option("delimiter", ",") // comma separated
.save("data/Titanic_Test.csv"); // save location
此外,DL4J 不支持训练集中的头信息,因此我故意跳过了写入头信息。
多层感知器网络构建
正如我在前一章中提到的,基于 DL4J 的神经网络由多个层组成。一切从MultiLayerConfiguration
开始,它组织这些层及其超参数。
超参数是一组决定神经网络学习方式的变量。有很多参数,例如:更新模型权重的次数和频率(称为epoch),如何初始化网络权重,使用哪种激活函数,使用哪种更新器和优化算法,学习率(即模型学习的速度),隐藏层有多少层,每层有多少神经元等等。
现在,我们来创建网络。首先,创建层。类似于我们在第一章中创建的 MLP,深度学习入门,我们的 MLP 将有四层:
-
第 0 层:输入层
-
第 1 层:隐藏层 1
-
第 2 层:隐藏层 2
-
第 3 层:输出层
更技术性地讲,第一层是输入层,然后将两层作为隐藏层放置。对于前三层,我们使用 Xavier 初始化权重,激活函数为 ReLU。最后,输出层放置在最后。这一设置如下图所示:
泰坦尼克号生存预测的多层感知器输入层
我们已经指定了神经元(即节点),输入和输出的数量相等,并且输出的神经元数量是任意的。考虑到输入和特征非常少,我们设置了一个较小的值:
DenseLayer input_layer = new DenseLayer.Builder()
.weightInit(WeightInit.XAVIER)
.activation(Activation.RELU)
.nIn(numInputs)
.nOut(16)
.build();
隐藏层 1
输入层的神经元数量等于输入层的输出。然后输出的数量是任意值。我们设置了一个较小的值,考虑到输入和特征非常少:
DenseLayer hidden_layer_1 = new DenseLayer.Builder()
.weightInit(WeightInit.XAVIER)
.activation(Activation.RELU)
.nIn(16).nOut(32)
.build();
隐藏层 2
输入层的神经元数量等于隐藏层 1 的输出。然后输出的数量是一个任意值。再次考虑到输入和特征非常少,我们设置了一个较小的值:
DenseLayer hidden_layer_2 = new DenseLayer.Builder()
.weightInit(WeightInit.XAVIER)
.activation(Activation.RELU)
.nIn(32).nOut(16)
.build();
输出层
输入层的神经元数量等于隐藏层 1 的输出。然后输出的数量等于预测标签的数量。再次考虑到输入和特征非常少,我们设置了一个较小的值。
我们使用了 Softmax 激活函数,它为我们提供了一个类的概率分布(输出的总和为 1.0),并且在二分类(XNET)中使用交叉熵作为损失函数,因为我们想将输出(概率)转换为离散类别,即零或一:
OutputLayer output_layer = new OutputLayer.Builder(LossFunction.XENT) // XENT for Binary Classification
.weightInit(WeightInit.XAVIER)
.activation(Activation.SOFTMAX)
.nIn(16).nOut(numOutputs)
.build();
XNET 用于二分类的逻辑回归。更多信息可以查看 DL4J 中的 LossFunctions.java
类。
现在我们通过指定 NeuralNetConfiguration
来创建一个 MultiLayerConfiguration
,然后进行训练。使用 DL4J 时,我们可以通过调用 NeuralNetConfiguration.Builder()
上的 layer
方法来添加一层,指定其在层的顺序中的位置(以下代码中的零索引层是输入层):
MultiLayerConfiguration MLPconf = new NeuralNetConfiguration.Builder().seed(seed)
.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
.weightInit(WeightInit.XAVIER)
.updater(new Adam(0.0001))
.list()
.layer(0, input_layer)
.layer(1, hidden_layer_1)
.layer(2, hidden_layer_2)
.layer(3, output_layer)
.pretrain(false).backprop(true).build();// no pre-traning required
除了这些之外,我们还指定了如何设置网络的权重。例如,如前所述,我们使用 Xavier 作为权重初始化,并使用 随机梯度下降(SGD)优化算法,Adam 作为更新器。最后,我们还指定不需要进行任何预训练(通常在 DBN 或堆叠自编码器中是需要的)。然而,由于 MLP 是一个前馈网络,我们将反向传播设置为 true。
网络训练
首先,我们使用之前的 MultiLayerConfiguration
创建一个 MultiLayerNetwork
。然后我们初始化网络并开始在训练集上训练:
MultiLayerNetwork model = new MultiLayerNetwork(MLPconf);
model.init();
log.info("Train model....");
for( int i=0; i<numEpochs; i++ ){
model.fit(trainingDataIt);
}
在前面的代码块中,我们通过调用 model.fit()
在训练集(在我们案例中为 trainingDataIt
)上开始训练模型。现在我们来讨论一下如何准备训练集和测试集。好吧,对于读取训练集或测试集格式不正确的数据(特征为数值,标签为整数),我创建了一个名为 readCSVDataset()
的方法:
private static DataSetIterator readCSVDataset(String csvFileClasspath, int batchSize,
int labelIndex, int numClasses) throws IOException, InterruptedException {
RecordReader rr = new CSVRecordReader();
File input = new File(csvFileClasspath);
rr.initialize(new FileSplit(input));
DataSetIterator iterator = new RecordReaderDataSetIterator(rr, batchSize, labelIndex, numClasses);
return iterator;
}
如果你看前面的代码块,你会发现它基本上是一个包装器,用来读取 CSV 格式的数据,然后 RecordReaderDataSetIterator()
方法将记录读取器转换为数据集迭代器。从技术上讲,RecordReaderDataSetIterator()
是分类的主要构造函数。它接受以下参数:
-
RecordReader
:这是提供数据来源的RecordReader
-
batchSize
:输出DataSet
对象的批量大小(即,示例数量) -
labelIndex
:由recordReader.next()
获取的标签索引可写值(通常是IntWritable
) -
numPossibleLabels
:分类的类别数量(可能的标签)
这将把输入的类别索引(在 labelIndex
位置,整数值为 0
到 numPossibleLabels-1
,包括)转换为相应的 one-hot 输出/标签表示。接下来让我们看看如何继续。首先,我们展示训练集和测试集的路径:
String trainPath = "data/Titanic_Train.csv";
String testPath = "data/Titanic_Test.csv";
int labelIndex = 7; // First 7 features are followed by the labels in integer
int numClasses = 2; // number of classes to be predicted -i.e survived or not-survived
int numEpochs = 1000; // Number of training eopich
int seed = 123; // Randome seed for reproducibilty
int numInputs = labelIndex; // Number of inputs in input layer
int numOutputs = numClasses; // Number of classes to be predicted by the network
int batchSizeTraining = 128;
现在,让我们准备要用于训练的数据:
DataSetIterator trainingDataIt = *readCSVDataset*(trainPath, batchSizeTraining, labelIndex, numClasses);
接下来,让我们准备要分类的数据:
int batchSizeTest = 128;
DataSetIterator testDataIt = *readCSVDataset*(testPath, batchSizeTest, labelIndex, numClasses);
太棒了!我们已经成功准备好了训练和测试的DataSetIterator
。记住,我们在为其他问题准备训练和测试集时,将几乎采用相同的方法。
评估模型
一旦训练完成,接下来的任务是评估模型。我们将在测试集上评估模型的性能。对于评估,我们将使用Evaluation()
;它创建一个包含两种可能类别(存活或未存活)的评估对象。从技术上讲,Evaluation 类计算评估指标,如精确度、召回率、F1、准确率和马修斯相关系数。最后一个用于评估二分类器。现在让我们简要了解这些指标:
准确率是正确预测样本与总样本的比例:
精确度是正确预测的正样本与总预测正样本的比例:
召回率是正确预测的正样本与实际类别中所有样本的比例——是的:
F1 分数是精确度和召回率的加权平均值(调和均值):
马修斯相关系数(MCC)是衡量二分类(两类)质量的指标。MCC 可以通过混淆矩阵直接计算,计算公式如下(假设 TP、FP、TN 和 FN 已经存在):
与基于 Apache Spark 的分类评估器不同,在使用基于 DL4J 的评估器解决二分类问题时,应该特别注意二分类指标,如 F1、精确度、召回率等。
好的,我们稍后再讨论这些。首先,让我们对每个测试样本进行迭代评估,并从训练好的模型中获取网络的预测。最后,eval()
方法将预测结果与真实类别进行对比:
*log*.info("Evaluate model....");
Evaluation eval = new Evaluation(2) // for class 1
while(testDataIt.hasNext()){
DataSet next = testDataIt.next();
INDArray output = model.output(next.getFeatureMatrix());
eval.eval(next.getLabels(), output);
}
*log*.info(eval.stats());
*log*.info("****************Example finished********************");
>>>
==========================Scores========================================
# of classes: 2
Accuracy: 0.6496
Precision: 0.6155
Recall: 0.5803
F1 Score: 0.3946
Precision, recall & F1: reported for positive class (class 1 - "1") only
=======================================================================
哎呀!不幸的是,我们在类别 1 的分类准确率上没有取得很高的成绩(即 65%)。现在,我们将为这个二分类问题计算另一个指标,叫做 MCC。
// Compute Matthews correlation coefficient
EvaluationAveraging averaging = EvaluationAveraging.*Macro*;
double MCC = eval.matthewsCorrelation(averaging);
System.*out*.println("Matthews correlation coefficient: "+ MCC);
>>>
Matthews's correlation coefficient: 0.22308172619187497
现在让我们根据 Matthews 论文(详情请见 www.sciencedirect.com/science/article/pii/0005279575901099)来解释这个结果,论文中描述了以下属性:C = 1 表示完全一致,C = 0 表示预测与随机预测一样,没有任何改善,而 C = -1 表示预测与观察结果完全不一致。
接下来,我们的结果显示出一种弱的正相关关系。好吧!尽管我们没有获得很好的准确率,但你们仍然可以尝试调整超参数,甚至更换其他网络,比如 LSTM,这是我们在下一部分将讨论的内容。但我们会为解决癌症预测问题而进行这些工作,这也是本章的主要目标。所以请继续关注我!
使用 LSTM 网络进行癌症类型预测
在前一部分中,我们已经看到了我们的数据(即特征和标签)是什么样的。现在,在这一部分中,我们尝试根据标签对这些样本进行分类。然而,正如我们所看到的,DL4J 需要数据以一个明确的格式,以便用于训练模型。所以让我们进行必要的数据预处理和特征工程。
数据集准备用于训练
由于我们没有任何未标记的数据,我想随机选择一些样本用于测试。还有一点是,特征和标签分为两个独立的文件。因此,我们可以先进行必要的预处理,然后将它们合并在一起,以便我们的预处理数据包含特征和标签。
然后剩余的部分将用于训练。最后,我们将训练集和测试集保存在单独的 CSV 文件中,以供以后使用。首先,让我们加载样本并查看统计信息。顺便说一下,我们使用 Spark 的 read()
方法,但也指定了必要的选项和格式:
Dataset<Row> data = spark.read()
.option("maxColumns", 25000)
.format("com.databricks.spark.csv")
.option("header", "true") // Use first line of all files as header
.option("inferSchema", "true") // Automatically infer data types
.load("TCGA-PANCAN-HiSeq-801x20531/data.csv");// set your path accordingly
然后我们看到一些相关的统计信息,例如特征数量和样本数量:
int numFeatures = data.columns().length;
long numSamples = data.count();
System.*out*.println("Number of features: " + numFeatures);
System.*out*.println("Number of samples: " + numSamples);
>>>
Number of features: 20532
Number of samples: 801
因此,数据集中有来自 801
名不同患者的 801
个样本,且数据集的维度过高,共有 20532
个特征。此外,在 图 2 中,我们看到 id
列仅表示患者的匿名 ID,因此我们可以直接删除它:
Dataset<Row> numericDF = data.drop("id"); // now 20531 features left
然后我们使用 Spark 的 read()
方法加载标签,并指定必要的选项和格式:
Dataset<Row> labels = spark.read()
.format("com.databricks.spark.csv")
.option("header", "true") // Use first line of all files as header
.option("inferSchema", "true") // Automatically infer data types
.load("TCGA-PANCAN-HiSeq-801x20531/labels.csv");
labels.show(10);
我们已经看到标签数据框是什么样子的。我们将跳过 id
列。然而,Class
列是类别型的。正如我所说,DL4J 不支持对类别标签进行预测。因此,我们需要将其转换为数字型(更具体地说是整数型);为此,我将使用 Spark 的 StringIndexer()
。
首先,创建一个 StringIndexer()
;我们将索引操作应用于 Class
列,并将其重命名为 label
。另外,我们会跳过空值条目:
StringIndexer indexer = new StringIndexer()
.setInputCol("Class")
.setOutputCol("label")
.setHandleInvalid("skip");// skip null/invalid values
然后我们通过调用 fit()
和 transform()
操作来执行索引操作,如下所示:
Dataset<Row> indexedDF = indexer.fit(labels)
.transform(labels)
.select(col("label")
.cast(DataTypes.IntegerType));// casting data types to integer
现在让我们看一下索引化后的 DataFrame:
indexedDF.show();
太棒了!现在我们所有的列(包括特征和标签)都是数字类型。因此,我们可以将特征和标签合并成一个单一的 DataFrame。为此,我们可以使用 Spark 的join()
方法,如下所示:
Dataset<Row> combinedDF = numericDF.join(indexedDF);
现在我们可以通过随机拆分combindedDF
来生成训练集和测试集,如下所示:
Dataset<Row>[] splits = combinedDF.randomSplit(newdouble[] {0.7, 0.3});//70% for training, 30% for testing
Dataset<Row> trainingData = splits[0];
Dataset<Row> testData = splits[1];
现在让我们查看每个数据集中的样本数量:
System.out.println(trainingData.count());// number of samples in training set
System.out.println(testData.count());// number of samples in test set
>>>
561
240
因此,我们的训练集有561
个样本,测试集有240
个样本。最后,将这两个数据集保存为单独的 CSV 文件,供以后使用:
trainingData.coalesce(1).write()
.format("com.databricks.spark.csv")
.option("header", "false")
.option("delimiter", ",")
.save("data/TCGA_train.csv");
testData.coalesce(1).write()
.format("com.databricks.spark.csv")
.option("header", "false")
.option("delimiter", ",")
.save("data/TCGA_test.csv");
现在我们已经有了训练集和测试集,我们可以用训练集训练网络,并用测试集评估模型。考虑到高维度,我更愿意尝试一个更好的网络,比如 LSTM,它是 RNN 的改进变体。此时,关于 LSTM 的一些背景信息将有助于理解其概念。
循环神经网络和 LSTM 网络
如在第一章《深入浅出深度学习》中讨论的那样,RNN 利用来自过去的信息;它们可以在具有高度时间依赖性的数据中进行预测。更明确的架构可以在下图中找到,其中时间共享的权重w2(用于隐藏层)必须与w1(用于输入层)和w3(用于输出层)一起学习。从计算角度来看,RNN 处理许多输入向量来生成输出向量。想象一下,以下图中每个矩形都有一个向量深度和其他特殊的隐藏特性:
一个 RNN 架构,其中所有层的权重都必须随着时间学习。
然而,我们通常只需要查看最近的信息来执行当前任务,而不是存储的信息或很久以前到达的信息。这在 NLP 中的语言建模中经常发生。让我们来看一个常见的例子:
如果相关信息之间的间隔较小,RNN 可以学会利用过去的信息。
假设我们想开发一个基于深度学习的自然语言处理(NLP)模型,来预测基于前几个词的下一个词。作为人类,如果我们试图预测“Berlin is the capital of…”中的最后一个词,在没有更多上下文的情况下,下一个词最可能是Germany。在这种情况下,相关信息与位置之间的间隔较小。因此,RNN 可以轻松地学会使用过去的信息。
然而,考虑一个稍长的例子:“Reza grew up in Bangladesh. He studied in Korea. He speaks fluent…” 现在要预测最后一个词,我们需要更多的上下文。在这个句子中,最新的信息告诉网络,下一个词很可能是某种语言的名称。然而,如果我们将焦点缩小到语言层面,孟加拉国(前面的话语中的信息)的背景将是必需的。
如果相关信息与所需位置之间的间隙更大,RNN 无法学习使用过去的信息
在这里,信息之间的间隔比之前的例子要大,因此 RNN 无法学习映射这些信息。然而,深层网络中的梯度是通过多层网络中激活函数的多个梯度相乘(即乘积)来计算的。如果这些梯度非常小或接近零,梯度将容易消失。另一方面,当它们大于 1 时,可能会导致梯度爆炸。因此,计算和更新变得非常困难。让我们更详细地解释这些问题。
RNN 的这两个问题被统称为 梯度消失-爆炸 问题,直接影响模型的性能。实际上,反向传播时,RNN 会展开,形成 一个非常深 的前馈神经网络。RNN 无法获得长期上下文的原因正是这个现象;如果在几层内梯度消失或爆炸,网络将无法学习数据之间的高时间距离关系。
因此,RNN 无法处理长期依赖关系、梯度爆炸和梯度消失问题是其严重缺点。此时,LSTM 就作为救世主出现了。
正如名字所示,短期模式不会在长期中被遗忘。LSTM 网络由相互连接的单元(LSTM 块)组成。每个 LSTM 块包含三种类型的门:输入门、输出门和遗忘门。它们分别实现对单元记忆的写入、读取和重置功能。这些门不是二元的,而是模拟的(通常由一个 sigmoid 激活函数管理,映射到 [0, 1] 范围内,其中零表示完全抑制,1 表示完全激活)。
我们可以将 LSTM 单元看作一个基本的单元,但它的训练会更快收敛,并且能够检测数据中的长期依赖关系。现在问题是:LSTM 单元是如何工作的?基本 LSTM 单元的架构如下图所示:
LSTM 单元的框图
现在,让我们来看一下这个架构背后的数学符号。如果我们不看 LSTM 盒子内部的内容,LSTM 单元本身看起来与常规内存单元完全相同,只是它的状态被分为两个向量,h(t) 和 c(t):
-
c 是单元
-
h(t) 是短期状态
-
c(t) 是长期状态
现在,让我们打开这个“盒子”!关键思想是网络能够学习以下内容:
-
存储什么在长期状态中
-
丢弃什么
-
阅读内容
用更简化的话来说,在 STM 中,原始 RNN 的所有隐藏单元都被内存块替代,每个内存块包含一个内存单元,用于存储输入历史信息,并且有三个门用于定义如何更新信息。这些门是输入门、遗忘门和输出门。
这些门的存在使得 LSTM 单元能够记住信息并维持无限期。实际上,如果输入门低于激活阈值,单元将保持前一个状态;如果当前状态启用,它将与输入值相结合。顾名思义,遗忘门重置单元的当前状态(当其值被清零时),而输出门决定是否执行单元的值。
尽管长期状态会被复制并通过 tanh 函数传递,但在 LSTM 单元内部,需要在两个激活函数之间进行整合。例如,在下面的图示中,tanh 决定了哪些值需要加入状态,而这依赖于 sigmoid 门的帮助:
LSTM 单元结构的内部组织
现在,由于本书并不打算讲授理论,我想在这里停止讨论,但有兴趣的读者可以在 DL4J 网站上找到更多细节:deeplearning4j.org/lstm.html
。
数据集准备
在上一节中,我们准备了训练集和测试集。然而,我们需要做一些额外的工作,使它们能够被 DL4J 使用。更具体地说,DL4J 期望训练数据是数字类型,且最后一列是标签列,剩下的列是特征。
现在,我们将尝试按此方式准备我们的训练集和测试集。首先,我们展示保存训练集和测试集的文件:
String trainPath = "data/TCGA_train.csv"; // training set
String testPath = "data/TCGA_test.csv"; // test set
然后,我们定义所需的参数,如特征数量、类别数量和批量大小。在这里,我使用128
作为batchSize
,但根据需要进行调整:
int labelIndex = 20531;// number of features
int numClasses = 5; // number of classes to be predicted
int batchSize = 128; // batch size (feel free to adjust)
这个数据集用于训练:
DataSetIterator trainingDataIt = readCSVDataset(trainPath, batchSize, labelIndex, numClasses);
这是我们想要分类的数据:
DataSetIterator testDataIt = *readCSVDataset*(testPath, batchSize, labelIndex, numClasses);
如果你看到前面两行,你可以意识到readCSVDataset()
本质上是一个读取 CSV 格式数据的包装器,然后RecordReaderDataSetIterator()
方法将记录读取器转换为数据集迭代器。更多详细信息,请参见 使用 DL4J 重新审视泰坦尼克号生存预测 部分。
LSTM 网络构建
如在泰坦尼克号生存预测部分讨论的那样,一切从MultiLayerConfiguration
开始,它组织这些层及其超参数。我们的 LSTM 网络由五层组成。输入层后面是三层 LSTM 层。最后一层是 RNN 层,也是输出层。
更技术性地说,第一层是输入层,然后有三层作为 LSTM 层。对于 LSTM 层,我们使用 Xavier 初始化权重。我们使用 SGD 作为优化算法,Adam 更新器,激活函数是 tanh。
最后,RNN 输出层有一个 softmax 激活函数,它为我们提供了一个类别的概率分布(即输出之和为1.0),并且 MCXENT 是多类别交叉熵损失函数。这个设置如图所示:
用于泰坦尼克号生存预测的多层感知机。它采用 20,531 个特征和固定的偏置(即 1),并生成多类别的输出。
为了创建 LSTM 层,DL4J 提供了 LSTM 和 GravesLSTM 类。后者是一个基于有监督序列标注的循环神经网络(详见 www.cs.toronto.edu/~graves/phd.pdf
)的 LSTM 递归网络。
GravesLSTM 与 CUDA 不兼容。因此,建议在 GPU 上进行训练时使用 LSTM。否则,GravesLSTM 比 LSTM 更快。
现在,在我们开始创建网络之前,让我们定义所需的超参数,例如输入/隐藏/输出节点(神经元)的数量:
// Network hyperparameters
int numInputs = labelIndex; // number of input features
int numOutputs = numClasses; // number of classes to be predicted
int numHiddenNodes = 5000; // too many features, so 5000 sounds good
我们现在创建一个网络配置并进行网络训练。使用 DL4J,你可以通过在NeuralNetConfiguration.Builder()
上调用layer
来添加一个层,并指定它在层的顺序中的位置(以下代码中的零索引层是输入层):
// Create network configuration and conduct network training
MultiLayerConfiguration LSTMconf = new NeuralNetConfiguration.Builder()
.seed(seed) //Random number generator seed for improved repeatability. Optional.
.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
.weightInit(WeightInit.XAVIER)
.updater(new Adam(0.001))
.list()
.layer(0, new LSTM.Builder()
.nIn(numInputs)
.nOut(numHiddenNodes)
.activation(Activation.RELU)
.build())
.layer(1, new LSTM.Builder()
.nIn(numHiddenNodes)
.nOut(numHiddenNodes)
.activation(Activation.RELU)
.build())
.layer(2, new LSTM.Builder()
.nIn(numHiddenNodes)
.nOut(numHiddenNodes)
.activation(Activation.RELU)
.build())
.layer(3, new RnnOutputLayer.Builder()
.activation(Activation.SOFTMAX)
.lossFunction(LossFunction.MCXENT)
.nIn(numHiddenNodes)
.nOut(numOutputs)
.build())
.pretrain(false).backprop(true).build();
最后,我们还指定了我们不需要进行任何预训练(这通常在 DBN 或堆叠自编码器中是必要的)。
网络训练
首先,我们使用之前的MultiLayerConfiguration
创建一个MultiLayerNetwork
。然后,我们初始化网络并开始在训练集上进行训练:
MultiLayerNetwork model = new MultiLayerNetwork(LSTMconf);
model.init();
log.info("Train model....");
for(int i=0; i<numEpochs; i++ ){
model.fit(trainingDataIt);
}
通常,这种类型的网络有许多超参数。让我们打印网络中的参数数量(以及每一层的参数数量):
Layer[] layers = model.getLayers();
int totalNumParams = 0;
for( int i=0; i<layers.length; i++ ){
int nParams = layers[i].numParams();
System.*out*.println("Number of parameters in layer " + i + ": " + nParams);
totalNumParams += nParams;
}
System.*out*.println("Total number of network parameters: " + totalNumParams);
>>>
Number of parameters in layer 0: 510655000
Number of parameters in layer 1: 200035000
Number of parameters in layer 2: 200035000
Number of parameters in layer 3: 25005
Total number of network parameters: 910750005
正如我所说,我们的网络有 9.1 亿个参数,这非常庞大。在调整超参数时,这也提出了很大的挑战。然而,我们将在常见问题解答部分看到一些技巧。
评估模型
一旦训练完成,接下来的任务就是评估模型。我们将评估模型在测试集上的表现。对于评估,我们将使用Evaluation()
方法。这个方法创建一个评估对象,包含五个可能的类别。首先,我们将遍历每个测试样本,并从训练好的模型中获取网络的预测。最后,eval()
方法会检查预测结果与真实类别的匹配情况:
*log*.info("Evaluate model....");
Evaluation eval = new Evaluation(5) // for 5 classes
while(testDataIt.hasNext()){
DataSet next = testDataIt.next();
INDArray output = model.output(next.getFeatureMatrix());
eval.eval(next.getLabels(), output);
}
*log*.info(eval.stats());
*log*.info("****************Example finished********************");
>>>
==========================Scores========================================
# of classes: 5
Accuracy: 0.9950
Precision: 0.9944
Recall: 0.9889
F1 Score: 0.9915
Precision, recall & F1: macro-averaged (equally weighted avg. of 5 classes)
========================================================================
****************Example finished********************
哇!不可思议!我们的 LSTM 网络准确地分类了这些样本。最后,让我们看看分类器在每个类别中的预测情况:
Predictions labeled as 0 classified by model as 0: 82 times
Predictions labeled as 1 classified by model as 1: 17 times
Predictions labeled as 1 classified by model as 2: 1 times
Predictions labeled as 2 classified by model as 2: 35 times
Predictions labeled as 3 classified by model as 3: 31 times
Predictions labeled as 4 classified by model as 4: 35 times
使用 LSTM 进行癌症类型预测的预测准确率异常高。我们的网络是不是欠拟合了?有没有办法观察训练过程?换句话说,问题是为什么我们的 LSTM 神经网络显示 100% 的准确率。我们将在下一节尝试回答这些问题。请继续关注!
常见问题解答(FAQ)
既然我们已经以一个可接受的准确度解决了泰坦尼克号生存预测问题,还有一些实际的方面需要考虑,这些方面不仅涉及此问题本身,也涉及整体的深度学习现象。在本节中,我们将看到一些可能已经在你脑海中的常见问题。这些问题的答案可以在附录 A 中找到。
-
我们不能使用 MLP 来解决癌症类型预测问题,处理这么高维的数据吗?
-
RNN 类型的网络可以使用哪些激活函数和损失函数?
-
递归神经网络的最佳权重初始化方法是什么?
-
应该使用哪种更新器和优化算法?
-
在泰坦尼克号生存预测问题中,我们的准确率不高。可能的原因是什么?我们如何提高准确率?
-
使用 LSTM 进行癌症类型预测的预测准确率异常高。我们的网络是不是欠拟合了?有没有办法观察训练过程?
-
我应该使用哪种类型的 RNN 变种,也就是 LSTM 还是 GravesLSTM?
-
为什么我的神经网络会抛出 nan 分数值?
-
如何配置/更改 DL4J UI 端口?
总结
在本章中,我们学习了如何基于从 TCGA 收集的高维基因表达数据集对癌症患者进行肿瘤类型分类。我们的 LSTM 架构成功地达到了 100% 的准确率,这是非常出色的。然而,我们也讨论了很多与 DL4J 相关的方面,这些内容将对后续章节非常有帮助。最后,我们还解答了一些关于该项目、LSTM 网络和 DL4J 超参数/网络调优的常见问题。
在下一章中,我们将看到如何开发一个端到端项目,使用基于 Scala 和 DL4J 框架的 CNN 来处理多标签(每个实体可以属于多个类别)的图像分类问题,数据集来自真实的 Yelp 图像数据。我们还将在开始之前讨论一些 CNN 的理论方面。然而,我们会讨论如何调整超参数,以获得更好的分类结果。
问题的答案
问题 1 的答案:答案是肯定的,但不太舒适。这意味着像深度 MLP 或 DBN 这样的非常深的前馈网络可以通过多次迭代进行分类。
然而,坦率地说,MLP 是最弱的深度架构,对于像这样的高维数据并不理想。此外,自 DL4J 1.0.0-alpha 版本以来,DL4J 已弃用了 DBN。最后,我仍然想展示一个 MLP 网络配置,以防你想尝试。
// Create network configuration and conduct network training
MultiLayerConfiguration MLPconf = new NeuralNetConfiguration.Builder().seed(seed)
.optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
.updater(new Adam(0.001)).weightInit(WeightInit.XAVIER).list()
.layer(0,new DenseLayer.Builder().nIn(numInputs).nOut(32)
.weightInit(WeightInit.XAVIER)
.activation(Activation.RELU).build())
.layer(1,new DenseLayer.Builder().nIn(32).nOut(64).weightInit(WeightInit.XAVIER)
.activation(Activation.RELU).build())
.layer(2,new DenseLayer.Builder().nIn(64).nOut(128).weightInit(WeightInit.XAVIER)
.activation(Activation.RELU).build())
.layer(3, new OutputLayer.Builder(LossFunction.XENT).weightInit(WeightInit.XAVIER)
.activation(Activation.SOFTMAX).weightInit(WeightInit.XAVIER).nIn(128)
.nOut(numOutputs).build())
.pretrain(false).backprop(true).build();
然后,只需将代码行 MultiLayerNetwork model = new MultiLayerNetwork(LSTMconf);
更改为 **MultiLayerNetwork** model = **new** **MultiLayerNetwork**(MLPconf);
。读者可以在 CancerPreddictionMLP.java
文件中看到完整的源代码。
问题 2 的答案: 关于激活函数的选择,有两个方面需要注意。
隐藏层的激活 函数: 通常,ReLU 或 leakyrelu 激活函数是不错的选择。其他一些激活函数(如 tanh、sigmoid 等)更容易出现梯度消失问题。然而,对于 LSTM 层,tanh 激活函数仍然是常用的选择。
这里有个注意点:一些人不想使用修正线性单元(ReLU)的原因是,它在与平滑的非线性函数(例如在 RNN 中使用的 sigmoid)相比,表现得似乎不太好(更多内容请参见 arxiv.org/pdf/1312.4569.pdf
)。即使 tanh 在 LSTM 中的效果也要好得多。因此,我在 LSTM 层使用了 tanh 作为激活函数。
输出层的激活 函数: 对于分类问题,建议使用 Softmax 激活函数并结合负对数似然/MCXENT。但是,对于回归问题,“IDENTITY”激活函数是一个不错的选择,损失函数使用 MSE。简而言之,选择取决于具体应用。
问题 3 的答案: 好吧,我们需要确保网络的权重既不太大也不太小。我不推荐使用随机初始化或零初始化;通常,Xavier 权重初始化是一个不错的选择。
问题 4 的答案: 除非 SGD 收敛得很好,否则 momentum/rmsprop/adagrad 优化器是一个不错的选择。然而,我常常使用 Adam 作为更新器,并且也观察到了良好的表现。
问题 5 的答案: 好吧,这个问题没有明确的答案。实际上,可能有几个原因。例如,可能我们没有选择合适的超参数。其次,数据量可能不足。第三,我们可能在使用其他网络,如 LSTM。第四,我们可能没有对数据进行标准化。
好吧,对于第三种方法,你当然可以尝试使用类似的 LSTM 网络;我在癌症类型预测中使用了它。对于第四种方法,标准化数据总是能带来更好的分类准确率。现在问题是:你的数据分布是什么样的?你是否正确地进行缩放?连续值必须在 -1 到 1、0 到 1 的范围内,或者是均值为 0、标准差为 1 的正态分布。
最后,我想给你一个关于 Titanic 示例中的数据标准化的具体例子。为此,我们可以使用 DL4J 的 NormalizerMinMaxScaler()
。一旦我们创建了训练数据集迭代器,就可以实例化一个 NormalizerMinMaxScaler()
对象,然后通过调用 fit()
方法对数据进行标准化。最后,使用 setPreProcessor()
方法执行转换,如下所示:
NormalizerMinMaxScaler preProcessor = new NormalizerMinMaxScaler();
preProcessor.fit(trainingDataIt);
trainingDataIt.setPreProcessor(preProcessor);
现在,对于测试数据集迭代器,我们应用相同的标准化方法以获得更好的结果,但不调用fit()
方法:
testDataIt.setPreProcessor(preProcessor);
更详细地说,NormalizerMinMaxScaler ()
作为数据集的预处理器,将特征值(以及可选的标签值)标准化到最小值和最大值之间(默认情况下,是 0 到 1 之间)。读者可以在CancerPreddictionMLP.java
文件中查看完整的源代码。在此标准化之后,我在类 1 上获得了稍微更好的结果,如下所示(你也可以尝试对类 0 做相同的操作):
==========================Scores========================================
# of classes: 2
Accuracy: 0.6654
Precision: 0.7848
Recall: 0.5548
F1 Score: 0.2056
Precision, recall & F1: reported for positive class (class 1 - "1") only
========================================================================
问题 6 的回答: 在实际情况中,神经网络达到 100%准确率是很罕见的。然而,如果数据是线性可分的,那么是有可能的!请查看以下散点图,图中黑线清楚地将红点和深蓝点分开:
非常清晰且线性可分的数据点
更技术性地说,由于神经元的输出(在通过激活函数之前)是输入的线性组合,因此由单个神经元组成的网络可以学习到这个模式。这意味着,如果我们的神经网络将这条线划对了,实际上是有可能达到 100%准确率的。
现在,回答第二部分:可能不是。为了证明这一点,我们可以通过观察训练损失、得分等,在 DL4J UI 界面上查看训练过程,DL4J UI 是用于在浏览器中实时可视化当前网络状态和训练进度的界面。
UI 通常用于帮助调整神经网络的参数,也就是选择超参数以获得良好的网络性能。这些内容已经包含在CancerPreddictionLSTM.java
文件中,所以不用担心,继续进行即可。
步骤 1:将 DL4J 的依赖项添加到你的项目中
在下面的依赖标签中,_2.11
后缀用于指定 Scala 版本,以便与 Scala Play 框架一起使用。你应该相应地进行设置:
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>deeplearning4j-ui_2.11</artifactId>
<version>${dl4j.version}</version>
</dependency>
步骤 2:在项目中启用 UI
这相对简单。首先,你需要按照以下方式初始化用户界面后端:
UIServer uiServer = UIServer.*getInstance*();
接着,你需要配置网络信息存储的位置。然后可以添加 StatsListener 来收集这些信息:
StatsStorage statsStorage = new InMemoryStatsStorage();
最后,我们将 StatsStorage 实例附加到 UI 界面:
uiServer.attach(statsStorage);
int listenerFrequency = 1;
model.setListeners(new StatsListener(statsStorage, listenerFrequency));
步骤 3:通过调用 fit()方法开始收集信息 当你在网络上调用fit
方法时,信息将会被收集并传送到 UI 界面。
步骤 4:访问 UI 配置完成后,可以在localhost:9000/train
访问 UI。现在,回答“我们的网络是否欠拟合?有没有方法观察训练过程?”我们可以在概览页面上观察到模型得分与迭代图表。如在deeplearning4j.org/visualization
的模型调优部分所建议,我们得到了以下观察结果**:**
-
总体分数与迭代次数应随时间下降
-
分数没有持续增长,而是在迭代过程中急剧下降
问题可能在于折线图中没有噪声,而这实际上是理想的情况(也就是说,折线应该在一个小范围内上下波动)。
现在为了解决这个问题,我们可以再次对数据进行归一化,并重新训练以查看性能差异。好吧,我希望大家自己去试试。还有一个提示是遵循我们在问题 5 中讨论的相同数据归一化方法。
LSTM 模型的得分随迭代次数变化
现在,还有一个观察结果值得提及。例如,梯度直到最后才消失,这从下图中可以看得更清楚:
LSTM 网络在不同迭代之间的梯度
最终,激活函数始终如一地发挥了作用,从下图中可以更清楚地看到这一点:
LSTM 网络的激活函数在不同层之间始终如一地发挥着作用
关键是还有许多因素需要考虑。然而,实际上,调整神经网络往往更多的是一种艺术而非科学,而且正如我所说,我们还没有考虑到许多方面。不过,不要担心;我们将在接下来的项目中看到它们。所以坚持住,让我们继续看下一个问题。
问题 7 的答案: LSTM 支持 GPU/CUDA,但 GravesLSTM 仅支持 CUDA,因此目前不支持 CuDNN。不过,如果你想要更快的训练和收敛,建议使用 LSTM 类型。
问题 8 的答案: 在训练神经网络时,反向传播涉及在非常小的梯度上进行乘法操作。这是由于在表示实数时有限精度的问题;非常接近零的值无法表示。
它引入了算术下溢问题,这种情况通常发生在像 DBN、MLP 或 CNN 这样的深度网络中。此外,如果你的网络抛出 NaN,那么你需要重新调整网络,以避免非常小的梯度。
问题 9 的答案: 你可以通过使用 org.deeplearning4j.ui.port 系统属性来设置端口。更具体地说,例如,要使用端口 9001
,在启动时将以下内容传递给 JVM:
-Dorg.deeplearning4j.ui.port=9001