原文:
annas-archive.org/md5/2c46eb32c632fab66dcbd5073c687a15相关服务:美国云服务器租用
译者:飞龙
前言
在大量数据计算资源的支持下,机器学习(ML)取得了重大进展。金融行业作为一个以信息处理为核心的企业,蕴藏着大量应用这些新技术的机会。
本书是一本现代机器学习在金融行业应用的实用指南。采用代码优先的方法,它将教你最有用的机器学习算法是如何工作的,以及如何利用它们解决现实世界的问题。
本书适合的人群
有三类人群将从本书中受益最大:
-
希望进入金融行业并了解可能的应用范围和相关问题的数据科学家
-
希望提升技能并将高级机器学习方法应用于建模过程的任何金融科技公司开发人员或量化金融专业人士
-
希望为进入劳动力市场做好准备并学习一些雇主重视的实用技能的学生
本书假设你具备一定的线性代数、统计学、概率论和微积分基础知识。然而,你不必是这些领域的专家。
为了跟随代码示例,你应该熟悉 Python 和常见的数据科学库,如 pandas、NumPy 和 Matplotlib。本书的示例代码是在 Jupyter Notebooks 中呈现的。
不需要显式的金融知识。
为了最大限度地利用本书
所有代码示例托管在 Kaggle 上。你可以免费使用 Kaggle 并获得 GPU,这将使你能够更快地运行示例代码。如果你的机器没有非常强大的 GPU,使用 Kaggle 运行代码将更为舒适。你可以在本书的 GitHub 页面找到所有笔记本的链接:github.com/PacktPublishing/Machine-Learning-for-Finance。
本书假设你具备一定的数学概念基础,如线性代数、统计学、概率论和微积分。但你不必是这些领域的专家。
同样,假设你了解 Python 和一些流行的数据科学库,如 pandas 和 Matplotlib。
下载示例代码文件
你可以从www.packt.com的账户中下载本书的示例代码文件。如果你是从其他地方购买的本书,可以访问www.packt.com/support,注册后即可直接通过邮件收到代码文件。
你可以按照以下步骤下载代码文件:
-
在
www.packt.com登录或注册。 -
选择支持标签。
-
点击代码下载与勘误。
-
在搜索框中输入书名,并按照屏幕上的指示操作。
文件下载后,请确保使用最新版本的工具解压或提取文件夹:
-
WinRAR / 7-Zip for Windows
-
Zipeg / iZip / UnRarX for Mac
-
7-Zip / PeaZip for Linux
我们还提供了来自我们丰富书籍和视频目录的其他代码包,访问github.com/PacktPublishing/了解更多!
下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789136364_ColorImages.pdf。
使用的约定
本书中使用了许多文本约定。
CodeInText:指文中出现的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账号。例如:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”
一段代码块的设置方式如下:
import numpy as np
x_train = np.expand_dims(x_train,-1)
x_test = np.expand_dims(x_test,-1)
x_train.shape
当我们希望引起你对代码块中特定部分的注意时,相关的行或项目会以粗体显示:
from keras.models import Sequential
img_shape = (28,28,1)
model = Sequential()
model.add(Conv2D(6,3,input_shape=img_shape))
任何命令行输入或输出如下所示:
Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 22s 374us/step - loss: 7707.2773 - acc: 0.6556 - val_loss: 55.7280 - val_acc: 0.7322
粗体:表示新术语、重要词汇,或在屏幕上看到的词语,例如在菜单或对话框中,也以此格式出现在文本中。例如:“从系统信息中选择管理面板。”
注意
警告或重要的说明会以此形式呈现。
提示
提示和技巧将以此形式出现。
联系我们
我们欢迎读者的反馈。
一般反馈:如果你对本书的任何方面有疑问,请在邮件主题中提到书名,并通过[email protected]联系我们。
勘误:尽管我们已尽力确保内容的准确性,但错误仍然会发生。如果你发现了本书中的错误,我们将非常感激你向我们报告。请访问www.packt.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果你在互联网上发现我们作品的非法复制版本,我们将非常感谢你提供具体的地址或网站名称。请通过[email protected]与我们联系,并附上链接。
如果你有兴趣成为作者:如果你在某个主题上有专长,并且有兴趣写书或为书籍贡献内容,请访问authors.packtpub.com。
评价
请留下评论。当你阅读并使用完本书后,为什么不在你购买本书的网站上留下评论呢?潜在的读者可以看到并参考你的公正意见来做出购买决定,我们 Packt 也能了解你对我们产品的看法,作者们也能看到你对他们书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问packt.com。
第一章:神经网络与基于梯度的优化
金融服务行业本质上是一个信息处理行业。投资基金处理信息以评估投资,保险公司处理信息以定价保险,而零售银行则处理信息以决定向哪些客户提供哪些产品。因此,金融行业早期采用计算机并非偶然。
第一个股票行情机是印刷电报,发明于 1867 年。第一台专门面向金融行业的机械加法机于 1885 年获得专利。然后,在 1971 年,自动柜员机(ATM)获得专利,允许客户使用塑料卡提取现金。同年,首个电子证券交易所——纳斯达克(NASDAQ)开盘,11 年后,即 1982 年,第一台彭博终端(Bloomberg Terminal)安装使用。金融行业与计算机之间的成功结合,原因在于该行业的成功,尤其是在投资领域,通常与是否拥有信息优势密切相关。
在华尔街的早期,镀金时代的传说肆意利用私人信息。例如,杰伊·古尔德(Jay Gould),当时最富有的人之一,在美国政府内部安插了一个间谍。这个间谍负责提前通报政府的黄金销售信息,并借此试图影响总统尤利西斯·S·格兰特及其秘书。到了 20 世纪 30 年代末,证券交易委员会(SEC)和商品期货交易委员会(CFTC)成为了投资者和这些信息优势之间的屏障。
随着信息优势不再成为超越市场表现的可靠来源,聪明的金融建模取而代之。对冲基金这一术语最早出现在 1949 年,哈里·马科维茨(Harry Markowitz)模型于 1953 年发布,1973 年,布莱克-斯科尔斯(Black-Scholes)公式首次发表。自那时以来,该领域取得了巨大的进展,并开发出了多种金融产品。然而,随着这些模型知识的普及,使用这些模型的回报逐渐减少。
当我们看到金融行业与现代计算技术结合时,显然信息优势重新回归。这一次,它不是以内幕信息和不正当交易的形式出现,而是通过自动化分析大量公开信息的方式展现。
今天的基金经理拥有比前辈们曾经梦寐以求的更多信息。然而,光有这些信息本身并不有用。例如,来看新闻报道。你可以通过互联网获取它们,而且容易访问,但要利用它们,计算机必须能够阅读、理解并将其置于上下文中。计算机必须知道一篇文章讲的是哪家公司,是好消息还是坏消息,并且是否可以从中了解这家公司与文章中提到的另一家公司之间的关系。这只是几个关于如何将故事放入上下文中的例子。那些擅长获取这种替代数据的公司,往往会拥有优势。
但事情并不止于此。金融专业人士是昂贵的人员,他们经常拥有六到七位数的薪水,并占据全球一些最昂贵的房地产。这是合理的,因为许多金融专业人士是聪明、受过良好教育且勤奋工作的人,他们稀缺并且需求量大。因此,任何公司都希望最大化这些个人的生产力。通过让最优秀的员工创造更多价值,公司将能够以更低的成本或更多样化的产品来提供服务。
例如,通过交易所交易基金进行的被动投资,对大笔资金的管理几乎无需干预。被动投资工具的费用,例如那些仅仅跟踪标普 500 指数的基金,通常低于 1%。但是随着现代计算技术的崛起,企业现在能够提高其资金经理的生产力,从而降低费用以保持竞争力。
本书的旅程
这本书不仅仅是关于金融领域的投资或交易;它更是计算机与金融之间爱情故事的直接结果。投资公司有客户,通常是保险公司或养老金基金,而这些公司本身就是金融服务公司,反过来,它们也有客户,那些拥有养老金或投保的普通人。
大多数银行客户也是普通人,而且越来越多的人通过手机应用与银行、保险公司或养老金机构互动。
在今天之前的几十年里,零售银行依赖于人们必须亲自到分行才能提取现金或进行交易这一事实。当人们在分行时,银行顾问还可以向他们推销其他产品,例如抵押贷款或保险。今天的客户仍然想要购买抵押贷款和保险,但他们不再需要亲自到分行办理。在今天的世界里,银行倾向于通过应用程序或网站在线为客户提供建议。
这种在线模式只有在银行能够通过数据理解客户需求并提供量身定制的在线体验时才能奏效。同样,从客户的角度来看,他们现在期望能够通过手机提交保险理赔并立即得到回应。在今天的世界中,保险公司需要能够自动评估理赔并做出决策,以满足客户的需求。
本书并非讲解如何编写交易算法以快速获利。它是关于如何利用机器学习驱动的系统艺术和技巧,在金融行业中构建有用的系统。
构建任何有价值的东西都需要大量的时间和精力。目前,构建有价值事物的市场,与经济学类比,是高度低效的。机器学习的应用将在未来几十年内变革这个行业,而本书将为你提供一个工具箱,帮助你成为这一变革的一部分。
本书中的许多例子使用了超出“财务数据”范畴的数据。本书中没有使用股市数据,做出这一决定有三个具体原因。
首先,所展示的例子演示了通常可以轻松应用于其他数据集的技术。因此,选择了能够展示一些专业人士(如你们)在实践中可能面临的常见挑战的数据集,同时这些数据集也保持了计算上的可处理性。
其次,金融数据本质上是时间相关的。为了使本书在更长时间跨度内仍然有用,并确保随着机器学习的普及,本书能继续成为你工具箱中的重要组成部分,我们使用了一些非财务数据,这样本书讨论的数据将保持相关性。
最后,使用替代性和非传统数据旨在激发你思考在你的流程中还可以使用哪些其他数据。你是否可以利用植物的无人机影像来增强你的粮食价格模型?你是否可以利用网页浏览行为来提供不同的金融产品?如果你想利用身边的数据,跳出框框思考是一个必要的技能。
什么是机器学习?
“机器学习是计算机科学的一个子领域,它赋予计算机在没有明确编程的情况下学习的能力。”
- 亚瑟·塞缪尔,1959 年
我们所说的机器学习是什么意思?今天的大多数计算机程序是由人类手工编写的。软件工程师精心设计每一个规则来规范软件的行为,然后将其转化为计算机代码。
如果你正在阅读这本电子书,现在可以看看你的屏幕。你所看到的一切都是因为某个软件工程师设计了一些规则。这种方法已经带我们走得很远,但这并不意味着它没有限制。有时,可能会有太多规则让人类去编写。我们可能无法想到规则,因为它们太复杂,即使是最聪明的开发人员也难以想出。
做一个简短的练习,花一分钟时间列出描述所有狗的规则,并且明确区分狗和所有其他动物。毛发?好吧,猫也有毛发。那如果一只狗穿着夹克呢?那还是狗,只不过穿了夹克。研究人员花了多年时间试图制定这些规则,但收效甚微。
人类似乎无法完美地解释为什么某样东西是狗,但我们一眼就能认出狗。作为一个物种,我们似乎能检测出一些难以描述的特定模式,这些模式合起来让我们能够将动物分类为狗。机器学习试图做的就是一样的事情。我们不是手工编写规则,而是让计算机通过模式检测来发展自己的规则。
这个方法有多种实现方式,接下来我们将介绍三种不同的学习类型:监督学习、无监督学习和强化学习。
监督学习
让我们回到狗分类器的话题。实际上,目前有很多这样的分类器正在被使用。例如,如果你使用谷歌图片搜索“狗”,它会使用图像分类器来展示狗的图片。这些分类器是在监督学习的范式下进行训练的。
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_01.jpg
监督学习
在监督学习中,我们有大量的训练样本,比如动物的图像,以及描述这些训练样本期望结果的标签。例如,上面的图像会附有标签“狗”,而猫的图像则会附有标签“不是狗”。
如果我们有大量带标签的训练示例,我们可以训练一个分类器,检测出区分狗和其他动物的微妙统计模式。
提示
注意:分类器并不知道狗的本质是什么。它只知道训练过程中将图像与狗联系起来的统计模式。
如果一个监督学习分类器遇到与训练数据非常不同的内容,它往往会感到困惑,并输出无意义的结果。
无监督学习
尽管监督学习在过去几年取得了很大的进展,但本书的大部分内容将重点讨论使用带标签的示例。然而,有时我们可能没有标签。在这种情况下,我们仍然可以使用机器学习来发现数据中的隐藏模式。
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_02.jpg
聚类是无监督学习的一种常见形式
想象一下,一个公司拥有若干客户群体。这些客户大致可以分为不同的市场细分,但我们不知道这些市场细分具体是什么。我们也不能直接询问客户他们属于哪个市场细分,因为他们可能并不知道。你是洗发水市场的哪一类消费者?你甚至知道洗发水公司是如何细分客户的吗?
在这个例子中,我们希望有一个算法能够查看大量来自客户的数据,并将他们分为不同的群体。这是一个无监督学习的例子。
这一领域的机器学习远不如监督学习发展成熟,但它依然蕴藏着巨大的潜力。
强化学习
在强化学习中,我们训练能够在环境中采取行动的智能体,例如在道路上行驶的自动驾驶汽车。尽管我们没有标签,也就是说,我们无法判断在任何情况下什么是正确的行动,但我们可以给予奖励或惩罚。例如,我们可以奖励保持与前车的适当距离。
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_03.jpg
强化学习
驾驶教练不会告诉学员“在转动方向盘两度右转的同时将刹车踩下去一半”,而是告诉学员他们做得好不好,学员则自己找出该使用多少刹车。
在过去的几年里,强化学习也取得了显著进展,许多人认为它是通向通用人工智能的一个有前途的方向,也就是计算机具备与人类相当的智能。
数据的非凡有效性
2009 年,三位谷歌工程师发表了一篇具有里程碑意义的论文,题为数据的非凡有效性。在论文中,他们描述了长期以来一直存在的相对简单的机器学习系统,在接入谷歌服务器上巨量的数据后,表现出了远超预期的性能。实际上,他们发现,当这些简单的系统获得更多数据时,它们能够掌握以前被认为不可能完成的任务。
从那时起,研究人员迅速开始重新审视旧有的机器学习技术,并发现人工神经网络在大数据集上训练时表现尤其出色。大约在那个时候,计算能力变得更加便宜和丰富,足以训练比以往更大的网络。
这些更大的人工神经网络如此有效,以至于它们被赋予了一个名字:深度神经网络,或深度学习。深度神经网络在模式识别方面特别擅长。它们可以找到复杂的模式,例如描述面部的光暗统计模式,且在拥有足够数据的情况下能够自动完成这一过程。
因此,机器学习最好被理解为一种程序设计范式的变革。我们不再精心手工编写规则,而是向计算机提供大量信息,训练它自行制定规则。
如果规则的数量非常庞大,或者这些规则难以描述,那么这种方法是更优的。因此,现代机器学习是理想的工具,用于处理金融行业所面临的大量数据。
所有模型都是错误的
统计学中有句名言:所有模型都是错误的,但有些是有用的。机器学习创造了极为复杂的统计模型,这些模型往往在深度学习中,难以被人类理解。它们确实很有用,并且具有很大的价值,但它们仍然是错误的。这是因为它们是复杂的“黑箱”,而人们往往不去质疑机器学习模型,尽管正因为它们是黑箱,我们更应该质疑它们。
总有一天,即使是最先进的深度神经网络,也会做出根本错误的预测,正如 2008 年金融危机中的担保债务凭证(CDO)模型所做的那样。更糟糕的是,黑箱式的机器学习模型将在贷款审批或保险等方面做出数百万个决定,影响普通人的生活,最终可能做出错误的决策。
有时候它们会有偏见。机器学习的效果永远取决于我们提供给它的数据,而这些数据常常在展示时存在偏见,这是我们将在本章稍后讨论的内容。我们必须花大量时间去解决这个问题,因为如果我们不加思考地部署这些算法,我们也会自动化歧视,进而可能引发另一场金融危机。
这一点在金融行业尤为真实,在这里,算法往往会对人们的生活产生严重影响,同时这些算法又是保密的。那些通过大量数学运算获得认可的、不容质疑的秘密黑箱,带来的社会威胁远远大于你在电影中看到的、自觉的人工智能接管世界的情节。
虽然这不是一本伦理学书籍,但对于任何该领域的从业者来说,了解自己工作的伦理影响是有意义的。除了推荐你阅读 Cathy O’Neil 的*《数学毁灭武器》,我还建议你宣誓《建模者的希波克拉底誓言》*。这项誓言由量化金融研究人员 Emanuel Derman 和 Paul Wilmott 于 2008 年在金融危机后提出:
“我会记得,我不是创造了这个世界,世界也不符合我的方程式。尽管我会大胆地使用模型来估计价值,但我不会过分看重数学。我绝不会为了优雅而牺牲现实,除非我能解释为什么这样做。我也不会让使用我的模型的人对其准确性产生虚假的安慰。相反,我会明确指出其假设和疏漏。我明白,我的工作可能对社会和经济产生巨大影响,其中许多影响超出了我的理解。”
近年来,机器学习取得了许多重大进展,研究人员已经掌握了曾经被认为无法解决的任务。从图像中的物体识别到语音转录,再到像围棋这样复杂的棋盘游戏,现代机器学习已经在众多任务上与人类表现相匹敌,并且持续超越人类表现。
有趣的是,深度学习正是所有这些进展背后的方法。事实上,绝大多数的进展来自于深度学习的一个子领域——深度神经网络。虽然许多从业者熟悉标准的计量经济学模型,比如回归分析,但很少有人熟悉这种新型的建模方法。
本书的大部分内容都集中在深度学习上。这是因为深度学习是机器学习中最有前景的技术之一,掌握它的人将具备解决曾经被认为不可能完成的任务的能力。
在本章中,我们将探索神经网络如何以及为什么能够工作,以便让你对这一主题有一个基本的理解。
设置工作环境
在我们开始之前,你需要设置工作环境。本书中的所有示例都旨在 Jupyter 笔记本中运行。Jupyter 笔记本是一种互动式开发环境,主要用于数据科学应用,是构建数据驱动应用程序的首选环境。
你可以在本地计算机上、云服务器上,或者像 Kaggle 这样的网站上运行 Jupyter 笔记本。
注意
注意:本书的所有代码示例可以在这里找到:github.com/PacktPublishing/Machine-Learning-for-Finance,第一章的代码请参考以下链接:www.kaggle.com/jannesklaas/machine-learning-for-finance-chapter-1-code。
深度学习对计算机的要求很高,本书中使用的示例数据常常超过一千兆字节。通过使用图形处理单元(GPU),这类任务可以加速处理,而 GPU 最初是为渲染视频和游戏而发明的。如果你拥有启用了 GPU 的计算机,你可以在本地运行这些示例。如果没有这样的机器,建议使用像 Kaggle Kernels 这样的服务。
学习深度学习过去是一项昂贵的任务,因为 GPU 是一块昂贵的硬件。虽然现在有更便宜的选择,但如果你购买一块强大的 GPU,它的价格可以高达 $10,000,而在云端租用它的费用约为每小时 $0.80。
如果你有许多长期运行的训练任务,可能值得考虑建立一个“深度学习”工作站,即一台配备 GPU 的台式电脑。网上有无数的教程可以帮助你构建,而一台普通的工作站可以花费几百美元,最高可达 $5,000。
这本书中的示例都可以在 Kaggle 上免费运行。实际上,它们是使用该网站开发的。
使用 Kaggle 内核
Kaggle 是一个由 Google 拥有的流行数据科学网站。它最初通过竞赛形式运作,参赛者需要构建机器学习模型以进行预测。然而,随着时间的推移,它还拥有了一个受欢迎的论坛、一个在线学习系统,最重要的是,为我们提供了一个托管的 Jupyter 服务。
要使用 Kaggle,你可以访问他们的网站 www.kaggle.com/。为了使用该网站,你需要创建一个账户。
创建账户后,你可以通过点击主菜单中的 Kernels 来访问 Kernels 页面,如下图所示:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_04.jpg
公共 Kaggle 内核
在前面的截图中,你可以看到其他人编写并发布的一些内核。内核可以是私有的,但发布内核是展示技能和分享知识的好方式。
要启动一个新的内核,请点击 New Kernel。在接下来的对话框中,你需要选择 Notebook:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_05.jpg
内核编辑器
你将进入内核编辑器,它看起来像前面的截图。
请注意,Kaggle 正在积极迭代其内核设计,因此某些元素可能会处于不同的位置,但基本功能是相同的。笔记本中最重要的部分是代码单元。在这里,你可以输入代码,并通过点击左下角的运行按钮来运行它,或者使用 Shift + Enter 的快捷键。
你在一个单元中定义的变量会变成环境变量,因此可以在另一个单元中访问它们。Markdown 单元允许你使用 markdown 格式编写文本,为你的代码添加说明。你可以通过位于右上角的小云按钮上传和下载笔记本。
要从内核编辑器发布笔记本,首先必须点击 Commit & Run 按钮,然后在设置中将笔记本设置为 Public。要启用笔记本中的 GPU,请确保选中右下角的 Enable GPU 按钮。需要记住的是,这将重启你的笔记本,因此你的环境变量将丢失。
一旦你运行了代码,运行按钮将变为停止按钮。如果你的代码卡住了,可以通过点击停止按钮来中断它。如果你想清除所有环境变量并重新开始,只需点击右下角的重启按钮。
使用这个系统,你可以将内核连接到 Kaggle 上托管的任何数据集,或者你也可以随时上传新的数据集。本书中的笔记本已经配置好了数据连接。
Kaggle 内核预装了最常用的包,因此大部分时间你无需担心安装包。
本书有时会使用 Kaggle 默认没有安装的自定义包。在这种情况下,你可以在设置菜单的底部添加自定义包。每当本书中使用自定义包时,会提供相关的安装说明。
Kaggle 内核是免费的,能为你节省大量时间和金钱,因此建议在 Kaggle 上运行代码示例。要复制一个笔记本,请访问每章代码部分开头提供的链接,然后点击Fork Notebook。请注意,Kaggle 内核最多可以运行六个小时。
本地运行笔记本
如果你的机器足够强大,可以运行深度学习操作,那么你可以在本地运行代码示例。在这种情况下,强烈建议通过 Anaconda 安装 Jupyter。
要安装 Anaconda,只需访问www.anaconda.com/download下载发行版。图形化安装程序将引导你完成安装 Anaconda 到系统的步骤。在安装 Anaconda 时,你还将安装一系列有用的 Python 库,如 NumPy 和 matplotlib,这些库将在本书中使用。
安装 Anaconda 后,你可以通过打开机器的终端并输入以下代码,来本地启动 Jupyter 服务器:
$ jupyter notebook
然后,你可以访问终端中显示的 URL。这将带你到本地的笔记本服务器。
要启动一个新的笔记本,点击右上角的New。
本书中的所有代码示例都使用 Python 3,因此请确保在本地笔记本中使用 Python 3。如果你在本地运行笔记本,还需要安装 TensorFlow 和 Keras,这两个人工智能深度学习库将在本书中使用。
安装 TensorFlow
在安装 Keras 之前,我们需要先安装 TensorFlow。你可以通过打开终端窗口并输入以下命令来安装 TensorFlow:
$ sudo pip install TensorFlow
若要了解如何安装支持 GPU 的 TensorFlow,只需点击此链接,你将在这里找到安装说明:www.tensorflow.org/。
值得注意的是,你需要一块支持 CUDA 的 GPU 才能运行带有 CUDA 的 TensorFlow。有关如何安装 CUDA 的说明,请访问docs.nvidia.com/cuda/index.html。
安装 Keras
安装 TensorFlow 后,你可以通过运行以下命令来安装 Keras:
$ sudo pip install Keras
现在 Keras 将自动使用 TensorFlow 作为后端。请注意,TensorFlow 1.7 将内置 Keras,我们将在本章稍后介绍这一点。
本地使用数据
要在本地使用书中的代码示例数据,请访问 Kaggle 上的笔记本并从那里下载相关数据集。请注意,数据的文件路径会根据你保存数据的位置而有所变化,因此在本地运行笔记本时,你需要替换文件路径。
Kaggle 还提供了一个命令行接口,使你能够更轻松地下载数据。访问github.com/Kaggle/kaggle-api了解如何实现。
使用 AWS 深度学习 AMI
亚马逊云服务(AWS)提供了一种易于使用、预配置的方式来在云端运行深度学习。
访问aws.amazon.com/machine-learning/amis/了解如何设置亚马逊机器镜像(AMI)。虽然 AMI 是付费的,但它们的运行时间比 Kaggle 内核长。所以,对于大项目,使用 AMI 而不是内核可能更值得。
要在 AMI 上运行本书的笔记本,首先设置好 AMI,然后从 GitHub 下载笔记本,之后将它们上传到你的 AMI。你还需要从 Kaggle 下载数据。有关说明,请参见本地使用数据部分。
逼近函数
关于神经网络的最佳理解方式有很多种看法,但也许最有用的方式是将它们看作是函数逼近器。数学中的函数将某个输入,x,与某个输出,y,相关联。我们可以将其写成如下公式:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_001.jpg
一个简单的函数可能是这样的:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_002.jpg
在这种情况下,我们可以给函数一个输入,x,然后它将其四倍化:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_003.jpg
你可能在学校见过这样的函数,但函数能做的不止这些;举个例子,它们可以将一个集合中的元素(函数接受的值的集合)映射到另一个集合中的元素。这些集合可以是除了简单数字以外的其他内容。
例如,一个函数还可以将图像映射到图像中的识别内容:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_004.jpg
这个函数将把一张猫的图片映射到标签“猫”,如下图所示:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_06.jpg
将图像映射到标签
我们需要注意的是,对于计算机来说,图像是充满数字的矩阵,任何图像内容的描述也将以数字矩阵的形式存储。
如果神经网络足够大,它可以逼近任何函数。数学上已经证明,任何无限大的网络都可以逼近每一个函数。虽然我们不需要使用无限大的网络,但我们确实在使用非常大的网络。
现代深度学习架构可以有几十层甚至几百层,并且有数百万个参数,所以仅仅存储模型就需要几 GB 的空间。这意味着如果神经网络足够大,它也可以逼近我们的函数 f,用于将图像映射到其内容。
神经网络必须“足够大”的这个条件解释了为什么深度(大)神经网络得到了广泛应用。因为“足够大”的神经网络可以逼近任何函数,这意味着它们对于许多任务都非常有用。
一次前向传递
在本书的过程中,我们将构建强大的神经网络,能够逼近极其复杂的函数。我们将把文本映射到命名实体,将图像映射到其内容,甚至将新闻文章映射到其摘要。但现在,我们将处理一个可以用逻辑回归解决的简单问题,逻辑回归是一种在经济学和金融学中广泛使用的技术。
我们将处理一个简单的问题。给定一个输入矩阵 X,我们想要输出矩阵的第一列 X[1]。在这个例子中,我们将从数学的角度来解决这个问题,以便对发生的事情获得一些直观理解。
在本章的后面,我们将用 Python 实现我们描述的内容。我们已经知道需要数据来训练神经网络,所以这里的数据显示了我们的练习数据集:
| X[1] | X[2] | X[3] | y |
|---|---|---|---|
| 0 | 1 | 0 | 0 |
| 1 | 0 | 0 | 1 |
| 1 | 1 | 1 | 1 |
| 0 | 1 | 1 | 0 |
在数据集中,每一行包含一个输入向量 X 和一个输出 y。
数据遵循以下公式:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_005.jpg
我们想要逼近的函数如下:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_006.jpg
在这种情况下,写下这个函数相对简单。然而,请记住,在大多数情况下,无法写出函数,因为深度神经网络表达的函数可能非常复杂。
对于这个简单的函数,只有一层的浅层神经网络就足够了。这样的浅层网络也被称为逻辑回归器。
一个逻辑回归器
正如我们刚才解释的,最简单的神经网络是逻辑回归器。逻辑回归接受任何范围的值,但只输出零和一之间的值。
逻辑回归器适用于各种应用。其中一个例子是预测房主违约的可能性,特别是抵押贷款的违约。
在尝试预测某人违约的可能性时,我们可能会考虑各种因素,比如债务人的工资、是否拥有汽车、工作是否稳定等等,但违约的可能性始终是一个介于 0 和 1 之间的值。即使是最差的债务人,也不可能有超过 100% 的违约可能性,最好的债务人也不可能低于 0%。
以下图示展示了一个逻辑回归模型。X 是我们的输入向量;这里它显示为三个分量,X[1],X[2],和 X[3]。
W 是一个包含三个权重的向量。你可以把它想象成每一条线的粗细。W 决定了每个 X 值对下一层的影响。b 是偏差,它可以将层的输出上下移动:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_07.jpg
逻辑回归模型
为了计算回归模型的输出,我们必须首先进行 线性步骤。我们计算输入 X 和权重 W 的点积。这相当于将每个 X 的值与它的权重相乘,然后求和。接着,我们在这个数值上加上偏差 b。之后,我们再进行 非线性步骤。
在非线性步骤中,我们将线性中间结果 z 通过一个 激活函数,在这种情况下是 Sigmoid 函数。Sigmoid 函数将输入值压缩到 0 和 1 之间的输出:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_08.jpg
Sigmoid 函数
我们的 Python 版本的逻辑回归模型
如果之前的数学有点过于理论化,别担心!现在我们将实现相同的内容,不过这次使用 Python。在我们的示例中,我们将使用一个名为 NumPy 的库,它可以让我们在 Python 中轻松快速地进行矩阵运算。
NumPy 已经预安装在 Anaconda 和 Kaggle 内核中。为了确保我们在所有实验中得到相同的结果,我们需要设置一个随机种子。我们可以通过运行以下代码来实现:
import numpy as np
np.random.seed(1)
由于我们的数据集相当小,我们将手动定义它为 NumPy 矩阵,如下所示:
X = np.array([[0,1,0],
[1,0,0],
[1,1,1],
[0,1,1]])
y = np.array([[0,1,1,0]]).T
我们可以通过一个 Python 函数定义 sigmoid 函数,它将所有值压缩到 0 和 1 之间,作为激活函数:
def sigmoid(x):
return 1/(1+np.exp(-x))
到目前为止,进展顺利。接下来我们需要初始化 W。在这种情况下,我们实际上已经知道 W 应该是什么值。但在其他问题中,我们还不知道函数时,不能直接知道。所以,我们需要随机分配权重。
权重通常是随机分配的,平均值为零,偏差通常默认为零。NumPy 的 random 函数期望接收一个元组作为参数,表示随机矩阵的形状,因此 random((3,1)) 创建一个 3x1 的矩阵。默认情况下,生成的随机值介于 0 和 1 之间,均值为 0.5,标准差为 0.5。
我们希望随机值的均值为 0,标准差为 1,因此我们首先将生成的值乘以 2,再减去 1。我们可以通过运行以下代码来实现:
W = 2*np.random.random((3,1)) - 1
b = 0
完成这些步骤后,所有变量都已设置好。现在我们可以进行线性步骤,方法如下:
z = X.dot(W) + b
现在我们可以进行非线性步骤,方法如下:
A = sigmoid(z)
现在,如果我们打印出A,我们将得到以下输出:
print(A)
out:
[[ 0.60841366]
[ 0.45860596]
[ 0.3262757 ]
[ 0.36375058]]
等等!这个输出与我们期望的输出y一点也不像!显然,我们的回归模型表示的是某个函数,但它离我们想要的函数还很远。
为了更好地近似我们期望的函数,我们必须调整权重W和偏置b。为此,在接下来的部分中,我们将优化模型参数。
优化模型参数
我们已经看到,为了更接近我们期望的函数,我们需要调整模型的权重和偏置,统称为模型参数。
换句话说,我们需要在可能由我们的模型表示的函数空间中寻找一个函数,https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_007.jpg,使得它与我们期望的函数f尽可能接近。
但我们怎么知道自己有多接近呢?事实上,由于我们不知道f,我们无法直接知道我们的假设,https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_008.jpg,与f有多接近。但我们可以做的是衡量https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_009.jpg的输出与f的输出之间的匹配程度。给定X,f的期望输出是标签y。所以,我们可以通过找到一个函数,https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_010.jpg,来近似f,使得它给定X的输出也为y。
我们知道以下结论是正确的:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_011.jpg
我们还知道:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_012.jpg
我们可以通过使用以下公式进行优化来尝试找到f:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_013.jpg
在这个公式中,https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_029.jpg是可以由我们的模型表示的函数空间,也称为假设空间,而D是距离函数,我们用它来评估https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_014.jpg和y之间的距离。
注意
注意:这种方法做出了一个关键假设,即我们的数据X和标签y代表了我们期望的函数f。但这并不总是成立。当我们的数据包含系统性的偏差时,我们可能会得到一个很好地拟合数据的函数,但它与我们想要的函数不同。
优化模型参数的一个例子来自人力资源管理。假设你正在尝试构建一个预测债务人违约风险的模型,目的是决定谁应该获得贷款。
作为训练数据,你可以使用多年来人类银行经理做出的贷款决策。然而,这也带来了一个问题,因为这些经理可能存在偏见。例如,少数族裔(如黑人)在历史上更难获得贷款。
话虽如此,如果我们使用这些训练数据,我们的函数也会呈现出相同的偏差。最终,你将得到一个反映甚至放大人类偏见的函数,而不是创建一个擅长预测谁是好债务人的函数。
一个常见的错误是认为神经网络会找到我们所寻找的直观函数。实际上,它会找到最适合数据的函数,而不管该函数是否为我们期望的函数。
衡量模型损失
我们之前已经看到如何通过最小化某个距离函数D来优化参数。这个距离函数,也叫做损失函数,是我们评估可能函数的性能标准。在机器学习中,损失函数衡量模型的表现如何。损失函数高意味着准确率低,而如果损失函数低,则表示模型表现良好。
在这种情况下,我们的问题是一个二分类问题。由于这一点,我们将使用二进制交叉熵损失,正如以下公式所示:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_015.jpg
让我们一步一步地分析这个公式:
-
D[BCE]:这是二进制交叉熵损失的距离函数。
-
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_016.jpg:一批N个样本的损失是所有样本平均损失。
-
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_017.jpg:只有当真实值*y[i]为 1 时,这部分损失才会起作用。如果y[i]*为 1,我们希望https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_018.jpg尽可能接近 1,以便我们能够实现较低的损失。
-
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_019.jpg:当*y[i]*为 0 时,这部分损失才会起作用。如果是这样,我们希望https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_020.jpg也尽量接近 0。
在 Python 中,这个损失函数的实现如下:
def bce_loss(y,y_hat):
N = y.shape[0]
loss = -1/N * (y*np.log(y_hat) + (1 - y)*np.log(1-y_hat))
return loss
我们的逻辑回归器的输出A等于https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_021.jpg,所以我们可以按照以下方式计算二进制交叉熵损失:
loss = bce_loss(y,A)
print(loss)
out:
0.82232258208779863
如我们所见,这是一个相当高的损失,因此我们现在应该看看如何改进我们的模型。这里的目标是将损失降到零,或者至少接近零。
你可以将与不同函数假设相关的损失看作一个表面,这个表面有时也被称为“损失面”。损失面就像一座山脉,山顶上是高点,山谷中是低点。
我们的目标是找到山脉中最低的点:最深的谷底,或者称为“全局最小值”。全局最小值是函数假设空间中的一个点,在该点损失达到最低。
相比之下,“局部最小值”是损失比周围空间中的值更低的点。局部最小值是问题所在,因为它们看起来可能是一个不错的函数,但实际上还有更好的函数。请记住这一点,现在我们将一起走过梯度下降,它是一种在函数空间中找到最小值的方法。
梯度下降
现在我们知道了如何评判我们的候选模型,梯度下降方法,那么我们如何调整参数以获得更好的模型呢?神经网络中最流行的优化算法叫做梯度下降。在这种方法中,我们慢慢沿着损失函数的坡度,也就是导数,移动。
想象一下你正在山林中徒步旅行,来到了一个迷失方向的地方,现在你正试图穿越树林找到山谷的底部。这里的问题是,由于树木太多,你无法看到山谷的底部,只能看到脚下的地面。
现在问问自己:你如何找到下山的路呢?一种合理的做法是沿着坡度前进,坡度向下的地方,你就走向哪里。这就是梯度下降算法所采用的相同方法。
回到我们之前的比喻,在这个森林的情境中,损失函数就像是山,而为了降低损失,算法沿着损失函数的坡度,也就是导数,前进。当我们走下山时,我们正在更新我们的坐标位置。
该算法更新神经网络的参数,正如我们在下面的图示中看到的那样:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_09.jpg
梯度下降
梯度下降要求损失函数相对于我们想要优化的参数有导数。这在大多数监督学习问题中效果很好,但当我们想解决没有明显导数的问题时,情况就变得更加复杂。
梯度下降只能优化模型的参数、权重和偏差。它做不到的是优化我们的模型有多少层,或者它应该使用哪些激活函数,因为无法计算关于模型拓扑的梯度。
这些设置是无法通过梯度下降优化的,它们被称为超参数,通常由人类来设置。你刚刚看到我们如何逐渐缩小损失函数的值,但我们如何更新参数呢?为此,我们需要另一种方法,叫做反向传播。
反向传播
反向传播使我们能够对模型的参数应用梯度下降更新。为了更新这些参数,我们需要计算损失函数相对于权重和偏差的导数。
如果你把我们模型中的参数想象成我们在山林类比中的地理坐标,那么计算损失函数相对于某个参数的导数就像是在检查朝北的山坡,看看是否应该朝北还是朝南走。
下图展示了逻辑回归模型的前向传播和反向传播过程:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_10.jpg
逻辑回归模型的前向传播和反向传播
为了简化,我们将损失函数对任何变量的导数称为d变量。例如,我们将损失函数相对于权重的导数写作dW。
为了计算模型中不同参数的梯度,我们可以利用链式法则。你可能还记得链式法则是这样的:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_023.jpg
这有时也可以写作如下:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_024.jpg
链式法则基本上说的是,如果你想通过多个嵌套函数来求导,你需要将内层函数的导数与外层函数的导数相乘。
这很有用,因为神经网络以及我们的逻辑回归模型都是嵌套函数。输入通过线性步骤,这是输入、权重和偏置的一个函数;然后线性步骤的输出z再经过激活函数。
因此,当我们计算损失函数相对于权重和偏置的导数时,我们首先计算损失函数相对于线性步骤的输出z的导数,并使用这个结果来计算dW。在代码中,它看起来是这样的:
dz = (A - y)
dW = 1/N * np.dot(X.T,dz)
db = 1/N * np.sum(dz,axis=0,keepdims=True)
参数更新
现在我们得到了梯度,如何改进我们的模型?回到我们的山脉类比,既然我们知道山脉在北方和东方上升,我们该往哪里走呢?当然是朝南和朝西了!
从数学角度来说,我们是沿着梯度的反方向走。如果梯度相对于某个参数是正的,也就是说,坡度是向上的,那么我们就减少该参数。如果梯度是负的,也就是说,坡度是向下的,我们就增加它。当坡度变陡时,我们的梯度也会变大。
参数p的更新规则如下:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_025.jpg
这里p是一个模型参数(可以是权重或偏置),dp是损失函数相对于p的导数,而https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_026.jpg是学习率。
学习率有点像汽车中的油门。它决定了我们希望应用梯度更新的幅度。它是一个需要手动设置的超参数,我们将在下一章讨论它。
在代码中,我们的参数更新看起来是这样的:
alpha = 1
W -= alpha * dW
b -= alpha * db
将所有内容汇总起来
干得好!我们现在已经查看了训练神经网络所需的所有部分。在本节的接下来的几步中,我们将训练一个单层神经网络,这也叫做逻辑回归器。
首先,我们将在定义数据之前导入 numpy。我们可以通过运行以下代码来完成这一步:
import numpy as np
np.random.seed(1)
X = np.array([[0,1,0],
[1,0,0],
[1,1,1],
[0,1,1]])
y = np.array([[0,1,1,0]]).T
下一步是定义 sigmoid 激活函数和损失函数,我们可以通过以下代码来实现:
def sigmoid(x):
return 1/(1+np.exp(-x))
def bce_loss(y,y_hat):
N = y.shape[0]
loss = -1/N * np.sum((y*np.log(y_hat) + (1 - y)*np.log(1-y_hat)))
return loss
然后,我们将随机初始化我们的模型,可以通过以下代码实现:
W = 2*np.random.random((3,1)) - 1
b = 0
在这个过程中,我们还需要设置一些超参数。第一个是 alpha,这里我们将其设置为 1。Alpha 最好理解为步长。较大的 alpha 意味着虽然我们的模型训练得更快,但也可能会超过目标。相反,较小的 alpha 允许梯度下降更小心地前进,找到它本来会跳过的小谷。
第二个参数是我们希望训练过程运行的次数,也叫做我们想要运行的周期数。我们可以通过以下代码来设置这些参数:
alpha = 1
epochs = 20
由于它在训练循环中使用,因此定义我们数据中的样本数量也很重要。我们还将定义一个空数组,以便跟踪模型的损失情况。为了实现这一点,我们只需要运行以下代码:
N = y.shape[0]
losses = []
现在我们进入主要的训练循环:
for i in range(epochs):
# Forward pass
z = X.dot(W) + b
A = sigmoid(z)
# Calculate loss
loss = bce_loss(y,A)
print('Epoch:',i,'Loss:',loss)
losses.append(loss)
# Calculate derivatives
dz = (A - y)
dW = 1/N * np.dot(X.T,dz)
db = 1/N * np.sum(dz,axis=0,keepdims=True)
# Parameter updates
W -= alpha * dW
b -= alpha * db
运行之前的代码后,我们将得到以下输出:
out:
Epoch: 0 Loss: 0.822322582088
Epoch: 1 Loss: 0.722897448125
Epoch: 2 Loss: 0.646837651208
Epoch: 3 Loss: 0.584116122241
Epoch: 4 Loss: 0.530908161024
Epoch: 5 Loss: 0.48523717872
Epoch: 6 Loss: 0.445747750118
Epoch: 7 Loss: 0.411391164148
Epoch: 8 Loss: 0.381326093762
Epoch: 9 Loss: 0.354869998127
Epoch: 10 Loss: 0.331466036109
Epoch: 11 Loss: 0.310657702141
Epoch: 12 Loss: 0.292068863232
Epoch: 13 Loss: 0.275387990352
Epoch: 14 Loss: 0.260355695915
Epoch: 15 Loss: 0.246754868981
Epoch: 16 Loss: 0.234402844624
Epoch: 17 Loss: 0.22314516463
Epoch: 18 Loss: 0.21285058467
Epoch: 19 Loss: 0.203407060401
你可以看到,在输出过程中,损失逐步减少,从 0.822322582088 开始,最终减少到 0.203407060401。
我们可以将损失绘制成图表,以便更好地查看它。为此,我们可以简单地运行以下代码:
import matplotlib.pyplot as plt
plt.plot(losses)
plt.xlabel('epoch')
plt.ylabel('loss')
plt.show()
然后,这将输出以下图表:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_11.jpg
之前代码的输出,显示损失率随时间改善
更深的网络
我们在本章前面已经确定,为了近似更复杂的函数,我们需要更大更深的网络。创建一个更深的网络是通过将层堆叠在一起实现的。
在这一节中,我们将构建一个两层神经网络,就像以下图所示的那样:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_12.jpg
两层神经网络的示意图
输入与第一组权重 W[1] 相乘,产生一个中间结果 z[1]。然后通过一个激活函数,生成第一层的激活值 A[1]。
这些激活值随后会与第二层权重 W[2] 相乘,产生一个中间结果 z[2]。这个结果会通过第二个激活函数,生成我们神经网络的输出 A[2]:
z1 = X.dot(W1) + b1
a1 = np.tanh(z1)
z2 = a1.dot(W2) + b2
a2 = sigmoid(z2)
注意
注意:此示例的完整代码可以在本书的 GitHub 仓库中找到。
如你所见,第一个激活函数不是 sigmoid 函数,而实际上是 tanh 函数。Tanh 是一个常用于隐藏层的激活函数,工作原理与 sigmoid 类似,唯一的区别是它将值压缩到-1 到 1 的范围内,而不是 0 到 1:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_13.jpg
tanh 函数
通过我们的更深的网络进行反向传播同样使用链式法则。我们回到网络中并乘以导数:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_14.jpg
通过两层神经网络的前向和反向传播
上述方程可以通过以下 Python 代码表示:
# Calculate loss derivative with respect to the output
dz2 = bce_derivative(y=y,y_hat=a2)
# Calculate loss derivative with respect to second layer weights
dW2 = (a1.T).dot(dz2)
# Calculate loss derivative with respect to second layer bias
db2 = np.sum(dz2, axis=0, keepdims=True)
# Calculate loss derivative with respect to first layer
dz1 = dz2.dot(W2.T) * tanh_derivative(a1)
# Calculate loss derivative with respect to first layer weights
dW1 = np.dot(X.T, dz1)
# Calculate loss derivative with respect to first layer bias
db1 = np.sum(dz1, axis=0)
请注意,虽然输入和输出的大小由你的问题决定,但你可以自由选择隐藏层的大小。隐藏层是另一个你可以调整的超参数。隐藏层单元数越大,你可以逼近的函数就越复杂。然而,这也有一个反面,即模型可能会过拟合。也就是说,模型可能会形成一个复杂的函数,拟合噪音而不是数据中的真实关系。
看一下以下图表。我们看到的是两个半月数据集,原本可以明显分开,但现在有很多噪音,这使得即便是人类也很难看到分隔。你可以在第一章的 GitHub 仓库中找到两层神经网络的完整代码以及这些样本的生成代码:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_15.jpg
两个半月数据集
以下图示展示了决策边界的可视化,即模型分隔两个类别的那条线,使用的是 1 个隐藏层单元:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_16.jpg
隐藏层单元数为 1 的决策边界
如你所见,网络并没有捕捉到数据的真实关系。这是因为它过于简化。在以下的图示中,你将看到一个具有 500 个隐藏层单元的网络的决策边界:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_17.jpg
隐藏层单元数为 500 的决策边界
该模型显然拟合了噪音,但并没有拟合两个半月的数据。在这种情况下,合适的隐藏层单元数大约是 3。
找到合适的隐藏层单元数和隐藏层的数量是设计有效学习模型的关键部分。使用 NumPy 构建模型有点笨重,而且很容易出错。幸运的是,现在有一个更快速、更简单的工具来构建神经网络——Keras。
Keras 简介
Keras 是一个高层次的神经网络 API,可以在 TensorFlow 之上运行,TensorFlow 是一个用于数据流编程的库。这意味着它可以以高度优化的方式运行神经网络所需的操作。因此,Keras 比 TensorFlow 更快且更易于使用。因为 Keras 作为 TensorFlow 的接口,它使得构建更复杂的神经网络变得更加简单。在本书的其余部分,我们将使用 Keras 库来构建我们的神经网络。
导入 Keras
导入 Keras 时,我们通常只导入我们会使用的模块。在这个例子中,我们需要两种类型的层:
-
Dense层是我们在本章中了解的基础层 -
Activation层允许我们添加激活函数
我们可以通过运行以下代码轻松导入它们:
from keras.layers import Dense, Activation
Keras 提供了两种构建模型的方式,通过顺序和功能性 API。由于顺序 API 更易于使用并且能更快速地构建模型,我们将在本书的大部分内容中使用它。不过,在后面的章节中,我们也会看一下功能性 API。
我们可以通过以下代码访问顺序 API:
from keras.models import Sequential
Keras 中的两层模型
在顺序 API 中构建神经网络的过程如下:
堆叠层
首先,我们创建一个没有层的空顺序模型:
model = Sequential()
然后,我们可以像堆叠蛋糕一样向这个模型添加层,使用 model.add()。
对于第一层,我们需要指定层的输入维度。在我们的例子中,数据有两个特征,即点的坐标。我们可以使用以下代码添加一个大小为 3 的隐藏层:
model.add(Dense(3,input_dim=2))
注意我们如何将函数嵌套在 model.add() 中。我们指定了 Dense 层,而位置参数是该层的大小。这个 Dense 层现在只执行线性步骤。
若要添加 tanh 激活函数,我们调用以下代码:
model.add(Activation('tanh'))
接着,我们通过调用以下代码,按相同方式添加输出层的线性步骤和激活函数:
model.add(Dense(1))
model.add(Activation('sigmoid'))
然后,为了获得当前模型中所有层的概览,我们可以使用以下命令:
model.summary()
这将生成以下模型概览:
out:
Layer (type) Output Shape Param #
=================================================================
dense_3 (Dense) (None, 3) 9
_________________________________________________________________
activation_3 (Activation) (None, 3) 0
_________________________________________________________________
dense_4 (Dense) (None, 1) 4
_________________________________________________________________
activation_4 (Activation) (None, 1) 0
=================================================================
Total params: 13
Trainable params: 13
Non-trainable params: 0
你可以看到层按顺序列出,包括它们的输出形状和层的参数数量。输出形状中的 None 表示该层在这一维度上没有固定的输入大小,将接受我们输入的任何数据。在我们的案例中,这意味着该层将接受任意数量的样本。
在几乎所有的网络中,你会看到第一维的输入维度是可变的,像这样可以容纳不同数量的样本。
编译模型
在开始训练模型之前,我们必须明确指定如何训练模型;更重要的是,我们需要指定要使用的优化器和损失函数。
我们迄今为止使用的简单优化器叫做随机梯度下降,或称SGD。要查看更多优化器,请参见第二章,将机器学习应用于结构化数据。
我们用于这个二分类问题的损失函数叫做二元交叉熵。我们还可以指定在训练过程中要跟踪哪些指标。在我们的案例中,准确度,简称 acc,将是我们感兴趣的跟踪指标:
model.compile(optimizer='sgd',
loss='binary_crossentropy',
metrics=['acc'])
训练模型
现在我们准备好运行训练过程,可以通过以下命令来完成:
history = model.fit(X,y,epochs=900)
这将训练模型 900 次迭代,也称为训练轮次。输出应类似于以下内容:
Epoch 1/900
200/200 [==============================] - 0s 543us/step -
loss: 0.6840 - acc: 0.5900
Epoch 2/900
200/200 [==============================] - 0s 60us/step -
loss: 0.6757 - acc: 0.5950
...
Epoch 899/900
200/200 [==============================] - 0s 90us/step -
loss: 0.2900 - acc: 0.8800
Epoch 900/900
200/200 [==============================] - 0s 87us/step -
loss: 0.2901 - acc: 0.8800
训练过程的完整输出已被截断中间,目的是节省书中的空间,但你可以看到损失值持续下降,而准确度持续上升。换句话说,成功了!
在本书的过程中,我们将不断为这些方法添加更多功能。但此时,我们已经对深度学习的理论有了相当扎实的理解。我们只缺少一个构建模块:Keras 到底是如何在后台工作的?什么是 TensorFlow?为什么深度学习在 GPU 上运算更快?
我们将在本章的下一节也是最后一节中回答这些问题。
Keras 和 TensorFlow
Keras 是一个高级库,可以作为与 TensorFlow 的简化接口。这意味着 Keras 本身不进行任何计算;它只是与后台运行的 TensorFlow 交互的一种简单方式。
TensorFlow 是由 Google 开发的软件库,在深度学习领域非常流行。在本书中,我们通常尝试通过 Keras 来使用 TensorFlow,因为这样比直接使用 TensorFlow 更加简便。然而,有时我们可能需要编写一些 TensorFlow 代码,以构建更高级的模型。
TensorFlow 的目标是尽可能快速地运行深度学习所需的计算。正如其名称所示,它通过在数据流图中处理张量来实现这一目标。从 1.7 版本开始,Keras 现在也成为了 TensorFlow 的核心部分。
所以,我们可以通过运行以下代码导入 Keras 层:
from tensorflow.keras.layers import Dense, Activation
本书将 Keras 视为一个独立的库。然而,未来你可能想为 Keras 使用不同的后端,因为如果我们缩短 import 语句,代码会更加简洁。
张量与计算图
张量是基于特定规则变换的数字数组。最简单的张量类型是一个单一的数字,这也叫做标量。标量有时被称为零阶张量。
下一个张量是向量,也叫做秩一张量。接下来的张量是矩阵,称为秩二张量;立方矩阵,称为秩三张量;以此类推。你可以在下表中看到这些秩次:
| 秩 | 名称 | 表示 |
|---|---|---|
| 0 | 标量 | 数值大小 |
| 1 | 向量 | 数值大小和方向 |
| 2 | 矩阵 | 数字表 |
| 3 | 立方矩阵 | 数字的立方 |
| n | n 维矩阵 | 你明白了 |
本书主要使用“张量”一词来指代三阶或更高阶的张量。
TensorFlow 和其他深度学习库通过计算图执行计算。在计算图中,操作(如矩阵乘法或激活函数)是网络中的节点。张量在图的边缘之间传递,连接不同的操作。
我们简单神经网络的前向传播图如下:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_18.jpg
一个简单的计算图
将计算结构化为图的优势在于,节点可以更容易地并行运行。通过并行计算,我们不需要一台非常快的机器;我们也可以通过许多慢速计算机来实现快速计算,它们将任务分摊开来。
这就是为什么 GPU 在深度学习中如此有用的原因。GPU 有许多小核心,而 CPU 只有少数几个快速核心。现代 CPU 可能只有四个核心,而现代 GPU 可以有数百甚至数千个核心。
整个图表可能看起来相当复杂,即便它只是一个非常简单的模型,但你可以看到密集层的组成部分。这里有一个矩阵乘法(matmul)、加上偏置和一个 ReLU 激活函数:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_19.jpg
TensorFlow 中单层计算图的示意图。截图来自 TensorBoard。
使用像这样的计算图的另一个优势是,TensorFlow 和其他库可以快速且自动地沿着这个图计算导数。正如我们在本章中所探讨的,计算导数是训练神经网络的关键。
练习
现在,我们已经完成了这段激动人心的旅程中的第一章,我给你一个挑战!你会找到一些与本章内容相关的练习,来进一步巩固你的知识!
那么,为什么不尝试以下操作呢:
-
将 Python 中的两层神经网络扩展为三层。
-
在 GitHub 的代码库中,你会找到一个名为
1 Excel Exercise的 Excel 文件。目标是通过葡萄品种数据对三种类型的葡萄酒进行分类。为此,在 Excel 中构建一个逻辑回归模型。 -
在 Excel 中构建一个两层神经网络。
-
在 2 层神经网络中,调整隐藏层大小和学习率。哪些选项能够提供最低的损失?最低的损失是否也能捕捉到真实的关系?
总结
就这样!我们已经学会了神经网络是如何工作的。在接下来的章节中,我们将讨论如何构建更复杂的神经网络,这些网络能够近似更复杂的函数。
事实证明,为了使基本结构在特定任务(如图像识别)中表现良好,需要做一些调整。然而,本章中介绍的基本思想保持不变:
-
神经网络作为近似器发挥作用
-
我们通过损失函数来评估我们近似的函数, https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_027.jpg,的表现。
-
模型的参数通过沿损失函数相对于参数的导数的反方向进行更新,从而得到优化。
-
通过链式法则反向计算导数,这个过程叫做反向传播。
本章的关键 takeaway 是,虽然我们在寻找函数 f,但我们可以通过优化一个函数,使其在数据集上表现得像 f 来尝试找到它。有一个微妙但重要的区别是,我们并不知道https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_01_027.jpg是否真的像 f 一样有效。一个经常被引用的例子是一个军事项目,尝试使用深度学习在图像中识别坦克。模型在数据集上训练得很好,但当五角大楼想要测试他们的新坦克识别设备时,却惨遭失败。
在坦克示例中,五角大楼花了一段时间才发现,他们用来开发模型的数据集中的所有坦克图片都是在阴天拍摄的,而没有坦克的图片则是在晴天拍摄的。模型没有学会识别坦克,而是学会了识别灰色的天空。
这只是一个示例,说明你的模型可能与你想象的或甚至计划中的工作方式大不相同。存在缺陷的数据可能会严重影响模型的表现,有时你甚至没有察觉到。然而,尽管有失败,深度学习领域却有许多成功的案例。它是将重塑金融面貌的高影响力技术之一。
在下一章,我们将亲自动手,处理金融领域中一种常见的数据类型——结构化表格数据。更具体地说,我们将解决欺诈问题,这是许多金融机构不得不面对的问题,而现代机器学习是一个非常有用的工具。我们将学习如何准备数据,并使用 Keras、scikit-learn 和 XGBoost 进行预测。
第二章:将机器学习应用于结构化数据
结构化数据是指任何存在于记录或文件中固定字段内的数据,其中两个典型的例子是关系型数据库和电子表格。通常,结构化数据以表格的形式呈现,其中每一列表示一种数据类型,每一行代表一个新条目。其结构化格式意味着这种类型的数据适合进行经典的统计分析,这也是为什么大多数数据科学和分析工作都集中在结构化数据上的原因。
在日常生活中,结构化数据也是企业最常见的数据类型,而且大多数需要解决的金融领域机器学习问题都涉及以某种方式处理结构化数据。任何现代企业的日常运营都建立在结构化数据的基础上,包括交易、订单簿、期权价格和供应商,这些通常是以电子表格或数据库的形式收集的信息。
本章将带你解决一个涉及信用卡欺诈的结构化数据问题,我们将通过特征工程成功地从数据集中识别出欺诈交易。同时,我们还将介绍端到端(E2E)方法的基础知识,以便我们可以解决常见的金融问题。
欺诈是所有金融机构不得不应对的一个不幸现实。这是企业保护其系统与欺诈者试图突破现有保护措施之间的持续博弈。长期以来,欺诈检测依赖于简单的启发式方法。例如,在你通常不居住的国家发生的大额交易,很可能会导致该交易被标记为可疑。
然而,随着欺诈者不断理解并规避规则,信用卡提供商正在部署越来越复杂的机器学习系统来应对这一挑战。
本章中,我们将探讨一家真实银行如何处理欺诈问题。这是一个现实世界的案例,展示了一个数据科学团队如何从启发式基线开始,逐步理解其特征,并从中构建越来越复杂的机器学习模型来检测欺诈行为。尽管我们使用的数据是合成的,但我们将采用的开发过程和工具与国际零售银行每天使用的工具和流程类似。
那么你从哪里开始呢?用我曾经与一位匿名欺诈检测专家交谈时的话来说,“我不断思考如果我要从雇主那里偷钱,我会怎么做,然后我创造一些特征来捕捉我的抢劫行为。要抓住一个欺诈者,就要像欺诈者一样思考。” 然而,即使是最有创造力的特征工程师,也未必能发现所有细微且有时是反直觉的欺诈迹象,这就是为什么业界正逐渐转向完全端到端(E2E)训练的系统。这些系统除了机器学习外,还将是本章的重点,我们将在本章中探讨几种常用的欺诈标记方法。
本章将作为第六章的一个重要基础,使用生成模型,在该章中我们将再次回顾信用卡欺诈问题,构建一个完整的端到端(E2E)模型,使用自编码器。
数据
我们将使用的数据集是一个由支付模拟器生成的合成交易数据集。本案例研究的目标以及本章的重点是从数据集中找出欺诈交易,这是许多金融机构面临的经典机器学习问题。
注意
注意:在继续之前,本章的数字版代码以及交互式笔记本可以通过以下两个链接在线访问:
本章的代码可以在以下网址找到交互式笔记本:www.kaggle.com/jannesklaas/structured-data-code
代码也可以在 GitHub 上找到,在本书的代码库中:github.com/PacktPublishing/Machine-Learning-for-Finance
我们使用的数据集来源于论文PaySim: A financial mobile money simulator for fraud detection,作者为 E. A. Lopez-Rojas、A. Elmir 和 S. Axelsson。该数据集可以在 Kaggle 上找到,网址为:www.kaggle.com/ntnu-testimon/paysim1。
在我们下一页进行详细讲解之前,先花点时间看看我们将在本章中使用的数据集。请记住,你可以通过前面的链接下载数据。
| 步骤 | 类型 | 金额 | nameOrig | 原始旧余额 | 原始新余额 | nameDest | 目的地旧余额 | 目的地新余额 | 是否欺诈 | 是否标记为欺诈 |
|---|---|---|---|---|---|---|---|---|---|---|
| 1 | PAYMENT | 9839.64 | C1231006815 | 170136.0 | 160296.36 | M1979787155 | 0.0 | 0.0 | 0 | 0 |
| 1 | PAYMENT | 1864.28 | C1666544295 | 21249.0 | 19384.72 | M2044282225 | 0.0 | 0.0 | 0 | 0 |
| 1 | TRANSFER | 181.0 | C1305486145 | 181.0 | 0.0 | C553264065 | 0.0 | 0.0 | 1 | 0 |
| 1 | CASH_OUT | 181.0 | C840083671 | 181.0 | 0.0 | C38997010 | 21182.0 | 0.0 | 1 | 0 |
| 1 | PAYMENT | 11668.14 | C2048537720 | 41554.0 | 29885.86 | M1230701703 | 0.0 | 0.0 | 0 | 0 |
| 1 | PAYMENT | 7817.71 | C90045638 | 53860.0 | 46042.29 | M573487274 | 0.0 | 0.0 | 0 | 0 |
| 1 | 支付 | 7107.77 | C154988899 | 183195.0 | 176087.23 | M408069119 | 0.0 | 0.0 | 0 | 0 |
| 1 | 支付 | 7861.64 | C1912850431 | 176087.23 | 168225.59 | M633326333 | 0.0 | 0.0 | 0 | 0 |
| 1 | 支付 | 4024.36 | C1265012928 | 2671.0 | 0.0 | M1176932104 | 0.0 | 0.0 | 0 | 0 |
| 1 | 借记 | 5337.77 | C712410124 | 41720.0 | 36382.23 | C195600860 | 41898.0 | 40348.79 | 0 | 0 |
如第一行所示,数据集有 11 列。在继续之前,让我们先解释一下每一列代表什么:
-
step: 映射时间,每一步对应一个小时。
-
type: 交易类型,可以是 CASH_IN、CASH_OUT、DEBIT、PAYMENT 或 TRANSFER。
-
amount: 交易金额。
-
nameOrig: 发起交易的来源账户。C 表示客户账户,而 M 表示商户账户。
-
oldbalanceOrig: 来源账户的旧余额。
-
newbalanceOrig: 交易后来源账户的最新余额。
-
nameDest: 目标账户。
-
oldbalanceDest: 目标账户的旧余额。对于商户账户(账户名以 M 开头),此信息不可用。
-
newbalanceDest: 目标账户的最新余额。商户账户没有此信息。
-
isFraud: 交易是否为欺诈交易。
-
isFlaggedFraud: 系统是否已将交易标记为欺诈。
在上面的表格中,我们可以看到 10 行数据。值得注意的是,我们的总数据集中约有 630 万个交易记录,所以我们所看到的只是总数的一小部分。由于我们关注的欺诈仅发生在标记为 TRANSFER 或 CASH_OUT 的交易中,因此其他所有交易都可以去除,这样我们剩下的约有 280 万个样本可供处理。
启发式、特征基础和 E2E 模型
在我们开始开发用于检测欺诈的模型之前,先花一点时间暂停一下,思考一下我们可以构建的不同类型的模型。
-
基于启发式的模型是一种简单的“经验法则”,完全由人类开发。通常,启发式模型源于对问题的专家知识。
-
基于特征的模型在很大程度上依赖于人类修改数据,以创建新的、有意义的特征,然后将这些特征输入到(简单的)机器学习算法中。这种方法将专家知识与数据学习相结合。
-
E2E 模型完全通过原始数据进行学习。没有使用人类的专业知识,模型直接从观察中学习一切。
在我们的例子中,可以创建一个基于启发式的模型,将所有类型为 TRANSFER 且金额超过 20 万美元的交易标记为欺诈。基于启发式的模型具有开发速度快、实施简便的优势;然而,这也有代价,它们的性能通常较差,欺诈者可以轻松地利用系统漏洞。假设我们采用了前述的启发式模型,欺诈者将金额设置为 199,999 美元,低于欺诈限制,就可以避免检测。
在交易领域,一个重要的启发式方法是动量策略。动量策略是指押注某只正在上涨的股票将继续上涨,人们会购买该股票。虽然这一策略看起来过于简单,似乎并不怎么样,但事实上,它是一种相当成功的策略,许多高频交易和量化交易公司如今都在使用。
为了创建特征,专家们设计出能够区分欺诈交易和真实交易的指标。这通常通过统计数据分析来完成,与我们之前提出的基于启发式的模型相比,虽然时间较长,但能够带来更好的结果。
基于特征工程的模型处于数据与人工规则之间的中间地带,人类的知识和创造力被用来设计好的特征,而数据和机器学习则用于从这些特征中创建模型。
E2E 模型完全依赖收集到的数据进行学习,而不使用专家知识。如前所述,这种方法通常能产生更好的结果,但代价是完成时间较长。该方法还包含一些值得考虑的额外因素。例如,收集所需的大量数据是一项昂贵的任务,因为需要人工标注数百万条记录。
尽管对于许多业内人士来说,他们认为发布一个差的模型通常比什么都不发布要好。毕竟,有一些防欺诈保护总比没有要强。
使用一种启发式方法,哪怕它能漏过一半的欺诈交易,也总比完全没有欺诈检测要好。图表展示了我们之前介绍的三种模型的表现,以及它们实现所需的时间。
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_02_01.jpg
在开发过程中使用的方法和系统的性能
最好的方法是将三者结合使用。如果我们部署一个符合任务基本要求的启发式模型,它就可以投入使用。通过这种方法,启发式模型成为任何其他方法必须超越的基准。一旦启发式模型部署完成,所有的努力应当转向构建基于特征的模型,当它超越最初部署的启发式模型后,就可以投入使用,同时继续优化模型。
正如我们之前讨论过的,基于特征的模型通常在结构化数据任务中表现得相当不错;这使得公司有时间进行耗时且昂贵的构建端到端(E2E)模型的工作,一旦新模型在性能上超越基于特征的模型,就可以投入使用。现在我们理解了要构建的模型类型,让我们来看一下构建这些模型所需的软件。
机器学习软件栈
在本章中,我们将使用一系列常见的机器学习库。我们先花点时间看一下我们的软件栈,它包括以下软件:
-
Keras:一个神经网络库,可以作为 TensorFlow 的简化接口。
-
NumPy:为大型、多维数组提供支持,并包含广泛的数学函数库。
-
Pandas:一个用于数据处理和分析的库。它类似于微软的 Excel,但在 Python 中,提供了用于处理表格的结构和操作这些表格的工具。
-
Scikit-learn:一个机器学习库,提供了广泛的算法和工具。
-
TensorFlow:一个数据流编程库,有助于处理神经网络。
-
Matplotlib:一个绘图库。
-
Jupyter:一个开发环境。本书中的所有代码示例都可以在 Jupyter Notebooks 中找到。
本书的大部分内容都专注于使用 Keras 库,而本章则广泛使用了其他提到的库。这里的目标并不是教你所有不同库的技巧和窍门,而是展示它们如何集成到创建预测模型的过程中。
注意
注意:本章所需的所有库在 Kaggle 内核中默认安装。如果你在本地运行此代码,请参考 第一章,神经网络与基于梯度的优化,并安装所需的所有库。
启发式方法
在本章前面,我们介绍了将用于检测欺诈的三个模型,现在是时候更详细地探索它们了。我们将从启发式方法开始。
让我们从定义一个简单的启发式模型开始,并衡量它在衡量欺诈率方面的效果。
使用启发式模型进行预测
我们将使用启发式方法对整个训练数据集进行预测,以了解这个启发式模型在预测欺诈交易方面的效果。
以下代码将创建一个新列 Fraud_Heuristic,并在类型为 TRANSFER 且金额超过 $200,000 的行中分配值 1:
df['Fraud_Heuristic '] = np.where(((df['type'] == 'TRANSFER') &(df['amount'] > 200000)),1,0)
只需两行代码,你就可以轻松看出这样一个简单的度量指标如何轻松编写,并且迅速部署。
F1 分数
我们必须考虑的一件重要事情是需要一个通用的度量标准,以便对我们的所有模型进行评估。在第一章中,神经网络与基于梯度的优化,我们使用准确率作为我们的模拟工具。然而,正如我们所见,欺诈交易远少于真实交易。因此,一个将所有交易分类为真实交易的模型,可能会有一个非常高的准确率。
解决这种偏斜分布问题的一种度量是 F1 分数,它考虑了真正阳性和假阳性以及假阴性,如下图所示:
| 预测为负 | 预测为正 | |
|---|---|---|
| 实际负例 | 真正阴性 (TN) | 假阳性 (FP) |
| 实际正例 | 假阴性 (FN) | 真正阳性 (TP) |
我们可以首先计算模型的精准度,精准度指定了预测为正的样本中,实际为正的比例,公式如下:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_02_001.jpg
召回率衡量的是预测为正例的样本占实际正例的比例,公式如下:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_02_002.jpg
F1 分数是通过计算两种度量的调和平均值来得出的,具体公式如下:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_02_003.jpg
在 Python 中计算此度量时,我们可以使用 scikit-learn 的 metrics 模块,简称 sklearn:
from sklearn.metrics import f1_score
给定我们所做的预测,现在我们可以使用以下命令轻松计算 F1 分数:
f1_score(y_pred=df['Fraud_Heuristic '],y_true=df['isFraud'])
out: 0.013131315551742895
你会看到前面的命令输出了一个数字——从 0.013131315 开始——这个数字的意思是我们的启发式模型表现不太好,因为最好的 F1 分数是 1,最差的是 0。在我们的例子中,这个数字代表了被标记为欺诈的所有样本中,正确识别出欺诈行为的比例的调和平均值。
使用混淆矩阵进行评估
一种更具定性且易于解释的评估模型的方式是使用混淆矩阵。顾名思义,混淆矩阵展示了我们的分类器如何混淆各类。
首先,让我们研究一下 plot_confusion_matrix 函数的代码附录:
from sklearn.metrics import confusion_matrix cm = confusion_matrix(
y_pred=df['Fraud_Heuristic '],y_true=df['isFraud'])
plot_confusion_matrix(cm,['Genuine','Fraud'])
当我们运行时,产生以下图形:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_02_02.jpg
启发式模型的混淆矩阵
那么,这个模型的准确度如何呢?从我们的混淆矩阵中可以看到,在我们有 2,770,409 个样本的数据集中,2,355,826 个被正确分类为真实交易,而 406,370 个被错误分类为欺诈。实际上,只有 2,740 个样本被正确分类为欺诈。
当我们的启发式模型将某笔交易分类为欺诈时,99.3%的情况下是准确的。总共只有 34.2%的欺诈被捕获。所有这些信息都被纳入了我们制定的 F1 分数中。然而,正如我们所见,从生成的混淆矩阵图形中更容易看出这一点。我们同时使用启发式模型和 F1 分数的原因是,良好的实践是拥有一个可以告诉我们哪个模型更好的单一数字,同时也提供一个更具图形化的视角来展示该模型的优势。
直白地说,我们的启发式模型表现得相当糟糕,仅检测到 34.2%的欺诈,这是远远不够的。因此,在接下来的部分中,我们将使用其他两种方法,看看是否能够做得更好。
特征工程方法
特征工程的目标是利用人类的定性洞察力来创建更好的机器学习模型。人类工程师通常使用三种类型的洞察力:直觉、专家领域知识和统计分析。很多时候,仅凭直觉就能为某个问题提出特征。
例如,在我们的欺诈案件中,直觉上认为欺诈者会为他们的欺诈计划创建新账户,而不会使用他们支付日常开销的同一银行账户。
领域专家能够利用他们对问题的深入了解,提出其他类似的直觉例子。他们会更了解欺诈者的行为,并能够设计出能够指示这种行为的特征。所有这些直觉通常都会通过统计分析得到验证,统计分析甚至可以用来开启发现新特征的可能性。
统计分析有时能发现一些奇特的模式,这些模式可以转化为预测特征。然而,在使用这种方法时,工程师必须警惕数据陷阱。数据中找到的预测特征可能仅仅存在于该数据中,因为任何数据集只要经过足够长时间的处理,都会产生一个预测特征。
数据陷阱指的是工程师在数据中无限挖掘特征,却从不质疑他们所寻找的特征是否相关。
陷入数据陷阱的数据科学家们不停地发现特征,然而过后才意识到,带有这些特征的模型实际上效果并不好。在训练集找到强预测特征对数据科学团队来说就像是毒品。是的,它有一个即时的回报,一个让人感觉自己技能被验证的快速胜利。然而,像许多毒品一样,数据陷阱可能导致后遗症,团队发现几周或几个月的工作寻找这些特征,实际上是无用的。
花一分钟问问自己,你是否处于这种状态?如果你发现自己一直在做分析、转换数据、追逐相关性值,那么你很可能已经陷入了数据陷阱。
为了避免数据陷阱,建立一个定性理由来解释为何这个统计预测特征存在,以及它为什么也应该存在于数据集之外是非常重要的。通过建立这个理由,你将保持自己和团队的警觉,避免创造代表噪声的特征。数据陷阱是人类形式的过拟合和在噪声中寻找模式的问题,这对模型也是一个问题。
人类可以利用他们的定性推理能力来避免拟合噪声,这是人类相较于机器的一个巨大优势。如果你是数据科学家,你应该运用这一技能来创建更具普适性的模型。
本节的目标不是展示特征工程可以在此数据集上执行的所有特征,而只是突出这三种方法以及它们如何转化为特征。
来自直觉的一个特点——欺诈者不睡觉
即使对欺诈行为了解不多,我们也能凭直觉描述欺诈者为在黑暗中活动的可疑人物。在大多数情况下,真实的交易发生在白天,因为人们在夜晚休息。
我们数据集中的时间步骤代表一个小时。因此,我们只需通过 24 取余,就能生成一天中的时间,正如这段代码所示:
df['hour'] = df['step'] % 24
从那里,我们可以计算不同时间的欺诈和真实交易数量。为了计算这个,我们必须运行以下代码:
frauds = []
genuine = []
for i in range(24):
f = len(df[(df['hour'] == i) & (df['isFraud'] == 1)])
g = len(df[(df['hour'] == i) & (df['isFraud'] == 0)])
frauds.append(f)
genuine.append(g)
然后,最后我们可以将一天中每个小时内的真实和欺诈交易的比例绘制成图表。为此,我们必须运行以下代码:
fig, ax = plt.subplots(figsize=(10,6))
ax.plot(genuine/np.sum(genuine), label='Genuine')
ax.plot(frauds/np.sum(frauds),dashes=[5, 2], label='Fraud')
plt.xticks(np.arange(24))
legend = ax.legend(loc='upper center', shadow=True)
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_02_03.jpg
一天中每小时进行的欺诈和真实交易的比例
如我们在前面的图表中所见,夜间的真实交易显著减少,而欺诈行为则持续发生。为了确认夜间是我们有可能抓到欺诈的时段,我们还可以将欺诈交易数量作为所有交易的比例进行绘制。为此,我们必须运行以下命令:
fig, ax = plt.subplots(figsize=(10,6))
ax.plot(np.divide(frauds,np.add(genuine,frauds)), label='Share of fraud')
plt.xticks(np.arange(24))
legend = ax.legend(loc='upper center', shadow=True)
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_02_04.jpg
每小时欺诈交易的比例
一旦我们运行那段代码,就能看到大约在早上 5 点时,超过 60% 的所有交易似乎是欺诈性的,这表明这是一天中抓住欺诈行为的最佳时机。
专家见解——先转账,再提现
数据集的描述伴随着另一个解释,解释了欺诈者的预期行为。首先,他们将钱转到他们控制的银行账户。然后,他们从 ATM 提现。
我们可以通过运行以下代码,检查是否有欺诈转账目标账户是欺诈提现的来源:
dfFraudTransfer = df[(df.isFraud == 1) & (df.type == 'TRANSFER')]
dfFraudCashOut = df[(df.isFraud == 1) & (df.type == 'CASH_OUT')]
dfFraudTransfer.nameDest.isin(dfFraudCashOut.nameOrig).any()
out: False
根据输出,似乎没有欺诈转账是欺诈性现金提取的源头。专家预期的行为在我们的数据中没有显示出来。这可能意味着两件事:首先,可能是欺诈者现在的行为方式不同;或者是我们的数据没有捕捉到他们的行为。无论哪种情况,我们无法在这里利用这一发现进行预测建模。
统计学上的怪异现象 —— 余额错误
对数据的进一步检查显示,有些交易中,目标账户的旧余额和新余额都是零,尽管交易金额不为零。这很奇怪,或者说是一种怪异现象,因此我们需要调查这种异常是否具有预测能力。
首先,我们可以通过运行以下代码来计算具有此属性的欺诈交易的比例:
dfOdd = df[(df.oldBalanceDest == 0) &
(df.newBalanceDest == 0) &
(df.amount)]
len(dfOdd[(df.isFraud == 1)]) / len(dfOdd)
out: 0.7046398891966759
如你所见,欺诈交易的比例为 70%,因此这种怪异现象似乎是检测交易欺诈的一个好特征。然而,我们需要问自己,这种怪异现象最初是如何进入我们数据的。一种可能性是这些交易根本没有通过。
这种情况可能由多种原因引起,包括可能存在另一个欺诈防范系统拦截了这些交易,或者交易的原始账户资金不足。
虽然我们无法验证是否有其他欺诈防范系统在运行,但我们可以检查原始账户是否资金不足。为此,我们需要运行以下代码:
len(dfOdd[(dfOdd.oldBalanceOrig <= dfOdd.amount)]) / len(dfOdd)
out: 0.8966412742382271
正如我们在输出中看到的,接近 90% 的异常交易在其原始账户中资金不足。由此,我们可以构建一个推理,即欺诈者比普通人更频繁地试图将银行账户中的所有资金取光。
我们需要这个推理来避免数据陷阱。一旦建立,推理必须不断地被审视。在我们的案例中,它未能解释 10% 的异常交易,如果这个比例上升,可能会影响我们模型在生产中的表现。
准备 Keras 库的数据
在第一章,神经网络与基于梯度的优化中,我们看到神经网络只接受数字作为输入。我们数据集中的问题是,表格中的并非所有信息都是数字,有些是以字符的形式呈现的。
因此,在这一部分,我们将专注于为 Keras 准备数据,以便我们能够有意义地处理它。
在我们开始之前,先来看一下三种数据类型:名义型、有序型和数值型:
-
名义数据:这种数据是离散的类别,不能进行排序。在我们的例子中,转账类型是一个名义变量。有四种离散类型,但没有意义对它们进行排序。例如,TRANSFER 不能比 CASH_OUT 大,因此,它们只是独立的类别。
-
顺序数据:这种数据也有离散的类别,但与名义数据不同,它是可以排序的。例如,如果咖啡有大、中、小三种尺寸,这些是不同的类别,因为它们是可以比较的。大杯比小杯装的咖啡更多。
-
数值数据:这种数据是可以排序的,而且我们还可以对其进行数学运算。例如,在我们的数据中,资金数量是一个数值数据,我们不仅可以比较数量,还可以进行加减操作。
名义数据和顺序数据都是类别数据,因为它们描述的是离散类别。虽然数值数据与神经网络的兼容性很好,但类别数据需要特别处理。
独热编码
编码类别数据最常用的方法叫做独热编码。在独热编码中,我们为每个类别创建一个新的变量,即所谓的虚拟变量。然后,如果交易属于某个特定类别,我们就将虚拟变量设置为 1,否则设置为 0。
我们可以将此方法应用到我们的数据集中的一个示例如下:
所以,在进行独热编码之前,类别数据看起来是这样的:
| Transaction | Type |
|---|---|
| 1 | TRANSFER |
| 2 | CASH_OUT |
| 3 | TRANSFER |
这是独热编码后数据的样子:
| Transaction | Type_TRANSFER | Type_CASH_OUT |
|---|---|---|
| 1 | 1 | 0 |
| 2 | 0 | 1 |
| 3 | 1 | 0 |
Pandas 软件库提供了一个函数,允许我们直接创建虚拟变量。然而,在这样做之前,为所有实际的交易类型添加Type_前缀是有意义的。虚拟变量将以该类别命名。通过在前面添加Type_,我们知道这些虚拟变量表示的是类型。
以下代码行做了三件事。首先,df['type'].astype(str)将Type列中的所有条目转换为字符串。其次,Type_前缀被添加到字符串中。第三,新的组合字符串列将替代原有的Type列:
df['type'] = 'Type_' + df['type'].astype(str)
我们现在可以通过运行以下代码获取虚拟变量:
dummies = pd.get_dummies(df['type'])
我们应该注意到,get_dummies()函数会创建一个新的数据框。接下来,我们将这个数据框附加到主数据框中,这可以通过运行以下代码来实现:
df = pd.concat([df,dummies],axis=1)
concat()方法,如前面的代码所示,将两个数据框连接起来。我们沿着轴 1 进行连接,将数据框作为新列添加。现在虚拟变量已经添加到主数据框中,我们可以通过运行以下代码删除原有列:
del df['type']
就这样!我们已经将类别变量转换为神经网络可以使用的形式。
实体嵌入
在本节中,我们将展示如何同时使用嵌入和 Keras 函数式 API,向您展示一般的工作流程。关于这两个主题,我们在第五章,使用自然语言处理解析文本数据中会做详细介绍,在那里我们将超越这里呈现的基本概念,并开始讨论像实现这样的话题。
如果你现在还不完全理解所有的内容也没关系;毕竟这是一个高级章节。如果你希望使用这两种技术,在阅读完本书后,你将为使用它们做好充分的准备,因为我们在书中详细解释了这两种方法的不同要素。
在本节中,我们将为类别数据创建嵌入向量。在开始之前,我们需要理解嵌入向量是表示类别值的向量。我们将嵌入向量作为神经网络的输入。我们与神经网络一起训练嵌入向量,这样随着时间的推移,我们可以获得更有用的嵌入。嵌入是我们手头非常有用的工具。
为什么嵌入如此有用?嵌入不仅减少了与独热编码相比所需的编码维度数量,从而减少了内存使用,而且它们还减少了输入激活的稀疏性,这有助于减少过拟合,并且它们能够将语义含义编码为向量。正是这些优势使得嵌入对于文本非常有用,第五章,使用自然语言处理解析文本数据中同样让它们对于类别数据非常有用。
类别的分词
与文本一样,我们必须在将输入数据传递到嵌入层之前进行分词处理。为此,我们需要创建一个映射字典,将类别映射到令牌。我们可以通过运行以下代码来实现:
map_dict = {}
for token, value in enumerate(df['type'].unique()):
map_dict[value] = token
这段代码会遍历所有独特的类别类型,并按顺序递增计数。第一个类别得到令牌 0,第二个得到令牌 1,依此类推。我们的map_dict看起来是这样的:
{'CASH_IN': 4, 'CASH_OUT': 2, 'DEBIT': 3, 'PAYMENT': 0, 'TRANSFER': 1}
我们现在可以将此映射应用于我们的数据框:
df["type"].replace(map_dict, inplace=True)
结果是,所有类型现在将被它们的令牌替代。
我们需要单独处理数据框中的非类别值。我们可以像这样创建一个不属于类型且不是目标的列列表:
other_cols = [c for c in df.columns if ((c != 'type') and (c != 'isFraud'))]
创建输入模型
我们正在创建的模型将有两个输入:一个是带有嵌入层的类型输入,另一个是所有其他非类别变量输入。为了便于以后将它们组合,我们将使用两个数组跟踪它们的输入和输出:
inputs = []
outputs = []
作为类型输入的模型接收一维输入,并通过嵌入层进行解析。嵌入层的输出随后会被重塑为平坦数组,正如我们在以下代码中看到的那样:
num_types = len(df['type'].unique())
type_embedding_dim = 3
type_in = Input(shape=(1,))
type_embedding = Embedding(num_types,type_embedding_dim,input_ length=1)(type_in)
type_out = Reshape(target_shape=(type_embedding_dim,))(type_embedding)
type_model = Model(type_in,type_out)
inputs.append(type_in)
outputs.append(type_out)
type嵌入层有三层。这是一个任意的选择,尝试不同维度的数量可能会提高结果。
对于所有其他输入,我们创建一个新的输入,该输入的维度与非类别变量的数量相同,并由一个没有激活函数的单一全连接层组成。全连接层是可选的;输入也可以直接传递到头模型中。也可以添加更多的层,包括以下几种:
num_rest = len(other_cols)
rest_in = Input(shape = (num_rest,))
rest_out = Dense(16)(rest_in)
rest_model = Model(rest_in,rest_out)
inputs.append(rest_in)
outputs.append(rest_out)
既然我们已经创建了两个输入模型,我们可以将它们连接起来。在这两个连接的输入上,我们还将构建我们的头模型。为了开始这个过程,我们首先必须运行以下代码:
concatenated = Concatenate()(outputs)
然后,通过运行以下代码,我们可以构建并编译整个模型:
x = Dense(16)(concatenated)
x = Activation('sigmoid')(x)
x = Dense(1)(concatenated)
model_out = Activation('sigmoid')(x)
merged_model = Model(inputs, model_out)
merged_model.compile(loss='binary_crossentropy',optimizer='adam', metrics=['accuracy'])
训练模型
在这一部分,我们将训练一个具有多个输入的模型。为此,我们需要为每个输入提供一个X值的列表。因此,首先我们必须拆分我们的数据框。我们可以通过运行以下代码来实现:
types = df['type']
rest = df[other_cols]
target = df['isFraud']
然后,我们可以通过提供两个输入和目标的列表来训练模型,正如我们在以下代码中所看到的:
history = merged_model.fit([types.values,rest.values],target.values, epochs = 1, batch_size = 128)
out:
Epoch 1/1
6362620/6362620 [==============================] - 78s 12us/step - loss: 0.0208 - acc: 0.9987
使用 Keras 创建预测模型
我们的数据现在包含以下列:
amount,
oldBalanceOrig,
newBalanceOrig,
oldBalanceDest,
newBalanceDest,
isFraud,
isFlaggedFraud,
type_CASH_OUT,
type_TRANSFER, isNight
现在我们已经得到了列,我们的数据已经准备好了,可以用它来创建一个模型。
提取目标
为了训练模型,神经网络需要一个目标。在我们的案例中,isFraud就是目标,因此我们必须将其从其他数据中分离出来。我们可以通过运行以下代码来实现:
y_df = df['isFraud']
x_df = df.drop('isFraud',axis=1)
第一步只返回isFraud列,并将其赋值给y_df。
第二步返回除了isFraud之外的所有列,并将其赋值给x_df。
我们还需要将数据从 pandas DataFrame 转换为 NumPy 数组。pandas DataFrame 是建立在 NumPy 数组之上的,但它包含许多额外的功能,使得我们之前的所有预处理工作得以实现。然而,要训练神经网络,我们只需要底层数据,我们可以通过简单地运行以下代码来获取:
y = y_df.values
X = x_df.values
创建测试集
当我们训练模型时,我们面临过拟合的风险。过拟合意味着我们的模型记住了训练数据集中x和y之间的映射关系,但没有找到描述x和y之间真实关系的函数。这是有问题的,因为一旦我们将模型应用到样本外的数据上,即在训练集之外的数据,它可能会表现得非常差。为了防止这种情况发生,我们将创建一个所谓的测试集。
测试集是一个保留数据集,我们只在认为模型已经表现得相当不错时,用它来评估模型的表现,以查看模型在尚未见过的数据上的表现如何。测试集通常是从完整数据中随机抽样得到的。Scikit-learn 提供了一个方便的函数来执行这一操作,正如我们在以下代码中所看到的:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)
元素train_test_split会随机地将行分配到训练集或测试集中。你可以指定test_size,即进入测试集的数据比例(在我们的例子中是 33%),以及随机状态。指定random_state可以确保尽管过程是伪随机的,但它总是返回相同的拆分,这使得我们的工作更具可重现性。请注意,数字的实际选择(例如42)并不重要。重要的是在所有实验中使用相同的数字。
创建验证集
现在你可能会忍不住尝试不同的模型,直到在测试集上获得非常高的表现。然而,问问自己:你怎么知道你没有选择一个偶然在测试集上表现良好,但在实际生活中无法应用的模型呢?
答案是,每次你在测试集上进行评估时,你都会产生一点“信息泄露”,也就是说,测试集的信息通过影响你选择的模型泄漏到模型中。逐渐地,测试集的价值会降低。验证集是一种“脏测试集”,你可以用它频繁地测试模型的外部样本表现,而不必担心。尽管需要注意的是,我们不希望过于频繁地使用测试集,但它仍然会被用来衡量外部样本表现。
为此,我们将创建一个“验证集”,也叫做开发集。
我们可以像创建测试集一样通过再次拆分训练数据来完成此操作,如下代码所示:
X_train, X_test, y_train, y_test = train_test_split(X_train, y_train, test_size=0.1, random_state=42)
过采样训练数据
记住,在我们的数据集中,只有极小一部分交易是欺诈性的,而一个总是将交易分类为真实交易的模型将会有非常高的准确率。为了确保我们在真实关系上训练我们的模型,我们可以过采样训练数据。
这意味着我们会将一些欺诈性数据添加到我们的数据集中,直到我们拥有与真实交易相等数量的欺诈交易。
注意
注意:一个用于这种任务的有用库是imblearn,它包含了 SMOTE 函数。请参见,contrib.scikitlearn.org/imbalanced-learn/。
合成少数类过采样技术(SMOTE)是一种巧妙的过采样方法。这种方法试图在保持类别决策边界不变的情况下创建新样本。我们可以通过运行以下代码轻松地使用 SMOTE 进行过采样:
From imblearn.over_sampling import SMOTE
sm = SMOTE(random_state=42)
X_train_res, y_train_res = sm.fit_sample(X_train, y_train)
构建模型
我们已经成功解决了几个关键的学习点,所以现在终于是时候构建神经网络了!正如在第一章中所述,神经网络与基于梯度的优化,我们需要使用以下代码导入所需的 Keras 模块:
from keras.models import Sequential
from keras.layers import Dense, Activation
在实际应用中,许多结构化数据问题需要非常低的学习率。为了设置梯度下降优化器的学习率,我们还需要导入优化器。我们可以通过运行以下代码实现:
from keras.optimizers import SGD
创建一个简单的基准模型
在深入研究更复杂的模型之前,最好从一个简单的逻辑回归基准开始。这是为了确保我们的模型能够成功训练。
要创建一个简单的基准模型,我们需要运行以下代码:
model = Sequential()
model.add(Dense(1, input_dim=9))
model.add(Activation('sigmoid'))
你可以看到这里的逻辑回归模型,它与单层神经网络是相同的:
model.compile(loss='binary_crossentropy', optimizer=SGD(lr=1e-5), metrics=['acc'])
在这里,我们将编译模型。我们不仅仅是传递SGD来指定随机梯度下降(Stochastic Gradient Descent)的优化器,而是创建一个自定义的 SGD 实例,在其中将学习率设置为 0.00001。在这个例子中,由于我们使用 F1 分数来评估模型,所以不需要跟踪准确率。尽管如此,它仍然揭示了一些有趣的行为,正如你在以下代码中看到的:
model.fit(X_train_res,y_train_res, epochs=5, batch_size=256, validation_data=(X_val,y_val))
注意,我们通过创建一个元组将验证数据传递给 Keras,在其中存储数据和标签。我们将训练这个模型 5 个周期:
Train on 3331258 samples, validate on 185618 samples Epoch 1/5
3331258/3331258 [==============================] - 20s 6us/step - loss:
3.3568 - acc: 0.7900 - val_loss: 3.4959 - val_acc: 0.7807 Epoch 2/5
3331258/3331258 [==============================] - 20s 6us/step - loss:
3.0356 - acc: 0.8103 - val_loss: 2.9473 - val_acc: 0.8151 Epoch 3/5
3331258/3331258 [==============================] - 20s 6us/step - loss:
2.4450 - acc: 0.8475 - val_loss: 0.9431 - val_acc: 0.9408 Epoch 4/5
3331258/3331258 [==============================] - 20s 6us/step - loss:
2.3416 - acc: 0.8541 - val_loss: 1.0552 - val_acc: 0.9338 Epoch 5/5
3331258/3331258 [==============================] - 20s 6us/step - loss:
2.3336 - acc: 0.8546 - val_loss: 0.8829 - val_acc: 0.9446
在这里注意几点:首先,我们训练了大约 330 万样本,这比我们最初拥有的数据还要多。这个突然的增加来自于我们在本章早些时候进行的过采样。其次,训练集的准确率明显低于验证集的准确率。这是因为训练集是平衡的,而验证集则不是。
我们通过向训练集添加更多的欺诈案例来对数据进行过采样,欺诈案例的数量比实际生活中更多,正如我们之前讨论的,这有助于我们的模型更好地检测欺诈。如果我们没有进行过采样,我们的模型可能会倾向于将所有交易分类为真实的,因为训练集中的大多数样本都是正常的。
通过添加欺诈案例,我们迫使模型学习欺诈案例的区别。然而,我们希望在真实数据上验证我们的模型。因此,我们的验证集不会人为地包含许多欺诈案例。
一个将所有样本分类为真实的模型在验证集上的准确率将超过 99%,但在训练集上的准确率只有 50%。准确率对于这样不平衡的数据集来说是一个有缺陷的指标。它是一个相对不错的代理,并且比仅仅使用损失函数更具可解释性,这就是为什么我们在 Keras 中仍然追踪准确率的原因。
为了评估我们的模型,我们应该使用本章开头讨论的 F1 分数。然而,由于计算 F1 分数的过程相对较慢,并且会导致训练速度变慢,Keras 无法在训练过程中直接跟踪 F1 分数。
注意
注意:请记住,在不平衡数据集上的准确率可能非常高,即使模型表现不佳。
如果模型在不平衡验证集上的准确度高于在平衡训练集上的准确度,那么这并不能说明模型表现良好。
比较训练集的表现与之前训练集的表现,验证集的表现与之前验证集的表现。同样,比较训练集和验证集的表现时要小心,尤其是在数据极度不平衡的情况下。然而,如果数据是平衡的,那么比较训练集和验证集的表现是判断过拟合的好方法。
现在我们可以对测试集进行预测,以评估基准模型。我们首先使用 model.predict 对测试集进行预测:
y_pred = model.predict(X_test)
在评估我们的基准模型之前,我们需要将模型给出的概率转换为绝对预测。在我们的示例中,我们将所有欺诈概率超过 50% 的内容分类为欺诈。为此,我们需要运行以下代码:
y_pred[y_pred > 0.5] = 1
y_pred[y_pred < 0.5] = 0
我们的 F1 分数已经比启发式模型好得多,如果你回头看,会发现它仅获得了 0.013131315551742895 的分数:
f1_score(y_pred=y_pred,y_true=y_test)
out: 0.054384286716408395
通过绘制混淆矩阵,我们可以看到基于特征的模型确实比启发式模型有所改进:
cm = confusion_matrix(y_pred=y_pred,y_true=y_test)
plot_confusion_matrix(cm,['Genuine','Fraud'], normalize=False)
这段代码应该会生成以下混淆矩阵:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_02_05.jpg
一个简单 Keras 模型的混淆矩阵
但如果我们想构建能够表达更微妙关系的复杂模型,而不是刚才构建的那个呢?现在我们来做这个吧!
构建更复杂的模型
在创建了简单的基准模型之后,我们可以继续构建更复杂的模型。以下代码是一个二层网络的示例:
model = Sequential()
model.add(Dense(16,input_dim=9))
model.add(Activation('tanh'))
model.add(Dense(1))
model.add(Activation('sigmoid'))
model.compile(loss='binary_crossentropy',optimizer=SGD(lr=1e-5), metrics=['acc'])
model.fit(X_train_res,y_train_res, epochs=5, batch_size=256, validation_data=(X_val,y_val))
y_pred = model.predict(X_test)
y_pred[y_pred > 0.5] = 1
y_pred[y_pred < 0.5] = 0
运行完那段代码后,我们将再次使用 F1 分数进行基准测试:
f1_score(y_pred=y_pred,y_true=y_test)
out: 0.087220701988752675
在这种情况下,复杂模型的表现优于之前创建的简单基准模型。看起来将交易数据映射到欺诈的函数是复杂的,且可以通过更深的网络来更好地逼近。
在这一部分中,我们构建并评估了简单和复杂的神经网络模型用于欺诈检测。我们小心地使用验证集来衡量初始的样本外表现。
有了这些,我们可以构建更复杂的神经网络(我们会这样做)。但首先,我们将看看现代企业级机器学习的核心:基于树的方法。
基于树的方法简要介绍
任何关于结构化数据的章节,如果没有提到基于树的方法,比如随机森林或 XGBoost,都不算完整。
了解这些方法是很有价值的,因为在结构化数据的预测建模领域,基于树的方法非常成功。然而,它们在更高级的任务中表现不佳,比如图像识别或序列到序列的建模。这也是为什么本书其余部分没有涉及基于树的方法的原因。
注意
注意:如果你想深入了解 XGBoost,可以查看 XGBoost 文档页面上的教程:xgboost.readthedocs.io。在网站的教程部分,有关于树形方法和梯度提升如何在理论和实践中工作的详细解释。
一个简单的决策树
基于树的算法的基本理念就是决策树。决策树通过将数据分割开来,最大化结果之间的差异。
假设一下,我们的isNight特征是预测欺诈的最佳特征。决策树会根据交易是否发生在夜间来分割我们的数据集。它会查看所有夜间的交易,寻找下一个最好的欺诈预测特征,然后对所有白天的交易做同样的处理。
Scikit-learn 提供了一个方便的决策树模块。我们可以通过运行以下代码来为我们的数据创建一个决策树:
from sklearn.tree import DecisionTreeClassifier
dtree=DecisionTreeClassifier()
dtree.fit(X_train,y_train)
结果树将是这样的:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_02_06.jpg
用于欺诈检测的决策树
像我们创建的简单决策树,可以为数据提供大量的洞察。例如,在我们的决策树中,最重要的特征似乎是原始账户的旧余额,因为它是树中的第一个节点。
一个随机森林
简单决策树的一个更高级版本是随机森林,随机森林是决策树的集合。通过从训练数据中取子集并在这些子集上训练决策树来训练森林。
通常,这些子集并不包括训练数据的每个特征。通过这种方式,不同的决策树可以拟合数据的不同方面,从而整体上捕获更多的信息。创建了足够多的树之后,它们的预测结果会被平均化,最终得出预测结果。
这个理念是,树形结构所呈现的错误是无关的,因此通过使用多棵树,你可以抵消这些错误。你可以像这样创建和训练一个随机森林分类器:
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=10,n_jobs=-1)
rf.fit(X_train_res,y_train_res)
你会注意到,在我们刚刚生成的代码中,随机森林比神经网络需要调节的参数要少得多。在这种情况下,我们只需要指定估计器的数量,也就是我们希望森林中包含的树的数量。
n_jobs参数告诉随机森林我们希望并行训练多少棵树。请注意,-1表示“使用所有 CPU 核心”:
y_pred = rf.predict(X_test)
f1_score(y_pred=y_pred,y_true=y_test)
out: 0.8749502190362406
随着 F1 分数接近 1(这是最高分),随机森林的表现比神经网络好一个数量级。其混淆图如下所示,显示随机森林显著减少了假阳性的数量:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_02_07.jpg
随机森林的混淆矩阵
浅层学习方法,如随机森林,通常在相对简单的问题上表现得比深度学习更好。原因在于,简单的低维数据关系对于深度学习模型来说可能很难学习,因为深度学习模型需要精确拟合多个参数,才能匹配简单的函数。
正如我们将在本书后续章节中看到的那样,一旦关系变得更加复杂,深度学习就能展现出它的优势。
XGBoost
XGBoost代表极限梯度提升。梯度提升的核心思想是先训练一个决策树,然后基于第一个决策树的错误训练第二个决策树。
通过这种方法,可以添加多个层次的决策树,这样可以逐渐减少模型的总错误数。XGBoost 是一个流行的库,它非常高效地实现了梯度提升。
注意
注意:XGBoost 默认已安装在 Kaggle 的内核环境中。如果你在本地运行这些示例,请参阅 XGBoost 手册以获取安装说明和更多信息:xgboost.readthedocs.io/。
梯度提升分类器可以像sklearn中的随机森林一样创建和训练,以下代码可以看到这一点:
import xgboost as xgb
booster = xgb.XGBClassifier(n_jobs=-1)
booster = booster.fit(X_train,y_train)
y_pred = booster.predict(X_test)
f1_score(y_pred=y_pred,y_true=y_test)
out: 0.85572959604286891
在这项任务中,梯度提升器的表现几乎与随机森林相同。一种常用的方法是同时使用随机森林和梯度提升器,然后对预测结果进行平均,以获得更好的模型。
当今大多数商业中的机器学习工作都基于相对简单的结构化数据。我们今天学到的方法,随机森林和梯度提升,因此是大多数从业者在现实世界中使用的标准工具。
在大多数企业机器学习应用中,价值的创造并不来自精心调整模型或设计出酷炫的架构,而是来自于调整数据和创建良好的特征。然而,随着任务变得更加复杂并且对非结构化数据的语义理解需求增加时,这些工具开始失效。
E2E 建模
我们当前的方法依赖于工程化特征。正如我们在本章开始时讨论的,另一种方法是 E2E 建模。在 E2E 建模中,使用的是关于事务的原始和非结构化数据。这可能包括转账的描述文本、监控现金机的摄像头视频流或其他数据来源。只要有足够的数据,E2E 通常比特征工程更成功。
要获得有效的结果,并成功地用 E2E 模型训练数据,可能需要数百万个示例。然而,这通常是获得可接受结果的唯一方法,特别是当某些内容很难用规则编码时。人类可以很好地识别图像中的事物,但很难提出明确的规则来区分这些事物,这正是 E2E 的优势所在。
在本章使用的数据集里,我们无法访问更多的数据,但本书的其他章节展示了各种端到端(E2E)模型。
练习
如果你访问kaggle.com,搜索一个包含结构化数据的竞赛。例如,泰坦尼克号竞赛就是一个例子。在这里,你可以创建一个新的内核,进行特征工程,并尝试构建一个预测模型。
通过在特征工程和模型调优上投资时间,你能提高多少性能?是否有一种端到端(E2E)的方法来解决这个问题?
总结
在本章中,我们将一个结构化数据问题从原始数据处理到强大且可靠的预测模型。我们学习了启发式方法、特征工程和端到端(E2E)建模。我们还了解了清晰的评估指标和基准的重要性。
在下一章,我们将探讨深度学习真正发挥作用的领域——计算机视觉。在这里,我们将发现计算机视觉的工作流程,从使用简单模型到深度神经网络,并辅以强大的预处理软件。计算机“看见”的能力使得它们能够进入全新的领域。
第三章:利用计算机视觉
当 Snapchat 首次推出带有霹雳舞热狗的滤镜时,该公司股价飙升。然而,投资者对热狗的倒立并不感兴趣;真正让他们着迷的是 Snapchat 成功构建了一种强大的计算机视觉技术。
Snapchat 应用程序现在不仅能够拍照,而且能够在这些照片中找到热狗可以跳霹雳舞的表面。然后,他们的应用程序会将热狗放置在那里,即使用户移动手机,热狗仍能保持在原地跳舞。

虽然跳舞的热狗可能是计算机视觉应用中最傻的一个,但它成功地向世界展示了这项技术的潜力。在一个充满摄像头的世界里,从数十亿的智能手机、安防摄像头和卫星,每天都在使用,到物联网(IoT)设备,能够解读图像为消费者和生产者带来了巨大的好处。
计算机视觉使我们能够在大规模上感知和解释现实世界。你可以这样理解:没有分析师能查看数百万张卫星图像,标记矿区并跟踪其活动;这是不可能的。然而,对于计算机来说,这不仅是可能的,而且已经成为现实。
实际上,现在一些公司正在现实世界中使用的一种技术是零售商通过计算停车场中的汽车数量来估算某一时期内的商品销售量。
计算机视觉的另一个重要应用可以在金融领域中看到,特别是在保险行业。例如,保险公司可能会使用无人机飞过屋顶,发现问题并在它们变成昂贵的麻烦之前解决。这也可以扩展到他们使用计算机视觉来检查他们承保的工厂和设备。
看看金融领域的另一个案例,银行需要遵守了解你的客户(KYC)规则,正在自动化后台流程和身份验证。在金融交易中,计算机视觉可以应用于蜡烛图,以便发现新的技术分析模式。我们甚至可以为计算机视觉的实际应用写一本书。
在本章中,我们将讨论计算机视觉模型的构建模块。内容将重点涵盖以下主题:
-
卷积层。
-
填充。
-
池化。
-
正则化,以防止过拟合。
-
基于动量的优化。
-
批量归一化。
-
超越分类的计算机视觉高级架构。
-
关于库的说明。
在我们开始之前,让我们看一下本章中将使用的所有不同库:
-
Keras:一个高层次的神经网络库,是 TensorFlow 的接口。
-
TensorFlow:一个数据流编程和机器学习库,我们用它进行 GPU 加速计算。
-
Scikit-learn:一个流行的机器学习库,包含许多经典算法的实现以及评估工具。
-
OpenCV:一个用于基于规则的增强的图像处理库。
-
NumPy:一个用于处理 Python 中矩阵的库。
-
Seaborn:一个绘图库。
-
tqdm:一个用于监控 Python 程序进度的工具。
值得花点时间注意,除了 OpenCV,所有这些库都可以通过pip安装;例如,pip install keras。
然而,OpenCV 的安装过程稍微复杂一些。虽然这超出了本书的范围,但你可以通过 OpenCV 的官方文档在线查看详细信息,网址如下:docs.opencv.org/trunk/df/d65/tutorial_table_of_content_introduction.html。
另外,值得注意的是,Kaggle 和 Google Colab 都预装了 OpenCV。要运行本章中的示例,请确保已安装 OpenCV 并能够使用import cv2进行导入。
卷积神经网络。
卷积神经网络、ConvNets,或简写为CNNs,是计算机视觉的驱动引擎。ConvNets 使我们能够处理更大的图像,同时保持网络的合理大小。
卷积神经网络的名称来源于将其与常规神经网络区分开来的数学运算。卷积是数学上正确的术语,指的是将一个矩阵滑过另一个矩阵。在接下来的章节中,我们将在MNIST 上的滤波器部分探讨这对于 ConvNets 的重要性,但也会讨论为什么这不是最适合它们的名称,以及为什么 ConvNets 实际上应该被称为滤波器网络。
你可能会问,“那为什么是滤波器网络呢?”答案很简单,因为它们之所以有效,是因为它们使用了滤波器。
在接下来的章节中,我们将使用 MNIST 数据集,它是一个手写数字的集合,已成为计算机视觉中的标准“Hello, World!”应用。
MNIST 上的滤波器。
计算机在看到一张图片时,究竟看到的是什么?实际上,图像的像素值以数字形式存储在计算机中。所以,当计算机看到一张数字 7 的黑白图像时,它实际上看到的是类似于以下内容的东西:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_01.jpg
MNIST 数据集中的数字 7。
上述是来自 MNIST 数据集的一个例子。图像中的手写数字已经被突出显示,使得数字七对人类可见,但对计算机而言,图像实际上只是数字的集合。这意味着我们可以对图像进行各种数学运算。
在检测数字时,有一些低级特征构成了一个数字。例如,在这张手写的数字 7 中,包含了一条垂直直线、顶部的一条水平线和一条穿过中间的水平线。相比之下,数字 9 由四条圆形的线条组成,顶部形成一个圆圈,下面是一个直立的垂直线。
现在我们能够呈现卷积神经网络(ConvNets)的核心思想。我们可以使用小的滤波器,检测某种低级特征,比如垂直线,然后将其滑过整张图像,检测图像中的所有垂直线。
以下截图显示了一个垂直线滤波器。为了检测图像中的垂直线,我们需要将这个 3x3 的矩阵滤波器滑过图像。
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_02.jpg
一个垂直线滤波器
使用接下来的 MNIST 数据集,我们从左上角开始,切出左上角的 3x3 像素网格,在这个例子中,网格里全是零。
然后我们对滤波器中的所有元素与图像切片中的所有元素进行逐元素相乘。接着,将这九个乘积相加,并加入偏置。这个值将形成滤波器的输出,并作为新像素传递到下一层:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_001.jpg
因此,我们的垂直线滤波器的输出将如下所示:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_03.jpg
垂直线滤波器的输出
请花点时间注意,垂直线清晰可见,而水平线则消失了,只有一些伪影残留。同时,注意滤波器如何从一侧捕捉到垂直线。
由于它对左侧的高像素值和右侧的低像素值作出反应,输出的右侧显示了强烈的正值。同时,左侧的线实际上显示了负值。在实践中,这并不是大问题,因为通常不同的滤波器用于检测不同种类的线条和方向。
添加第二个滤波器
我们的垂直滤波器正在起作用,但我们已经注意到,我们还需要对图像进行水平线的滤波,以便检测数字 7。
我们的水平线滤波器可能如下所示:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_04.jpg
一个水平线滤波器
使用这个例子后,我们现在可以像使用垂直滤波器时那样,滑动这个滤波器在图像上,得到以下的输出:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_05.jpg
垂直线滤波器的输出
看看这个滤波器如何去除垂直线,并几乎只留下水平线?现在的问题是,我们该将什么传递到下一层?嗯,我们将两个滤波器的输出堆叠在一起,形成一个三维的立方体:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_06.jpg
MNIST 卷积
通过添加多个卷积层,我们的卷积神经网络能够提取越来越复杂和语义化的特征。
彩色图像上的滤波器
当然,我们的滤波器技术不仅限于黑白图像。在本节中,我们将看看彩色图像。
大多数彩色图像由三个层或通道组成,通常称为 RGB,代表这三个层的首字母。它们由一个红色通道、一个蓝色通道和一个绿色通道组成。当这三个通道叠加在一起时,它们相加形成我们所知的传统彩色图像。
基于这个概念,图像因此不是平的,而实际上是一个立方体,一个三维矩阵。结合这个想法和我们的目标,我们希望将滤波器应用到图像上,并一次性应用到所有三个通道。因此,我们将对两个三维立方体进行逐元素相乘。
我们的 3x3 滤波器现在有三个深度,因此有九个参数,再加上偏置:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_07.jpg
一个滤波器立方体或卷积核的示例
这个立方体被称为卷积核,它像之前的二维矩阵一样在图像上滑动。然后,逐元素的乘积会再次求和,添加偏置,结果表示下一层中的一个像素。
滤波器始终捕捉前一层的整个深度。滤波器在图像的宽度和高度上滑动。同样,滤波器不会在图像的深度(即不同的通道)上滑动。用技术术语来说,权重,即构成滤波器的数字,在宽度和高度上是共享的,但在不同的通道上则不是。
Keras 中的卷积神经网络构建块
在本节中,我们将构建一个简单的卷积神经网络,用于分类 MNIST 字符,同时学习现代卷积神经网络的不同组成部分。
我们可以通过运行以下代码直接从 Keras 导入 MNIST 数据集:
from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
我们的数据集包含 60,000 张 28x28 像素的图像。MNIST 字符是黑白的,因此数据形状通常不包括通道:
x_train.shape
out: (60000, 28, 28)
我们稍后会更详细地研究颜色通道,但现在,让我们扩展数据维度,显示我们只有一个颜色通道。我们可以通过运行以下代码来实现:
import numpy as np
x_train = np.expand_dims(x_train,-1)
x_test = np.expand_dims(x_test,-1)
x_train.shape
out: (60000, 28, 28, 1)
运行代码后,你可以看到我们现在添加了一个单一的颜色通道。
Conv2D
现在我们来到了卷积神经网络的核心内容:在 Keras 中使用卷积层。Conv2D 就是实际的卷积层,一个 Conv2D 层包含多个滤波器,如以下代码所示:
from keras.layers import Conv2D
from keras.models import Sequential
model = Sequential()
img_shape = (28,28,1)
model.add(Conv2D(filters=6,
kernel_size=3,
strides=1,
padding='valid',
input_shape=img_shape))
创建新的 Conv2D 层时,我们必须指定要使用的滤波器数量以及每个滤波器的大小。
卷积核大小
滤波器的大小也叫做kernel_size,因为单个滤波器有时被称为核。如果我们只指定一个数字作为卷积核大小,Keras 会假设我们的滤波器是方形的。在这种情况下,例如,我们的滤波器将是 3x3 像素。
然而,我们确实可以通过将元组传递给kernel_size参数来指定非正方形的卷积核大小。例如,我们可以选择使用一个 3x4 像素的过滤器,通过kernel_size = (3,4)来指定。然而,这种情况非常罕见,在大多数情况下,过滤器的大小要么是 3x3,要么是 5x5。经验表明,这种大小的过滤器能产生良好的效果。
步幅大小
strides参数指定了步幅大小,也就是卷积过滤器滑过图像时的步长,通常称为步幅大小。在绝大多数情况下,过滤器逐像素滑动,因此它们的步幅大小被设置为 1。然而,也有一些研究者使用较大的步幅大小来减少特征图的空间大小。
就像kernel_size一样,如果我们只指定一个值,Keras 会假设我们在水平方向和垂直方向使用相同的步幅大小,并且在绝大多数情况下,这种假设是正确的。然而,如果我们希望水平方向使用步幅为 1,而垂直方向使用步幅为 2,我们可以通过以下方式将元组传递给参数:strides=(1,2)。就像在过滤器大小的情况一样,这种情况比较少见。
填充
最后,我们必须在卷积层中添加padding。填充会在图像的四周添加零。如果我们不想让特征图缩小,可以使用这种方式。
让我们考虑一个 5x5 像素的特征图和一个 3x3 的过滤器。由于过滤器只能在特征图上滑动九次,因此我们最终会得到一个 3x3 的输出。这不仅减少了我们在下一个特征图中能够捕获的信息量,还减少了输入特征图的外部像素在任务中可能的贡献。过滤器从未以它们为中心;它只经过它们一次。
填充有三种选择:不使用填充,称为"无"填充,"同"填充和"有效"填充。
让我们看看三种填充方式中的每一种。首先是:不使用填充:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_08.jpg
选项 1:不使用填充
然后是同样的填充方式:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_09.jpg
选项 2:同样填充
为了确保输出与输入的大小相同,我们可以使用same填充。Keras 将会在输入特征图的四周添加足够的零,以保持输入大小不变。然而,默认的填充设置是valid。这种填充方式并不会保持特征图的大小,仅仅是确保过滤器和步幅大小能够在输入特征图上有效应用:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_10.jpg
选项 3:有效填充
输入形状
Keras 要求我们指定输入的形状。不过,这仅仅是第一次使用层时需要的要求。对于后续的每一层,Keras 将会根据上一层的输出形状推断出输入形状。
简化的 Conv2D 表示法
前一层使用 28x28x1 的输入,并滑动六个 2x2 的过滤器逐像素处理输入。指定同一层的更常见方式是使用以下代码:
model.add(Conv2D(6,3,input_shape=img_shape))
过滤器的数量(这里是 6)和过滤器的大小(这里是 3)作为位置参数设置,而 strides 和 padding 默认分别为 1 和 valid。如果这是网络中的更深层,我们甚至不需要指定输入形状。
ReLU 激活
卷积层只执行线性步骤。构成图像的数字与过滤器相乘,这是一个线性操作。
因此,为了逼近复杂的函数,我们需要通过激活函数引入非线性。计算机视觉中最常见的激活函数是修正线性单元(ReLU),我们可以在这里看到它:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_11.jpg
ReLU 激活函数
用于生成上述图表的 ReLU 公式如下:
ReLU(x) = max(x, 0)
换句话说,ReLU 函数返回输入值,如果输入值为正。如果不是,它将返回零。这个非常简单的函数已被证明非常有用,使得梯度下降收敛更快。
通常有人认为,ReLU 更快,因为其在零以上的所有值的导数都为 1,并且不像某些极值的导数那样变得非常小,例如在 sigmoid 或 tanh 中。
ReLU 也比 sigmoid 和 tanh 的计算开销更小。它不需要任何计算开销大的计算,输入值小于零的部分直接设为零,其他部分则输出。不幸的是,ReLU 激活函数有点脆弱,可能会“死亡”。
当梯度非常大并将多个权重朝负方向移动时,ReLU 的导数将始终为零,因此权重永远不会再更新。这可能意味着一个神经元永远不会再次激活。然而,这可以通过较小的学习率来缓解。
由于 ReLU 快且计算代价低,它已成为许多从业者的默认激活函数。要在 Keras 中使用 ReLU 函数,我们只需在激活层中将其命名为所需的激活函数,运行以下代码:
from keras.layers import Activation
model.add(Activation('relu'))
MaxPooling2D
在多个卷积层后使用池化层是常见做法。池化减少了特征图的空间大小,从而减少了神经网络中所需的参数数量,进而减少了过拟合。
下面,我们可以看到一个最大池化的示例:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_12.jpg
最大池化
最大池化返回池中的最大元素。这与 AveragePooling2D 的平均池化相反,后者返回池的平均值。最大池化通常比平均池化提供更优的结果,因此它是大多数从业者使用的标准。
最大池化可以通过运行以下代码来实现:
from keras.layers import MaxPool2D
model.add(MaxPool2D(pool_size=2,
strides=None,
padding='valid'))
在 Keras 中使用最大池化层时,我们必须指定所需的池大小。最常见的值是 2x2 的池。与 Conv2D 层一样,我们也可以指定步幅大小。
对于池化层,默认的步幅大小是None,在这种情况下,Keras 会将步幅大小设置为与池化大小相同。换句话说,池化层是彼此相邻的,不会重叠。
我们还可以指定填充,其中valid是默认选择。然而,指定same填充用于池化层是极为罕见的,因为池化层的目的是减少特征图的空间大小。
我们的MaxPooling2D层在这里取 2x2 像素的池化区域,彼此相邻且没有重叠,并返回最大元素。指定相同层的更常见方法是执行以下操作:
model.add(MaxPool2D(2))
在这种情况下,strides和padding都设置为其默认值,分别是None和valid。通常,池化层后没有激活函数,因为池化层不执行线性步骤。
Flatten
你可能已经注意到我们的特征图是三维的,而我们期望的输出是一个一维的向量,包含 10 个类别的概率。那么,我们如何从三维到一维呢?好吧,我们Flatten了我们的特征图。
Flatten操作类似于 NumPy 的flatten操作。它接收一批特征图,维度为(batch_size, height, width, channels),并返回一个维度为(batch_size, height * width * channels)的向量集合。
它不执行任何计算,仅对矩阵进行重塑。此操作没有超参数需要设置,如你在以下代码中所见:
from keras.layers import Flatten
model.add(Flatten())
Dense
卷积神经网络(ConvNets)通常由特征提取部分(卷积层)和分类部分组成。分类部分由我们在第一章,神经网络与基于梯度的优化,以及第二章,将机器学习应用于结构化数据 中已经探讨过的简单全连接层组成。
为了将普通层与其他类型的层区分开,我们称它们为Dense层。在一个密集层中,每个输入神经元都与一个输出神经元连接。我们只需要指定所需的输出神经元数量,在这个例子中是 10 个。
这可以通过运行以下代码来完成:
from keras.layers import Dense
model.add(Dense(10))
在密集层的线性步骤之后,我们可以添加softmax激活函数来进行多类回归,就像我们在前两章中所做的那样,通过运行以下代码:
model.add(Activation('softmax'))
训练 MNIST
现在让我们将所有这些元素组合起来,这样我们就可以在 MNIST 数据集上训练卷积神经网络了。
模型
首先,我们必须指定模型,可以使用以下代码来完成:
from keras.layers import Conv2D, Activation, MaxPool2D, Flatten, Dense
from keras.models import Sequential
img_shape = (28,28,1)
model = Sequential()
model.add(Conv2D(6,3,input_shape=img_shape))
model.add(Activation('relu'))
model.add(MaxPool2D(2))
model.add(Conv2D(12,3))
model.add(Activation('relu'))
model.add(MaxPool2D(2))
model.add(Flatten())
model.add(Dense(10))
model.add(Activation('softmax'))
在以下代码中,你可以看到典型卷积神经网络的一般结构:
Conv2D
Pool
Conv2D
Pool
Flatten
Dense
卷积层和池化层通常在这些模块中一起使用;你可以找到一些神经网络,重复使用Conv2D和MaxPool2D的组合多次。
我们可以通过以下命令获得模型的概览:
model.summary()
这将给我们以下输出:
Layer (type) Output Shape Param #
=================================================================
conv2d_2 (Conv2D) (None, 26, 26, 6) 60
_________________________________________________________________
activation_3 (Activation) (None, 26, 26, 6) 0
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 13, 13, 6) 0
_________________________________________________________________
conv2d_3 (Conv2D) (None, 11, 11, 12) 660
_________________________________________________________________
activation_4 (Activation) (None, 11, 11, 12) 0
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 5, 5, 12) 0
_________________________________________________________________
flatten_2 (Flatten) (None, 300) 0
_________________________________________________________________
dense_2 (Dense) (None, 10) 3010
_________________________________________________________________
activation_5 (Activation) (None, 10) 0
=================================================================
Total params: 3,730
Trainable params: 3,730
Non-trainable params: 0
_________________________________________________________________
在这个总结中,你可以清楚地看到池化层是如何减少特征图的大小的。仅从总结中看这点不太明显,但你可以看到第一个Conv2D层的输出是 26x26 像素,而输入图像是 28x28 像素。
通过使用valid填充,Conv2D也减少了特征图的大小,尽管只是减少了少量。第二个Conv2D层也发生了同样的事情,它将特征图从 13x13 像素缩小到 11x11 像素。
你还可以看到,第一个卷积层只有 60 个参数,而Dense层有 3,010 个参数,超过了卷积层的 50 倍。卷积层通常用非常少的参数就能实现惊人的效果,这也是它们如此受欢迎的原因。通过卷积层和池化层,网络中的总参数数目通常能显著减少。
加载数据
我们使用的 MNIST 数据集已预装在 Keras 中。加载数据时,如果你想直接通过 Keras 使用数据集,请确保你的设备已连接互联网,因为 Keras 需要先下载该数据集。
你可以使用以下代码导入数据集:
from keras.datasets import mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
如本章开始时所解释的,我们希望将数据集重塑,使其也能具有通道维度。原始数据集本身还没有通道维度,但这是我们可以做的:
x_train.shape
out:
(60000, 28, 28)
所以,我们使用 NumPy 添加一个通道维度,代码如下:
import numpy as np
x_train = np.expand_dims(x_train,-1)
x_test = np.expand_dims(x_test,-1)
现在有了一个通道维度,正如我们在这里看到的:
x_train.shape
out:
(60000, 28, 28,1)
编译和训练
在之前的章节中,我们使用了独热编码的目标进行多类回归。虽然我们已经重新塑形了数据,但目标仍然保持原始形式。它们是一个扁平的向量,包含每个手写数字的数值数据表示。记住,MNIST 数据集中有 60,000 个这样的数据:
y_train.shape
out:
(60000,)
通过独热编码转换目标是一个常见且令人烦恼的任务,因此 Keras 允许我们直接指定一个损失函数,在运行时将目标转换为独热编码。这个损失函数叫做sparse_categorical_crossentropy。
它与前面章节中使用的分类交叉熵损失函数相同,唯一的区别是它使用的是稀疏目标,即不是独热编码的目标。
和之前一样,你仍然需要确保你的网络输出的维度与类别数相匹配。
我们现在已经到达了可以编译模型的阶段,我们可以使用以下代码来进行编译:
model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])
如你所见,我们正在使用 Adam 优化器。Adam 的具体工作原理将在下一节中解释,为我们的神经网络增添更多功能,但现在,你可以将它理解为一种比随机梯度下降更复杂的版本。
在训练时,我们可以通过运行以下代码直接指定验证集:
history = model.fit(x_train,y_train,batch_size=32,epochs=5,validation_data=(x_test,y_test))
一旦我们成功运行了这段代码,我们将得到以下输出:
Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 19s 309us/step - loss: 5.3931 - acc: 0.6464 - val_loss: 1.9519 - val_acc: 0.8542
Epoch 2/10
60000/60000 [==============================] - 18s 297us/step - loss: 0.8855 - acc: 0.9136 - val_loss: 0.1279 - val_acc: 0.9635
....
Epoch 10/10
60000/60000 [==============================] - 18s 296us/step - loss: 0.0473 - acc: 0.9854 - val_loss: 0.0663 - val_acc: 0.9814
为了更好地了解正在发生的情况,我们可以使用以下代码绘制训练进度:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10,6))
gen = ax.plot(history.history['val_acc'], label='Validation Accuracy')
fr = ax.plot(history.history['acc'],dashes=[5, 2], label='Training Accuracy')
legend = ax.legend(loc='lower center', shadow=True)
plt.show()
这将为我们提供以下图表:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_13.jpg
可视化的验证和训练准确度输出
正如你在前面的图表中看到的,模型达到了大约 98%的验证准确率,这非常不错!
为我们的神经网络添加更多功能
让我们花一点时间来看看我们神经网络的其他一些元素。
动量
在前几章中,我们通过某人试图仅通过跟随地面坡度找到山下的路径来解释梯度下降。动量可以通过与物理学的类比来解释,其中一个小球沿着同样的山坡滚下。山坡上的小颠簸不会使小球滚向完全不同的方向。小球已经有了一些动量,这意味着它的运动会受到之前运动的影响。
我们不是直接用梯度更新模型参数,而是使用指数加权的移动平均来更新它们。我们用一个离群点梯度更新参数,然后取移动平均,这将平滑掉离群点,并捕捉梯度的整体方向,正如我们在下面的图示中看到的那样:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_14.jpg
动量如何平滑梯度更新
指数加权移动平均是一个巧妙的数学技巧,用于计算移动平均,而无需记住一组之前的值。某个值的指数加权平均V,https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_002.jpg,将如下所示:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_003.jpg
beta 值为 0.9 意味着 90%的均值将来自先前的移动平均,https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_004.jpg,10%将来自新的值,https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_005.jpg。
使用动量使得学习对梯度下降中的陷阱(如离群梯度、局部最小值和鞍点)更具鲁棒性。
我们可以通过在 Keras 中为 beta 设置一个值来增加标准的随机梯度下降优化器的动量,这就是我们在下面的代码中所做的:
from keras.optimizers import SGD
momentum_optimizer = SGD(lr=0.01, momentum=0.9)
这段小代码片段创建了一个具有学习率 0.01 和 beta 值为 0.9 的随机梯度下降优化器。当我们编译模型时,可以使用它,就像我们现在要做的这样:
model.compile(optimizer=momentum_optimizer,loss='sparse_categorical_crossentropy',metrics=['acc'])
Adam 优化器
在 2015 年,Diederik P. Kingma 和 Jimmy Ba 创建了Adam(自适应动量估计)优化器。这是另一种使梯度下降更加高效的方法。在过去的几年里,这种方法显示出了非常好的效果,因此,已成为许多实践者的标准选择。例如,我们已经在 MNIST 数据集上使用过它。
首先,Adam 优化器像动量优化器一样计算梯度的指数加权平均。它通过以下公式来实现这一点:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_006.jpg
然后它还会计算平方梯度的指数加权平均值:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_007.jpg
然后它会像这样更新模型参数:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_008.jpg
这里的https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_009.jpg是一个非常小的数字,用于避免除以零的情况。
对平方梯度的根进行除法操作,会在梯度非常大的时候减慢更新速度。它还通过防止学习算法受到异常值的干扰,稳定了学习过程。
使用 Adam 时,我们有一个新的超参数。除了只有一个动量因子https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_010.jpg外,我们现在有两个,https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_011.jpg和https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_012.jpg。对于这些超参数的推荐值是
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_013.jpg
和https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_014.jpg分别是 0.9 和 0.999。
我们可以像这样在 Keras 中使用 Adam:
from keras.optimizers import adam
adam_optimizer=adam(lr=0.1,beta_1=0.9, beta_2=0.999, epsilon=1e-08)
model.compile(optimizer=adam_optimizer,loss='sparse_categorical_crossentropy',metrics=['acc'])
正如你在本章前面看到的那样,我们也可以通过仅传递adam字符串作为优化器来编译模型。在这种情况下,Keras 会为我们创建一个 Adam 优化器,并选择推荐的值。
正则化
正则化是一种用于防止过拟合的技术。过拟合是指模型对训练数据拟合得过于完美,导致它不能很好地泛化到开发数据或测试数据上。你可能会看到,过拟合有时也被称为“高方差”,而欠拟合,即在训练、开发和测试数据上都得到较差的结果,则被称为“高偏差”。
在经典的统计学习中,很多重点都放在了偏差-方差权衡上。观点是,一个非常符合训练集的模型很可能会过拟合,因此必须接受一定程度的欠拟合(偏差),以便获得良好的结果。在经典统计学习中,防止过拟合的超参数往往也会防止训练集拟合得很好。
神经网络中的正则化,如这里所介绍的,主要借鉴了经典学习算法。然而,现代机器学习研究开始接受“正交性”这一概念,认为不同的超参数会影响偏差和方差。
通过分离这些超参数,可以打破偏差-方差权衡,从而找到能够很好地泛化并提供准确预测的模型。然而,到目前为止,这些努力仅仅取得了小小的成果,因为低偏差和低方差的模型需要大量的训练数据。
L2 正则化
一种常用的防止过拟合的技术是 L2 正则化。L2 正则化将权重平方的和添加到损失函数中。我们可以在下面的公式中看到这个例子:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_015.jpg
这里的N是训练示例的数量,而https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_016.jpg是正则化超参数,它决定了我们希望进行多大程度的正则化,常见值大约为 0.01。
将此正则化添加到损失函数中意味着较高的权重会增加损失,从而促使算法减少权重。较小的权重,接近零的权重,意味着神经网络会减少对它们的依赖。
因此,一个正则化的算法会减少对每个特征和每个节点激活的依赖,而是更全面地看待问题,考虑多个特征和激活。这将防止算法发生过拟合。
L1 正则化
L1 正则化与 L2 正则化非常相似,但它不是加和平方,而是加和绝对值,如我们在这个公式中看到的:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_018.jpg
实际操作中,通常不确定哪种方法效果最佳,但两者之间的差异并不大。
Keras 中的正则化
在 Keras 中,应用于权重的正则化器称为kernel_regularizer,应用于偏置的正则化器称为bias_regularizer。你还可以直接将正则化应用于节点的激活,以防止它们被过度激活,使用activity_regularizer。
现在,我们在网络中添加一些 L2 正则化。为此,我们需要运行以下代码:
from keras.regularizers import l2
model = Sequential()
model.add(Conv2D(6,3,input_shape=img_shape, kernel_regularizer=l2(0.01)))
model.add(Activation('relu'))
model.add(MaxPool2D(2))
model.add(Conv2D(12,3,activity_regularizer=l2(0.01)))
model.add(Activation('relu'))
model.add(MaxPool2D(2))
model.add(Flatten())
model.add(Dense(10,bias_regularizer=l2(0.01)))
model.add(Activation('softmax'))
在 Keras 的第一个卷积层中将kernel_regularizer设置为正则化权重。将bias_regularizer设置为正则化偏置,设置activity_regularizer则是正则化一个层的输出激活。
在以下示例中,正则化器被设置为启用,但在这里它们实际上会损害我们网络的性能。从前面的训练结果可以看出,我们的网络并没有发生过拟合,因此启用正则化器会损害性能,导致模型出现欠拟合。
正如我们在以下输出中看到的,在这种情况下,模型达到了大约 87%的验证准确度:
model.compile(loss='sparse_categorical_crossentropy',optimizer = 'adam',metrics=['acc'])
history = model.fit(x_train,y_train,batch_size=32,epochs=10,validation_data=(x_test,y_test))
Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 22s 374us/step - loss: 7707.2773 - acc: 0.6556 - val_loss: 55.7280 - val_acc: 0.7322
Epoch 2/10
60000/60000 [==============================] - 21s 344us/step - loss: 20.5613 - acc: 0.7088 - val_loss: 6.1601 - val_acc: 0.6771
....
Epoch 10/10
60000/60000 [==============================] - 20s 329us/step - loss: 0.9231 - acc: 0.8650 - val_loss: 0.8309 - val_acc: 0.8749
你会注意到,模型在验证集上的准确度高于训练集;这是过拟合的明显迹象。
Dropout
正如 Srivastava 等人 2014 年论文标题所揭示的,Dropout 是一种简单的方法来防止神经网络过拟合。它通过随机去除神经网络中的节点来实现这一点:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_15.jpg
Dropout 方法的示意图。来自 Srivastava 等人,“Dropout: A Simple Way to Prevent Neural Networks from Overfitting”,2014 年
使用 dropout 时,每个节点都有小概率将其激活值设置为零。这意味着学习算法不再可以过度依赖单一节点,就像 L2 和 L1 正则化一样。因此,dropout 也具有正则化作用。
在 Keras 中,dropout 是一种新的层。它被放置在你希望应用 dropout 的激活函数后面。它会传递激活值,但有时会将其设置为零,从而实现与直接在单元中使用 dropout 相同的效果。我们可以通过以下代码看到这一点:
from keras.layers import Dropout
model = Sequential()
model.add(Conv2D(6,3,input_shape=img_shape))
model.add(Activation('relu'))
model.add(MaxPool2D(2))
model.add(Dropout(0.2))
model.add(Conv2D(12,3))
model.add(Activation('relu'))
model.add(MaxPool2D(2))
model.add(Dropout(0.2))
model.add(Flatten())
model.add(Dense(10,bias_regularizer=l2(0.01)))
model.add(Activation('softmax'))
如果过拟合是一个严重问题,dropout 值为 0.5 被认为是一个不错的选择,而大于 0.5 的值则不太有用,因为网络会处理太少的值。在这种情况下,我们选择了 0.2 的 dropout 值,这意味着每个单元有 20% 的概率被设置为零。
请注意,dropout 是在池化之后使用的:
model.compile(loss='sparse_categorical_crossentropy',optimizer = 'adam',metrics=['acc'])
history = model.fit(x_train,y_train,batch_size=32,epochs=10,validation_data=(x_test,y_test))
Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 22s 371us/step - loss: 5.6472 - acc: 0.6039 - val_loss: 0.2495 - val_acc: 0.9265
Epoch 2/10
60000/60000 [==============================] - 21s 356us/step - loss: 0.2920 - acc: 0.9104 - val_loss: 0.1253 - val_acc: 0.9627
....
Epoch 10/10
60000/60000 [==============================] - 21s 344us/step - loss: 0.1064 - acc: 0.9662 - val_loss: 0.0545 - val_acc: 0.9835
较低的 dropout 值为我们创造了良好的结果,但同样,网络在验证集上的表现优于训练集,这是出现欠拟合的明显标志。请注意,dropout 只在训练时应用。当模型用于预测时,dropout 不会起作用。
Batchnorm
Batchnorm,即批量归一化,是一种对输入数据进行“归一化”的技术,按批次进行处理。每个 batchnorm 计算数据的均值和标准差,并应用变换,使得均值为零,标准差为一。
这使得训练变得更加容易,因为损失曲面变得更加“圆滑”。不同输入维度的均值和标准差意味着网络必须学习一个更复杂的函数。
在 Keras 中,batchnorm 也是一个新的层,如以下代码所示:
from keras.layers import BatchNormalization
model = Sequential()
model.add(Conv2D(6,3,input_shape=img_shape))
model.add(Activation('relu'))
model.add(MaxPool2D(2))
model.add(BatchNormalization())
model.add(Conv2D(12,3))
model.add(Activation('relu'))
model.add(MaxPool2D(2))
model.add(BatchNormalization())
model.add(Flatten())
model.add(Dense(10,bias_regularizer=l2(0.01)))
model.add(Activation('softmax'))
model.compile(loss='sparse_categorical_crossentropy',optimizer = 'adam',metrics=['acc'])
history = model.fit(x_train,y_train,batch_size=32,epochs=10,validation_data=(x_test,y_test))
Train on 60000 samples, validate on 10000 samples
Epoch 1/10
60000/60000 [==============================] - 25s 420us/step - loss: 0.2229 - acc: 0.9328 - val_loss: 0.0775 - val_acc: 0.9768
Epoch 2/10
60000/60000 [==============================] - 26s 429us/step - loss: 0.0744 - acc: 0.9766 - val_loss: 0.0668 - val_acc: 0.9795
....
Epoch 10/10
60000/60000 [==============================] - 26s 432us/step - loss: 0.0314 - acc: 0.9897 - val_loss: 0.0518 - val_acc: 0.9843
Batchnorm 通常通过简化训练过程来加速训练。你可以看到在第一轮训练中准确率的跃升:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_17.jpg
使用 batchnorm 的 MNIST 分类器的训练和验证准确率
Batchnorm 也有轻微的正则化效果。极端值通常会被过拟合,而 batchnorm 减少了这些极端值,类似于激活正则化。这一切使得 batchnorm 在计算机视觉中成为一个非常流行的工具。
处理大规模图像数据集
图像通常是大文件。事实上,可能无法将整个图像数据集加载到机器的内存中。
因此,我们需要“及时”从磁盘加载图像,而不是提前加载所有图像。在这一部分,我们将设置一个图像数据生成器,按需加载图像。
在这个例子中,我们将使用植物幼苗的数据集。该数据集由 Thomas Giselsson 等人于 2017 年通过他们的出版物《A Public Image Database for Benchmark of Plant Seedling Classification Algorithms》提供。
该数据集可以通过以下链接访问:arxiv.org/abs/1711.05458。
你可能会想,为什么我们要研究植物呢?毕竟,植物分类在金融领域并不是一个常见的问题。简单的回答是,这个数据集适合展示许多常见的计算机视觉技术,并且它是开放域许可的,因此是一个非常适合我们使用的训练数据集。
想要在更相关的数据集上测试自己知识的读者,可以查看State Farm Distracted Driver数据集和Planet: Understanding the Amazon from Space数据集。
注意
本节以及关于堆叠预训练模型的章节中的代码和数据可以在此处找到并运行:www.kaggle.com/jannesklaas/stacking-vgg。
Keras 自带了一个图像数据生成器,可以直接从磁盘加载文件。为此,您只需运行:
from keras.preprocessing.image import ImageDataGenerator
为了从文件中获取生成器,我们首先需要指定生成器。在 Keras 中,ImageDataGenerator提供了一系列图像增强工具,但在我们的示例中,我们只会使用重新缩放功能。
重新缩放是将图像中的所有值与常数相乘。对于大多数常见的图像格式,颜色值的范围是从 0 到 255,所以我们希望将其缩放为 1/255。我们可以通过运行以下代码来实现:
imgen = ImageDataGenerator(rescale=1/255)
然而,这还不是一个加载图像的生成器。ImageDataGenerator类提供了一系列可以通过调用其函数来创建的生成器。
为了获取一个加载文件的生成器,我们需要调用flow_from_directory。
然后,我们需要指定 Keras 应使用的目录、我们希望的批次大小,在本例中为32,以及图像应调整为的目标大小,在本例中为 150x150 像素。为此,我们可以简单地运行以下代码:
train_generator = imgen.flow_from_directory('train',batch_size=32, target_size=(150,150))
validation_generator = imgen.flow_from_directory('validation',batch_size=32, tar get_size=(150,150))
Keras 是如何找到图像的,如何知道这些图像属于哪个类别的呢?Keras 的生成器需要以下文件夹结构:
-
根目录:
-
类别 0
-
img
-
img
-
…
-
-
类别 1
-
img
-
img
-
…
-
-
类别 1
- img
-
我们的数据集已经按这种方式设置好了,通常将图像分类以匹配生成器的预期并不困难。
使用预训练模型
训练大型计算机视觉模型不仅困难,而且计算开销巨大。因此,通常使用最初为其他目的训练的模型,并对其进行微调以适应新任务。这就是迁移学习的一个例子。
迁移学习的目标是将一个任务的学习迁移到另一个任务中。作为人类,我们非常擅长转移我们学到的知识。当你看到一只以前没见过的狗时,你不需要为这只特定的狗重新学习关于狗的所有知识;相反,你只是将新学到的知识转移到你已经知道的关于狗的知识上。每次重新训练一个大网络并不经济,因为你会发现模型中有一些部分是可以重用的。
在本节中,我们将对 VGG-16 进行微调,该模型最初是在 ImageNet 数据集上训练的。ImageNet 竞赛是一个年度计算机视觉竞赛,ImageNet 数据集包含了数百万张现实世界物体的图像,从狗到飞机。
在 ImageNet 竞赛中,研究人员竞争构建最精确的模型。事实上,ImageNet 推动了计算机视觉领域近年来的许多进展,而为 ImageNet 竞赛构建的模型也是微调其他模型的常见基础。
VGG-16 是由牛津大学视觉几何组开发的模型架构。该模型由卷积部分和分类部分组成。我们只会使用卷积部分。此外,我们将添加我们自己的分类部分,用于植物分类。
可以通过以下代码在 Keras 中下载 VGG-16:
from keras.applications.vgg16 import VGG16
vgg_model = VGG16(include_top=False,input_shape=(150,150,3))
out:
Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.1/vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5
58892288/58889256 [==============================] - 5s 0us/step
下载数据时,我们希望让 Keras 知道我们不希望包含顶部部分(分类部分);我们还希望告诉 Keras 期望的输入形状。如果不指定输入形状,模型将接受任何图像大小,并且无法在其上添加Dense层:
vgg_model.summary()
out:
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
input_1 (InputLayer) (None, 150, 150, 3) 0
_________________________________________________________________
block1_conv1 (Conv2D) (None, 150, 150, 64) 1792
_________________________________________________________________
block1_conv2 (Conv2D) (None, 150, 150, 64) 36928
_________________________________________________________________
block1_pool (MaxPooling2D) (None, 75, 75, 64) 0
_________________________________________________________________
block2_conv1 (Conv2D) (None, 75, 75, 128) 73856
_________________________________________________________________
block2_conv2 (Conv2D) (None, 75, 75, 128) 147584
_________________________________________________________________
block2_pool (MaxPooling2D) (None, 37, 37, 128) 0
_________________________________________________________________
block3_conv1 (Conv2D) (None, 37, 37, 256) 295168
_________________________________________________________________
block3_conv2 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_conv3 (Conv2D) (None, 37, 37, 256) 590080
_________________________________________________________________
block3_pool (MaxPooling2D) (None, 18, 18, 256) 0
_________________________________________________________________
block4_conv1 (Conv2D) (None, 18, 18, 512) 1180160
_________________________________________________________________
block4_conv2 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_conv3 (Conv2D) (None, 18, 18, 512) 2359808
_________________________________________________________________
block4_pool (MaxPooling2D) (None, 9, 9, 512) 0
_________________________________________________________________
block5_conv1 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv2 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_conv3 (Conv2D) (None, 9, 9, 512) 2359808
_________________________________________________________________
block5_pool (MaxPooling2D) (None, 4, 4, 512) 0
=================================================================
Total params: 14,714,688
Trainable params: 14,714,688
Non-trainable params: 0
_________________________________________________________________
如你所见,VGG 模型非常庞大,具有超过 1470 万个可训练参数。它还由Conv2D和MaxPooling2D层组成,这两种层我们在处理 MNIST 数据集时已经学习过。
从此点开始,我们有两种不同的方式可以继续:
-
添加层并构建一个新模型。
-
通过预训练模型预处理所有图像,然后训练一个新的模型。
修改 VGG-16
在本节中,我们将在 VGG-16 模型的基础上添加层,然后从那里开始训练新的大模型。
然而,我们并不想重新训练那些已经训练好的卷积层。因此,我们必须首先“冻结”VGG-16 中的所有层,我们可以通过运行以下代码来实现:
for the layer in vgg_model.layers:
layer.trainable = False
Keras 将 VGG 下载为一个函数式 API 模型。我们将在第六章《使用生成模型》中详细学习函数式 API,但目前我们只想使用顺序 API,它允许我们通过model.add()堆叠层。我们可以使用以下代码将一个函数式 API 模型转换为顺序 API 模型:
finetune = Sequential(layers = vgg_model.layers)
运行代码的结果是,我们现在已经创建了一个新的模型,名为finetune,它的功能与普通的顺序模型相同。我们需要记住,使用顺序 API 转换模型仅在模型能够实际表达为顺序 API 时才有效。一些更复杂的模型无法转换。
由于我们刚才所做的一切,向我们的模型中添加层现在变得很简单:
finetune.add(Flatten())
finetune.add(Dense(12))
finetune.add(Activation('softmax'))
新添加的层默认是可训练的,而重用的模型插槽则不是。我们可以像训练任何其他模型一样,在我们之前定义的数据生成器上训练这个堆叠模型。可以通过运行以下代码来执行:
finetune.compile(loss='categorical_crossentropy',optimizer='adam',metrics = ['acc'])
finetune.fit_generator(train_generator,epochs=8,steps_per_epoch= 4606 // 32, validation_data=validation_generator, validation_steps= 144//32)
运行后,模型的验证准确率大约能达到 75%。
随机图像增强
机器学习中的一个普遍问题是,不管我们有多少数据,更多的数据总是更好的,因为它可以提高输出质量,同时防止过拟合,并让我们的模型处理更广泛的输入。因此,通常会对图像应用随机增强,例如旋转或随机裁剪。
这个思路是通过从一张图像中生成大量不同的图像,从而减少模型过拟合的机会。对于大多数图像增强目的,我们可以直接使用 Keras 的ImageDataGenerator。
更高级的增强可以通过 OpenCV 库来实现。然而,专注于这一点超出了本章的范围。
使用 ImageDataGenerator 进行增强
当使用增强数据生成器时,我们通常只在训练中使用它。验证生成器不应使用增强特性,因为当我们验证模型时,我们希望评估它在未见过的真实数据上的表现,而不是增强数据。
这与基于规则的增强不同,在基于规则的增强中,我们试图创建更容易分类的图像。因此,我们需要创建两个ImageDataGenerator实例,一个用于训练,另一个用于验证。这可以通过运行以下代码实现:
train_datagen = ImageDataGenerator(
rescale = 1/255,
rotation_range=90,
width_shift_range=0.2,
height_shift_range=0.2,
shear_range=0.2,
zoom_range=0.1,
horizontal_flip=True,
fill_mode='nearest')
这个训练数据生成器利用了一些内建的增强技术。
注意
注意:Keras 中还有更多命令可用。完整的命令列表请参考 Keras 文档:keras.io/。
在下面的列表中,我们已突出显示几个常用的命令:
-
rescale对图像中的值进行缩放。我们之前使用过它,也将在验证中使用它。 -
rotation_range是一个范围(0 到 180 度),用于随机旋转图像。 -
width_shift_range和height_shift_range是范围(相对于图像大小,这里为 20%),用于随机地水平或垂直拉伸图像。 -
shear_range是一个范围(同样,相对于图像)用于随机应用剪切。 -
zoom_range是随机缩放图像的范围。 -
horizontal_flip指定是否随机翻转图像。 -
fill_mode指定如何填充由旋转等操作创建的空白区域。
我们可以通过将一张图片多次通过生成器来查看它的作用。
首先,我们需要导入 Keras 图像工具并指定图像路径(这个是随机选择的)。这可以通过运行以下代码实现:
from keras.preprocessing import image
fname = 'train/Charlock/270209308.png'
然后我们需要加载图像并将其转换为 NumPy 数组,这可以通过以下代码实现:
img = image.load_img(fname, target_size=(150, 150))
img = image.img_to_array(img)
如前所述,我们需要向图像添加一个批处理大小维度:
img = np.expand_dims(img,axis=0)
然后,我们使用刚才创建的ImageDataGenerator实例,但不是使用flow_from_directory,而是使用flow,它允许我们直接将数据传递给生成器。然后我们传递我们想要使用的那一张图像,可以通过运行以下代码来实现:
gen = train_datagen.flow(img, batch_size=1)
在一个循环中,我们接着对生成器调用next四次:
for i in range(4):
plt.figure(i)
batch = next(gen)
imgplot = plt.imshow(image.array_to_img(batch[0]))
plt.show()
这将产生以下输出:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_18.jpg
一些随机修改过的图像样本
模块化的权衡
本章展示了通过一些基于规则的系统来辅助机器学习模型是可能的,且往往是有用的。你可能还注意到数据集中图像都被裁剪,只显示了一种植物。
虽然我们本可以构建一个模型来定位并分类植物,但除了分类之外,我们也可以构建一个系统,直接输出植物应接受的治疗。这就引出了一个问题:我们应该让系统的模块化程度达到什么水平?
端到端深度学习在几年前非常流行。如果有大量数据,深度学习模型可以学习到原本需要多个组件的系统才能学会的内容。然而,端到端深度学习也有几个缺点:
-
端到端深度学习需要大量的数据。因为模型有很多参数,所以需要大量数据来避免过拟合。
-
端到端深度学习很难调试。如果你用一个黑盒模型替换整个系统,你几乎没有希望找出为什么某些事情发生了。
-
一些东西很难学习,但很容易写成代码,特别是理智检查规则。
最近,研究人员开始让他们的模型更加模块化。一个很好的例子是 Ha 和 Schmidthuber 的世界模型,你可以在这里阅读:worldmodels.github.io/。在这个模型中,他们编码了视觉信息,做出了关于未来的预测,并通过三个不同的模型选择行动。
从实际角度来看,我们可以看看 Airbnb,他们将结构化建模与机器学习结合,用于其定价引擎。你可以在这里阅读更多内容:medium.com/airbnb-engineering/learning-market-dynamics-for-optimal-pricing-97cffbcc53e3。建模人员知道预订大致遵循泊松分布,并且也有季节性效应。因此,Airbnb 构建了一个模型,直接预测分布的参数和季节性,而不是让模型直接预测预订量。
如果你只有少量数据,那么你的算法性能需要依赖于人类的洞察力。如果某些子任务可以轻松地用代码表达,那么通常最好通过代码来表达它们。如果你需要可解释性,并且希望理解为何做出某些选择,模块化设置与清晰可解释的中间输出是一个不错的选择。然而,如果任务很难,且你不清楚它包含哪些子任务,并且有大量数据,那么通常最好使用端到端方法。
使用纯端到端方法是非常罕见的。例如,图像总是经过相机芯片的预处理,你从不直接处理原始数据。
聪明地划分任务可以提高性能并降低风险。
计算机视觉超越分类
如我们所见,有许多技术可以帮助我们的图像分类器提高性能。这些技术贯穿本书的多个章节,不仅限于计算机视觉应用。
在本章的最后一节中,我们将讨论一些超越图像分类的方法。这些任务通常需要比本章所讨论的内容更具创意的神经网络使用方法。
为了充分理解本节内容,你不需要过多担心所呈现技术的细节,而是要关注研究人员在使用神经网络时的创意方法。我们采取这种方法是因为你会经常发现,解决的问题往往也需要类似的创造性。
面部识别
面部识别在零售行业有许多应用。例如,如果你在前台,可能希望在 ATM 上自动识别客户,或者你可能想提供基于面部的安全功能,如 iPhone 提供的那样。然而,在后台,你需要遵守 KYC(了解你的客户)法规,要求你识别正在合作的客户。
表面上,面部识别看起来像是一个分类任务。你将一张面部图像输入机器,它会预测这张脸属于哪个人。问题是,你可能有数百万个客户,但每个客户只有一两张照片。
此外,你可能会不断获得新客户。每次获得新客户时,你无法改变模型,如果它必须在数百万个类别中选择,而每个类别只有一个样本,那么简单的分类方法将失败。
这里的创意思路是,与其对客户的面部进行分类,不如判断两张图像是否展示的是同一张面孔。你可以在以下示意图中看到这一思路的可视化表示:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_19.jpg
一种孪生网络示意图
为此,您需要首先将两张图像通过网络。Siamese 网络是一类包含两个或更多相同子网络的神经网络架构,这些子网络完全相同并且包含相同的权重。在 Keras 中,您可以通过先定义层,然后在两个网络中使用它们来实现这种设置。然后,这两个网络的输出会输入到一个单一的分类层,来判断这两张图像是否显示相同的面孔。
为了避免每次想要识别人脸时,都将我们数据库中的所有客户图像通过整个 Siamese 网络,一般会保存 Siamese 网络的最终输出。Siamese 网络对图像的最终输出被称为人脸嵌入。当我们想要识别一个客户时,我们将客户面部图像的嵌入与我们数据库中存储的嵌入进行比较。我们可以通过单一分类层来实现这一点。
存储人脸嵌入非常有益,因为它能节省大量计算成本,并且允许进行人脸聚类。人脸会根据性别、年龄、种族等特征聚集在一起。通过仅将图像与同一聚类中的图像进行比较,我们可以进一步节省计算资源,从而实现更快的识别。
训练 Siamese 网络有两种方法。我们可以通过创建匹配和不匹配图像的对,并使用二元交叉熵分类损失来训练整个模型,从而将它们与分类器一起训练。然而,另一种方法,通常被认为更好的方法,是直接训练模型生成面部嵌入。这种方法在 Schroff、Kalenichenko 和 Philbin 的 2015 年论文《FaceNet: A Unified Embedding for Face Recognition and Clustering》中有详细描述,您可以在这里阅读:arxiv.org/abs/1503.03832。
这个想法是创建图像三元组:一个锚定图像,一个显示与锚定图像相同面孔的正样本图像,以及一个显示与锚定图像不同面孔的负样本图像。使用三元组损失来使锚定图像的嵌入与正样本嵌入之间的距离更小,而锚定图像与负样本之间的距离更大。
损失函数看起来是这样的:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_019.jpg
这里https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_020.jpg是一个锚定图像,https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_021.jpg是 Siamese 网络的输出,锚定图像的嵌入。三元组损失是锚定图像与正样本之间的欧几里得距离减去锚定图像与负样本之间的欧几里得距离。一个小常数,https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_022.jpg,是在正负样本对之间施加的边距。为了达到零损失,距离之间的差异需要是https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_023.jpg。
你应该能够理解,你可以使用神经网络来预测两个项目是否在语义上相同,从而解决大规模分类问题。你可以通过一些二分类任务来训练 Siamese 模型,也可以通过将输出视为嵌入并使用三元组损失来训练。这一见解不仅适用于人脸。如果你想比较时间序列以进行事件分类,那么你可以使用完全相同的方法。
边界框预测
可能有某个时刻,你会对在图像中定位物体感兴趣。例如,假设你是一家保险公司,需要检查所承保的房屋屋顶。让人爬上屋顶进行检查非常昂贵,因此一种替代方案是使用卫星图像。在获得这些图像后,你现在需要找到其中的屋顶,就像我们在以下屏幕截图中看到的那样。然后,你可以裁剪出屋顶图像并将其发送给专家,让他们进行检查:
https://github.com/OpenDocCN/freelearn-quant-zh/raw/master/docs/ml-fin/img/B10354_03_20.jpg
标出屋顶边界框的加州住宅
你需要的是边界框预测。边界框预测器输出多个边界框的坐标,并预测每个框中显示的物体。
获取这种边界框有两种方法。
基于区域的卷积神经网络(R-CNN)重用分类模型。它获取一张图像,并将分类模型滑动到图像上。结果是对图像的不同部分进行多次分类。利用这个特征图,区域提议网络执行回归任务,生成边界框,分类网络则为每个边界框创建分类。
这种方法已经得到了改进,并在 Ren 等人 2016 年的论文《Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks》中达到了顶峰,论文可以在arxiv.org/abs/1506.01497找到,但滑动分类器在图像上这一基本概念依然保持不变。
一回归看全(YOLO)则使用一个仅包含卷积层的单一模型。它将图像划分为网格,并为每个网格单元预测一个物体类别。然后,它为每个网格单元预测几个可能包含物体的边界框。
对于每个边界框,它回归坐标以及宽度和高度值,并给出一个置信度评分,表示该边界框确实包含一个物体。然后,它会去除那些置信度太低或与其他置信度较高的边界框重叠过多的边界框。
注意
想要更详细的描述,请阅读 Redmon 和 Farhadi 在 2016 年的论文,YOLO9000: Better, Faster, Stronger,可在 arxiv.org/abs/1612.08242 阅读。进一步阅读可以参考 2018 年的论文,YOLOv3: An Incremental Improvement,该论文可以在 arxiv.org/abs/1804.027 阅读。
这两篇论文都写得很好,带有些许讽刺幽默,能够更详细地解释 YOLO 的概念。
YOLO 相较于 R-CNN 的主要优势是速度更快。无需滑动一个庞大的分类模型,效率要高得多。然而,R-CNN 的主要优势是它在准确性上比 YOLO 模型略强。如果你的任务需要实时分析,应该使用 YOLO;但是如果你不需要实时速度,而只是追求最佳的准确性,那么使用 R-CNN 更合适。
边界框检测通常作为多个处理步骤之一来使用。在保险案例中,边界框检测器会裁剪出所有的屋顶部分。然后,这些屋顶图像可以由人类专家进行判断,或者由一个单独的深度学习模型来分类损坏的屋顶。当然,你也可以训练一个物体定位器来直接区分损坏和完好的屋顶,但实际上,这通常不是一个好的主意。
如果你有兴趣进一步阅读相关内容,时间序列理解(第四章") 中有一篇关于模块化的精彩讨论。
练习
Fashion MNIST 是 MNIST 的替代版本,但它分类的是衣物而非手写数字。尝试在 Fashion MNIST 上应用我们在本章中使用的技术。它们是如何协同工作的?哪些方法能得到良好的结果?你可以在 Kaggle 上找到该数据集,网址为 www.kaggle.com/zalando-research/fashionmnist。
参与鲸鱼识别挑战,阅读顶尖的 kernel 和讨论帖子。链接在这里:www.kaggle.com/c/whale-categorization-playground。通过尾鳍识别鲸鱼的任务与通过面部识别人类的任务相似。有一些不错的 kernel 展示了边界框和孪生网络。我们还没有涵盖解决该任务所需的所有技术工具,因此不必过于担心代码的细节,应该专注于展示的概念。
总结
在本章中,你已经了解了计算机视觉模型的基本构建模块。我们学习了卷积层,以及 ReLU 激活函数和正则化方法。你还看到了几种创造性地使用神经网络的方法,如孪生网络和边界框预测器。
你还成功地在一个简单的基准任务——MNIST 数据集上实现并测试了所有这些方法。我们扩大了训练规模,使用了预训练的 VGG 模型来对数千张植物图像进行分类,然后使用 Keras 生成器从磁盘动态加载图像,并定制 VGG 模型以适应我们的新任务。
我们还学习了图像增强和构建计算机视觉模型时模块化权衡的重要性。许多这些构建模块,如卷积、批归一化和丢弃法,除了在计算机视觉中使用外,也广泛应用于其他领域。它们是基本工具,你将会在计算机视觉应用之外的其他地方看到它们。通过在这里学习它们,你为自己发现其他领域的广泛可能性奠定了基础,例如时间序列或生成模型。
计算机视觉在金融行业有许多应用,特别是在后台职能和替代阿尔法生成方面。它是现代机器学习的一种应用,能够为许多公司带来实际价值。越来越多的公司在决策中纳入基于图像的数据来源;现在,你已经准备好迎接这些问题的挑战。
在本章的过程中,我们看到成功的计算机视觉项目涉及到一个完整的管道,并且在管道上工作的收益往往与在模型上工作的收益相似或更大。
在下一章中,我们将探讨最具代表性和常见的金融数据形式:时间序列。我们将使用更传统的统计方法,如ARIMA(即自回归积分滑动平均),以及基于现代神经网络的方法来解决预测网页流量的任务。你还将学习如何通过自相关和傅里叶变换进行特征工程。最后,你将学习如何比较和对比不同的预测方法,并构建一个高质量的预测系统。