C--机器学习实用指南-全-
C# 机器学习实用指南(全)
原文:
annas-archive.org/md5/88e1c6764480ef08707c80e193f734c3译者:飞龙
前言
在我们以信息技术为主的工作中,机器学习的必要性无处不在,并且被所有开发者、程序员和分析人员所需求。但为什么使用 C#进行机器学习呢?答案是,大多数生产级企业应用程序都是用 C#编写的,使用了诸如 Visual Studio、SQL Server、Unity 和 Microsoft Azure 等工具。
本书通过各种概念、机器学习技术和各种机器学习工具的介绍,为用户提供了一种直观理解,这些工具可以帮助用户将诸如图像和运动检测、贝叶斯直觉、深度学习和信念等智能功能添加到 C# .NET 应用程序中。
使用本书,你将实现监督学习和无监督学习算法,并准备好创建良好的预测模型。你将学习许多技术和算法,从简单的线性回归、决策树和 SVM 到更高级的概念,如人工神经网络、自编码器和强化学习。
在本书结束时,你将培养出机器学习思维,并能够利用 C#工具、技术和包来构建智能、预测和现实世界的商业应用程序。
本书面向的对象
本书面向有 C#和.NET 经验的开发者。不需要或假设有其他经验——只需对机器学习、人工智能和深度学习有热情即可。
本书涵盖的内容
第一章,机器学习基础,介绍了机器学习以及我们在这本书中希望实现的目标。
第二章,ReflectInsight – 实时监控,介绍了 ReflectInsight,这是一个强大、灵活且丰富的框架,我们将在整本书中用它来记录和了解我们的算法。
第三章,贝叶斯直觉 – 解决追尾神秘事件和执行数据分析,向读者展示了贝叶斯直觉。我们还将检查并解决著名的“追尾”问题,其中我们试图确定谁逃离了事故现场。
第四章,风险与回报 – 强化学习,展示了强化学习是如何工作的。
第五章,模糊逻辑 – 操纵障碍赛,实现了模糊逻辑来引导我们的自主引导车辆绕过障碍赛道。我们将展示如何加载各种地图,以及我们的自主车辆如何因为做出正确和错误的决定而获得奖励和惩罚。
第六章,颜色混合 – 自组织映射和弹性神经网络,通过展示我们如何将随机颜色混合在一起,向读者展示了 SOM(自组织映射)的力量。这为读者提供了一个关于自组织映射的非常简单的直觉。
第七章,面部和动作检测 – 图像滤波器,给读者提供了一个非常简单的框架,可以快速将面部和动作检测功能添加到他们的程序中。我们提供了面部和动作检测的多种示例,解释了我们将使用的各种算法,并介绍了我们的专用法国斗牛犬助手 Frenchie!
第八章,百科全书与神经元 – 旅行商问题,利用神经元来解决古老的旅行商问题,我们的销售人员被给予了一张必须访问以销售百科全书的房屋地图。为了达到他的目标,他必须选择最短路径,同时只访问每座房子一次,并最终回到起点。
第九章,我应该接受这份工作吗 – 决策树的应用,通过两个不同的开源框架向读者介绍决策树。我们将使用决策树来回答问题,我应该接受这份工作吗?
第十章,深度信念 - 深度网络与梦境,介绍了一个开源框架 SharpRBM。我们将深入到玻尔兹曼和受限玻尔兹曼机的世界。我们将提出并回答问题,当计算机做梦时,它们会梦到什么?
第十一章,微基准测试和激活函数,向读者介绍了一个开源微基准测试框架 Benchmark.Net。我们将向读者展示如何基准测试代码和函数。我们还将解释什么是激活函数,并展示我们如何对今天使用的许多激活函数进行了微基准测试。读者将获得关于每个函数所需时间的宝贵见解,以及使用浮点数和双精度数之间的计时差异。
第十二章,C# .NET 中的直观深度学习,介绍了一个名为 Kelp.Net 的开源框架。这个框架是 C# .NET 开发者可用的最强大的深度学习框架。我们将向读者展示如何使用该框架执行许多操作和测试,并将其与 ReflectInsight 集成,以获取关于我们的深度学习算法的令人难以置信的丰富信息。
第十三章,量子计算 – 未来,向读者展示未来,量子计算的世界。
为了充分利用这本书
-
您应该熟悉 C#和.NET 的基本开发
-
你应该对机器学习和开源项目有热情
下载示例代码文件
您可以从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/Hands-On-Machine-Learning-with-CSharp。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有来自我们丰富的书籍和视频目录中的其他代码包可供选择,请访问github.com/PacktPublishing/。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/HandsOnMachineLearningwithCSharp_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
CodeInText: 表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“我们将使用真值表中的AddLine函数来添加这些信息。”
代码块设置如下:
// build the truth tables
UberDriver?.Table?.AddLine(0.85, true);
WitnessSawUberDriver?.Table?.AddLine(0.80, true, true);
WitnessSawUberDriver?.Table?.AddLine(0.20, true, false);
network.Validate();
当我们希望您注意代码块的特定部分时,相关的行或项目将以粗体显示:
config.Add(new CsvExporter(CsvSeparator.CurrentCulture,
new BenchmarkDotNet.Reports.SummaryStyle
{
PrintUnitsInHeader = true,
PrintUnitsInContent = false,
TimeUnit = TimeUnit.Microsecond,
SizeUnit = BenchmarkDotNet.Columns.SizeUnit.KB
}));
粗体: 表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“消息详细信息面板显示所选消息的扩展详细信息。”
警告或重要提示如下所示。
技巧和窍门如下所示。
联系我们
我们欢迎读者的反馈。
一般反馈: 请通过电子邮件发送至feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请通过电子邮件发送至questions@packtpub.com。
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您向我们报告。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版: 如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过copyright@packtpub.com与我们联系,并附上材料的链接。
如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评价
请留下您的评价。一旦您阅读并使用了这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
想了解更多关于 Packt 的信息,请访问packtpub.com。
第一章:机器学习基础
大家好,欢迎来到《使用 C#和.NET 进行机器学习》。本书的目标是向您,一位经验丰富的 C# .NET 开发者,介绍许多可用的开源机器学习框架,以及如何使用这些包的示例。在这个过程中,我们将讨论日志记录、面部和动作检测、决策树、图像识别、直观的深度学习、量子计算等内容。在许多情况下,你将在几分钟内就能启动并运行。我真诚地希望这个系列中总有一款适合每个人。最重要的是,作为一名已经与开发者打交道 30 年的作者,以下是为什么我写这本书的原因。
作为一名终身微软开发者,我经常看到开发者们为了解决日常问题而苦苦寻找所需的资源。让我们面对现实吧,我们中没有一个人有足够的时间按照自己的方式做事,而且我们中很少有人有幸在一个真正的研发部门工作。然而,多年来我们已经走了很长的路,从那些还记得我们桌上有 C 程序员参考书和 50 多本书的我们,到如今能够快速在谷歌上搜索并找到我们确切(好吧,有时是确切)想要的东西。但现在,随着人工智能时代的到来,事情发生了一些变化。作为 C#开发者,当涉及到机器学习时,谷歌搜索并不总是我们的最佳朋友,因为几乎所有被使用的东西都是 Python、R、MATLAB 和 Octave。我们还必须记住,机器学习已经存在很多年了;只是最近,美国企业才开始拥抱它,我们看到越来越多的人参与其中。现在,计算能力已经可用,学术界在将其推广到世界各地的过程中取得了惊人的进步。但朋友们,正如你们无疑已经听说的那样,世界是一个可怕的地方!C# .NET 开发者该往哪里去呢?让我们在下一节中通过一个简短的故事来回答这个问题,不幸的是,这个故事的真实性就像天空一样。至少在这里阳光明媚的佛罗里达州是这样!
在本章中,我们将学习以下主题:
-
数据挖掘
-
人工智能(AI)和生物人工智能
-
深度学习
-
概率与统计
-
监督学习
-
无监督学习
-
强化学习
-
是购买、构建还是开源
机器学习简介
我曾经有一个老板,我告诉他我正在使用机器学习来发现更多关于我们数据的信息。他的回应是:“你认为你能学到我不知道的东西吗!”如果你在你的职业生涯中没有遇到过这样的人,恭喜你。也请告诉我如果你有任何空缺!但你更有可能已经遇到了,或者将会遇到。下面是如何处理的。而且,我没有辞职!
我说:“目标是了解更多关于我们拥有的基金以及它们可能如何与用户实际意图相关联的信息和细节。”
他:“但我已经知道这一切了。机器学习只是一个时髦词,最终都是数据,我们都是数据管理员。其余的都是时髦词。我们为什么要做这件事,它最终将如何帮助我?”
我:“好吧,让我问你这个问题。你认为你在谷歌上搜索某个东西时会发生什么?”
他:眼神呆滞,带着一丝愤怒。
他:“你是什么意思?显然,谷歌会把我搜索的内容与其他历史上搜索过相同内容的其他搜索进行对比。”
我:“好吧,那它是怎么完成的?”
他:愤怒和挫败感略有加剧。
他:“显然是它的计算机在搜索网络,并将我的搜索条件与其他搜索进行匹配。” 我:“但你有没有想过,这个搜索是如何在数十亿其他搜索中匹配的,以及所有搜索背后的数据是如何不断更新的?显然,人们无法参与其中,否则就无法扩展。”
他:“当然,算法经过精心调整,给出了我们想要的结果,或者至少,推荐。”
我:“没错,这正是机器学习所做的事情。”(不一定总是,但足够接近!)
他:“好吧,我觉得从数据中我学不到更多,让我们看看结果如何。”
所以,让我们坦诚一点,朋友们。有时候,无论逻辑多么严密,都无法克服盲点或对改变的抵抗,但这个故事背后有着截然不同且更为重要的意义,远非一个违背我们生物学所学的一切的老板。在机器学习的世界中,向一个不身处日常开发前线的人证明/展示正在发生的事情,无论事情是否在正常运作,它们是如何运作的,为什么它们(或不)在运作,等等,都要困难得多。即使如此,你也可能很难理解算法正在做什么。
在决定机器学习是否适合你时,以下是一些你应该问自己的问题:
-
你只是在尝试符合时髦词(这可能就是真正被要求的东西)吗,或者你对这种解决方案有真正的需求?
-
你有你需要的数据吗?
-
数据是否足够干净以供使用(关于这一点稍后还会讨论)?
-
你知道你能否获得可能缺失的数据,以及你是否知道数据实际上缺失了吗?
-
你有大量数据还是只有少量数据?
-
是否存在已知且经过验证的解决方案,我们可以用它来代替?
-
你知道你试图实现什么吗?
-
你知道你将如何实现它吗?
-
你将如何向他人解释?
-
当被问及时,你将如何证明引擎盖下正在发生的事情?
这些只是我们在开始机器学习之旅时将共同解决的问题的一部分。这全部关乎培养我所说的机器学习思维模式。
现在,似乎如果有人执行了一个返回多行数据的 SQL 查询,他们就会自称是数据科学家。对于简历来说,这是公平的;每个人偶尔都需要得到一些认可,即使这是自我提供的。但我们真的在以数据科学家的身份运作吗?数据科学家究竟是什么意思?我们真的在做机器学习吗?那究竟是什么意思?好吧,到这本书的结尾,我们希望找到所有这些答案,或者至少创造一个环境,让您自己找到答案!
我们中并非所有人都有在研究或学术界工作的奢侈。我们中的许多人每天都在应对各种挑战,而正确的解决方案可能就是一个必须在 2 小时内就位的具体战术解决方案。这就是我们作为 C#开发者所做的事情。我们整天坐在办公桌后面,如果幸运的话,戴着耳机,敲击键盘。但我们真的得到了我们想要或需要的全部时间来以我们喜欢的方式开发项目吗?如果我们做到了,我们的项目中就不会有那么多技术债务,对吧(你真的在跟踪你的技术债务,对吧)?
我们需要聪明地考虑如何超越曲线,有时我们通过思考多于编码来实现这一点,尤其是在一开始。学术方面是无价的;知识根本无法替代。但美国企业中的大多数生产代码并不是用 Python、R、Matlab 和 Octave 这样的学术语言编写的。尽管所有这些学术财富都可用,但它们并不是以最适合我们工作的形式提供的。
同时,让我们停下来赞扬那些为开源社区做出贡献的人。正是因为他们,我们才有一些出色的第三方开源解决方案可以利用,来完成我们的工作。开源社区允许我们利用他们所开发的内容,这是一种特权。本书的目的是向您展示其中的一些工具,并展示您如何使用它们。在这个过程中,我们将尽力为您提供一些基本的后台知识,这样就不会让一切都变得像黑洞一样神秘!
您无处不在都能听到这些热门词汇。我过去每天都要花费 2-4 个小时的时间通勤,我记不清我看到了多少个广告牌上有“机器学习”或 AI 的字样。它们无处不在,但这一切究竟意味着什么呢?AI、机器学习、数据科学、自然语言处理(NLP)、数据挖掘、神经元,等等!似乎一旦美国企业介入,曾经精细调校的艺术变成了一个混乱的自由竞争,一个微观管理的项目,有着完全不切实际的目标。我甚至听到一个潜在客户说:“我不确定它是什么意思,但我不想被落下!”
我们必须做的第一件事是学习正确的方法来处理机器学习项目。让我们从一些定义开始:
托马斯·米切尔将机器学习定义为:
“如果一个计算机程序在经验 E 方面对任务 T 和性能度量 P 有所学习,那么它在 T 中的任务性能,按照 P 来衡量,会随着经验 E 的提高而提高。”
我们的定义将会略有不同。希望这能成为你在被要求捍卫你选择的道路时可以用到的东西:
“机器学习是一系列技术,可以以最有效和最有效的方式处理大量数据,从而从数据中为我们提供可操作的结果和洞察。”
现在,关于我们称之为技巧的那些东西,不要误解;比如概率、统计学,它们都在那里,只是隐藏在表面之下。而我们用来执行示例的工具也会像 Python、R 和其他类似工具一样隐藏细节!话虽如此,如果我们不至少让你意识到一些基础知识,那将是对你的极大不公,这些基础知识我们稍后会涉及。我并不是要降低任何一项的重要性,因为它们都是同等重要的,但我们的目标是让所有 C#开发者尽可能快地开始使用。我们将提供足够的信息,让你符合行业术语,然后你将知道的不仅仅是块状 API 调用!我鼓励你们每个人都尽可能在这个领域追求更多的学术知识。机器学习和人工智能似乎每天都在变化,所以请始终跟上最新的发展。你知道的越多,你在获得项目认可方面就会越出色。
既然我们提到了行业术语,让我们从一开始就澄清一些术语。数据挖掘、机器学习、人工智能,等等。现在我只介绍几个术语,但这里有一个简单的方式来思考它。
你和家人在旅行。假设你有孩子,我们先不考虑“我们到那里了吗”这样的对话!你正在高速公路上驾驶,其中一个孩子(一个非常小的幼儿)大声喊“卡车”并指向窗外的一辆卡车。这个孩子非常小,那么他是怎么知道那辆特定车辆是卡车的(让我们假设它真的是卡车!)他们知道那是卡车,因为每次他们做同样的事情时,你都会说“是的”或“不”。那就是机器学习。然后,当你告诉他们“是的,那是一辆大卡车”时,你就是在为强化学习添加上下文,这让我们进入了深度学习。你有没有注意到你一直在教给你的孩子你甚至不知道的事情?
希望这有所帮助。
数据挖掘
数据挖掘涉及在大量数据中搜索非常具体的信息。您正在通过数据寻找特定的事物。例如,信用卡公司会通过分析购买行为及其位置来使用数据挖掘了解买家的习惯。这些信息随后变得非常有用,例如用于定向广告。
另一方面,机器学习专注于使用您提供的算法执行搜索该数据的实际任务。这说得通吗?
现在就说到这里,但这里有一个非常棒的链接,您可以从中了解更多关于数据挖掘的信息:blog.udacity.com/2014/12/24-data-science-resources-keep-finger-pulse.html
人工智能
人工智能是机器学习的高级形式。有些人将其定义为当机器表现得和人类一样聪明,甚至比人类更聪明的时候。至于我,对这个问题的结论仍然悬而未决。我越看每天的新闻,就越想知道哪一种智能是人工的,以及真正的智能究竟是什么!有如此多的定义在流传,但简而言之,人工智能被认为是做人类能够或应该做的事情的机器,以至于任何合理的人都不可能在机器的响应中将其与人类区分开来。无论如何,人工智能是一个非常广泛的主题,不幸的是,人们对这个术语的理解和定义也各不相同!
生物人工智能
生物人工智能指的是将生物成分与计算组件结合在一起。基因型、表型、神经元、镜像神经元、典型神经元、突触……您将在这一类别下听到所有这些提及,人工神经网络(ANNs)!生物人工智能主要应用于医疗领域。目前,我们不需要担心这一点,但只需知道这个术语存在,并且生物学是其结合的基础。
深度学习
多年来,人们认为神经网络(使用一个称为隐藏层概念)只需要一个隐藏层就能解决任何问题。随着计算能力的提高、计算硬件成本的降低和神经网络算法的进步,网络中拥有数百甚至数千个隐藏层是很常见的。隐藏层数量的增加,以及其他因素,正是深度学习在非常简短的概念中的核心!这里有一个视觉比较,可能有助于使事情更清晰:

没有隐藏层
如您在下面的表示性图像中可以看到,网络中有几个隐藏层。

许多隐藏层(白色圆圈)
概率与统计学
信不信由你,这就是你正在做的事情;只是它从你的视角来看非常抽象。但让我给你一个极其、过于简化的、快速入门指南……以防你生疏了!
你看到一只北极熊在雪地里行走。你好奇它会留下什么样的脚印。这就是概率。接下来,你看到雪地里的脚印,想知道那是不是北极熊的。这就是统计学。砰!现在你准备好了!你可能也在想这位作者有什么问题,所以也许再举一个例子以防万一!
-
概率论涉及预测未来事件的可能性。
-
统计学涉及分析过去事件的发生频率。
接近你的机器学习项目
接下来,让我们谈谈我们将如何接近我们的机器学习项目,并在做这件事的同时,继续定义/细化我们的机器学习思维模式。让我们从定义每次我们接近这些项目时需要使用的基本步骤开始。基本上,我们可以将它们分解为以下几类。
数据收集
你有无数种类型的数据可供使用,从 SQL 和 NoSQL 数据库、Excel 文件、Access 数据库、文本文件等等。你需要决定你的数据在哪里,它的格式是什么,你将如何导入和精炼它。你需要始终记住,大量测试和训练数据以及其质量是没有替代品的。垃圾输入,垃圾输出在机器学习中可能会变得非常混乱!
数据准备
如我们之前所说,数据质量是没有替代品的。有没有缺失、格式不正确或错误的数据?而且别忘了你将熟悉的一个术语,数据离群值。那些是那些与你的其他数据不太匹配的讨厌的小数据片段!你有这些吗?如果有,它们应该在那里吗?如果是,它们将如何被处理?如果你不确定,这里是你绘制数据时可能看到的数据离群值的例子:

在统计学中,离群值是指与其他观察值距离较远的观察点,有时非常远,有时则不然。离群值本身可能是由于测量中的变化引起的,表明实验缺陷,或者实际上可能是有效的。如果你在你的数据中看到离群值,你需要了解原因。它们可能表明某种形式的测量错误,而你使用的算法可能不足以处理这些离群值。
模型选择和训练
在创建和训练模型时,这里有几点你需要考虑。
-
你需要为手头的任务选择合适的机器学习算法,这将代表你正在处理的数据。然后你将这个数据分成 2-3 个子集:训练、验证和测试。正确的比例规则取决于你处理的数据量。例如,如果你有 10,000 行数据,那么 20%用于训练和 80%用于测试可能很好。但如果你有 10⁸行数据,可能 5%用于训练和 95%用于测试会更好。
-
有一个规则你必须始终严格遵守。无论你决定为你的测试、训练和验证集使用什么比例,所有数据必须来自同一个数据集。这一点非常重要。你永远不希望从某个数据集取一些数据来训练,然后从完全不同的数据集取数据来测试。那样只会导致挫败感。始终积累大量数据集来训练、测试和验证!
-
验证数据可以在使用测试数据集之前用来验证你的测试数据。有些人使用它,有些人不使用。无论你如何划分你的数据,你总会有一个数据集来训练,一个数据集来测试。你的算法的目标必须是足够灵活,能够处理它之前未见过的数据,而你如果用你开发的数据集进行测试,就无法做到这一点。以下有两种数据划分的方法。这两种方法展示了你可以如何分离测试和训练集(一个包含交叉验证集,另一个不包含):

模型评估
一旦你使用了你的训练数据,你将进入使用你之前准备的测试数据集来测试/评估你的模型。这就是我们了解我们的模型在之前未见过的数据上的表现如何。如果我们的模型在这里失败,我们就返回去,不要收集$200,并改进我们的流程!
模型调优
当你评估你的模型时,你可能会在某些时候确定你需要选择一个不同的模型或引入更多的特征/变量/超参数来提高你模型的效率和性能。一个减少你暴露的好方法是在数据收集部分和数据准备部分花更多的时间。正如我们之前所说的,大量的正确数据是没有任何替代品的。
如果你必须调整你的模型,你将会这样做,有许多方法可以做到这一点。这里只列举几个:
-
网格搜索
-
随机搜索
-
贝叶斯优化
-
基于梯度的优化
-
进化优化
让我们看看一个示例数据集——臭名昭著且总是被使用的 Iris 数据集。
Iris 数据集
爱丽丝数据集是由生物学家罗纳德·费希尔先生在 1936 年引入的花卉数据集。这个数据集包含来自三种鸢尾花(鸢尾花、鸢尾花、鸢尾花)的 50 个样本。每个样本由四个特征组成(萼片长度、花瓣长度、萼片宽度、花瓣宽度)。结合这些数据,可以产生一个线性判别模型,区分不同的物种。
那么,我们如何从花朵转换到数据:

现在,我们需要将我们对我们正在处理的可视表示(花朵)的知识转化为计算机可以理解的东西。我们通过将我们对花朵的所有知识分解为列(特征)和行(数据项)来实现这一点,如下所示:

现在所有的测量值都已经转换成计算机可以理解的形式,我们的第一步应该是确保我们没有缺失或格式不正确的数据,因为这会带来麻烦。如果你查看之前的截图中的黄色高亮部分,你可以看到我们缺少数据。我们需要确保在将其提供给应用程序之前,这些数据得到填充。一旦数据得到适当的准备和验证,我们就可以开始了。如果我们从Encog3[4]运行爱丽丝验证器,我们的输出应该反映出我们有150个数据集,它确实如此:

机器学习的类型
现在,让我们简要地熟悉一下本书中将要讨论的不同类型的机器学习,从下一章开始。重要的是,你至少要熟悉这些术语,因为它们肯定会在某一天出现,而且你知道和理解得越多,你就能更好地处理你的问题并向他人解释。
这里有一个简单的图表,展示了机器学习的三个主要类别:

监督学习
这些类型的机器学习模型用于根据呈现给它的数据预测结果。提供的指令是明确和详细的,或者至少应该是,这就是它获得了监督学习这一标签的原因。我们基本上是在学习一个函数,该函数根据输入和输出对将输入映射到输出。这个函数是从称为标记的训练数据中推断出来的,因为它具体告诉函数它期望什么。在监督学习中,始终有一个输入和相应的输出(或者更准确地说,是一个期望的输出值)。更正式地说,这类算法使用称为归纳偏差的技术来实现这一点,这基本上意味着有一组算法将用于预测给定输入的输出的假设。
在监督学习中,我们通常可以访问一组 X 特征(X[1],X[2],X[3],... X[x]),这些特征是在观察中测量的,以及一个响应 Y,也是在相同的 n 次观察中测量的。然后我们尝试使用 X[1],X[2],X[3],... X[n] 来预测 Y。
支持向量机(SVM)、线性回归、朴素贝叶斯和基于树的算法只是监督学习的一些例子。
接下来,让我们简要讨论一下在监督学习中我们需要关注的一些事情。它们没有特定的顺序:
-
偏差-方差权衡
-
训练数据量
-
输入空间维度
-
不正确的输出值
-
数据异质性
偏差-方差权衡
在我们讨论偏差-方差权衡之前,确保你对这些个别术语本身熟悉是很有意义的。
当我们谈论偏差-方差权衡时,偏差指的是学习算法中由于不正确的假设而产生的错误。高偏差会导致所谓的欠拟合,这种现象会导致算法在数据中错过相关的特征-输出层关系。
另一方面,方差是对训练集中微小波动的敏感错误。高方差可能导致你的算法模型随机噪声而不是实际预期的输出,这种现象称为过拟合。
偏差和方差之间存在权衡,每个机器学习开发者都需要理解。它与数据的欠拟合和过拟合有直接关系。我们说,如果一个学习算法对不同的训练集预测不同的输出结果,那么它就有高方差,这当然是不好的。
一个具有低偏差的机器学习算法必须足够灵活,以便它能很好地拟合数据。如果算法设计得太灵活,每个训练和测试数据集都会以不同的方式拟合,从而导致高方差。
你的算法必须足够灵活,可以通过固有的算法知识或用户可调整的参数来调整这种权衡。
下图展示了一个具有高偏差(左侧)的简单模型和一个具有高方差(右侧)的更复杂模型。

训练数据量
正如我们反复所说的,拥有足够的数据来完成工作,这是没有替代品的。这直接关联到你的学习算法的复杂性。一个复杂度较低、偏差高、方差低的算法可以从较少的数据中学习得更好。然而,如果你的学习算法复杂(许多输入特征、参数等),那么你需要一个更大的训练集,从中学习以获得低偏差和高方差。
输入空间维度
对于每一个学习问题,我们的输入都将以向量的形式存在。特征向量,即数据本身的特征,可以极大地影响学习算法。如果输入的特征向量非常大,这被称为高维性,那么即使你只需要其中的一小部分特征,学习也可能变得更加困难。有时,额外的维度会混淆你的学习算法,从而导致高方差。反过来,这意味着你将不得不调整你的算法以降低方差并提高偏差。如果适用,有时从你的数据中移除额外的特征会更容易,从而提高你的学习函数准确性。
话虽如此,一种称为降维的流行技术被几个机器学习算法所使用。这些算法将识别并移除无关的特征。
不正确的输出值
我们在这里问自己的问题是,我们的机器学习算法期望的输出中存在多少错误。如果我们遇到这种情况,学习算法可能正在尝试将数据拟合得太好,从而导致我们之前提到的问题,过拟合。过拟合可能源于错误的数据,或者对于当前任务来说过于复杂的学习算法。如果发生这种情况,我们需要调整我们的算法或者寻找一个能够提供更高偏差和更低方差的算法。
数据异质性
根据韦伯斯特词典,异质性意味着由不同或多样化的元素组成的质量或状态:异质性的质量或状态。对我们来说,这意味着特征向量包括许多不同种类的特征。如果这适用于我们的应用,那么我们可能需要为该任务应用不同的学习算法。一些学习算法还要求我们的数据被缩放到适合某些范围,例如 [0 - 1],[-1 - 1] 等。当我们深入研究利用距离函数作为其基础的学习算法时,例如最近邻和支持向量方法,你会看到它们对此非常敏感。另一方面,像基于树的算法(决策树等)处理这种现象相当好。
我们将以这句话结束这次讨论:我们应该始终从最简单、最合适的算法开始,并确保我们的数据被正确收集和准备。从那里,我们总是可以尝试不同的学习算法,并调整它们以查看哪个最适合我们的情况。不要误解,调整算法可能不是一项简单的任务,最终,它消耗的时间可能比我们可用的还要多。始终确保首先有适当数量的数据!
无监督学习
与监督学习相反,无监督学习在确定结果方面通常有更多的灵活性。数据被处理成,对于算法来说,数据集中没有哪个特征比其他特征更重要。这些算法从没有预期输出数据的标签的输入数据集中学习。k-means 聚类(聚类分析)是一个无监督模型的例子。它非常擅长在数据中找到有意义的模式,这些模式与输入数据相关。我们在这里学到的与监督部分学到的最大区别是,我们现在有x个特征X[1],X[2],X[3],... X[x]在n个观察上进行了测量。但我们不再对Y的预测感兴趣,因为我们不再有Y。我们唯一的兴趣是发现我们拥有的特征上的数据模式:

在之前的图中,你可以看到像这样的数据更适合采用非线性方法,其中数据似乎相对于重要性呈现出聚类状态。它是非线性的,因为我们无法得到一条直线来准确地区分和分类数据。无监督学习允许我们用一个几乎没有任何关于结果会是什么样或应该是什么样的想法来处理问题。结构是从数据本身中得出的,而不是应用在输出标签上的监督规则。这种结构通常是通过聚类数据之间的关系来得出的。
例如,假设我们从我们的基因组数据科学实验中有 10⁸个基因。我们希望将这些数据分组到相似的片段中,比如发色、寿命、体重等等。
第二个例子是广为人知的鸡尾酒会效应[3],它基本上指的是大脑的听觉能力能够集中注意力在一件事上,并过滤掉它周围的噪音。
这两个例子都可以使用聚类来实现它们的目标。
强化学习
强化学习是一个案例,其中机器被训练以实现特定的结果,唯一目的是最大化效率和/或性能。算法因做出正确的决策而奖励,因做出错误的决策而惩罚。持续训练用于不断改进性能。持续学习过程意味着更少的人为干预。马尔可夫模型是强化学习的一个例子,自动驾驶的自主汽车是这样一个应用的绝佳例子。它不断地与它的环境互动,观察障碍物、速度限制、距离、行人等等,以(希望)做出正确的决策。
我们与强化学习最大的不同之处在于我们不处理正确的输入和输出数据。这里的重点是性能,意味着以某种方式在未见数据与算法已经学习到的内容之间找到平衡。
该算法对其环境执行操作,根据其行为获得奖励或惩罚,然后重复,如下面的图像所示。你可以想象一下,在那个可爱的小自动驾驶出租车把你从酒店接走的时候,每秒钟会发生多少次这样的操作。

构建、购买或开源
接下来,让我们自问一个始终重要的问题?购买、构建还是开源?
这将是我的一些建议,当然也是我写这本书的原因之一,那就是让你接触开源世界。我意识到许多开发者都患有“这不是在这里建造的”综合症,但在我们走这条路之前,我们应该真的对自己诚实。我们真的认为我们有专业知识做得更好、更快,并且在我们的时间限制内进行测试,与已经存在的相比吗?我们应该首先尝试看看我们能否使用已经存在的。有如此多的开源工具包供我们使用,那些工具包的开发者已经投入了大量的时间和精力来开发和测试它们。显然,开源并不是每次都对每个人都是解决方案,但即使你无法在你的应用程序中使用它,通过使用和实验它们,你肯定可以从中获得大量的知识。
购买通常不是一种选择。如果你足够幸运能找到可以购买的东西,你可能不会得到批准,因为它会花费一大笔钱!如果你需要修改产品以完成你需要的事情,会发生什么?好运,得到访问源代码或让支持团队为了你改变他们的优先级。这不可能发生,至少不会像我们可能需要的那么快!
至于自己构建,嘿,我们是开发者,这是我们所有人都想做的事情,对吧?但在你启动 Visual Studio 并起飞之前,仔细思考一下你将要进入的是什么。
因此,开源应该始终是首选。你可以将其引入内部(假设许可允许你这样做),如果需要的话,适应你的标准(代码联系、更多单元测试、更好的文档等)。
额外阅读
尽管代码是用 Python 和 R 编写的,但我鼓励那些对在本章中讨论的内容进行扩展感兴趣的人访问 Jason Brownlee 的网站,machinelearningmastery.com/。他对机器学习的解释和热情无与伦比,你可以从他的网站上获得难以置信的大量信息。解释清晰、充满激情,涵盖了难以置信的深度。我强烈推荐浏览他的博客和网站,尽可能多地学习。
摘要
在本章中,我们讨论了使用 C#进行机器学习的许多方面,不同的代码实现策略——如构建、购买或开源——以及简要地触及一些重要定义。我希望这让你为即将到来的章节做好了准备。
在我们深入到源代码和应用之前,我想花些时间与你们讨论一下对我来说非常亲近和重要的事情:日志记录。这是我们每个人(或者应该做)的事情,而且如果你还不知道的话,这里有一个非常出色的工具你需要了解。在这本书中,我们将大量使用它,所以花些时间在前面了解它肯定是有帮助的,从下一章开始。
参考文献
-
By Nicoguaro - Own work, CC BY 4.0,
commons.wikimedia.org/w/index.php?curid=46257808 -
创用 CC 署名-相同方式共享 3.0 未本地化版本
-
Encog 框架版权属于 Jeff Heaton/Heaton 研究
第二章:ReflectInsight – 实时监控
每个开发者都需要一个好的日志工具。不幸的是,我看到的情况往往是开发者们紧跟最新的所有事物,但却没有进行日志记录。从某种意义上说,这是好事,最好的日志框架是你甚至不知道它在工作的框架。然而,如果你之前没有使用过 ReflectSoftware 的 ReflectInsight,你将喜欢这一章。拥有正确的日志工具非常重要,尤其是在机器学习中,而这个工具提供的丰富、健壮的日志功能无与伦比!
你绝对需要了解你算法内部发生的事情,ReflectSoftware 提供了最丰富的日志功能。特别是当涉及到机器学习算法时,没有任何东西能与之相提并论。当你进入深度学习领域时,你会非常高兴能够看到底层发生的事情。
在算法可能运行数天后,从正确的日志记录中获得的洞察力是无价的:

ReflectInsight 由一个 软件开发工具包 (SDK)、一个路由器、一个日志查看器和实时查看器组成。我们将分别介绍每一个,并详细讨论它们。
在本章中,我们将涵盖以下主题:
-
路由器
-
日志查看器
-
实时查看器
-
消息导航
-
在你的消息中搜索
-
时区格式化
-
自动保存/清除
-
SDK
-
配置编辑器
-
路由器
路由器是日志系统的核心部分。所有日志消息都发送到路由器,它可以从那里将消息分发到监听器,如查看器、文本文件、二进制文件、事件日志、数据库等。你通常会将路由器安装在除日志系统之外的其他机器上,但并非必须如此。一旦安装和配置(出厂配置通常适用于大多数情况),路由器作为 Windows 服务运行,没有用户界面,如下面的截图所示:

接下来,我们将讨论日志和实时查看器。
日志查看器
日志查看器旨在查看已手动保存或从路由器/查看器配置中保存的历史日志文件。如果你通过系统流式传输大量消息,你无疑会收集到大量的日志文件;它们可能需要被查看。我为一位客户编写了一个企业级微服务系统,该系统以 ReflectInsight 作为其系统的核心,并将消息流式传输到 RabbitMQ 系统中。
平均每天我们大约流式传输了大约一百万条消息(它仍在生产中使用);当出现问题时,日志查看器的历史日志功能非常有价值。
实时查看器
Live Viewer 是您将最常用来查看实时日志的工具。Live Viewer 的功能非常广泛。简而言之,高性能日志记录允许我们通过在 Live Viewer 中显示日志消息来实时监控已配置的应用程序。我们可以记录非常丰富的细节,例如异常、对象、数据集、图像、进程和线程信息,以及格式良好的 XML。我们还可以快速轻松地导航和跟踪我们的应用程序以找到所需的信息。消息详细信息面板显示选定消息的扩展详细信息。这些详细信息可能只是消息本身,也可能是复杂的数据,如对象、数据集、二进制 blob、图像、进程和线程信息,以及集合的内容。对于 SQL、XML 和与 HTML 相关的消息等选定的消息类型,提供语法高亮显示,以及完整的 Unicode 支持,这有助于查看这些类型的消息。
消息导航
ReflectInsight 支持多种方式在您的已记录消息中进行导航。
您可以使用以下方法之一进行导航:
-
查找匹配的进入/退出方法块
-
跳转到父级进入/退出方法块
-
从用户定义视图中跳转到所有消息视图
-
通过行号跳转到消息
-
高级搜索
-
快速搜索(仅限活动视图)
-
消息类型浏览导航器
-
书签
消息属性
此面板允许我们进一步检查选定的消息。我们可以查看各种日期时间值、时区、进程 ID、线程 ID、请求 ID、类别、机器名称等。我们还可以在日志过程中将用户定义的属性附加到单个或多个消息上,以扩展消息属性面板:

监视器
仅在实时查看器中可用,监视器面板允许我们显示非持久信息,以便快速进行数据更改。我们可以直接写入监视器,或者如果使用 ReflectInsight PostSharp AOP扩展,我们可以轻松地使用预定义的自定义属性装饰对象属性。
此属性强制 ReflectInsight 在属性值更改时显示其值:

书签
书签面板允许我们查看当前日志会话的书签:

它可以与日志文件一起持久化,以便稍后检索。我们可以过滤活动视图或给定视图的书签,或查看所有视图中的所有书签。我们还可以导航到任何书签,并立即激活视图以选择书签消息的位置:
调用栈
调用栈面板显示当前选定消息的调用栈级别。调用栈条目是通过进入/退出方法生成的,或者如果消息包含在TraceMethod使用块中。
我们可以通过双击调用堆栈条目轻松导航调用堆栈,带我们到活动消息日志面板中 Enter/Exit 消息块的顶部:

在您的消息中进行搜索
Live Viewer 提供两种通过标准搜索消息的方式,如下截图所示。它们是快速搜索和高级搜索:
快速搜索主要用于简单、快速、基于文本的搜索。
高级搜索
这主要用于搜索需要更复杂搜索标准的消息。搜索标准可以包括以下组合:
-
消息内容
-
消息类型
-
内容 AND 消息类型
-
内容 OR 消息类型
-
除了正则表达式
高级搜索视图提供了导航到搜索结果或将其书签化的能力:

时区格式化
我们可以在标准时间格式或军事时间格式中显示我们的时间细节。选择最适合您位置的时区类型,例如源、本地、UTC 或自定义(从可用的系统时区中选择)。

自动保存/清除
除了库的自动保存滚动日志文件的能力外,Live Viewer 还具有类似的功能,除了自动清除滚动日志文件的顶部部分。您可以通过应用以下方法之一来配置 Live Viewer 以自动保存或自动清除:

-
自动保存 - 此方法强制 Live Viewer 在满足特定标准(即在新的一天和/或消息限制)时保存文件。
-
自动清除 - 此方法强制 Live Viewer 根据当前日志文件预定义的大小百分比清除日志记录的顶部部分。
仅仅说如果我们查看以下截图,我们可以看到从我们的算法和应用中可能收集到的信息量;它是巨大的:

示例
我们已经提到,像这样的工具在机器学习方面是多么有价值,因此我们向您展示我们确切的意思是公平的。接下来是一个实际机器学习算法将数据输出到 Live Viewer 的截图。如果没有这些实时信息,我们将无法了解我们应用程序的有效性和性能!

ReflectInsight 工具:
消息统计可以通过这个极其有价值的工具查看和调整,以满足您的标准。您可以通过名称、类别、用户等多种方式搜索各种类型的消息。从那里您可以看到这些消息的组成。

监视器
监视器实时观察以下参数和变量。你可以通过编程定义自己的监视器,并定期更新它们的值和/或参数。

软件开发套件
SDK 允许我们将 ReflectInsight 连接到我们的应用程序。随着我们进一步了解,我们将看到这有多么容易。与其它 SDK 相比,这个 SDK 的美丽之处在于为每个消息分配了丰富的图像集。当你每秒有数千条消息流过时,颜色和图像可以帮助你集中注意力,只关注你需要看到的内容。
以下是一个截图,展示了我的确切意思。例如,如果我们使用 SendException 消息,那么红色的X将在 Live Viewer/Log Viewer 左侧远端的面板中显示。对于以下截图中的所有其他消息,情况相同:

配置编辑器
配置编辑器是进入对应用程序配置参数进行更改的能力的可视界面。这使得它比更改文本文件参数更简单、更直观。
概览
我们可以使用基于 XML 的配置文件与我们的应用程序配合,使 ReflectInsight 查看器按照我们的意愿运行。可用的配置类别有很多,从自动保存和过滤,到消息着色等等。
XML 配置
ReflectInsight 使用 XML 配置文件进行配置。配置信息可以嵌入到其他 XML 配置文件中,例如应用程序或web.config文件,或者单独的文件。配置易于阅读和更新,同时保持表达所有配置的灵活性。
或者,ReflectInsight 也可以通过编程进行配置。在这本书中,我们将结合使用这两种方法,主要配置通常通过app.config文件完成。
动态配置
ReflectInsight 会自动监控其配置文件的变化,并在更改时动态应用这些更改。在许多情况下,可以在不终止相关进程的情况下诊断应用程序问题。这可以是我们部署的应用程序中调查问题的非常有价值的工具。
主屏幕
ReflectInsight 洞察配置编辑器通过可视化界面帮助用户轻松创建配置文件,但高级用户可以使用 XML 进行操作。
该工具在编辑设置、定义消息模式/格式、定义扩展、定义监听器、将颜色与消息类型关联等方面非常有用:
-
易于理解的布局
-
它会记住最近文件列表
-
预定义选择和动态部分查找
-
关键值弹出编辑器
-
消息模式弹出编辑器
-
方法类型弹出编辑器
-
颜色定义和消息颜色弹出编辑器
以下是对配置编辑器及其参数变化的截图:

您可以从 www.reflectsoftware.com 下载 ReflectInsight 的试用版。在购买时提及此书,可享受零售价的大幅折扣!
摘要
在本章中,我们了解了 ReflectInsight 以及它能为您带来的惊人好处。我们特别看到了它如何帮助机器学习开发者清楚地看到他们算法内部正在发生什么。我鼓励您下载您的副本并尝试一下。您将永远不会以同样的方式看待日志记录。我们下一章将介绍著名的旅行商问题,我们将首次接触到我的第二生命之爱——神经元,以及与著名的贝叶斯定理一起工作!
ReflectInsight 是 ReflectSoftware 的版权所有。所有图像、文本和标志均经许可使用。
第三章:贝叶斯直觉 – 解决肇事逃逸之谜和进行数据分析

注意到本章的开头是一个直接面对算法吗?我想确保你看到的第一件事就是这个公式。这强调了它在你机器学习生涯中的重要性。把它写下来,把它贴在你的显示器上的便利贴上,或者把它记在心里!
在本章中,我们将:
-
将著名的贝叶斯定理应用于解决计算机科学中一个非常著名的问题
-
展示你如何使用贝叶斯定理和朴素贝叶斯来绘制数据,从真值表中发现异常,等等
概述贝叶斯定理
说实话,贝叶斯定理的解释和关于它的书籍一样多。前面展示的是我们将要讨论的主要解释。我也鼓励你参考brilliant.org/wiki/bayes-theorem/进行进一步阅读。
为了使这个内容更具体和正式,让我们从一点直觉和正式性开始;这将帮助我们为即将到来的内容做好准备。
当我们使用贝叶斯定理时,我们是在衡量某件事的信念程度,即事件发生的可能性。现在就让我们保持这个简单的理解:

前面的公式表示在 B 的条件下 A 的概率。
概率通常被量化为一个介于 0 和 1 之间的数,包括 0 和 1;0 表示不可能性,1 表示绝对确定性。概率越高,确定性就越大。掷骰子得到 6 和抛硬币得到正面的概率是你无疑非常熟悉的概率例子。还有一个你熟悉且每天都会遇到的例子:垃圾邮件。
我们中的大多数人通常都会全天(有些人甚至整夜)打开电子邮件,就放在我们身边!随着我们期待收到的邮件,也伴随着那些我们不期待且不希望收到的邮件。我们都讨厌处理垃圾邮件,那种与任何事物都无关,只与伟哥有关的讨厌的电子邮件;然而,我们似乎总是能收到它。我每天收到的那些邮件中,任何一封是垃圾邮件的概率是多少?我关心它的内容的概率是多少?我们如何才能知道呢?
所以,让我们简单谈谈垃圾邮件过滤器是如何工作的,因为,你看,这可能是我们能用到的最好的概率例子!为了更精确和更正式,我们正在处理条件概率,即在事件 B 发生的情况下事件 A 的概率。
大多数垃圾邮件过滤器的工作方式,至少在非常基本层面上,是通过定义一个单词列表,这些单词用来指示我们不希望或未请求收到的电子邮件。如果电子邮件包含这些单词,它就被认为是垃圾邮件,我们相应地处理它。因此,使用贝叶斯定理,我们寻找给定一组单词的电子邮件是垃圾邮件的概率,这在公式化视角下看起来是这样的:

给定一组单词的电子邮件是垃圾邮件的概率:维基百科的用户 Qniemiec 有一个令人难以置信的视觉图解,它全面解释了概率视角的每一种组合,这由两个事件树的叠加表示。如果你像我一样是一个视觉型的人,这里是对贝叶斯定理的完整可视化,由两个事件树图叠加表示:

现在,让我们转向一个非常著名的问题。它被许多人称为不同的名字,但基本问题就是众所周知的出租车问题。这是我们的场景,我们将尝试使用概率和贝叶斯定理来解决这个问题。
一名优步司机卷入了一起肇事逃逸事故。著名的黄色出租车和优步司机是这座城市中运营的两家公司,随处可见。我们得到了以下数据:
-
这座城市中 85%的出租车是黄色的,15%是优步。
-
一名目击者指认了肇事逃逸事故中涉及的车辆,并表示该车上有优步标志。话虽如此,我们知道目击者证词的可靠性,因此法院决定测试用户并确定其可靠性。使用事故当晚相同的情况,法院得出结论,目击者 80%的时间正确地识别了这两辆车中的每一辆,但 20%的时间失败了。这一点很重要,所以请跟我一起继续看下去!
我们的困境:事故中涉及的车辆是优步司机还是黄色出租车的概率是多少?
从数学上讲,我们是这样得到我们需要的答案的:
- 正确识别的优步司机总数为:
15 * 0.8 = 12
- 目击者 20%的时间是错误的,所以错误识别的车辆总数为:
85 * 0.2 = 17
- 因此,目击者总共识别的车辆数为 12 + 17 = 29。因此,他们正确识别优步司机的概率是:
12/29 = @41.3%
现在,让我们看看我们是否可以开发一个简单的程序来帮助我们得到这个数字,以证明我们的解决方案是可行的。为了完成这个任务,我们将深入我们的第一个开源工具包:Encog。Encog 被设计来处理正好像这样的问题。
Encog 框架是一个完整的机器学习框架,由杰夫·希顿先生开发。希顿先生还出版了关于 Encog 框架以及其他主题的几本书,如果您计划广泛使用此框架,我鼓励您去寻找它们。我个人拥有它们的所有,并将它们视为开创性的作品。
让我们看看解决我们问题所需的代码。正如您将注意到的,数学、统计学、概率……这些都从您那里抽象出来。Encog 可以使您专注于您试图解决的商业问题。
完整的执行块看起来像以下代码。我们将在稍后开始分析它。
public void Execute(IExampleInterface app)
{
// Create a Bayesian network
BayesianNetwork network = new BayesianNetwork();
// Create the Uber driver event
BayesianEvent UberDriver = network.CreateEvent("uber_driver");
// create the witness event
BayesianEvent WitnessSawUberDriver = network.CreateEvent("saw_uber_driver");
// Attach the two
network.CreateDependency(UberDriver, WitnessSawUberDriver);
network.FinalizeStructure();
// build the truth tables
UberDriver?.Table?.AddLine(0.85, true);
WitnessSawUberDriver?.Table?.AddLine(0.80, true, true);
WitnessSawUberDriver?.Table?.AddLine(0.20, true, false);
network.Validate();
Console.WriteLine(network.ToString());
Console.WriteLine($"Parameter count: {network.CalculateParameterCount()}");
EnumerationQuery query = new EnumerationQuery(network);
// The evidence is that someone saw the Uber driver hit the car
query.DefineEventType(WitnessSawUberDriver, EventType.Evidence);
// The result was the Uber driver did it
query.DefineEventType(UberDriver, EventType.Outcome);
query.SetEventValue(WitnessSawUberDriver, false);
query.SetEventValue(UberDriver, false);
query.Execute();
Console.WriteLine(query.ToString());
}
好的,让我们将其分解成更易于消化的部分。我们首先要做的是创建一个贝叶斯网络。这个对象将是解决我们谜题的中心。BayesianNetwork对象是一个概率和分类引擎的包装器。
贝叶斯网络由一个或多个BayesianEvents组成。一个事件将是三种不同类型之一——Evidence、Outcome或Hidden——并且通常对应于训练数据中的一个数字。Event总是离散的,但如果存在并且需要,连续值可以映射到一系列离散值。
在创建初始网络对象之后,我们为声称看到司机卷入追尾逃逸事件的 Uber 司机以及目击者创建一个事件。我们将创建 Uber 司机和目击者之间的依赖关系,然后最终确定我们网络的结构:
public void Execute(IExampleInterface app)
{
// Create a Bayesian network
BayesianNetwork network = new BayesianNetwork();
// Create the Uber driver event
BayesianEvent UberDriver = network.CreateEvent("uber_driver");
// create the witness event
BayesianEvent WitnessSawUberDriver = network.CreateEvent("saw_uber_driver");
// Attach the two
network.CreateDependency(UberDriver, WitnessSawUberDriver);
network.FinalizeStructure();
// build the truth tables
UberDriver?.Table?.AddLine(0.85, true);
WitnessSawUberDriver?.Table?.AddLine(0.80, true, true);
WitnessSawUberDriver?.Table?.AddLine(0.20, true, false);
network.Validate();
Console.WriteLine(network.ToString());
Console.WriteLine($"Parameter count: {network.CalculateParameterCount()}");
EnumerationQuery query = new EnumerationQuery(network);
// The evidence is that someone saw the Uber driver hit the car
query.DefineEventType(WitnessSawUberDriver, EventType.Evidence);
// The result was the Uber driver did it
query.DefineEventType(UberDriver, EventType.Outcome);
query.SetEventValue(WitnessSawUberDriver, false);
query.SetEventValue(UberDriver, false);
query.Execute();
Console.WriteLine(query.ToString());
}
接下来,我们需要构建实际的真值表。真值表是一个函数可以具有的所有可能值的列表。有一行或多行,其复杂性逐渐增加,最后一行是最终函数值。如果您记得逻辑理论,您基本上可以有三个操作:NOT、AND和OR。0 通常代表false,而 1 通常代表true。
如果我们再深入一点,我们会看到以下规则:
如果 A = 0,则-A = 1;如果 A = 1,则-A = 0;A+B = 1,除非 A 和 B = 0;如果 A 和 B = 0,则 A+B = 0;A*B = 0,除非 A 和 B = 1;如果 A 和 B = 1,则 A*B = 1
现在,回到我们的代码。
要构建真值表,我们需要知道概率和结果值。在我们的问题中,Uber 司机卷入事故的概率是 85%。至于目击者,有 80%的可能性他们在说真话,有 20%的可能性他们犯了错误。我们将使用真值表的AddLine函数来添加这些信息:
// build the truth tables
UberDriver?.Table?.AddLine(0.85, true);
WitnessSawUberDriver?.Table?.AddLine(0.80, true, true);
WitnessSawUberDriver?.Table?.AddLine(0.20, true, false);
network.Validate();
让我们再谈谈真值表。这里是一个扩展的真值表,显示了两个变量P和Q的所有可能的真值函数。

如果我们要更广泛地编程我们的真值表,以下是一个例子:
a?.Table?.AddLine(0.5, true); // P(A) = 0.5
x1?.Table?.AddLine(0.2, true, true); // p(x1|a) = 0.2
x1?.Table?.AddLine(0.6, true, false);// p(x1|~a) = 0.6
x2?.Table?.AddLine(0.2, true, true); // p(x2|a) = 0.2
x2?.Table?.AddLine(0.6, true, false);// p(x2|~a) = 0.6
x3?.Table?.AddLine(0.2, true, true); // p(x3|a) = 0.2
x3?.Table?.AddLine(0.6, true, false);// p(x3|~a) = 0.6
现在我们已经构建了网络和真值表,是时候定义一些事件了。正如我们之前提到的,事件可以是证据、隐藏或结果中的任何一个。隐藏事件,既不是证据也不是结果,但它仍然涉及到贝叶斯图本身。我们不会使用隐藏,但我希望你知道它确实存在。
为了解决我们的谜团,我们必须积累证据。在我们的例子中,我们有的证据是目击者报告说看到一名优步司机参与了追尾逃逸。我们将定义一个证据类型的事件并将其分配给目击者报告的内容。结果是,它是一名优步司机,因此我们将分配一个结果类型的事件。
最后,我们必须考虑到,至少在某些时候,目击者报告看到优步司机参与追尾逃逸是不正确的。因此,我们必须为两种概率创建事件值——目击者没有看到优步司机,以及优步司机没有参与:
EnumerationQuery query = new EnumerationQuery(network);
// The evidence is that someone saw the Uber driver hit the car
query.DefineEventType(WitnessSawUberDriver, EventType.Evidence);
// The result was the Uber driver did it
query.DefineEventType(UberDriver, EventType.Outcome);
query.SetEventValue(WitnessSawUberDriver, false);
query.SetEventValue(UberDriver, false);
query.Execute();
注意,我们将要执行的是一个枚举查询。这个对象允许对贝叶斯网络进行概率查询。这是通过计算隐藏节点的所有组合并使用总概率来找到结果来实现的。如果我们的贝叶斯网络很大,性能可能会较弱,但幸运的是,对我们来说,它并不大。
最后,我们对贝叶斯网络定义执行查询并打印结果:

结果,正如我们所希望的,是 41%。
作为一项练习,看看你现在是否可以使用 Encog 解决另一个非常著名的例子。在这个例子中,我们早上醒来发现草地是湿的。是下雨了,还是洒水器打开了,或者两者都有?这是我们用笔和纸上的真值表:

下雨的概率:

完整的真值表:

概述朴素贝叶斯和绘制数据
尽管我们讨论了贝叶斯定理,如果我们不讨论朴素贝叶斯,那将是对你极大的不公。朴素贝叶斯无处不在,并且有很好的理由。它几乎总是工作得很好(因此得名,朴素),你肯定会在你的机器学习生涯中接触到它。它是一种基于这样一个前提的简单技术:任何单个特征的价值完全独立于任何其他特征的价值。例如,一个橙子是圆的,颜色是橙色的,皮不是光滑的,直径在 10-20 厘米之间。朴素贝叶斯分类器将考虑之前描述的每个特征独立地贡献,认为这是一个橙子而不是苹果、柠檬等等,即使其特征之间有一些数据关系。
如前所述,朴素贝叶斯在解决复杂情况时出奇地高效。尽管在某些情况下它可能无法超越其他算法,但它可以是一个很好的初次尝试算法,适用于你的问题。与许多其他模型相比,我们只需要非常少量的训练数据。
数据绘图
在我们的下一个应用中,我们将使用出色的 Accord.NET 机器学习框架为你提供一个工具,你可以用它输入数据,观察数据被绘制,并了解假阳性和假阴性。我们将能够为存在于我们的数据空间中的对象输入数据,并将它们分类为绿色或蓝色。我们将能够更改这些数据,并看到它们是如何被分类的,更重要的是,如何被直观地表示。我们的目标是学习新案例属于哪个集合;它们要么是绿色,要么是蓝色。此外,我们还想跟踪假阳性和假阴性。朴素贝叶斯将根据我们数据空间中的数据为我们完成这项工作。记住,在我们训练朴素贝叶斯分类器之后,最终目标是它能从它以前从未见过的数据中识别出新对象。如果它不能,那么我们需要回到训练阶段。
我们简要地讨论了真值表,现在是我们回到并在这个定义上增加一些正式性的时候了。更具体地说,让我们用混淆矩阵来讨论。在机器学习中,混淆矩阵(错误矩阵或匹配矩阵)是一个表格布局,它让你能够可视化算法的性能。每一行代表预测的类实例,每一列代表实际的类实例。它被称为混淆矩阵,因为这种可视化使得很容易看出你是否混淆了这两个。
真值表的抽象视图可能看起来像这样:
| X 存在 | X 不存在 | ||
|---|---|---|---|
| 测试结果为正 | 真阳性 | 假阳性 | 总正数 |
| 测试结果为负 | 假阴性 | 真阴性 | 总负数 |
| 带有 X 的总数 | 没有 X 的总数 | 总计 |
对于相同的真值表,一个更直观的视图可能看起来像这样:

真值表的直观视图
最后,一个更正式的真混淆矩阵的视图:

在机器学习的领域,真值表/混淆矩阵允许你直观地评估算法的性能。正如你将在我们接下来的应用中看到的那样,每次你添加或更改数据时,你都将能够看到是否发生了任何这些错误或负面的条件。
目前,我们将开始使用的测试数据在绿色和蓝色物体之间均匀分配,因此没有合理的概率表明任何新案例更有可能是其中之一而不是另一个。这种合理的概率,有时被称为 信念,更正式地称为 先验概率(这个词又出现了!)。先验概率是基于我们对数据的先前经验,在许多情况下,这些信息被用来预测事件发生之前的结果。给定先验概率或信念,我们将制定一个结论,然后这成为我们的 后验信念。
在我们的情况下,我们正在查看:
-
绿色物体的先验概率是绿色物体的总数/我们数据空间中物体的总数
-
蓝色物体的先验概率是蓝色物体的总数/我们数据空间中物体的总数
让我们进一步了解一下发生了什么。
你可以在下面的屏幕截图中看到我们的数据看起来是什么样子。X 和 Y 列表示数据空间中沿 x 和 y 轴的坐标,而 G 列表示物体是否为绿色的标签。记住,监督学习应该给我们我们试图达到的客观结果,朴素贝叶斯应该使我们能够轻松地看到这是否为真。

如果我们取前面的数据并创建其散点图,它将看起来像下面的屏幕截图。正如你所看到的,我们数据空间中的所有点都被绘制出来,其中 G 列值为 0 的点被绘制为蓝色,而值为 1 的点被绘制为绿色。
每个数据点都绘制在我们数据空间中的 X/Y 位置上,由 x/y 轴表示:

但当我们向数据空间添加朴素贝叶斯分类器无法正确分类的新物体时会发生什么?我们最终得到的是所谓的假阴性和假阳性,如下所示:

由于我们只有两种数据类别(绿色和蓝色),我们需要确定这些新数据对象将如何被正确分类。正如你所看到的,我们有 14 个新的数据点,着色显示它们与 x 和 y 轴的对齐情况。
现在,让我们以完整的形式查看我们的应用程序。以下是我们主屏幕的屏幕截图。在屏幕左侧的“数据样本”选项卡下,我们可以看到我们已经加载了数据空间。在屏幕右侧,我们可以看到一个散点图视觉图,它帮助我们可视化数据空间。正如你所看到的,所有数据点都已正确绘制并着色:

如果我们查看概率的分类和绘制方式,你可以看到数据几乎呈现为两个封闭但重叠的簇:

当我们空间中的一个数据点与不同颜色的不同数据点重叠时,那就是我们需要朴素贝叶斯为我们做工作的地方。
如果我们切换到我们的模型测试标签页,我们可以看到我们添加的新数据点。

接下来,让我们修改一些我们添加的数据点,以展示任何一个数据点如何变成误报或漏报。请注意,我们从这个练习开始有七个误报和七个漏报。

我们之前所做的数据修改导致以下图表。如您所见,我们现在有额外的误报:

我将把实验数据并继续你的朴素贝叶斯学习留给你去尝试!
摘要
在本章中,我们学习了概率论、贝叶斯定理、朴素贝叶斯,以及如何将其应用于现实世界的问题。我们还学习了如何开发一个工具,帮助我们测试我们的分类器,看看我们的数据是否包含任何误报或漏报。
在下一章中,我们将更深入地探讨机器学习的世界,并讨论强化学习。
参考文献
-
Creative Commons Attribution—ShareAlike License
-
Heaton, J. (2015). Encog: Library of Interchangeable Machine Learning Models for Java and C#, Journal of Machine Learning Research, 16, 1243-1247
-
版权所有 2008-2014,Heaton Research,Inc
-
版权所有(c)2009-2017,Accord.NET 作者 authors@accord-framework.net
-
案例研究:重新审视基础率谬误(Koehler,1996)
第四章:风险与回报 – 强化学习
在本章中,我们将更深入地探讨机器学习中的一个热门话题:强化学习。我们将涵盖几个令人兴奋的示例,以展示你如何在应用程序中使用它。我们将介绍几个算法,然后在我们的第一个更正式的示例之后,我们将带你到一个最终令人兴奋的示例,你一定会喜欢的!
本章将涵盖以下主题:
-
强化学习的概述
-
学习类型
-
Q 学习
-
SARSA
-
运行我们的应用程序
-
汉诺塔
强化学习的概述
如第一章“机器学习基础”中提到的,强化学习是一个机器被训练以实现特定结果的情况,其唯一目的是最大化效率和/或性能。算法因做出正确决策而获得奖励,因做出错误决策而受到惩罚,如下面的图表所示:

持续训练用于不断改进性能。这里的重点是性能,意味着在未知数据和算法已学到的内容之间找到某种平衡。算法对其环境采取行动,根据其行为获得奖励或惩罚,然后重复此过程。
在本章中,我们将直接深入到应用程序的应用,并使用令人难以置信的 Accord.NET 开源机器学习框架来突出展示我们如何使用强化学习帮助一个自主物体从其起始位置(由黑色物体表示)到达一个期望的终点(由红色物体表示)。
这个概念与自主车辆从 A 点到 B 点的行为类似,尽管复杂度要低得多。我们的示例将允许你使用不同复杂度的地图,这意味着在自主物体和期望位置之间可能会有各种障碍。让我们看看我们的应用程序:

在这里,你可以看到我们加载了一个非常基础的地图,一个没有障碍物,只有外部限制墙的地图。黑色方块(起点)是我们的自主物体,红色方块(停止)是我们的目的地。在这个应用程序中,我们的目标是导航墙壁以到达我们期望的位置。如果我们下一步移动到白色方块上,我们的算法将获得奖励。如果我们下一步移动到墙壁上,它将受到惩罚。从这个角度来看,我们的自主物体应该能够到达其目的地。问题是:它能多快学会?在这个例子中,它的路径上绝对没有任何障碍,所以应该没有问题在尽可能少的移动次数内解决问题。
以下是我们环境中的另一个相对复杂的地图示例:

学习类型
在我们应用程序的右侧是我们的设置,如下面的截图所示。我们看到的第一件事是学习算法。在这个应用程序中,我们将处理两种不同的学习算法,Q-learning和状态-动作-奖励-状态-动作(SARSA)。让我们简要讨论这两种算法:

Q-learning
Q-learning 可以在给定状态下识别出最佳动作(在每个状态下具有最高价值的动作),而无需对环境有一个完全定义的模型。它也非常擅长处理具有随机转换和奖励的问题,而无需调整或适应。
这里是 Q-learning 的数学直觉:

如果我们提供一个非常高级的抽象示例,可能更容易理解。代理从状态 1 开始。然后执行动作 1 并获得奖励 1。接下来,它四处张望,看看在状态 2 中动作的最大可能奖励是多少;它使用这个来更新动作 1 的价值。以此类推!
SARSA
SARSA(根据名字,你可以猜到这一点)的工作方式如下:
-
代理从状态 1 开始
-
然后执行动作 1 并获得奖励 1
-
然后,它移动到状态 2,执行动作 2,并获得奖励 2
-
然后,代理返回并更新动作 1 的价值
如你所见,两种算法的区别在于寻找未来奖励的方式。Q-learning 使用从状态 2 可能采取的最高动作,而 SARSA 使用实际采取的动作的价值。
这里是 SARSA 的数学直觉:

运行我们的应用程序
现在,让我们使用默认参数开始使用我们的应用程序。只需点击“开始”按钮,学习过程就会开始。一旦完成,你将能够点击“显示答案”按钮,学习路径将从开始到结束进行动画展示。
点击“开始”将开始学习阶段,并持续到黑色物体达到目标:

在这里,你会看到随着学习的进行,我们将输出发送到ReflectInsight,以帮助我们了解和学习算法内部正在做什么。你会看到对于每一次迭代,都会评估不同的物体位置,以及它们的行为和奖励:

一旦学习完成,我们可以点击“显示答案”按钮来重新播放最终解决方案。当完成后,黑色物体将位于红色物体之上:

现在,让我们看看我们应用程序中的代码。我们之前强调了两种学习方式。以下是 Q-learning 的看起来:
int iteration = 0;
TabuSearchExploration tabuPolicy = (TabuSearchExploration)qLearning.ExplorationPolicy;
EpsilonGreedyExploration explorationPolicy = (EpsilonGreedyExploration)tabuPolicy.BasePolicy;
while ((!needToStop) && (iteration < learningIterations))
{
explorationPolicy.Epsilon = explorationRate - ((double)iteration / learningIterations) * explorationRate;
qLearning.LearningRate = learningRate - ((double)iteration / learningIterations) * learningRate;
tabuPolicy.ResetTabuList();
var agentCurrentX = agentStartX;
var agentCurrentY = agentStartY;
int steps = 0;
while ((!needToStop) && ((agentCurrentX != agentStopX) || (agentCurrentY != agentStopY)))
{
steps++;
int currentState = GetStateNumber(agentCurrentX, agentCurrentY);
int action = qLearning.GetAction(currentState);
double reward = UpdateAgentPosition(ref agentCurrentX, ref agentCurrentY, action);
int nextState = GetStateNumber(agentCurrentX, agentCurrentY);
// do learning of the agent - update his Q-function, set Tabu action
qLearning.UpdateState(currentState, action, reward, nextState);
tabuPolicy.SetTabuAction((action + 2) % 4, 1);
}
System.Diagnostics.Debug.WriteLine(steps);
iteration++;
SetText(iterationBox, iteration.ToString());
}
SARSA 学习有何不同?让我们看看 SARSA 学习的while循环,并理解:
int iteration = 0;
TabuSearchExploration tabuPolicy = (TabuSearchExploration)sarsa.ExplorationPolicy;
EpsilonGreedyExploration explorationPolicy = (EpsilonGreedyExploration)tabuPolicy.BasePolicy;
while ((!needToStop) && (iteration < learningIterations))
{
explorationPolicy.Epsilon = explorationRate - ((double)iteration / learningIterations) * explorationRate;
sarsa.LearningRate = learningRate - ((double)iteration / learningIterations) * learningRate;
tabuPolicy.ResetTabuList();
var agentCurrentX = agentStartX;
var agentCurrentY = agentStartY;
int steps = 1;
int previousState = GetStateNumber(agentCurrentX, agentCurrentY);
int previousAction = sarsa.GetAction(previousState);
double reward = UpdateAgentPosition(ref agentCurrentX, ref agentCurrentY, previousAction);
while ((!needToStop) && ((agentCurrentX != agentStopX) || (agentCurrentY != agentStopY)))
{
steps++;
tabuPolicy.SetTabuAction((previousAction + 2) % 4, 1);
int nextState = GetStateNumber(agentCurrentX, agentCurrentY);
int nextAction = sarsa.GetAction(nextState);
sarsa.UpdateState(previousState, previousAction, reward, nextState, nextAction);
reward = UpdateAgentPosition(ref agentCurrentX, ref agentCurrentY, nextAction);
previousState = nextState;
previousAction = nextAction;
}
if (!needToStop)
{
sarsa.UpdateState(previousState, previousAction, reward);
}
System.Diagnostics.Debug.WriteLine(steps);
iteration++;
SetText(iterationBox, iteration.ToString());
}
我们的最后一步是看看我们如何可以动画化解决方案。这将帮助我们确认我们的算法达到了目标。以下是代码:
TabuSearchExploration tabuPolicy;
if (qLearning != null)
tabuPolicy = (TabuSearchExploration)qLearning.ExplorationPolicy;
else if (sarsa != null)
tabuPolicy = (TabuSearchExploration)sarsa.ExplorationPolicy;
else
throw new Exception();
var explorationPolicy = (EpsilonGreedyExploration)tabuPolicy?.BasePolicy;
explorationPolicy.Epsilon = 0;
tabuPolicy?.ResetTabuList();
int agentCurrentX = agentStartX, agentCurrentY = agentStartY;
Array.Copy(map, mapToDisplay, mapWidth * mapHeight);
mapToDisplay[agentStartY, agentStartX] = 2;
mapToDisplay[agentStopY, agentStopX] = 3;
这里是我们的while循环,所有的魔法都在这里发生!
while (!needToStop)
{
cellWorld.Map = mapToDisplay;
Thread.Sleep(200);
if ((agentCurrentX == agentStopX) && (agentCurrentY == agentStopY))
{
mapToDisplay[agentStartY, agentStartX] = 2;
mapToDisplay[agentStopY, agentStopX] = 3;
agentCurrentX = agentStartX;
agentCurrentY = agentStartY;
cellWorld.Map = mapToDisplay;
Thread.Sleep(200);
}
mapToDisplay[agentCurrentY, agentCurrentX] = 0;
int currentState = GetStateNumber(agentCurrentX, agentCurrentY);
int action = qLearning?.GetAction(currentState) ?? sarsa.GetAction(currentState);
UpdateAgentPosition(ref agentCurrentX, ref agentCurrentY, action);
mapToDisplay[agentCurrentY, agentCurrentX] = 2;
}
让我们将这个问题分解成更易于消化的部分。我们首先建立 tabu 策略。如果你不熟悉 tabu 搜索,请注意,它旨在通过放宽其规则来提高局部搜索的性能。在每一步,如果没有其他选择(具有奖励的动作),有时允许动作变差是可以接受的。
此外,为了确保算法不会回到之前访问过的解决方案,我们设置了禁止(tabu)规则。
TabuSearchExploration tabuPolicy;
if (qLearning != null)
tabuPolicy = (TabuSearchExploration)qLearning.ExplorationPolicy;
else if (sarsa != null)
tabuPolicy = (TabuSearchExploration)sarsa.ExplorationPolicy;
else
throw new Exception();
var explorationPolicy = (EpsilonGreedyExploration)tabuPolicy?.BasePolicy;
explorationPolicy.Epsilon = 0;
tabuPolicy?.ResetTabuList();
接下来,我们必须定位我们的代理并准备地图。

下面是我们的主要执行循环,它将展示动画解决方案:
while (!needToStop)
{
cellWorld.Map = mapToDisplay;
Thread.Sleep(200);
if ((agentCurrentX == agentStopX) && (agentCurrentY == agentStopY))
{
mapToDisplay[agentStartY, agentStartX] = 2;
mapToDisplay[agentStopY, agentStopX] = 3;
agentCurrentX = agentStartX;
agentCurrentY = agentStartY;
cellWorld.Map = mapToDisplay;
Thread.Sleep(200);
}
mapToDisplay[agentCurrentY, agentCurrentX] = 0;
int currentState = GetStateNumber(agentCurrentX, agentCurrentY);
int action = qLearning?.GetAction(currentState) ?? sarsa.GetAction(currentState);
UpdateAgentPosition(ref agentCurrentX, ref agentCurrentY, action);
mapToDisplay[agentCurrentY, agentCurrentX] = 2;
}
汉诺塔
由于我们已经讨论了 Q 学习,我想在本章的剩余部分突出展示 Kenan Deen 的一些出色工作。他的汉诺塔解决方案是使用强化学习解决现实世界问题的绝佳例子。
这种形式的强化学习更正式地被称为马尔可夫决策过程(MDP)。MDP 是一个离散时间随机控制过程,这意味着在每个时间步,过程处于状态 x。决策者可以选择该状态下的任何可用动作,过程将在下一个时间步通过随机移动到新状态并向决策者提供奖励来响应。过程移动到新状态的概率由所选动作决定。因此,下一个状态取决于当前状态和决策者的动作。给定状态和动作,下一步完全独立于所有先前状态和动作。
汉诺塔由三根杆和几个按顺序排列的盘子组成,最左边的杆上。目标是使用尽可能少的移动次数将所有盘子从最左边的杆移动到最右边的杆。
你必须遵循的两个重要规则是,你一次只能移动一个盘子,你不能把一个更大的盘子放在一个较小的盘子上面;也就是说,在任何杆上,盘子的顺序必须始终是从底部最大的盘子到顶部最小的盘子,如下所示:

假设我们正在使用三个盘子,如上图所示。在这种情况下,有 3³种可能的状态,如下所示:

汉诺塔谜题中所有可能状态的总数是盘子的数量 3 的幂。
||S|| = 3^n
其中 ||S|| 是状态集中的元素数量,n 是盘子的数量。
因此,在我们的例子中,我们有 3 x 3 x 3 = 27 种独特的磁盘分布状态,包括空杆;但最多只能有两个空杆处于某种状态。
在定义了总状态数之后,这里列出了我们的算法从一种状态移动到另一种状态所具有的所有可能动作:

此谜题可能的最少移动次数为:LeastPossibleMoves = 2^n - 1
其中 n 是盘子的数量。
Q 学习算法可以正式定义为以下内容:

在这个 Q 学习算法中,我们使用了以下变量:
-
Q 矩阵:一个二维数组,最初为所有元素填充一个固定值(通常是 0)。它用于存储所有状态的计算策略;也就是说,对于每个状态,它存储相应可能动作的奖励。
-
R 矩阵:一个二维数组,包含初始奖励并允许程序确定特定状态的可能的动作列表。
-
折扣因子:决定了智能体如何处理奖励的策略。接近 0 的折扣因子将使智能体变得贪婪,只考虑当前奖励,而接近 1 的折扣因子将使其更具战略性和远见,以获得长期更好的奖励。
我们应该简要概述我们 Q 学习类的一些方法:
-
Init:用于生成所有可能的状态以及学习过程的开始。 -
学习:具有学习过程的连续步骤。 -
InitRMatrix:使用这些值之一初始化奖励矩阵:-
0:在当前状态下采取此动作时,我们没有关于奖励的信息 -
X:在当前状态下无法采取此动作 -
100:这是我们最终状态的大奖励,我们希望达到这个状态
-
-
TrainQMatrix:包含 Q 矩阵的实际迭代值更新规则。完成后,我们期望有一个训练好的智能体。 -
NormalizeQMatrix:将 Q 矩阵的值标准化,使其成为百分比。 -
测试: 从用户处提供文本输入并显示解决谜题的最佳最短路径。
让我们更深入地了解我们的TrainQMatrix代码:
private void TrainQMatrix(int _StatesMaxCount)
{
pickedActions = new Dictionary<int, int>();
// list of available actions (will be based on R matrix which
// contains the allowed next actions starting from some state as 0 values in the array
List<int> nextActions = new List<int>();
int counter = 0;
int rIndex = 0;
// _StatesMaxCount is the number of all possible states of a puzzle
// from my experience with this application, 4 times the number
// of all possible moves has enough episodes to train Q matrix
while (counter < 3 * _StatesMaxCount)
{
var init = Utility.GetRandomNumber(0, _StatesMaxCount);
do
{
// get available actions
nextActions = GetNextActions(_StatesMaxCount, init);
// Choose any action out of the available actions randomly
if (nextActions != null)
{
var nextStep = Utility.GetRandomNumber(0, nextActions.Count);
nextStep = nextActions[nextStep];
// get available actions
nextActions = GetNextActions(_StatesMaxCount, nextStep);
// set the index of the action to take from this state
for (int i = 0; i < 3; i++)
{
if (R != null && R[init, i, 1] == nextStep)
rIndex = i;
}
// this is the value iteration update rule, discount factor is 0.8
Q[init, nextStep] = R[init, rIndex, 0] + 0.8 * Utility.GetMax(Q, nextStep, nextActions);
// set the next step as the current step
init = nextStep;
}
}
while (init != FinalStateIndex);
counter++;
}
}
使用三个盘子运行应用程序:

使用四个盘子运行应用程序:

这里是使用七个盘子运行的情况。最佳移动次数为 127,您可以看到解决方案如何快速地乘以可能的组合:

摘要
在本章中,我们学习了强化学习,与之相关的各种学习算法,以及如何将其应用于现实世界的学习问题。在下一章中,我们将跳入模糊逻辑,不仅了解其含义,还将了解如何将其应用于日常问题。
参考文献
-
维基百科,创意共享署名许可
-
Watkins, C.J.C.H. (1989), 延迟奖励学习(博士论文),剑桥大学
-
使用连接主义系统的在线 Q-Learning,Rummery & Niranjan (1994)
-
Wiering, Marco; Schmidhuber, Jürgen (1998-10-01), 快速在线 Q(λ). 机器学习. 33 (1): 105-115
-
版权所有 (c) 2009-2017,Accord.NET 作者,联系邮箱:
authors@accord-framework.net -
Kenan Deen,
kenandeen.wordpress.com/
第五章:模糊逻辑 – 在障碍赛道中导航
模糊逻辑。这是那些你经常听到的 buzzword-compliant 术语之一。但它究竟意味着什么,它是否意味着不止一件事?我们马上就要找出答案了。我们将使用模糊逻辑来帮助引导自动驾驶车辆绕过障碍赛道,如果我们做得正确,我们将避免途中的障碍。我们的自动导引车(AGV)将绕过障碍赛道导航,感知其路径上的障碍。它将使用推理系统来帮助引导。作为用户,你将能够创建障碍或通道,AGV 要么必须避开,要么可以利用。你可以观看追踪光束的工作,以及跟踪 AGV 在其路径上的行进。AGV 每迈出的一步都会在用户界面上更新,这样你就可以看到发生了什么。
在布尔逻辑中,事物要么是真的,要么是假的,要么是开,要么是关,要么是黑,要么是白。许多人不知道的是,还有一种被称为多值逻辑的逻辑,其中真值介于 1 和 0 之间。模糊逻辑是多值逻辑的概念实现,它处理部分真值。许多人也不知道的是,你将在我们关于激活函数的章节中听到的著名 sigmoid 函数,实际上是一种模糊化方法。
维基百科有一个很好的视觉表示,如下所示:

根据维基百科:
"在这张图中,冷、暖和热这些表达的含义是通过映射温度尺度的函数来表示的。该尺度上的一个点有三个“真值”——每个函数对应一个。图中的垂直线代表三个箭头(真值)所测量的特定温度。由于红色箭头指向零,这个温度可以解释为“不热”。橙色箭头(指向 0.2)可能描述它为“略微温暖”,而蓝色箭头(指向 0.8)则“相当冷”。"
这个图表和描述非常准确地代表了我们将要深入探讨的内容。我们为什么要展示这个?因为我们的第一个例子将正好展示这一点。为了说明模糊逻辑,我们将使用 AForge.NET 开源机器学习框架。对于用户来说,这是一个展示如何轻松使用推理引擎来完成任务的优秀框架。
在本章中,我们将涵盖:
-
模糊逻辑
-
自主导引车辆
-
障碍物避让和识别
模糊逻辑
我们的应用程序将有两个简单的按钮,一个用于运行模糊集测试,另一个用于运行语言变量测试。以下是我们示例应用程序的快速快照:

创建此示例的代码相对较小且简单。当我们点击运行模糊集测试按钮时,它看起来是这样的。我们将创建两个模糊集(一个用于凉爽,一个用于温暖),为每个添加一些隶属度数据值,然后绘制它们:
TrapezoidalFunction function1 = new TrapezoidalFunction( 13, 18, 23, 28 );
FuzzySet fsCool = new FuzzySet( "Cool", function1 );
TrapezoidalFunction function2 = new TrapezoidalFunction( 23, 28, 33, 38 );
FuzzySet fsWarm = new FuzzySet( "Warm", function2 );
double[,] coolValues = new double[20, 2];
for ( int i = 10; i < 30; i++ )
{
coolValues[i - 10, 0] = i;
coolValues[i - 10, 1] = fsCool.GetMembership( i );
}
double[,] warmValues = new double[20, 2];
for ( int i = 20; i < 40; i++ )
{
warmValues[i - 20, 0] = i;
warmValues[i - 20, 1] = fsWarm.GetMembership( i );
}
chart?.UpdateDataSeries( "COOL", coolValues );
chart?.UpdateDataSeries( "WARM", warmValues );
运行语言变量测试的代码如下。同样,我们创建模糊集,但这次我们创建了四个而不是两个。就像我们的第一个测试一样,我们添加隶属度数据然后绘制:
LinguisticVariable lvTemperature = new LinguisticVariable( "Temperature", 0, 80 );
TrapezoidalFunction function1 = new TrapezoidalFunction( 10, 15, TrapezoidalFunction.EdgeType.Right );
FuzzySet fsCold = new FuzzySet( "Cold", function1 );
TrapezoidalFunction function2 = new TrapezoidalFunction( 10, 15, 20, 25 );
FuzzySet fsCool = new FuzzySet( "Cool", function2 );
TrapezoidalFunction function3 = new TrapezoidalFunction( 20, 25, 30, 35 );
FuzzySet fsWarm = new FuzzySet( "Warm", function3 );
TrapezoidalFunction function4 = new TrapezoidalFunction( 30, 35, TrapezoidalFunction.EdgeType.Left );
FuzzySet fsHot = new FuzzySet( "Hot", function4 );
lvTemperature.AddLabel( fsCold );
lvTemperature.AddLabel( fsCool );
lvTemperature.AddLabel( fsWarm );
lvTemperature.AddLabel( fsHot );
double[][,] chartValues = new double[4][,];
for ( int i = 0; i < 4; i++ )
chartValues[i] = new double[160, 2];
最后,我们绘制这些值:
int j = 0;
for ( float x = 0; x < 80; x += 0.5f, j++ )
{
double y1 = lvTemperature.GetLabelMembership( "Cold", x );
double y2 = lvTemperature.GetLabelMembership( "Cool", x );
double y3 = lvTemperature.GetLabelMembership( "Warm", x );
double y4 = lvTemperature.GetLabelMembership( "Hot", x );
chartValues[0][j, 0] = x;
chartValues[0][j, 1] = y1;
chartValues[1][j, 0] = x;
chartValues[1][j, 1] = y2;
chartValues[2][j, 0] = x;
chartValues[2][j, 1] = y3;
chartValues[3][j, 0] = x;
chartValues[3][j, 1] = y4;
}
chart.UpdateDataSeries( "COLD", chartValues[0] );
chart.UpdateDataSeries( "COOL", chartValues[1] );
chart.UpdateDataSeries( "WARM", chartValues[2] );
chart.UpdateDataSeries( "HOT", chartValues[3] );
语言变量形状:

如您所见,我们能够轻松地展示出维基百科定义所呈现的精确视觉定义和清晰度。
模糊 AGV
在这个例子中,我们将比我们第一个例子更深入地探讨。在我们继续之前,让我先向您展示我们的应用程序将是什么样子,然后简要解释一下推理引擎:

虽然 AForge.NET 使我们创建InferenceSystem对象变得非常简单和透明,但我们可能首先应该向您简要介绍一下这样一个系统是什么。模糊推理系统是一个能够执行模糊计算的模型。这是通过使用数据库、语言变量和规则库来实现的,所有这些都可以在内存中。模糊推理系统的典型操作如下:
-
获取数值输入
-
利用包含语言变量的数据库来获取每个数值输入的语言意义
-
验证哪些规则从规则库中被激活
-
将激活的规则的结果组合起来以获得模糊输出
对于我们来说,大部分工作将在初始化我们的模糊逻辑系统中完成。让我们将其分解为我们之前概述的各个步骤。
首先,我们准备构成我们将拥有的距离的语言标签(模糊集),它们是近、中和远:
FuzzySet fsNear = new FuzzySet( "Near", new TrapezoidalFunction( 15, 50, TrapezoidalFunction.EdgeType.Right ) );
FuzzySet fsMedium = new FuzzySet( "Medium", new TrapezoidalFunction( 15, 50, 60, 100 ) );
FuzzySet fsFar = new FuzzySet( "Far", new TrapezoidalFunction( 60, 100, TrapezoidalFunction.EdgeType.Left ) );
接下来,我们初始化我们将需要的语言变量。第一个,lvRight,将是一个用于右侧距离测量的输入变量:
LinguisticVariable lvRight = new LinguisticVariable( "RightDistance", 0, 120 );
lvRight.AddLabel( fsNear );
lvRight.AddLabel( fsMedium );
lvRight.AddLabel( fsFar );
现在,我们对左侧距离输入测量也做同样的处理:
LinguisticVariable lvLeft = new LinguisticVariable( "LeftDistance", 0, 120 );
lvLeft.AddLabel( fsNear );
lvLeft.AddLabel( fsMedium );
lvLeft.AddLabel( fsFar );
我们最后一个语言变量将用于前距离测量:
LinguisticVariable lvFront = new LinguisticVariable( "FrontalDistance", 0, 120 );
lvFront.AddLabel( fsNear );
lvFront.AddLabel( fsMedium );
lvFront.AddLabel( fsFar );
现在,我们专注于构成角度的语言标签(模糊集)。我们需要执行这一步骤,以便我们可以创建我们的最终语言变量:
FuzzySet fsVN = new FuzzySet( "VeryNegative", new TrapezoidalFunction( -40, -35, TrapezoidalFunction.EdgeType.Right));
FuzzySet fsN = new FuzzySet( "Negative", new TrapezoidalFunction( -40, -35, -25, -20 ) );
FuzzySet fsLN = new FuzzySet( "LittleNegative", new TrapezoidalFunction( -25, -20, -10, -5 ) );
FuzzySet fsZero = new FuzzySet( "Zero", new TrapezoidalFunction( -10, 5, 5, 10 ) );
FuzzySet fsLP = new FuzzySet( "LittlePositive", new TrapezoidalFunction( 5, 10, 20, 25 ) );
FuzzySet fsP = new FuzzySet( "Positive", new TrapezoidalFunction( 20, 25, 35, 40 ) );
FuzzySet fsVP = new FuzzySet( "VeryPositive", new TrapezoidalFunction( 35, 40, TrapezoidalFunction.EdgeType.Left));
现在,我们可以创建我们的最终语言变量,用于角度:
LinguisticVariable lvAngle = new LinguisticVariable( "Angle", -50, 50 );
lvAngle.AddLabel( fsVN );
lvAngle.AddLabel( fsN );
lvAngle.AddLabel( fsLN );
lvAngle.AddLabel( fsZero );
lvAngle.AddLabel( fsLP );
lvAngle.AddLabel( fsP );
lvAngle.AddLabel( fsVP );
现在,我们可以继续创建我们的模糊数据库。对于我们的应用程序,这是一个内存中的语言变量字典,但如果你愿意,没有理由你不能将其实现为一个 SQL、NoSQL 或任何其他类型的具体数据库:
Database fuzzyDB = new Database( );
fuzzyDB.AddVariable( lvFront );
fuzzyDB.AddVariable( lvLeft );
fuzzyDB.AddVariable( lvRight );
fuzzyDB.AddVariable( lvAngle );
接下来,我们将创建主要推理引擎。接下来这一行代码最有趣的地方是 CentroidDifuzzifier。在我们推理过程的末尾,我们需要一个数值来控制过程的其它部分。为了获得这个数值,执行去模糊化方法。让我解释一下。
我们模糊推理系统的输出是一组具有大于零的激发强度的规则。这种激发强度对规则的后续模糊集施加约束。当我们把所有这些模糊集放在一起时,它们的结果是一个形状,表示语言输出意义。质心法将计算我们形状区域的中心,以获得输出的数值表示。它使用数值近似,因此将选择几个区间。随着区间数量的增加,我们输出的精度也增加:
IS = new InferenceSystem(fuzzyDB, new CentroidDefuzzifier(1000));
接下来,我们可以开始向我们的推理系统添加规则:

在所有这些工作之后,我们的推理系统准备就绪!
我们应用程序的主要代码循环将看起来像这样。我们将详细描述每个函数:
if (FirstInference)
GetMeasures();
try
{
DoInference();
MoveAGV();
GetMeasures();
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
让我们快速看一下 GetMeasures 函数。
在获取当前位图以及我们的 AGV 位置后,我们调用 HandleAGVOnWall 函数,该函数处理我们的 AGV 靠墙且无处可移动的情况。之后,DrawAGV 处理在地图中绘制我们的 AGV。最后,RefreshTerrain 函数正是其名称所暗示的:
private void GetMeasures()
{
// Getting AGV's position
pbTerrain.Image = CopyImage(OriginalMap);
Bitmap b = (Bitmap) pbTerrain.Image;
Point pPos = new Point(pbRobot.Left - pbTerrain.Left + 5, pbRobot.Top - pbTerrain.Top + 5);
// AGV on the wall
HandleAGVOnWall(b, pPos);
DrawAGV(pPos, b);
RefreshTerrain();
}
DrawAGV 获取我们面前、左侧和右侧的任何障碍物。如果你选中了“显示光束”复选框,你将看到前、左和右光束避障检测器显示:
private void DrawAGV(Point pPos, Bitmap b)
{
Point pFrontObstacle = GetObstacle(pPos, b, -1, 0);
Point pLeftObstacle = GetObstacle(pPos, b, 1, 90);
Point pRightObstacle = GetObstacle(pPos, b, 1, -90);
// Showing beams
Graphics g = Graphics.FromImage(b);
if (cbLasers.Checked)
{
g.DrawLine(new Pen(Color.Red, 1), pFrontObstacle, pPos);
g.DrawLine(new Pen(Color.Red, 1), pLeftObstacle, pPos);
g.DrawLine(new Pen(Color.Red, 1), pRightObstacle, pPos);
}
// Drawing AGV
if (btnRun.Text != RunLabel)
{
g.FillEllipse(new SolidBrush(Color.Blue), pPos.X - 5, pPos.Y - 5, 10, 10);
}
g.DrawImage(b, 0, 0);
g.Dispose();
// Updating distances texts
txtFront.Text = GetDistance(pPos, pFrontObstacle).ToString();
txtLeft.Text = GetDistance(pPos, pLeftObstacle).ToString();
txtRight.Text = GetDistance(pPos, pRightObstacle).ToString();
}
DoInference 函数运行我们模糊推理系统的一个纪元(实例、生成等)。最终,它负责确定我们 AGV 的下一个角度。
private void DoInference()
{
// Setting inputs
IS?.SetInput("RightDistance", Convert.ToSingle(txtRight.Text));
IS?.SetInput("LeftDistance", Convert.ToSingle(txtLeft.Text));
IS?.SetInput("FrontalDistance", Convert.ToSingle(txtFront.Text));
// Setting outputs
try
{
double NewAngle = IS.Evaluate("Angle");
txtAngle.Text = NewAngle.ToString("##0.#0");
Angle += NewAngle;
}
catch (Exception)
{
}
}
MoveAGV 函数负责将我们的 AGV 移动一步。大约 50% 的代码在这个函数中是专门用于绘制你的 AGV 的历史轨迹,如果你勾选了“跟踪路径”:
private void MoveAGV()
{
double rad = ((Angle + 90) * Math.PI) / 180;
int Offset = 0;
int Inc = -4;
Offset += Inc;
int IncX = Convert.ToInt32(Offset * Math.Cos(rad));
int IncY = Convert.ToInt32(Offset * Math.Sin(rad));
// Leaving the track
if (cbTrajeto.Checked)
{
Graphics g = Graphics.FromImage(OriginalMap);
Point p1 = new Point(pbRobot.Left - pbTerrain.Left + pbRobot.Width / 2, pbRobot.Top - pbTerrain.Top + pbRobot.Height / 2);
Point p2 = new Point(p1.X + IncX, p1.Y + IncY);
g.DrawLine(new Pen(new SolidBrush( Color.Green)), p1, p2);
g.DrawImage(OriginalMap, 0, 0);
g.Dispose();
}
pbRobot.Top = pbRobot.Top + IncY;
pbRobot.Left = pbRobot.Left + IncX;
}
选择“显示光束”的主要应用:

在我们的应用程序运行时,AGV 成功地导航障碍物,路径和光束都显示出来。角度是 AGV 当前面对的角度,传感器读数与显示的前、左、右光束传感器相关:

我们的 AGV 正在成功完成障碍课程并通过:

可以分别选择“跟踪路径”和“显示光束”:

这显示了如何使用左右鼠标按钮添加障碍物和通道,分别阻止和允许 AGV 通过:

摘要
在本章中,我们学习了各种模糊逻辑的实现方式,并亲眼见证了使用 AForge.NET 将这种逻辑添加到我们的应用程序是多么简单。在下一章中,我们将深入探索自组织图,将我们的机器学习技能提升到新的水平。如果你还记得你小学时的美术课,那么这一章一定会唤起你的回忆!
参考文献
AForge.NET: 版权所有 © AForge.NET, 2006-2013
第六章:颜色混合 - 自组织映射和弹性神经网络
自组织映射(SOM),或者如您所听到的Kohonen 图,是自组织神经网络的基本类型之一。自组织的能力提供了对以前未见过的输入数据的适应性。它被理论化为学习最自然的方式之一,就像我们的大脑所使用的那样,其中没有预定义的模式被认为是存在的。这些模式在学习过程中形成,并且擅长以远低于维度的水平表示多维数据,例如 2D 或 1D。此外,这个网络以这种方式存储信息,即训练集中的任何拓扑关系都保持不变。
更正式地说,SOM 是一种聚类技术,它将帮助我们在大数据集中发现有趣的数据类别。它是一种无监督神经网络,其中神经元排列在一个单一的二维网格中。网格必须是矩形的,即,一个纯矩形或六边形。在整个迭代过程中(我们将指定),我们的网格中的神经元将逐渐聚集在数据点密度较高的区域(我们显示的左侧称为点)。随着神经元的移动,它们弯曲和扭曲网格,直到它们更接近感兴趣的点并反映数据的形状。
在本章中,我们将介绍以下主题:
-
Kohonen SOM
-
使用 AForge.NET 进行工作
SOM 的内部机制
因此,现在不可避免的问题出现了:这些是如何工作的?
简而言之,我们在网格上有神经元;通过迭代,它们逐渐适应我们的数据形状(在我们的例子中,如下面的图像所示,在点面板的左侧)。让我们更多地谈谈迭代过程本身。
- 第一步是在网格上随机放置数据。我们将随机在我们的数据空间中放置我们的网格神经元,如下所示:

-
第二步是算法将选择一个单一的数据点。
-
在第三步,我们需要找到与所选数据点最近的神经元(数据点)。然后这将成为我们的最佳匹配单元。
-
第四步是将我们的最佳匹配单元移动到该数据点。我们移动的距离由我们的学习率决定,该学习率在每个迭代后最终会减小。
-
第五,我们将把最佳匹配单元的邻居移动得更近,距离较远的神经元移动的距离小于较近的神经元。屏幕上您看到的初始半径变量是我们用来识别邻居的。这个值,就像初始学习率一样,会随着时间的推移而减小。如果您已经启动并运行了ReflectInsight(RI),您可以观察初始学习率随时间的变化,如下面的截图所示:

- 我们第六步和最后一步将是更新初始学习率和初始半径,正如我们之前所描述的,然后重复。我们将继续这个过程,直到我们的数据点稳定并处于正确的位置。
现在我们已经向你介绍了一些关于 SOMs 的直觉,让我们再谈谈本章我们将要做什么。我们选择了一个非常常见的机制来教授我们的原则,即颜色的映射。颜色本身是由红色、绿色和蓝色表示的 3D 对象,但我们将它们组织到二维中。你将在这里看到两个关于颜色组织的要点。首先,颜色被聚类到不同的区域,其次,具有相似属性的区域通常相邻。
我们的第二个例子,稍微复杂一些,将使用我们之前描述的人工神经网络(ANN);这是一种高级的机器学习方法,用于创建与提供给它的组织映射相匹配的组织映射。让我们看看我们的第一个例子。
这是我们示例的截图。正如你所见,我们有一个随机的颜色模式,完成后,这些颜色将被组织成相似颜色的簇:

如果我们成功——我们会成功的——我们的结果应该如下所示:

让我们首先按照以下步骤遵循处理过程:
- 我们将首先使用 500 次迭代来实现我们的目标。使用较小的数字可能不会产生我们最终想要的混合效果。例如,如果我们使用了 500 次迭代,我们的结果将如下所示:

-
正如你所见,我们离我们想要达到的目标还很远。能够改变迭代次数让你可以尝试正确的设置。我可以告诉你 500 次迭代比我们需要的要多,所以我会把它留给你作为练习,找出进度停止并且你对组织满意的数量。
-
在设置迭代次数后,我们唯一要做的就是确保我们有我们想要的随机颜色模式,这可以通过点击“随机化”按钮来实现。
-
一旦你得到了你想要的模式,你只需点击“开始”按钮并观察结果。
-
一旦你点击“开始”,“停止”按钮将被激活,你可以在任何时候停止进度。当你达到指定的迭代次数时,组织将自动停止。
在我们进入实际代码之前,让我给你展示一些组织模式的一些截图。通过简单地改变不同的参数,你可以取得惊人的效果,我们将在后面详细描述。在下面的截图中,我们将迭代次数设置为 3000,初始半径为 10:

在下面的屏幕截图中,我们使用了 4000 次迭代和初始半径为 18:

在以下屏幕截图中,我们将迭代次数设置为 4000,初始半径为 5:

在这里,我们将迭代次数设置为 5000,初始学习率为 0.3,初始半径为 25,如以下屏幕截图所示,以获得期望的结果:

如承诺的那样,现在让我们深入代码。
在这个例子中,我们将使用AForge并使用DistanceNetwork对象。距离网络是一个只有单一距离的神经网络。除了用于 SOM 之外,它还用于弹性网络操作,这是我们用来展示在进展过程中对象之间弹性连接的。
我们将使用三个输入神经元和1000个将在幕后工作的神经元来创建我们的距离网络:
network = new DistanceNetwork(3, 100 * 100);
当你点击随机化按钮来随机化颜色时,下面是幕后发生的事情:
private void RandomizeNetwork()
{
if (network != null)
{
foreach (var neuron in (network?.Layers.SelectMany(layer
=> layer?.Neurons)).Where(neuron => neuron != null))
neuron.RandGenerator =
new UniformContinuousDistribution
(new Range(0, 255));
network?.Randomize();
}
UpdateMap();
}
你会注意到我们正在处理的随机化范围保持在任何颜色的红色、绿色或蓝色特征的范围内,即255。
接下来,我们将查看我们的学习循环,它看起来是这样的。我们稍后会深入探讨:
SOMLearning trainer = new SOMLearning(network);
double[] input = new double[3];
double fixedLearningRate = learningRate / 10;
double driftingLearningRate = fixedLearningRate * 9;
int i = 0;
while (!needToStop)
{
trainer.LearningRate = driftingLearningRate
* (iterations - i)
/ iterations + fixedLearningRate;
trainer.LearningRadius = radius * (iterations - i)
/ iterations;
if (rand != null)
{
input[0] = rand.Next(256);
input[1] = rand.Next(256);
input[2] = rand.Next(256);
}
trainer.Run(input);
// update map once per 50 iterations
if ((i % 10) == 9)
{
UpdateMap();
}
i++;
SetText(currentIterationBox, i.ToString());
if (i >= iterations)
break;
}
如果我们仔细观察,我们首先创建的是一个SOMLearning对象。这个对象针对正方形空间学习进行了优化,意味着它期望它正在工作的网络的高度与宽度相同。这使得找到网络神经元数量的平方根变得更容易:
SOMLearning trainer = new SOMLearning(network);
接下来,我们需要创建变量来保存我们的红色、绿色和蓝色输入颜色,我们将不断地随机化输入颜色以达到我们的目标:
if (rand != null)
{
input[0] = rand.Next(256);
input[1] = rand.Next(256);
input[2] = rand.Next(256);
}
一旦我们进入while循环,我们将不断地更新我们的变量,直到达到我们选择的迭代总数。在这个更新循环中,有几件事情正在发生。首先,我们将更新学习率和学习半径,并将其存储在我们的SOMLearning对象中:
trainer.LearningRate = driftingLearningRate * (iterations - i) /
iterations + fixedLearningRate;
trainer.LearningRadius = radius * (iterations - i) / iterations;
学习率决定了我们的学习速度。学习半径,它可以对视觉输出产生相当大的影响,决定了相对于获胜神经元的距离要更新的神经元数量。指定半径的圆圈由神经元组成,它们在学习过程中不断地被更新。一个神经元越接近获胜神经元,它将接收到的更新就越多。请注意,如果在你的实验中,你将此值设置为 0,那么只有获胜神经元的权重将被更新,其他神经元则不会。
尽管我们将有一个非常漂亮的视觉效果可以观看,但我们仍然需要了解我们应用程序内部的情况,这就是 RI 的作用所在:
RILogManager.Default.ViewerSendWatch("Learning Rate", $"{trainer.LearningRate}");
RILogManager.Default.ViewerSendWatch("Learning Radius", $"{trainer.LearningRadius}");
RILogManager.Default.ViewerSendWatch("Red", $"{RGBInput[0]}");
RILogManager.Default.ViewerSendWatch("Green", $"{RGBInput[1]}");
RILogManager.Default.ViewerSendWatch("Blue", $"{RGBInput[2]}");
RI,如我们之前提到的,有一个监视面板,让你可以持续跟踪你感兴趣的任何变量。在我们的情况下,我们感兴趣的是监视学习率、学习半径以及每个随机化的 RGB 颜色。我们只需要提供标签和值,RI 就会完成剩下的工作,正如我们稍后将看到的。
最后,与 RI 相关,我们还想在我们的消息窗口中看到 RGB 值,因此我们将添加一个调试消息:
RILogManager.Default.SendDebug($"Red {RGBInput[0]}, Green {RGBInput[1]}, Blue
{RGBInput[2]}");
我们现在为这次迭代进行一次训练Run,并将RGBInput数组传递给它:
trainer.Run(RGBInput);
让我们暂时谈谈学习。正如我们提到的,每次迭代都会尝试学习越来越多的信息。这次学习迭代返回一个学习误差,即神经元权重与输入向量RGBInput之间的差异。如前所述,距离是根据获胜神经元(权重值与RGBInput中提供的值最接近的神经元)的距离来衡量的。过程如下。
训练器运行一次学习迭代,找到获胜神经元(权重值与RGBInput中提供的值最接近的神经元),并更新其权重。它还更新邻近神经元的权重。随着每次学习迭代的进行,网络越来越接近最优解。
接下来是应用程序运行的截图。背景是 RI,这样你可以看到我们如何记录每个迭代,我们在更新地图时使用什么颜色值,以及学习率和学习半径。随着你的机器学习程序和算法变得越来越复杂,你会意识到这种对应用程序的洞察变得极其宝贵。它也是一个不可或缺的实时调试和诊断工具!

由于 SOM 是自我组织的,我们的第二个例子将更加图形化。我们希望它能帮助你更好地理解幕后发生的事情。
在这个例子中,我们再次使用 AForge.NET 构建一个由几个组组织起来的二维对象平面。我们将从单个位置开始,直观地到达那些形状的位置。从概念上讲,这与我们的颜色示例相同,该示例使用了三维空间中的点,但这次我们的点是二维的。可视化发生在地图面板中,是二维空间中发生情况的俯视图,以便得到一维图形视图。
在 SOM 网格中,神经元最初处于随机位置,但它们逐渐被调整成与我们的数据形状相匹配的模具轮廓。这是一个迭代过程,尽管将动画.gif放入书中是一项我们尚未实现的壮举,但我已经在不同迭代点拍摄了快照,以展示发生了什么。你可以亲自运行示例,以实时查看。
我们从所有对象在左侧的位置开始。我们将运行 500 次迭代以展示演变。我们将从一个空白白色面板到一个,希望,类似于点面板的面板:

现在我们点击“开始”按钮,它就出发了!你会看到点开始移动到它们正确的位置,这(希望)将反映出我们指定的点:

经过 199 次迭代后:

经过 343 次迭代后:

完成后,你可以看到物体已经按照我们最初创建的图案组织起来。如果你想象自己正俯瞰地图,即使你站在一张平坦的纸上,只要你足够专注,你就能看到三维体验。蓝色的小点代表活跃的神经元,浅灰色的小点代表不活跃的神经元,画出的线条是神经元之间的弹性连接。
地图下方的复选框允许你轻松选择是否显示这些中的任何一个或两个:

如果你截图时不显示连接和不活跃的神经元,你会看到地图上的组织模式达到了与我们的目标相同的聚类,对我们来说这意味着成功:

所有这一切是如何工作的,是我们接下来要调查的主题。像往常一样,让我们看看我们的主要执行循环。正如你所看到的,我们将使用之前讨论过的相同的DistanceNetwork和SOMLearning对象:
DistanceNetwork network = new DistanceNetwork(2, networkSize
* networkSize);
// set random generators range
foreach (var neuron in network.Layers.SelectMany(layer =>
layer.Neurons))
neuron.RandGenerator = new UniformContinuousDistribution(
new Range(0, Math.Max
(pointsPanel.ClientRectangle.Width,
pointsPanel.ClientRectangle.Height)));
// create learning algorithm
SOMLearning trainer = new SOMLearning(network, networkSize,
networkSize);
// create map
map = new int[networkSize, networkSize, 3];
double fixedLearningRate = learningRate / 10;
double driftingLearningRate = fixedLearningRate * 9;
// iterations
int i = 0;
// loop
while (!needToStop)
{
trainer.LearningRate = driftingLearningRate
* (iterations - i) / iterations + fixedLearningRate;
trainer.LearningRadius = (double)learningRadius *
(iterations - i) / iterations;
// run training epoch
trainer.RunEpoch(trainingSet);
// update map
UpdateMap(network);
// increase current iteration
i++;
// set current iteration's info
SetText(currentIterationBox, i.ToString());
// stop ?
if (i >= iterations)
break;
}
如我们之前提到的,学习率和学习半径会随着每一次迭代而不断进化。这次,让我们谈谈训练器的RunEpoch方法。这个方法虽然非常简单,但旨在接受一个输入值向量,然后返回该迭代的 学习误差(正如你现在可以看到的,有时也称为epoch)。它是通过计算向量中的每个输入样本来实现的。学习误差是神经元权重和输入之间的绝对差异。差异是根据获胜神经元的距离来衡量的。如前所述,我们针对一次学习迭代/epoch 进行计算,找到获胜者,并更新其权重(以及相邻权重的)。我应该指出,当我提到“获胜者”时,我指的是权重值最接近指定输入向量的神经元,即网络输入的最小距离。
接下来,我们将展示如何更新“地图”本身;我们的计算项目应该与初始输入向量(点)相匹配:
// get first layer
Layer layer = network.Layers[0];
// lock
Monitor.Enter(this);
// run through all neurons
for (int i = 0; i < layer.Neurons.Length; i++)
{
Neuron neuron = layer.Neurons[i];
int x = i % networkSize;
int y = i / networkSize;
map[y, x, 0] = (int)neuron.Weights[0];
map[y, x, 1] = (int)neuron.Weights[1];
map[y, x, 2] = 0;
}
// collect active neurons
for (int i = 0; i < pointsCount; i++)
{
network.Compute(trainingSet[i]);
int w = network.GetWinner();
map[w / networkSize, w % networkSize, 2] = 1;
}
// unlock
Monitor.Exit(this);
//
mapPanel.Invalidate();
如您从这段代码中可以看到,我们获取第一层,计算所有神经元的map,收集活跃的神经元以便我们确定获胜者,然后更新map。
由于我们已经多次提到了获胜者,让我向您展示一下计算获胜者所需的代码量:
public int GetWinner()
{
// find the MIN value
double min = output[0];
int minIndex = 0;
for (int i = 1; i < output.Length; i++)
{
if (output[i] < min)
{
// found new MIN value
min = output[i];
minIndex = i;
}
}
return minIndex;
}
就这样!我们正在做的只是寻找权重与网络输入距离最小的神经元的索引。
摘要
在本章中,我们学习了如何利用 SOMs(自组织映射)和弹性神经网络的强大功能。你现在已经正式从机器学习跨越到神经网络;恭喜你!
在我们接下来的章节中,我们将运用一些知识来开始面部和动作检测程序,并享受一些真正的乐趣!你将有机会与我的章节合作伙伴 Frenchie 一起工作!
第七章:面部检测和运动检测 – 图像过滤器
您无处不在都见过和听说过它。面部识别,运动检测。我们家里的安全系统中就有运动传感器。每个人都正在进行面部识别——我们的街道、机场,甚至可能在我们家里都有安全摄像头。如果我们考虑自动驾驶汽车必须完成的所有事情,哇!在写作的时候,有一个关于面部识别技术如何在 5 万名面孔的群众中识别出嫌疑人的链接。www.digitaltrends.com/cool-tech/facial-recognition-china-50000/
但这究竟意味着什么?它是如何做到这一点的?幕后发生了什么?我如何在应用程序中使用它?在本章中,我们将展示两个独立的示例,一个用于面部检测,另一个用于运动检测。我们将向您展示具体发生了什么,以及您如何快速将这些功能添加到您的应用程序中。
在本章中,我们将涵盖:
-
面部检测
-
运动检测
- 将检测添加到您的应用程序
面部检测
让我们从面部检测开始。在我们的例子中,我将使用我那友好的小法国斗牛犬 Frenchie 作为我们的助手。我曾试图让我的美丽妻子来完成这个任务,但化妆、发型;嗯,我相信您知道那个故事!然而,Frenchie 这只斗牛犬却没有怨言。
在我开始之前,请重新阅读章节标题。无论您读多少次,您可能都会错过这里的关键点,它非常重要。注意它说的是面部检测而不是面部识别。这一点非常重要,以至于我需要停下来再次强调。我们不是试图识别乔、鲍勃或萨莉。我们试图识别的是,通过我们的摄像头看到的所有事物中,我们可以检测到一张脸。我们并不关心这是谁的脸,只是它是一张脸!这一点非常重要,在我们继续之前我们必须理解这一点!否则,您的期望可能会被错误地偏向(另一个清单上的时髦词)而让您自己感到困惑和沮丧,我们不希望这样!
面部检测,就像我稍后会再次强调的那样,是面部识别的第一部分,这是一个更加复杂的生物。如果您不能从屏幕上的所有事物中识别出一张或更多张脸,那么您将永远无法识别那是谁的脸!
让我们先快速看一下我们的应用程序:

如您所见,我们有一个非常简单的屏幕。在我们的案例中,笔记本电脑的摄像头是我们的视频捕获设备。Frenchie 友好地站在摄像头前,享受着生活。但是,当我们启用面部追踪时,看看会发生什么:

Frenchie 的面部特征现在正在被追踪。你所看到围绕 Frenchie 的追踪容器(白色方框),它们告诉我们我们知道有一个面部以及它的位置,还有我们的角度检测器(红色线条),它为我们面部水平方向提供了一些洞察。
当我们移动 Frenchie 时,追踪容器和角度检测器会追踪他。这很好,但是如果我们在一个真实的人脸上启用面部追踪会发生什么呢?正如你所看到的,追踪容器和角度正在追踪我们客座者的面部特征,就像它们追踪 Frenchie 一样:

当我们的模特从一侧移动到另一侧时,相机会追踪这个动作,你可以看到角度检测器正在调整以适应它所识别的面部水平角度。在这种情况下,你会注意到色彩空间是黑白而非彩色。这是一个直方图反向投影,这是一个你可以更改的选项:

即使当我们远离相机,其他物体进入视野时,面部检测器也能在噪声中追踪我们的面部,如下面的截图所示。这正是电影中你看到的面部识别系统的工作方式,尽管更高级;而且,使用我们很快就会展示的代码和示例,你也能在几分钟内启动自己的面部识别应用!我们将提供检测;你提供识别:

现在我们已经看到了我们的应用从外部看起来是什么样子,让我们来看看引擎盖下正在发生的事情。
让我们先问问自己,我们在这里试图解决什么问题。正如我们在前面的章节中提到的,我们试图检测(再次提醒,我没有说是识别)面部图像。虽然这对人类来说很容易,但计算机需要非常详细的指令集来完成这个壮举。
幸运的是,有一个非常著名的算法叫做 Viola-Jones 算法,它将为我们完成繁重的工作。我们为什么选择这个算法?
-
它具有非常高的检测率和非常低的误报率。
-
它非常擅长实时处理。
-
它非常擅长从非面部检测面部。检测面部是面部识别的第一步!
这个算法要求相机有一个完整的正面、垂直的面部视图。为了被检测到,面部需要直视相机,不能倾斜,也不能向上或向下看。再次提醒;目前,我们只对面部检测感兴趣!
要深入了解技术方面,我们的算法需要四个阶段来完成其任务。它们是:
-
Haar 特征选择
-
创建一个积分图像
-
AdaBoost 训练
-
级联分类器
让我们思考一下面部检测实际上完成了什么。无论是人类、动物还是其他生物,所有面部都具备一些相似的特征。例如,眼睛比上脸颊要暗,鼻梁比眼睛要亮,而你的额头可能比脸部其他部分要亮。我们的算法通过使用所谓的Haar 特征来匹配这些直觉。我们可以通过观察眼睛、嘴巴、鼻梁等部位的位置和大小来得出可匹配的面部特征。然而,我们确实面临一个障碍。
在一个 24x24 像素的窗口中,总共有 162,336 个可能的特征。显然,如果试图评估它们所有,无论是时间还是计算成本都会非常高昂,而且如果真的能工作的话。因此,我们将使用一种称为自适应提升的技术,或者更常见的是,AdaBoost。这是你 buzzword-compliant 列表中的另一个。如果你已经深入研究或研究过机器学习,我敢肯定你已经听说过一种称为提升的技术。这正是 AdaBoost。我们的学习算法将使用 AdaBoost 来选择最佳特征,并训练分类器来使用它们。
AdaBoost 可以与许多类型的机器学习算法一起使用,并且被认为是许多需要提升的任务中最好的现成算法。你通常不会注意到它有多好、有多快,直到你切换到另一个算法并对其进行基准测试。我已经做了无数次这样的测试,我可以告诉你,这种差异是非常明显的。
在我们继续之前,让我们先对提升(boosting)的概念进行更详细的定义。
提升算法将其他弱学习算法的输出与一个加权求和结合起来,这个加权求和是提升分类器的最终输出。AdaBoost 的自适应部分来自于后续的学习者被调整以有利于那些被先前分类器错误分类的实例。然而,我们必须小心我们的数据准备,因为 AdaBoost 对噪声数据和异常值(记得我们在第一章,机器学习基础中强调的那些)很敏感。该算法比其他算法更容易对数据进行过度拟合,这就是为什么在我们早期的章节中,我们强调了缺失数据和异常值的数据准备。最终,如果弱学习算法比随机猜测更好,AdaBoost 可以成为我们流程中的一个宝贵补充。
在有了这个简短的描述之后,让我们揭开面纱,看看背后发生了什么。在这个例子中,我们再次使用Accord 框架,并且我们将与视觉面部跟踪样本一起工作。
我们首先创建一个 FaceHaarCascade 对象。此对象包含一组类似 Haar 特征的弱分类阶段。将提供许多阶段,每个阶段都包含一组用于决策过程的分类树。我们现在实际上是在处理一个决策树。Accord 框架的美丽之处在于 FaceHaarCascade 会自动为我们创建所有这些阶段和树,而不暴露给我们细节。
让我们看看特定阶段可能的样子:
List<HaarCascadeStage> stages = new List<HaarCascadeStage>();
List<HaarFeatureNode[]> nodes;
HaarCascadeStage stage;
stage = new HaarCascadeStage(0.822689414024353);
nodes = new List<HaarFeatureNode[]>();
nodes.Add(new[] { new HaarFeatureNode(0.004014195874333382,
0.0337941907346249, 0.8378106951713562,
new int[] { 3, 7, 14, 4, -1 },
new int[] { 3, 9, 14, 2, 2 }) });
nodes.Add(new[] { new HaarFeatureNode(0.0151513395830989,
0.1514132022857666, 0.7488812208175659,
new int[] { 1, 2, 18, 4, -1 },
new int[] { 7, 2, 6, 4, 3 }) });
nodes.Add(new[] { new HaarFeatureNode(0.004210993181914091,
0.0900492817163467, 0.6374819874763489,
new int[] { 1, 7, 15, 9, -1 },
new int[] { 1, 10, 15, 3, 3 })
});
stage.Trees = nodes.ToArray(); stages.Add(stage);
现在不要被吓到。正如你所见,我们通过为每个阶段的节点提供每个特征的数值,在底层构建了一个决策树。
一旦创建,我们可以使用我们的 cascade 对象来创建我们的 HaarObjectDetector,这是我们用于检测的工具。它需要:
-
我们的 facial cascade 对象
-
搜索对象时使用的最小窗口大小
-
我们的搜索模式,鉴于我们正在搜索单个对象
-
在搜索过程中重新调整搜索窗口时使用的缩放因子
HaarCascade cascade = new FaceHaarCascade();
detector = new HaarObjectDetector(cascade, 25,
ObjectDetectorSearchMode.Single, 1.2f,
ObjectDetectorScalingMode.GreaterToSmaller);
现在我们已经准备好处理视频收藏源的话题。在我们的例子中,我们将简单地使用本地摄像头来捕捉所有图像。然而,Accord.NET 框架使得使用其他图像捕获源变得容易,例如 .avi 文件、动画的 .jpg 文件等等。
我们连接到摄像头,选择分辨率,然后就可以开始了:
VideoCaptureDevice videoSource = new
VideoCaptureDevice(form.VideoDevice);
foreach (var cap in device.VideoCapabilities)
{
if (cap.FrameSize.Height == 240)
return cap;
if (cap.FrameSize.Width == 320)
return cap;
}
return device.VideoCapabilities.Last();
现在应用程序正在运行,并且我们已选择了视频源,我们的应用程序将看起来像这样。再次,请输入法尼奇牛头犬!请原谅这儿的混乱;法尼奇不是一个整洁的助手,他甚至把他的空咖啡杯留在了我的桌子上!

在这个演示中,你会注意到法尼奇正对着摄像头,在背景中,我们还有两个 55 英寸的显示器以及许多我妻子喜欢称之为垃圾的其他物品。我自己更喜欢把它看作是随机噪声!这是为了展示人脸检测算法如何在其他所有东西中区分法尼奇的脸。如果我们的检测器无法处理这种情况,它就会在噪声中迷失方向,对我们几乎没有用处。
现在我们已经有了视频源输入,我们需要在接收到新帧时得到通知,以便我们可以处理它,应用我们的标记等等。我们通过附加到视频源播放器的 NewFrameReceived 事件处理器来完成此操作。作为一个 C#开发者,我假设你熟悉此类事件:
this.videoSourcePlayer.NewFrameReceived += new
Accord.Video.NewFrameEventHandler
(this.videoSourcePlayer_NewFrame);
现在我们已经有了视频源和视频输入,让我们看看每次我们收到通知说有新的视频帧可用时会发生什么。
我们首先需要做的是 downsample 图像,使其更容易处理:
ResizeNearestNeighbor resize = new ResizeNearestNeighbor(160, 120);
UnmanagedImage downsample = resize.Apply(im);
在将图像大小调整为更易于管理后,我们将处理帧。如果我们没有找到面部区域,我们将保持在跟踪模式中等待一个具有可检测面部的帧。一旦我们找到了面部区域,我们将重置我们的跟踪器,定位面部,减小其尺寸以清除任何背景噪声,初始化跟踪器,并将标记窗口应用于图像。所有这些操作都通过以下代码完成:
Rectangle[] regions = detector?.ProcessFrame(downsample);
if (regions != null && regions.Length > 0)
{
tracker?.Reset();
// Will track the first face found
Rectangle face = regions[0];
// Reduce the face size to avoid tracking background
Rectangle window = new Rectangle(
(int)((regions[0].X + regions[0].Width / 2f) * xscale),
(int)((regions[0].Y + regions[0].Height / 2f) *
yscale), 1, 1);
window.Inflate((int)(0.2f * regions[0].Width * xscale),
(int)(0.4f * regions[0].Height * yscale));
// Initialize tracker
if (tracker != null)
{
tracker.SearchWindow = window;
tracker.ProcessFrame(im);
}
marker = new RectanglesMarker(window);
marker.ApplyInPlace(im);
args.Frame = im.ToManagedImage();
tracking = true;
}
else
{
detecting = true;
}
一旦检测到面部,我们的图像帧看起来像这样:

如果法式倾斜他的头,我们的图像现在看起来像这样:

运动检测
你已经可以看到,我们不仅在进行面部检测,还在进行运动检测。所以让我们将注意力扩大到更广泛的范围,检测任何运动,而不仅仅是面部。同样,我们将使用 Accord.NET 来此,并使用运动检测示例。就像面部检测一样,你将看到如何简单地将此功能添加到您的应用程序中,并立即成为工作中的英雄!
使用运动检测,我们将用红色突出显示屏幕上移动的任何东西。运动的量由任何区域中红色的厚度表示。所以,使用以下图像,你可以看到手指在移动,但其他一切都没有运动:

随着手部运动的增加,你可以看到整个手部的运动增加:

一旦整个手开始移动,你不仅能看到更多的红色,还能看到相对于移动的红色总量增加:

如果我们不希望处理整个屏幕区域进行运动检测,我们可以定义运动区域;运动检测将仅在这些区域内发生。在以下图像中,你可以看到我已经定义了一个运动区域。你将在接下来的图像中注意到,这是唯一一个将处理运动的区域:

现在,如果我们为相机创建一些运动(手指移动),我们将看到只有我们定义区域内的运动被处理:

你还可以看到,在定义了运动区域并且彼得这个冥想的哥布林在区域前面时,我们仍然能够检测到他后面的运动,同时过滤掉不感兴趣的项目;但他的面部不是识别的一部分。当然,你可以结合这两个过程,以获得两者的最佳效果:

我们还可以使用的另一个选项是网格运动高亮。这将在定义的网格中用红色方块突出显示检测到的运动区域。基本上,运动区域现在是一个红色框,正如你所看到的那样:

将检测添加到您的应用程序中
下面是一个简单示例,展示了你需要在应用程序中添加视频识别所需的所有操作。正如你所见,这简直不能再简单了!
// create motion detector
MotionDetector detector = new MotionDetector(
new SimpleBackgroundModelingDetector(),
new MotionAreaHighlighting());
// continuously feed video frames to motion detector
while ()
{
// process new video frame and check motion level
if (detector.ProcessFrame(videoFrame) > 0.02)
{
// ring alarm or do somethng else
}
}
Opening our video source
videoSourcePlayer.VideoSource = new AsyncVideoSource(source);
当我们接收到一个新的视频帧时,所有的魔法就开始了。以下是使处理新视频帧成功所需的全部代码:
private void videoSourcePlayer_NewFrame(object sender,
NewFrameEventArgs args)
{
lock (this)
{
if (detector != null)
{
float motionLevel = detector.ProcessFrame(args.Frame);
if (motionLevel > motionAlarmLevel)
{
// flash for 2 seconds
flash = (int)(2 * (1000 / alarmTimer.Interval));
}
// check objects' count
if (detector.MotionProcessingAlgorithm is BlobCountingObjectsProcessing)
{
BlobCountingObjectsProcessing countingDetector =
(BlobCountingObjectsProcessing)
detector.MotionProcessingAlgorithm;
detectedObjectsCount = countingDetector.ObjectsCount;
}
else
{
detectedObjectsCount = -1;
}
// accumulate history
motionHistory.Add(motionLevel);
if (motionHistory.Count > 300)
{
motionHistory.RemoveAt(0);
}
if (showMotionHistoryToolStripMenuItem.Checked)
DrawMotionHistory(args.Frame);
}
}
关键在于检测帧中发生的运动量,这可以通过以下代码完成。对于这个例子,我们使用的是运动警报级别2,但你可以使用你喜欢的任何值。一旦这个阈值被超过,你可以实现你想要的逻辑,例如发送警报电子邮件、短信,以及开始视频捕获等等:
float motionLevel = detector.ProcessFrame(args.Frame);
if (motionLevel > motionAlarmLevel)
{
// flash for 2 seconds
flash = (int)(2 * (1000 / alarmTimer.Interval));
}
摘要
在本章中,我们学习了面部和动作检测。我们发现了你可以在当前环境中使用,以便轻松将此功能集成到你的应用程序中的各种算法。我们还展示了一些简单且易于使用的代码,你可以用它们快速添加这些功能。在下一章中,我们将步入人工神经网络的世界,解决一些非常激动人心的问题!
第八章:百科全书和神经元 – 旅行商问题
在本章中,我们将解决机器学习者面临的最著名的问题之一。我们还将进入图论的世界(只是稍微了解一下)以及神经网络神经元。让我们先从解释旅行商问题开始,怎么样?
在本章中,我们将涵盖:
-
旅行商问题
-
学习率参数
旅行商问题
我们有一个必须旅行于n个城市之间的旅行商。他不在乎这个顺序是怎样的,也不在乎他首先或最后访问哪个城市。他唯一关心的是他只访问每个城市一次,并最终回到起点。
每个城市是一个节点,每个节点通过边与其他相邻节点相连(想象它就像一条路、飞机、火车、汽车等等)。
现在,每个这些连接都关联着一个或多个权重,我们将称之为成本。
成本描述了沿着该连接旅行的难度,例如飞机票的成本、汽车所需的汽油量等等。
我们的旅行商有一个老板,就像我们在第一章“机器学习基础”中遇到的那样,所以他的命令是尽可能降低成本和旅行距离。
你可能会问:“这对我现实生活中有什么用?”这个问题实际上在现实生活中有几种应用,例如
-
为您的计算机设计电路板。由于有数百万个晶体管,电路板需要精确地钻孔和制造。
-
它也出现在 DNA 测序的子问题中,这已经成为许多人机器学习的一部分。
对于那些已经学习过或熟悉图论的人来说,你希望记得无向加权图。这正是我们在这里处理的内容。城市是顶点,路径是边,路径距离是边权重。你没想到会再次使用这些知识,对吧?本质上,我们有一个最小化问题,即在访问过每个其他顶点一次后,从特定的顶点开始并结束。实际上,我们可能最终得到一个完全图,其中每对顶点都通过边相连。
接下来,我们必须谈谈不对称性和对称性,因为这个问题可能最终会变成其中之一。我们究竟是什么意思呢?嗯,我们可能有一个不对称的旅行商问题或一个对称的旅行商问题。这完全取决于两个城市之间的距离。如果每个方向上的距离都相同,我们就有一个对称的旅行商问题,对称性有助于我们找到可能的解决方案。如果路径在两个方向上都不存在,或者距离不同,我们就有一个有向图。下面是一个展示上述描述的图表:

旅行商问题可以是对称的也可以是不对称的。在本章中,我们将带您进入遗传算法的奇妙领域。让我们从一个极其简化的描述开始,看看将要发生什么。
在生物学领域,当我们想要创建一个新的基因型时,我们会从父母A那里拿一点,从父母B那里拿剩下的。如果你正在更新你的时髦词汇清单,这被称为交叉变异!在这个发生之后,这些基因型会受到干扰,或者轻微地改变。这被称为变异(再次更新你的时髦词汇清单),这就是如何创造遗传物质的。
接下来,我们删除原始一代,用新一代替换,并对每个基因型进行测试。新的基因型,作为它们先前成分的较好部分,现在将偏向于更高的适应度;平均而言,这一代应该比前一代得分更高。
这个过程会持续很多代,随着时间的推移,种群的平均适应度将演变并增加。这并不总是有效,就像现实生活中一样,但一般来说,它是有效的。
在遗传算法编程研讨会之后,让我们深入我们的应用。
这里是我们的示例应用的样子。它基于 Accord.NET 框架。在我们定义了需要访问的房屋数量之后,我们只需点击生成按钮:

在我们的测试应用中,我们可以非常容易地更改我们想要访问的房屋数量,如高亮区域所示。
我们可能有一个非常简单的问题空间,或者一个更复杂的问题空间。这里是一个非常简单的问题空间的例子:

这里是一个更复杂的问题空间的例子:

我们还有三种不同的选择方法供我们的算法使用,即精英、排名和轮盘赌:

-
精英:指定下一代中要工作的最佳染色体的数量。
-
轮盘赌:根据染色体的排名(适应度值)选择染色体。
-
排名:根据染色体的排名(适应度值)选择染色体。这与轮盘赌选择方法不同,因为在计算中轮盘和扇区的大小不同。
最后,我们选择算法要使用的总迭代次数。我们选择计算路线按钮,如果一切顺利,我们最终会得到一个与这个类似的地图:

让我们看看当我们选择我们想要的城市的数量然后点击生成按钮时会发生什么:
private void GenerateMap( )
{
Random rand = new Random( (int) DateTime.Now.Ticks );
// create coordinates array
map = new double[citiesCount, 2];
for ( int i = 0; i < citiesCount; i++ )
{
map[i, 0] = rand.Next( 1001 );
map[i, 1] = rand.Next( 1001 );
}
// set the map
mapControl.UpdateDataSeries( "map", map );
// erase path if it is
mapControl.UpdateDataSeries( "path", null );
}
我们首先做的事情是初始化我们的随机数生成器并对其进行播种。接下来,我们获取用户指定的城市总数,然后根据这个创建一个新的数组。最后,我们绘制每个点并更新我们的地图。这个地图是来自 Accord.NET 的图表控件,将为我们处理大量的视觉绘图。
完成这些后,我们就准备好计算我们的路线,并(希望)解决这个问题。
接下来,让我们看看我们的主要搜索解决方案是什么样子:
Neuron.RandRange = new Range( 0, 1000 );
DistanceNetwork network = new DistanceNetwork( 2, neurons );
ElasticNetworkLearning trainer = new ElasticNetworkLearning(
network );
double fixedLearningRate = learningRate / 20;
double driftingLearningRate = fixedLearningRate * 19;
double[,] path = new double[neurons + 1, 2];
double[] input = new double[2];
int i = 0;
while ( !needToStop )
{
trainer.LearningRate = driftingLearningRate * ( iterations - i ) / iterations + fixedLearningRate;
trainer.LearningRadius = learningRadius * ( iterations - i ) / iterations;
int currentCity = rand.Next( citiesCount );
input[0] = map[currentCity, 0];
input[1] = map[currentCity, 1];
trainer.Run( input );
for ( int j = 0; j < neurons; j++ )
{
path[j, 0] = network.Layers[0].Neurons[j].Weights[0];
path[j, 1] = network.Layers[0].Neurons[j].Weights[1];
}
path[neurons, 0] = network.Layers[0].Neurons[0].Weights[0];
path[neurons, 1] = network.Layers[0].Neurons[0].Weights[1];
chart?.UpdateDataSeries( "path", path );
i++;
SetText( currentIterationBox, i.ToString( ) );
if ( i >= iterations )
break;
}
让我们尝试将所有这些分解成更可用的块供你使用。我们首先确定我们将使用什么选择方法来排名我们的染色体:
// create fitness function
TSPFitnessFunction fitnessFunction = new TSPFitnessFunction( map );
// create population
Population population = new Population( populationSize,
( greedyCrossover ) ? new TSPChromosome( map ) : new PermutationChromosome( citiesCount ),
fitnessFunction,
( selectionMethod == 0 ) ? (ISelectionMethod) new EliteSelection( ) :
( selectionMethod == 1 ) ? (ISelectionMethod) new RankSelection( ) :
(ISelectionMethod) new RouletteWheelSelection( ));
我想借此机会指出你在这里看到的TSPChromosome。这个对象基于一个短数组染色体(一个范围从 2 到 65,536 的无符号短整数值的数组)。有两个特定的特性:
-
所有基因在染色体中都是唯一的,这意味着没有两个基因具有相同的值
-
每个基因的最大值等于染色体长度减 1
接下来,我们必须为我们创建一个路径变量来填充我们的数据点:
double[,] path = new double[citiesCount + 1, 2];
完成此步骤后,我们可以进入我们的while循环并开始处理。为此,我们将通过运行一个 epoch 来处理单个生成。你可以把 epoch 看作是一个迭代:
// run one epoch of genetic algorithm
RILogManager.Default?.SendDebug("Running Epoch " + i);
population.RunEpoch( );
我们然后从那个努力中获得最佳值:
ushort[] bestValue = ((PermutationChromosome) population.BestChromosome).Value;
我们在每座城市之间更新和创建我们的路径:
for ( int j = 0; j < citiesCount; j++ )
{
path[j, 0] = map[bestValue[j], 0];
path[j, 1] = map[bestValue[j], 1];
}
path[citiesCount, 0] = map[bestValue[0], 0];
path[citiesCount, 1] = map[bestValue[0], 1];
然后将该值提供给我们的图表控件:
mapControl.UpdateDataSeries( "path", path );
让我们看看根据我们选择的排名方法,我们的路线可能是什么样子的一些例子。
精英选择:

排名选择:

这与轮盘赌选择方法的区别在于轮盘和其扇区大小计算方法。轮盘的大小等于size * (size +1) / 2,其中size是当前种群的大小。最差的染色体其扇区大小等于 1,下一个大小为 2,以此类推。
轮盘赌选择:

这个算法根据它们的适应度值选择新一代的染色体。值越高,成为新一代成员的机会就越大。
当你生成你的路线时,你会注意到精英方法立即找到了它的解决方案。排名方法在整个迭代过程中持续优化其路线,而轮盘赌方法则进一步优化其路线。
为了说明我的意思,定义一个巨大的销售员今天的负载。比如说他今天要访问 200 座房子,因为我们需要今天卖很多百科全书。这就是我们算法之美的所在。如果我们处理的是五座房子,很容易创建最优的地图和路线。但如果我们处理的是 200 座房子,那就大不相同了!

现在我们已经解决了这个问题,让我们看看我们能否将我们从之前章节中学到的关于自组织映射(SOM)的知识应用到这个问题上,从不同的角度来解决这个问题。如果你还记得,在第六章,“颜色混合 - 自组织映射和弹性神经网络”中,我们讨论了 SOM 的一般情况。所以我们将避免在这里发生学术讨论!我们将使用一种称为弹性网络训练的技术,这是一种针对我们这里的问题的非常好的无监督方法。

让我们先简要地谈谈什么是弹性地图。弹性地图提供了一种创建非线性降维的工具。它们是我们数据空间中弹性弹簧的系统,近似于低维流形。有了这种能力,我们可以从完全无结构的聚类(无弹性)到更接近线性主成分分析流形,弹簧有高弯曲/低拉伸。当你使用我们的示例应用程序时,你会发现线条并不一定像我们之前的解决方案那样僵硬。在许多情况下,它们甚至可能不会进入我们访问的城市中心(线条从中心生成)但仅接近城市边缘,就像前面的例子一样!
再次强调,我们将处理神经元,这是我所有最爱之一。这次我们将有更多的控制权,因为我们能够指定我们的学习率和半径。就像我们之前的例子一样,我们将能够指定我们的旅行商今天必须访问的总城市数。不过这次我们对他宽容一些吧!
首先,我们将访问 50 个城市,并使用学习率0.3和半径0.75。最后,我们将运行 50,000 次迭代(别担心;这会很快完成)。我们的输出将看起来像这样:

现在,如果我们把半径改为不同的值,比如 0.25,会发生什么呢?注意一些城市之间的角度变得更为明显:

接下来,让我们将学习率从 0.3 改为 0.75:

尽管我们的路线最终看起来非常相似,但有一个重要的区别。在上一个例子中,我们的旅行商的路线图是在所有迭代完成后才绘制的。通过提高学习率,路线图会在完美的路线完成之前绘制几次。以下是一些显示进度的图片:

我们现在处于我们的解决方案的第 5777 次迭代:

这显示了我们的解决方案在第 44636 次迭代时的样子。

这一张显示了我们的解决方案在第 34299 次迭代时的样子:

现在,让我们看看一小段代码,看看我们的搜索解决方案这次有何不同:
// create fitness function
TSPFitnessFunction fitnessFunction = new TSPFitnessFunction( map );
// create population
Population population = new Population( populationSize,
( greedyCrossover ) ? new TSPChromosome( map ) : new PermutationChromosome( citiesCount ),
fitnessFunction, ( selectionMethod == 0 ) ? new EliteSelection( )
: ( selectionMethod == 1 ) ? new RankSelection( ) :
(ISelectionMethod) new RouletteWheelSelection( ));
// iterations
int i = 1;
// path
double[,] path = new double[citiesCount + 1, 2];
// loop
while ( !needToStop )
{
// run one epoch of genetic algorithm
RILogManager.Default?.SendDebug("Running Epoch " + i);
population.RunEpoch( );
// display current path
ushort[] bestValue = ((PermutationChromosome) population.BestChromosome).Value;
for ( int j = 0; j < citiesCount; j++ )
{
path[j, 0] = map[bestValue[j], 0];
path[j, 1] = map[bestValue[j], 1];
}
path[citiesCount, 0] = map[bestValue[0], 0];
path[citiesCount, 1] = map[bestValue[0], 1];
mapControl.UpdateDataSeries( "path", path );
// set current iteration's info
SetText( currentIterationBox, i.ToString( ) );
SetText( pathLengthBox, fitnessFunction.PathLength( population.BestChromosome ).ToString( ) );
// increase current iteration
i++;
//
if ( ( iterations != 0 ) && ( i > iterations ) )
break;
}
您首先看到我们做的事情是创建一个DistanceNetwork对象。这个对象只包含一个DistanceLayer,即一个距离神经元层。距离神经元通过计算其权重和输入之间的距离来输出其输出——权重值和输入值之间绝对差异的总和。所有这些共同构成了我们的 SOM(自组织映射)和,更重要的是,我们的弹性网络。
接下来,我们必须用一些随机权重初始化我们的网络。我们将通过为每个神经元创建均匀连续分布来完成此操作。均匀连续分布,或矩形分布,是一种对称的概率分布,对于该家族的每个成员,分布支持上相同长度的所有区间都有相同的概率。您通常会看到它写成 U(a, b),其中参数a和b分别是最小值和最大值。
foreach (var neuron in network.Layers.SelectMany(layer => layer?.Neurons).Where(neuron => neuron != null))
{
neuron.RandGenerator = new UniformContinuousDistribution(new Range(0, 1000));
}
接下来,我们创建我们的弹性学习器对象,它允许我们训练我们的距离网络:
ElasticNetworkLearning trainer = new ElasticNetworkLearning(network);
这是ElasticNetworkLearning构造函数的内部样子:

现在我们计算我们的学习率和半径:
double fixedLearningRate = learningRate / 20;
double driftingLearningRate = fixedLearningRate * 19;
最后,我们进入了我们的中央处理循环,我们将一直保持在这里,直到被告知停止:
while (!needToStop)
{
// update learning speed & radius
trainer.LearningRate = driftingLearningRate * (iterations - i) / iterations + fixedLearningRate;
trainer.LearningRadius = learningRadius * (iterations - i) / iterations;
// set network input
int currentCity = rand.Next(citiesCount);
input[0] = map[currentCity, 0];
input[1] = map[currentCity, 1];
// run one training iteration
trainer.Run(input);
// show current path
for (int j = 0; j < neurons; j++)
{
path[j, 0] = network.Layers[0].Neurons[j].Weights[0];
path[j, 1] = network.Layers[0].Neurons[j].Weights[1];
}
path[neurons, 0] = network.Layers[0].Neurons[0].Weights[0];
path[neurons, 1] = network.Layers[0].Neurons[0].Weights[1];
chart.UpdateDataSeries("path", path);
i++;
SetText(currentIterationBox, i.ToString());
if (i >= iterations)
break;
}
在前面的循环中,训练器在每个循环增量中运行一个 epoch(迭代)。以下是trainer.Run函数的样子,这样您可以看到发生了什么。基本上,该方法找到获胜神经元(其权重值最接近指定输入向量的神经元)。然后它更新其权重以及邻近神经元的权重:
public double Run( double[] input )
{
double error = 0.0;
// compute the network
network.Compute( input );
int winner = network.GetWinner( );
// get layer of the network
Layer layer = network.Layers[0];
// walk through all neurons of the layer
for ( int j = 0; j < layer.Neurons.Length; j++ )
{
Neuron neuron = layer.Neurons[j];
// update factor
double factor = Math.Exp( -distance[Math.Abs( j - winner )] / squaredRadius2 );
// update weights of the neuron
for ( int i = 0; i < neuron.Weights.Length; i++ )
{
// calculate the error
double e = ( input[i] - neuron.Weights[i] ) * factor;
error += Math.Abs( e );
// update weight
neuron.Weights[i] += e * learningRate;
}
}
return error;
}
我们将要深入了解的这种方法的主要两个功能是计算网络和获取获胜者(高亮显示的项目)。
我们如何计算网络?基本上,我们通过距离层逐层向下工作,进入每个神经元,以正确更新权重,类似于您在这里看到的情况:
public virtual double[] Compute( double[] input )
{
// local variable to avoid mutlithread conflicts
double[] output = input;
// compute each layer
for ( int i = 0; i < layers.Length; i++ )
{
output = layers[i].Compute( output );
}
// assign output property as well (works correctly for single threaded usage)
this.output = output;
return output;
}
最后,我们需要计算获胜者,即权重最小、距离最小的神经元:
public int GetWinner( )
{
// find the MIN value
double min = output[0];
int minIndex = 0;
for ( int i = 1; i < output.Length; i++ )
{
if ( output[i] < min )
{
// found new MIN value
min = output[i];
minIndex = i;
}
}
return minIndex;
}
让我们简要谈谈您可以在屏幕上输入的参数。
学习率参数
学习率是一个决定学习速度的参数。更正式地说,它决定了我们根据损失梯度调整网络权重的程度。如果它太低,我们在斜坡上移动的速度会变慢。尽管我们希望学习率低,但这可能意味着我们将花费很长时间才能达到收敛。学习率还会影响我们的模型多快能收敛到局部最小值(最佳精度)。
当处理神经元时,它决定了用于训练的权重神经元的获取时间(学习新经验所需的时间)。

各种学习率对收敛的影响(图片来源:cs231n)
学习半径
学习半径决定了围绕获胜神经元需要更新的神经元数量。在学习过程中,任何位于半径圆内的神经元都将被更新。神经元越靠近,更新的次数越多;距离越远,更新的次数越少。
摘要
在本章中,我们学习了神经元,这是一个非常迷人的研究领域,多年来一直备受关注。我们还学习了著名的旅行商问题,它是什么,以及我们如何用计算机来解决它。这个小小的例子在现实世界中有着广泛的应用。在我们下一章中,我们将把我们所获得的所有神经知识应用到受限玻尔兹曼机(RBM)和深度信念网络(DBN)上。这一章肯定会为你的术语清单增添许多术语!在下一章中,我们将回答我们作为开发者都面临的问题:我应该接受这份工作吗?
第九章:我应该接受这份工作吗——决策树的实际应用
本章将重点关注:
-
决策树
-
如何为您的应用程序创建决策树
-
理解真值表
-
关于假阴性和假阳性的视觉直觉
在我们深入探讨之前,让我们获取一些对我们有帮助的背景信息。
为了使决策树完整且有效,它必须包含所有可能性,这意味着每个进入和离开的路径。事件序列也必须提供,并且是互斥的,这意味着如果一个事件发生,另一个事件就不能发生。
决策树是一种监督式机器学习的形式,这意味着我们必须解释输入和输出应该是什么。决策树中有决策节点和叶子节点。叶子节点是决策点,无论是最终决策还是非最终决策,而节点是决策分支发生的地方。
虽然有许多算法可供我们使用,但我们将使用迭代二分器 3(ID3)算法。在每次递归步骤中,根据一个标准(信息增益、增益率等)选择最佳分类我们正在处理的输入集的属性。必须指出的是,无论我们使用哪种算法,都没有保证能够产生可能的最小树。这直接影响了我们算法的性能。记住,在决策树中,学习仅基于启发式,而不是真正的优化标准。让我们用一个例子来进一步解释这一点。
以下例子来自jmlr.csail.mit.edu/papers/volume8/esmeir07a/esmeir07a.pdf,它说明了 XOR 学习概念,这是我们所有开发者(或应该熟悉)的。你将在后面的例子中看到这一点,但现在,a[3]和a[4]对我们试图解决的问题完全无关。它们对我们的答案没有影响。话虽如此,ID3 算法将选择其中之一属于树,实际上,它将使用a[4]作为根!记住,这是算法的启发式学习,而不是优化发现:

希望这个视觉图能更容易地理解我们的意思。这里的目的是不要深入决策树的机械和理论。在所有这些之后,你可能会问为什么我们还在谈论决策树。尽管它们可能存在任何问题,决策树仍然是许多算法的基础,特别是那些需要人类描述结果的算法。它们也是我们在前一章中使用过的 Viola & Jones(2001)实时面部检测算法的基础。作为一个更好的例子,微软 Xbox 360 的Kinect也使用了决策树。
我们将再次转向 Accord.NET 开源框架来阐述我们的概念。在我们的示例中,我们将处理以下决策树对象,因此最好提前讨论它们。
决策树
这是我们的主要类。
决策节点
这是我们的决策树中的一个节点。每个节点可能或可能没有与之关联的子节点。
决策变量
这个对象定义了树和节点可以处理的每个决策变量的性质。这些值可以是范围、连续或离散的。
决策分支节点集合
这个集合包含了一组决策节点,以及关于它们决策变量的额外信息,以便进行比较。
这里是一个用于确定财务风险的决策树的示例。通过简单地导航节点,决定走哪条路,直到得到最终答案,它非常容易跟随。在这种情况下,有人正在申请信贷,我们需要对其信用度做出决定。决策树是解决这个问题的绝佳方法:

在我们有了这个简单的视觉图之后,让我们来看看我们试图解决的问题。这是我们所有开发者(希望如此)时不时都会遇到的问题。
我是否应该接受这份工作?
你刚刚得到了一份新的工作,你需要决定是否接受这份工作。有一些事情对你来说很重要,所以我们将使用这些作为决策树的输入变量或特征。以下是你认为重要的因素:薪酬、福利、公司文化和当然,我能否在家工作?
我们将不会从磁盘存储加载数据,而是将创建一个内存数据库,并通过这种方式添加我们的特征。我们将创建DataTable并创建Columns作为特征,如下所示:
DataTable data = new DataTable("Should I Go To Work For Company
X");
data.Columns.Add("Scenario");
data.Columns.Add("Pay");
data.Columns.Add("Benefits");
data.Columns.Add("Culture");
data.Columns.Add("WorkFromHome");
data.Columns.Add("ShouldITakeJob");
在此之后,我们将加载几行数据,每行都有不同的特征集,以及我们的最后一列ShouldITakeJob,它可以是是或否,作为我们的最终决定:
data.Rows.Add("D1", "Good", "Good", "Mean", "Yes", "Yes");
data.Rows.Add("D2", "Good", "Good", "Mean", "No", "Yes");
data.Rows.Add("D3", "Average", "Good", "Good", "Yes", "Yes");
data.Rows.Add("D4", "Average", "Good", "Good", "No", "Yes");
data.Rows.Add("D5", "Bad", "Good", "Good", "Yes", "No");
data.Rows.Add("D6", "Bad", "Good", "Good", "No", "No");
data.Rows.Add("D7", "Good", "Average", "Mean", "Yes", "Yes");
data.Rows.Add("D8", "Good", "Average", "Mean", "No", "Yes");
data.Rows.Add("D9", "Average", "Average", "Good", "Yes", "No");
data.Rows.Add("D10", "Average", "Average", "Good", "No", "No");
data.Rows.Add("D11", "Bad", "Average", "Good", "Yes", "No");
data.Rows.Add("D12", "Bad", "Average", "Good", "No", "No");
data.Rows.Add("D13", "Good", "Bad", "Mean", "Yes", "Yes");
data.Rows.Add("D14", "Good", "Bad", "Mean", "No", "Yes");
data.Rows.Add("D15", "Average", "Bad", "Good", "Yes", "No");
data.Rows.Add("D16", "Average", "Bad", "Good", "No", "No");
data.Rows.Add("D17", "Bad", "Bad", "Good", "Yes", "No");
data.Rows.Add("D18", "Bad", "Bad", "Good", "No", "No");
data.Rows.Add("D19", "Good", "Good", "Good", "Yes", "Yes"); data.Rows.Add("D20", "Good", "Good", "Good", "No", "Yes");
一旦所有数据都创建并放入我们的表中,我们需要将我们之前的功能以计算机可以理解的形式表示出来。由于所有我们的特征都是类别,如果我们保持一致,那么我们如何表示它们并不重要。由于数字更容易处理,我们将通过称为编码的过程将我们的特征(类别)转换为代码簿。这个代码簿有效地将每个值转换为一个整数。请注意,我们将我们的data类别作为输入传递:
Codification codebook = new Codification(data);
接下来,我们需要为我们的决策树创建决策变量。树将试图帮助我们确定是否应该接受我们的新工作邀请。对于这个决定,将有几个输入类别,我们将它们指定在我们的决策变量数组中,以及两个可能的决策,是或否。
DecisionVariable数组将保存每个类别的名称以及该类别可能属性的总量。例如,Pay类别有三个可能的值,Good、Average或Poor。因此,我们指定类别名称和数字3。然后我们对所有其他类别重复此操作,除了最后一个,即我们的决策:
DecisionVariable[] attributes =
{
new DecisionVariable("Pay", 3),
new DecisionVariable("Benefits", 3),
new DecisionVariable("Culture", 3),
new DecisionVariable("WorkFromHome", 2)
};
int outputValues = 2; // 2 possible output values: yes or no
DecisionTree tree = new DecisionTree(attributes, outputValues);
现在我们已经创建了决策树,我们必须教会它我们要解决的问题。在这个阶段,它实际上什么也不知道。为了做到这一点,我们必须为树创建一个学习算法。在我们的例子中,那将是之前讨论过的 ID3 算法。由于这个样本只有分类值,ID3 算法是最简单的选择。请随意将其替换为 C4.5、C5.0 或您想尝试的任何其他算法:
ID3Learning id3 = new ID3Learning(tree);
Now, with our tree fully created and ready to go, we
are ready to classify new samples.
// Translate our training data into integer symbols using our codebook:
DataTable symbols = codebook.Apply(data);
int[][] inputs = symbols.ToArray<int>("Pay", "Benefits", "Culture",
"WorkFromHome");
int[] outputs = symbols.ToIntArray("ShouldITakeJob").GetColumn(0);
// Learn the training instances!
id3.Run(inputs, outputs);
一旦运行了学习算法,它就被训练好并准备好使用。我们只需向算法提供一个样本数据集,它就能给我们一个答案。在这种情况下,工资好,公司文化好,福利好,我可以在家工作。如果决策树训练得当,答案将是响亮的Yes:
int[] query = codebook.Translate("Good", "Good", "Good", "Yes");
int output = tree.Compute(query);
string answer = codebook.Translate("ShouldITakeJob", output);
// answer will be "Yes".
接下来,我们将关注使用numl开源机器学习包来向您展示另一个训练和使用决策树的示例。
numl
numl是一个非常著名的开源机器学习工具包。与大多数机器学习框架一样,它也使用Iris数据集的许多示例,包括我们将用于决策树的示例。
这里是numl输出的一个示例:

让我们看看那个示例背后的代码:
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
var description = Descriptor.Create<Iris>();
Console.WriteLine(description);
var generator = new DecisionTreeGenerator();
var data = Iris.Load();
var model = generator.Generate(description, data);
Console.WriteLine("Generated model:");
Console.WriteLine(model);
Console.ReadKey();
}
这绝对不是最复杂的函数,对吧?这就是在您的应用程序中使用numl的美丽之处;它极其容易使用和集成。
上述代码创建了一个描述符和DecisionTreeGenerator,加载了Iris数据集,然后生成了一个模型。这里只是加载的数据的一个样本:
public static Iris[] Load()
{
return new Iris[]
{
new Iris { SepalLength = 5.1m, SepalWidth = 3.5m, PetalLength =
1.4m, PetalWidth = 0.2m, Class = "Iris-setosa" },
new Iris { SepalLength = 4.9m, SepalWidth = 3m, PetalLength =
1.4m, PetalWidth = 0.2m, Class = "Iris-setosa" },
new Iris { SepalLength = 4.7m, SepalWidth = 3.2m, PetalLength =
1.3m, PetalWidth = 0.2m, Class = "Iris-setosa" },
new Iris { SepalLength = 4.6m, SepalWidth = 3.1m, PetalLength =
1.5m, PetalWidth = 0.2m, Class = "Iris-setosa" },
new Iris { SepalLength = 5m, SepalWidth = 3.6m, PetalLength =
1.4m, PetalWidth = 0.2m, Class = "Iris-setosa" },
new Iris { SepalLength = 5.4m, SepalWidth = 3.9m, PetalLength =
1.7m, PetalWidth = 0.4m, Class = "Iris-setosa" },
等等...
Accord.NET 决策树
Accord.NET 框架也有自己的决策树示例,我们应该指出这一点。它采用不同的、更图形化的方法来处理决策树,但选择权在您手中,您可以选择您喜欢和感觉最舒适的方法。
一旦数据加载完毕,您就可以创建决策树并为其学习做好准备。您将看到类似于这里的数据图,使用两个类别 X 和 Y:

下一个标签页将让您查看树节点、叶子和决策。在右侧还有一个树的从上到下的图形视图。最有用的信息位于左侧的树视图中,您可以在这里看到节点、它们的值和所做的决策:

最后,最后一个标签页将允许您执行模型测试:

学习代码
以下是一些学习代码:
// Specify the input variables
DecisionVariable[] variables =
{
new DecisionVariable("x", DecisionVariableKind.Continuous),
new DecisionVariable("y", DecisionVariableKind.Continuous),
};
// Create the C4.5 learning algorithm
var c45 = new C45Learning(variables);
// Learn the decision tree using C4.5
tree = c45.Learn(inputs, outputs);
// Show the learned tree in the view
decisionTreeView1.TreeSource = tree;
// Get the ranges for each variable (X and Y)
DoubleRange[] ranges = table.GetRange(0);
// Generate a Cartesian coordinate system
double[][] map = Matrix.Mesh(ranges[0],
200, ranges[1], 200);
// Classify each point in the Cartesian coordinate system
double[,] surface = map.ToMatrix().
InsertColumn(tree.Decide(map));
CreateScatterplot(zedGraphControl2, surface);
//Testing
// Creates a matrix from the entire source data table
double[][] table = (dgvLearningSource.DataSource as
DataTable).ToJagged(out columnNames);
// Get only the input vector values (first two columns)
double[][] inputs = table.GetColumns(0, 1);
// Get the expected output labels (last column)
int[] expected = table.GetColumn(2).ToInt32();
// Compute the actual tree outputs and turn
a Boolean into a 0 or 1
int[] actual = tree.Decide(inputs);
// Use confusion matrix to compute some statistics.
ConfusionMatrix confusionMatrix =
new ConfusionMatrix(actual, expected, 1, 0);
dgvPerformance.DataSource = new [] { confusionMatrix };
// Create performance scatter plot
CreateResultScatterplot(zedGraphControl1,
inputs, expected.ToDouble(), actual.ToDouble());
// As you can see above, the tree is making
the decision via the code line
int[] actual = tree.Decide(inputs);
这个值随后被输入到ConfusionMatrix中。对于那些不熟悉这个的人来说,让我简要解释一下。
混淆矩阵
混淆矩阵是一个用于描述分类模型性能的表格。它在一个已知真实值的测试数据集上运行。这就是我们得到如下内容的方式。
真阳性
这是一个我们预测会发生,而且事实确实发生了的情况。
真阴性
这是一个我们预测不会发生,而且事实确实发生了的情况。
假阳性
这是一个我们预测会发生的但事实并非如此的情况。你有时可能会看到这种情况被称为I 型错误。
假阴性
这是一个我们预测不会发生但事实确实发生了的情况。你有时可能会看到这种情况被称为II 型错误。
现在,说到这里,我们需要讨论另外两个重要的术语,精确度和召回率。
让我们这样描述它们。在过去的一周里,每天都下雨。那就是 7 天中的 7 天。很简单。一周后,你被问及上周下雨的频率是多少?
召回率
这是你在那一周内正确回忆起下雨的天数与总正确事件的数量的比率。如果你说下了 7 天雨,那就是 100%。如果你说下了 4 天雨,那么那就是 57%的召回率。在这种情况下,这意味着你的回忆并不那么精确,所以我们有精确度来识别。
精确度
这是你在那一周内正确回忆起将要下雨的次数与那一周总天数的比率。
对于我们来说,如果我们的机器学习算法在召回率方面做得好,这并不意味着它在精确度方面也做得好。这说得通吗?这让我们进入了其他事物,比如 F1 分数,我们将在另一天讨论。
错误类型可视化
这里有一些可能有助于理解的可视化:

真阳性与假阴性的识别:

在使用混淆矩阵计算统计数据后,会创建散点图,并识别所有内容:

摘要
在本章中,我们投入了大量时间来介绍决策树;它们是什么,我们如何使用它们,以及它们如何在我们应用中带来好处。在下一章中,我们将进入深度信念网络(DBNs)的世界,了解它们是什么,以及我们如何使用它们。我们甚至还会谈谈计算机做梦时的情况!
参考文献
-
Bishop, C. M., 2007. 模式识别与机器学习(信息科学和统计学). 第 1 版,2006 年校对第 2 次印刷版。s.l.: Springer.
-
Fayyad, U. M. & Irani, K. B., 1992.
deepblue.lib.umich.edu/bitstream/2027.42/46964/1/10994_2004_Article_422458.pdf. 机器学习, 1 月,8(1),第 87-102 页。 -
Quinlan, J. R., 1986.
www.dmi.unict.it/~apulvirenti/agd/Qui86.pdf. Machine Learning, 第 1 卷, 第 1 期, 第 81-106 页. -
Quinlan, J. R., 1993. C4.5: 机器学习程序 (Morgan Kaufmann 机器学习系列). 第 1 版. s.l.: Morgan Kaufmann.
-
Shotton, J. 等人, 2011.
research.microsoft.com/apps/pubs/default.aspx?id=145347. s.l., s.n. -
Viola, P. & Jones, M., 2001. 鲁棒实时目标检测. s.l., s.n.
-
Mitchell, T. M., 1997. 决策树学习. In:: 机器学习 (McGraw-Hill 计算机科学系列). s.l.: McGraw Hill.
-
Mitchell, T. M., 1997. 机器学习 (McGraw-Hill 计算机科学系列). 波士顿 (MA): WCB/McGraw-Hill.
-
Esmeir, S. & Markovitch, S., 2007.
jmlr.csail.mit.edu/papers/volume8/esmeir07a/esmeir07a.pdf. J. Mach. Learn. Res., 五月, 第 8 卷, 第 891-933 页. -
Hyafil, L. & Rivest, R. L., 1976. 构建最优二叉决策树是 NP 完全的. 信息处理快报, 第 5 卷, 第 1 期, 第 15-17 页.
第十章:深度信念 – 深度网络与梦境
我们都听说过深度学习,但有多少人知道深度信念网络是什么?让我们从这个章节开始,回答这个问题。深度信念网络是一种非常高级的机器学习方法,其含义正在迅速演变。作为一名机器学习开发者,了解这个概念很重要,这样当你遇到它或它遇到你时,你会熟悉它!
在机器学习中,深度信念网络在技术上是一种深度神经网络。我们应该指出,当提到深度学习或深度信念时,“深度”的含义是指网络由多个层(隐藏单元)组成。在深度信念网络中,这些连接在层内的每个神经元之间进行,但不在不同层之间。深度信念网络可以被训练以无监督地学习,以便以概率重建网络的输入。然后这些层作为“特征检测器”来识别或分类图像、字母等。你还可以观察深度信念网络做梦,这是一个非常有趣的话题。
在本章中,我们将涵盖:
-
受限玻尔兹曼机
-
使用 C#创建和训练深度信念网络
受限玻尔兹曼机
构建深度信念网络的一种流行方法是将其构建为一个由受限玻尔兹曼机(RBMs)组成的分层集合。这些 RBMs 作为自编码器运行,每个隐藏层都作为下一层的可见层。这种结构导致了一种快速、逐层和无监督的训练过程。深度信念网络在预训练阶段将包含 RBM 层,然后在微调阶段使用前馈网络。训练的第一步是从可见单元学习一层特征。下一步是将之前训练的特征的激活作为新的可见单元。然后我们重复这个过程,以便在第二个隐藏层中学习更多特征。然后这个过程会继续应用于所有隐藏层。
在这里,我们应该提供两条信息。
首先,我们应该解释一下什么是自编码器以及它做什么。自编码器是所谓表示学习的核心。它们编码输入,通常是显著特征的压缩向量,以及通过无监督学习重建的数据。
其次,我们应该注意,在深度信念网络中堆叠 RBMs 只是处理这个问题的一种方法。堆叠带有 dropout 的受限线性单元(ReLUs)并进行训练,然后配合反向传播,这再次成为了最先进的技术。我说再次是因为 30 年前,监督方法是唯一可行的方法。与其让算法查看所有数据并确定感兴趣的特征,有时我们作为人类实际上可以更好地找到我们想要的特征。
我认为深度信念网络最显著的两个特性如下:
-
存在一个高效、逐层的学习过程,用于学习自上而下的生成权重。它决定了某一层的变量如何依赖于其上层的变量。
-
学习完成后,可以通过从底层观察到的数据向量开始的单个自下而上的遍历,很容易地推断出每一层变量的值,并使用生成权重反向重建数据。
话虽如此,现在让我们也来谈谈 RBM 以及一般的霍尔兹曼机。
霍尔兹曼机是一种具有二元单元和单元之间无向边的循环神经网络。对于那些在图论课程中没注意听讲的同学,无向意味着边(或链接)是双向的,它们不指向任何特定方向。对于那些不熟悉图论的人来说,以下是一个具有无向边的无向图的示意图:

霍尔兹曼机是第一批能够学习内部表示的神经网络之一,并且如果给定足够的时间,它们可以解决难题。然而,它们在扩展方面并不擅长,这让我们转向下一个主题,即 RBMs。
RBM 被引入来处理霍尔兹曼机无法扩展的问题。它们有隐藏层,隐藏单元之间的连接受到限制,但不在这些单元之外,这有助于高效学习。更正式地说,我们必须稍微深入研究一下图论,才能正确解释这一点。
RBM 必须让它们的神经元形成所谓的二分图,这是一种更高级的图论形式;两组单元(可见层和隐藏层)中的每一对节点之间可能存在对称连接。任何一组内的节点之间不能有连接。二分图,有时称为双图,是一组图顶点分解为两个不相交的集合,使得同一集合内的两个顶点不相邻。
这里有一个很好的例子,可以帮助可视化这个主题。
注意,同一集合内(左侧的红色或右侧的黑色)没有连接,但两个集合之间存在连接:

更正式地说,RBM 被称为对称二分图。这是因为所有可见节点的输入都传递给所有隐藏节点。我们称之为对称,因为每个可见节点都与一个隐藏节点相关联;二分是因为有两个层次;而图是因为,嗯,它是一个图,或者如果你更喜欢,它是一组节点!
想象一下,我们的 RBM 被呈现了猫和狗的图像,并且我们有两个输出节点,一个用于每种动物。在我们的正向学习过程中,我们的 RBM 会问自己“看到这些像素,我应该为猫还是狗发送更强的权重信号?”在反向过程中,它会思考“作为一个狗,我应该看到什么样的像素分布?”朋友们,这就是今天关于联合概率的教训:给定A的X和给定X的A的同时概率。在我们的案例中,这个联合概率以两层之间的权重表示,并且是 RBMs 的一个重要方面。
在掌握了今天的联合概率和图论的小课程之后,我们现在将讨论重建,这是 RBMs(限制玻尔兹曼机)所做的重要部分。在我们一直在讨论的例子中,我们正在学习哪些像素组在一系列图像中发生(意味着处于开启状态)。当一个隐藏层节点被一个显著权重激活(无论这个权重是如何确定的以将其开启),它代表了某些事件同时发生的共现,在我们的案例中,是狗或猫。如果图像是一只猫,尖耳朵+圆脸+小眼睛可能就是我们要找的特征。大耳朵+长尾巴+大鼻子可能使图像成为一只狗。这些激活代表了我们的 RBM“认为”原始数据看起来像什么。从所有目的和用途来看,我们实际上正在重建原始数据。
我们还应该迅速指出,RBM 有两个偏差而不是一个。这一点非常重要,因为它将 RBM 与其他自动编码算法区分开来。隐藏偏差帮助我们的 RBM 在正向过程中产生所需的激活,而可见层偏差帮助在反向过程中学习正确的重建。隐藏偏差很重要,因为它的主要任务是确保在数据可能非常稀疏的情况下,某些节点仍然会激活。你将在稍后看到这如何影响深度信念网络做梦的方式。
层次化
一旦我们的 RBM 学会了输入数据的结构,这与我们在第一隐藏层中做出的激活有关,数据就会传递到下一个隐藏层。第一隐藏层随后成为新的可见层。我们在隐藏层中创建的激活现在成为我们的输入。它们将被新的隐藏层中的权重相乘,产生另一组激活。这个过程会继续通过我们网络中的所有隐藏层。隐藏层变成可见层,我们有了另一个我们将使用的权重的隐藏层,我们重复这个过程。每个新的隐藏层都会导致权重的调整,直到我们达到可以识别来自前一层的输入的点。
为了更详细地说明(帮助你保持术语的合规性),这从技术上讲被称为无监督、贪婪、分层训练。不需要输入来改进每一层的权重,这意味着没有任何类型的外部影响。这进一步意味着我们应该能够使用我们的算法在之前未见过的不监督数据上进行训练。正如我们一直强调的那样,数据越多,我们的结果越好!随着每一层变得更好,希望也更准确,我们就有更好的位置通过每一隐藏层增加我们的学习,权重在这个过程中负责引导我们到达正确的图像分类。
但当我们讨论重建时,我们应该指出,在我们重建努力中,每当一个数字(权重)不为零,这表明我们的 RBM 已经从数据中学习到了一些东西。从某种意义上说,你可以将返回的数字当作你对待百分比指示器一样来处理。数字越高,算法对其所看到的东西就越有信心。记住,我们有一个主数据集,我们试图恢复到这个数据集,并且我们有一个参考数据集用于我们的重建工作。随着我们的 RBM 遍历每一张图像,它还不知道它正在处理什么图像;这正是它试图确定的事情。
让我们简要澄清一下。当我们说我们使用贪婪算法时,我们真正意思是我们的 RBM 将采取最短路径以实现最佳结果。我们将从我们看到的图像中采样随机像素,并测试哪些像素能引导我们到达正确答案。RBM 将测试每个假设与主数据集(测试集)的对比,这是我们正确的最终目标。记住,每张图像只是我们试图分类的一组像素。这些像素包含了数据的特征和特性。例如,一个像素可以有不同亮度的阴影,其中深色像素可能表示边界,浅色像素可能表示数字,等等。
但当事情不按我们的意愿发展时会发生什么?如果我们在任何给定步骤中学到的任何东西都不正确,会发生什么?如果发生这种情况,这意味着我们的算法猜错了。我们的行动方案是回过头去再试一次。这并不像看起来那么糟糕,也不那么耗时。当然,一个错误假设有一个时间成本,但最终目标是我们必须提高我们的学习效率并减少每个阶段的错误。每个错误的加权连接将像我们在强化学习中做的那样受到惩罚。这些连接的权重将减少,不再那么强大。希望下一次遍历将提高我们的准确性,同时减少错误,权重越强,它的影响就越大。
因此,让我们假设一个场景,并且稍微思考一下。假设我们正在对数字图像进行分类,即数字。一些图像会有曲线,例如 2、3、6、8、9 等等。其他数字,如 1、4 和 7,则不会有。这样的知识非常重要,因为我们的 RBM 将利用它来继续改进其学习并减少错误。如果我们认为我们正在处理数字 2,那么指向这个方向的权重将比其他权重更重。这是一个极端的简化,但希望这足以帮助你理解我们即将开始的事情。
当我们将所有这些放在一起时,我们现在有了深度信念网络的理论框架。尽管我们比其他章节更深入地探讨了理论,但当你看到我们的示例程序运行时,一切都将开始变得有意义。而且你将更好地准备好在你的应用中使用它,了解幕后发生的事情。记住,黑洞与黑箱!
为了展示深度信念网络和 RBMs,我们将使用由 Mattia Fagerlund 编写的出色开源软件 SharpRBM。这款软件是对开源社区的巨大贡献,我毫不怀疑你将花费数小时,甚至数天的时间与之共事。这款软件附带了一些非常令人惊叹的演示。对于本章,我们将使用字母分类演示。
以下截图是我们深度信念测试应用。你是否好奇当计算机在睡眠时它在想什么?好吧,我的朋友,你即将找到答案!

和往常一样,我们也会使用 ReflectInsight 来提供幕后发生的事情的视角:

你首先会注意到我们的演示应用中有很多事情在进行。让我们花点时间将其分解成更小的部分。
在程序屏幕的左上角是我们要指定的层,我们有三层隐藏层,所有这些层在测试之前都需要适当的训练。我们可以一次训练一层,从第一层开始。你可以根据自己的喜好训练多长时间,但训练越多,你的系统会越好:

在我们的训练选项之后是下一节,我们的进度。在我们训练的过程中,所有相关信息,如生成、重建错误、检测器错误和学习率,都显示在这里:

下一节是关于我们特征检测器的绘图,如果勾选了“绘制”复选框,它将在整个训练过程中更新自己:

当你开始训练一层时,你会注意到重建和特征检测器基本上是空的。随着你的训练进展,它们会自我完善。记住,我们正在重建我们已经知道是真实的内容!随着训练的继续,重建的数字变得越来越清晰,我们的特征检测器也随之变得更加明显:

这是应用程序在训练过程中的一个快照。正如你所见,它处于第 31 代,重建的数字非常清晰。它们仍然不完整或不正确,但你可以看到我们正在取得多大的进步:

计算机做梦是什么?
“当计算机做梦时,它在想什么?” 这是一句著名的话。对我们来说,这将是一个特性,它允许我们在计算机重建阶段看到它在想什么。当程序试图重建我们的数字时,特征检测器本身在整个过程中会呈现各种形式。我们就是在梦境窗口(用红色圆圈表示)中显示这些形式:

好吧,我们已经花了很多时间查看我们应用程序的截图。我认为现在是时候看看代码了。让我们先看看我们是如何创建 DeepBeliefNetwork 对象本身的:
DeepBeliefNetwork = new DeepBeliefNetwork(28 * 29, 500, 500, 1000);
创建完成后,我们需要创建我们的网络训练器,我们根据正在训练的层的权重来做这件事:
DeepBeliefNetworkTrainer trainer = new
DeepBeliefNetworkTrainer(DeepBeliefNetwork,
DeepBeliefNetwork?.LayerWeights?[layerId], inputs);
这两个对象都在我们的主 TrainNetwork 循环中使用,这是我们应用程序中活动发生的主要部分。这个循环将持续进行,直到被通知停止。
private void TrainNetwork(DeepBeliefNetworkTrainer trainer)
{
try
{
Stopping = false;
ClearBoxes();
_unsavedChanges = true;
int generation = 0;
SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS
| EXECUTION_STATE.ES_SYSTEM_REQUIRED);
while (Stopping == false)
{
Stopwatch stopwatch = Stopwatch.StartNew();
TrainingError error = trainer?.Train();
label1.Text = string.Format(
"Gen {0} ({4:0.00} s): ReconstructionError=
{1:0.00}, DetectorError={2:0.00},
LearningRate={3:0.0000}",
generation, error.ReconstructionError,
error.FeatureDetectorError,
trainer.TrainingWeights.AdjustedLearningRate,
stopwatch.ElapsedMilliseconds / 1000.0);
Application.DoEvents();
ShowReconstructed(trainer);
ShowFeatureDetectors(trainer);
Application.DoEvents();
if (Stopping)
{
break;
}
generation++;
}
DocumentDeepBeliefNetwork();
}
finally
{
SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);
}
}
在前面的代码中,我们突出了 trainer.Train() 函数,这是一个基于数组的机器学习算法,其形式如下:
public TrainingError Train()
{
TrainingError trainingError = null;
if (_weights != null)
{
ClearDetectorErrors(_weights.LowerLayerSize,
_weights.UpperLayerSize);
float reconstructionError = 0;
ParallelFor(MultiThreaded, 0, _testCount,
testCase =>
{
float errorPart =
TrainOnSingleCase(_rawTestCases,
_weights?.Weights, _detectorError,
testCase, _weights.LowerLayerSize,
_weights.UpperLayerSize, _testCount);
lock (_locks?[testCase %
_weights.LowerLayerSize])
{
reconstructionError += errorPart;
}
});
float epsilon =
_weights.GetAdjustedAndScaledTrainingRate(_testCount);
UpdateWeights(_weights.Weights,
_weights.LowerLayerSize, _weights.UpperLayerSize,
_detectorError, epsilon);
trainingError = new
TrainingError(_detectorError.Sum(val =>
Math.Abs(val)), reconstructionError);
_weights?.RegisterLastTrainingError(trainingError);
return trainingError;
}
return trainingError;
}
这段代码使用并行处理(突出部分)来并行训练单个案例。这个函数负责处理输入和隐藏层的转换,正如我们在本章开头所讨论的。它使用 TrainOnSingleCase 函数,其形式如下:
private float TrainOnSingleCase(float[] rawTestCases, float[] weights, float[] detectorErrors, int testCase,
int lowerCount, int upperCount, int testCaseCount)
{
float[] model = new float[upperCount];
float[] reconstructed = new float[lowerCount];
float[] reconstructedModel = new float[upperCount];
int rawTestCaseOffset = testCase * lowerCount;
ActivateLowerToUpperBinary(rawTestCases, lowerCount,
rawTestCaseOffset, model, upperCount, weights); // Model
ActivateUpperToLower(reconstructed, lowerCount, model,
upperCount, weights); // Reconstruction
ActivateLowerToUpper(reconstructed, lowerCount, 0,
reconstructedModel, upperCount, weights); //
Reconstruction model
return AccumulateErrors(rawTestCases, lowerCount,
rawTestCaseOffset, model, upperCount, reconstructed,
reconstructedModel, detectorErrors); // Accumulate
detector errors
}
最后,我们在处理过程中累积错误,这是我们的模型应该相信的内容与它实际执行的内容之间的差异。显然,错误率越低越好,这样我们才能更准确地重建我们的图像。AccumulateErrors 函数如下所示:
private float AccumulateErrors(float[] rawTestCases, int lowerCount, int rawTestCaseOffset, float[] model,
int upperCount, float[] reconstructed, float[] reconstructedModel, float[] detectorErrors)
{
float reconstructedError = 0;
float[] errorRow = new float[upperCount];
for (int lower = 0; lower < lowerCount; lower++)
{
int errorOffset = upperCount * lower;
for (int upper = 0; upper < upperCount; upper++)
{
errorRow[upper] = rawTestCases[rawTestCaseOffset +
lower] * model[upper] +
// What the model should believe in
-reconstructed[lower] *
reconstructedModel[upper];
// What the model actually believes in
}
lock (_locks[lower])
{
for (int upper = 0; upper < upperCount; upper++)
{
detectorErrors[errorOffset + upper] -=
errorRow[upper];
}
}
reconstructedError +=
Math.Abs(rawTestCases[rawTestCaseOffset + lower] -
reconstructed[lower]);
}
return reconstructedError;
}
摘要
好吧,朋友们,这就是全部内容!在本章中,你学习了 RBMs、一点图论,以及如何在 C# 中创建和训练深度信念网络。你的 buzzword-compliant 检查清单几乎已经完成!我建议你尝试代码,将网络层训练到不同的阈值,并观察你的计算机在重建过程中是如何“做梦”的。记住,训练越多越好,所以花时间在每个层上,确保它有足够的数据来完成准确的重建工作。
一个简短的警告:如果你启用了绘制你的特征检测器和重建输入的功能,你会注意到性能会有大幅下降。如果你正在尝试训练你的层,你可能希望在首先不进行可视化的情况下训练它们,以减少所需的时间。相信我,如果你将每个级别训练到高迭代次数,那么使用可视化将感觉像永恒!在你进步的过程中,随时保存你的网络。祝你好运,祝你梦想成真!
在下一章中,我们将学习微基准测试,并有机会使用有史以来最强大的开源微基准测试工具包之一!
参考文献
-
Mattias Fagerlund:
lotsacode.wordpress.com/2010/09/14/sharprbm-restricted-boltzmann-machines-in-c-net/#comments -
Nykamp DQ, 无向图定义, 来自 Math Insight:
mathinsight.org/definition/undirected_graph
第十一章:微基准测试和激活函数
在本章中,我们将学习以下内容:
-
什么是微基准测试
-
如何将其应用到您的代码中
-
激活函数是什么
-
如何绘制和基准测试激活函数
每个开发者都需要一个良好的基准测试工具在手。定性基准测试无处不在;你每天都会听到,“我们减少了 10%,增加了 25%”。记住那句古老的谚语,“当你听到一个数字被抛出来时,98.4%的情况下那个数字是假的”?顺便说一句,那个数字也是我随便编的。当你听到这样的引用时,要求那个人证明它,你得到的是什么?任务管理器吗?作为数据科学家,我们不需要定性结果;我们需要可以证明并一致复制的定量结果。可复现的结果非常重要,不仅因为一致性,还因为可信度和准确性。这正是微基准测试发挥作用的地方。
我们将使用无可替代的BenchmarkDotNet库,您可以在以下链接找到它:github.com/dotnet/BenchmarkDotNet.
如果您还没有使用这个库,您需要立即放下手头的工作并安装它。我认为这是您可以使用的最无可替代的框架之一,并且我认为它在重要性上与单元测试和集成测试并列。
为了展示这个工具的价值,我们将绘制几个激活函数并比较它们的运行时间。作为其中的一部分,我们将考虑预热、遗留和RyuJIT、冷启动以及程序执行的更多方面。最后,我们将得到一组定量结果,证明我们函数的确切测量值。如果,比如说在 2.0 版本中,我们发现某些东西运行得更慢,我们可以重新运行基准测试并比较。
我强烈建议将此集成到您的持续集成/持续构建过程中,以便在每次发布时,您都有基准数字进行比较。这不仅仅是我们自己的代码。我创建了一个庞大的 CI/CD 系统,涵盖了大量的程序、微服务、环境和构建及部署步骤。我们还会定期基准测试我们经常使用的某些.NET 库函数,以验证;在.NET 框架版本之间,没有任何变化。
在本章中,我们将展示两个示例。第一个是一个激活函数查看器;它将绘制每个激活函数,以便您可以查看其外观。您可以在我认为最有价值的开源程序之一,由科林·格林先生开发的SharpNEAT中找到它。这个包绝对令人难以置信,我几乎每天都在使用它。我还在此基础上创建了新的用户界面以及满足我需求的先进版本,这是一个非常灵活的工具。我每天都在研究将镜像神经元和规范神经元集成到可扩展基板中的工作,像 SharpNEAT 这样的工具是不可思议的。未来的高级书籍将更多地突出 SharpNEAT,所以现在就熟悉它吧!这个第一个示例应用程序包含在最新的 SharpNEAT 包中,您可以在github.com/colgreen/sharpneat找到它。
可视化激活函数绘制
这是 SharpNEAT 自定义版本绘制的局部和全局最小值的图。在这个领域,您可以用这个产品做很多事情,真是太令人惊叹了!

正如我提到的,我们将绘制并基准测试几个激活函数。我们到处都在听到这个术语“激活函数”,但我们真的知道它是什么意思吗?让我们先快速解释一下,以防您不熟悉。
激活函数用于决定神经元是否被激活。有些人喜欢用“激活”这个词来替换“触发”。无论哪种方式,它最终决定了某物是开启还是关闭,是否触发,是否激活。
让我们从向您展示单个激活函数的图开始:

当单独绘制时,这是Logistic Steep近似和Swish 激活函数的外观,因为存在许多类型的激活函数,所以当它们一起绘制时,这就是我们所有的激活函数将看起来像什么:

在这一点上,您可能想知道,“我们为什么甚至关心这些图看起来像什么?”这是一个很好的问题。我们关心,因为一旦您进入神经网络等领域,您将大量使用这些函数。知道您的激活函数是否会将神经元的值置于开启或关闭状态,以及它将保持或需要的值范围是非常有用的。毫无疑问,您作为机器学习开发者将在职业生涯中遇到并/或使用激活函数,了解TanH和LeakyReLU激活函数之间的区别非常重要。
绘制所有函数
所有激活函数的绘制都是在单个函数中完成的,这个函数令人惊讶地被命名为PlotAllFunctions:
private void PlotAllFunctions()
{
// First, clear out any old GraphPane's from the MasterPane
collection MasterPane master = zed.MasterPane;
master.PaneList.Clear();
// Display the MasterPane Title, and set the
outer margin to 10 points
master.Title.IsVisible = true;
master.Margin.All = 10;
// Plot multiple functions arranged on a master pane.
PlotOnMasterPane(Functions.LogisticApproximantSteep,
"Logistic Steep (Approximant)");
PlotOnMasterPane(Functions.LogisticFunctionSteep,
"Logistic Steep (Function)");
PlotOnMasterPane(Functions.SoftSign, "Soft Sign");
PlotOnMasterPane(Functions.PolynomialApproximant,
"Polynomial Approximant");
PlotOnMasterPane(Functions.QuadraticSigmoid,
"Quadratic Sigmoid");
PlotOnMasterPane(Functions.ReLU, "ReLU");
PlotOnMasterPane(Functions.LeakyReLU, "Leaky ReLU");
PlotOnMasterPane(Functions.LeakyReLUShifted,
"Leaky ReLU (Shifted)");
PlotOnMasterPane(Functions.SReLU, "S-Shaped ReLU");
PlotOnMasterPane(Functions.SReLUShifted,
"S-Shaped ReLU (Shifted)");
PlotOnMasterPane(Functions.ArcTan, "ArcTan");
PlotOnMasterPane(Functions.TanH, "TanH");
PlotOnMasterPane(Functions.ArcSinH, "ArcSinH");
PlotOnMasterPane(Functions.ScaledELU,
"Scaled Exponential Linear Unit");
// Refigure the axis ranges for the GraphPanes.
zed.AxisChange();
// Layout the GraphPanes using a default Pane Layout.
using (Graphics g = this.CreateGraphics()) {
master.SetLayout(g, PaneLayout.SquareColPreferred);
}
主要的绘图函数
在幕后,Plot函数负责执行和绘制每个函数:
private void Plot(Func<double, double> fn, string fnName,
Color graphColor, GraphPane gpane = null)
{
const double xmin = -2.0;
const double xmax = 2.0;
const int resolution = 2000;
zed.IsShowPointValues = true;
zed.PointValueFormat = "e";
var pane = gpane ?? zed.GraphPane;
pane.XAxis.MajorGrid.IsVisible = true;
pane.YAxis.MajorGrid.IsVisible = true;
pane.Title.Text = fnName;
pane.YAxis.Title.Text = string.Empty;
pane.XAxis.Title.Text = string.Empty;
double[] xarr = new double[resolution];
double[] yarr = new double[resolution];
double incr = (xmax - xmin) / resolution;
double x = xmin;
for(int i=0; i < resolution; i++, x+=incr)
{
xarr[i] = x;
yarr[i] = fn(x);
}
PointPairList list1 = new PointPairList(xarr, yarr);
LineItem li = pane.AddCurve(string.Empty, list1, graphColor,
SymbolType.None);
li.Symbol.Fill = new Fill(Color.White);
pane.Chart.Fill = new Fill(Color.White,
Color.LightGoldenrodYellow, 45.0F);
}
在此代码中,值得关注的主要部分用黄色突出显示。这是执行我们传递的激活函数并使用其值作为Y轴绘图值的地方。著名的ZedGraph开源绘图包用于所有图形绘制。每个函数执行后,相应的绘图将被制作。
基准测试
BenchmarkDotNet生成多个报告,其中之一是类似于您在这里看到的 HTML 报告:

Excel 报告提供了运行程序所使用的每个参数的详细信息,这是您最全面的信息来源。在许多情况下,这些参数的大部分将使用默认值,并且可能比您需要的更多,但至少您将有机会移除不需要的部分:

在下一节中,当我们回顾创建您所看到内容的源代码时,我们将描述一些这些参数:
static void Main(string[] args)
{
var config = ManualConfig.Create(DefaultConfig.Instance);
// Set up an results exporter.
// Note. By default results files will be located in
.BenchmarkDotNet.Artifactsresults directory.
config.Add(new CsvExporter(CsvSeparator.CurrentCulture,
new BenchmarkDotNet.Reports.SummaryStyle
{
PrintUnitsInHeader = true,
PrintUnitsInContent = false,
TimeUnit = TimeUnit.Microsecond,
SizeUnit = BenchmarkDotNet.Columns.SizeUnit.KB
}));
// Legacy JITter tests.
config.Add(new Job(EnvMode.LegacyJitX64,
EnvMode.Clr, RunMode.Short)
{
Env = { Runtime = Runtime.Clr, Platform = Platform.X64 },
Run = { LaunchCount = 1, WarmupCount = 1,
TargetCount = 1, RunStrategy =
BenchmarkDotNet.Engines.RunStrategy.Throughput },
Accuracy = { RemoveOutliers = true }
}.WithGcAllowVeryLargeObjects(true));
// RyuJIT tests.
config.Add(new Job(EnvMode.RyuJitX64, EnvMode.Clr,
RunMode.Short)
{
Env = { Runtime = Runtime.Clr, Platform = Platform.X64 },
Run = { LaunchCount = 1, WarmupCount = 1,
TargetCount = 1, RunStrategy =
BenchmarkDotNet.Engines.RunStrategy.Throughput },
Accuracy = { RemoveOutliers = true }
}.WithGcAllowVeryLargeObjects(true));
// Uncomment to allow benchmarking of non-optimized assemblies.
//config.Add(JitOptimizationsValidator.DontFailOnError);
// Run benchmarks.
var summary = BenchmarkRunner.Run<FunctionBenchmarks>(config);
}
让我们更深入地分析这段代码。
首先,我们将创建一个手动配置对象,用于保存我们用于基准测试的配置参数:
var config = ManualConfig.Create(DefaultConfig.Instance);
接下来,我们将设置一个导出器来保存我们将用于导出结果的参数。我们将使用微秒作为时间单位和千字节作为大小来将结果导出到.csv文件:
config.Add(new CsvExporter(CsvSeparator.CurrentCulture,
new BenchmarkDotNet.Reports.SummaryStyle
{
PrintUnitsInHeader = true,
PrintUnitsInContent = false,
TimeUnit = TimeUnit.Microsecond,
SizeUnit = BenchmarkDotNet.Columns.SizeUnit.KB
}));
接下来,我们将创建一个基准作业,用于处理LegacyJitX64在 x64 架构上的测量。您可以随意更改此参数或其他任何参数以进行实验,或包括您测试场景中需要或想要的任何结果。在我们的案例中,我们将使用 x64 平台;LaunchCount、WarmupCount和TargetCount均为1;以及RunStrategy为Throughput。我们也将对 RyuJIT 做同样的处理,但在此不展示代码:
config.Add(new Job(EnvMode.LegacyJitX64, EnvMode.Clr,
RunMode.Short)
{
Env = { Runtime = Runtime.Clr, Platform = Platform.X64 },
Run = { LaunchCount = 1, WarmupCount = 1, TargetCount = 1,
RunStrategy = Throughput },
Accuracy = { RemoveOutliers = true }
}.WithGcAllowVeryLargeObjects(true));
最后,我们将运行BenchmarkRunner以执行我们的测试:
// Run benchmarks.
var summary = BenchmarkRunner.Run<FunctionBenchmarks>(config);
BenchmarkDotNet将以 DOS 命令行应用程序的形式运行,以下是一个执行先前代码的示例:

让我们来看一个激活函数绘制的例子:
[Benchmark]
public double LogisticFunctionSteepDouble()
{
double a = 0.0;
for(int i=0; i<__loops; i++)
{
a = Functions.LogisticFunctionSteep(_x[i % _x.Length]);
}
return a;
}
您会注意到使用了[Benchmark]属性。这表示对于BenchmarkDotNet来说,这将是一个需要基准测试的测试。内部,它调用以下函数:

对于LogisticFunctionSteep函数,其实现方式,就像大多数激活函数一样,很简单(假设你知道公式)。在这种情况下,我们不是在绘制激活函数,而是在对其进行基准测试。你会注意到该函数接收并返回double类型。我们还通过使用和返回float变量对相同的函数进行了基准测试,因此我们正在基准测试使用double和float之间的差异。因此,人们可以看到,有时性能影响可能比他们想象的要大:

摘要
在本章中,我们学习了如何将微基准测试应用于你的代码。我们还看到了如何绘制和基准测试激活函数,以及如何使用微基准测试进行这些操作。你现在拥有了一个非常强大的基准测试库,你可以将其添加到所有代码中。在下一章中,我们将深入探讨直观深度学习,并展示一个针对 C#开发者可用的最强大的机器学习测试框架之一。
第十二章:C# .NET 中的直观深度学习
本章的目标是向您展示 Kelp.Net 提供的强大功能。
在本章中,你将学习:
-
如何使用 Kelp.Net 进行自己的测试
-
如何编写测试
-
如何对函数进行基准测试
-
如何扩展 Kelp.Net
Kelp.Net[4]是一个用 C#和.NET 编写的深度学习库。它能够将函数链入函数堆栈,提供了一个非常灵活和直观的平台,具有极大的功能。它还充分利用了 OpenCL 语言平台,以实现 CPU 和 GPU 设备上的无缝操作。深度学习是一个功能强大的工具,对 Caffe 和 Chainer 模型加载的原生支持使这个平台更加强大。正如你将看到的,你只需几行代码就可以创建一个拥有 100 万个隐藏层的深度学习网络。
Kelp.Net 还使得将模型保存到磁盘存储和从磁盘加载变得非常容易。这是一个非常强大的功能,允许你进行训练、保存模型,然后根据需要加载和测试。它还使得将代码投入生产并真正分离训练和测试阶段变得更加容易。
在其他方面,Kelp.Net 是一个功能强大的工具,可以帮助你更好地学习和理解各种类型的函数、它们的交互和性能。例如,你可以对同一网络使用不同的优化器进行测试,通过更改一行代码来查看结果。你也可以轻松设计测试,以查看使用不同批量大小、隐藏层数量、epoch 等时的差异。在.NET 中,几乎没有提供 Kelp.Net 所具有的强大功能和灵活性的深度学习工作台。
让我们先简单谈谈深度学习。
深度学习是什么?
要讨论深度学习,我们需要回顾一下,不久前,大数据出现在我们面前。这个术语当时,现在仍然无处不在。这是一个每个人都必须拥有的技能,一个符合流行术语的清单项。但这个术语究竟意味着什么呢?嗯,它只是意味着,我们不再使用孤立的 SQL 数据库和文件通过 FTP 传输来使用,而是从社交媒体、互联网搜索引擎、电子商务网站等地方爆发了大量的数字数据。当然,这些数据以各种形式和格式出现。更正式地说,我们突然开始处理非结构化数据。不仅由于 Facebook、Twitter、Google 等应用程序的数据爆炸,而且爆炸还在继续。越来越多的人相互连接并保持联系,分享大量关于自己的信息,这些信息他们如果通过电话询问,是绝对不敢提供的,对吧?我们对这些数据的格式和质量几乎没有控制权。随着我们继续前进,这将成为一个重要的观点。
现在,这庞大的数据量是很好的,但人类几乎无法吸收他们每天所接触到的,更不用说数据爆炸了。因此,在这个过程中,人们意识到机器学习和人工智能可以适应这样的任务。从简单的机器学习算法到多层网络,人工智能和深度学习诞生了(至少企业界喜欢相信是这样发生的!)。
深度学习,作为机器学习和人工智能的一个分支,使用许多层级的神经网络层(如果你喜欢,可以称之为分层)来完成其任务。在许多情况下,这些网络被构建来反映我们对我们所了解的人脑的认识,神经元像复杂层叠的网一样连接各个层。这使得数据处理可以以非线性方式发生。每一层处理来自前一层的(当然,第一层除外)数据,并将信息传递给下一层。如果有任何运气,每一层都会改进模型,最终,我们达到目标并解决问题。
OpenCL
Kelp.Net 大量使用开放计算语言,即 OpenCL。根据维基百科:
“OpenCL 将计算系统视为由多个计算设备组成,这些设备可能是中央处理器(CPU),或者是连接到主机处理器(CPU)的加速器,如图形处理单元(GPU)。在 OpenCL 设备上执行的功能称为内核。单个计算设备通常由多个计算单元组成,这些计算单元又由多个处理元素(PE)组成。单个内核执行可以在所有或许多 PE 上并行运行。”
在 OpenCL 中,任务是在命令队列上安排的。每个设备至少有一个命令队列。OpenCL 运行时将安排的数据并行任务分解成片段,并将任务发送到设备处理元素。
OpenCL 定义了一个内存层次结构:
-
Global: 由所有处理元素共享,具有高延迟
-
Read-only: 较小,延迟较低,可由主机 CPU 写入但不能由计算设备写入
-
Local: 由进程元素组共享
-
Per-element: 私有内存
OpenCL 还提供了一个更偏向数学的 API。这可以从固定长度向量类型(如 float4,单精度浮点数的四个向量)的暴露中看出,长度为 2、3、4、8 和 16。随着你对 Kelp.Net 的更多了解以及开始创建自己的函数,你将遇到 OpenCL 编程。现在,只需知道它存在并且被广泛使用就足够了。
OpenCL 层次结构
在 Kelp.Net 中,各种 OpenCL 资源的层次结构如下所示:


让我们更详细地描述这些。
计算内核
内核对象封装了程序中声明的特定内核函数以及执行此内核函数时要使用的参数值。
计算程序
由一组内核组成的 OpenCL 程序。程序还可以包含由内核函数调用的辅助函数和常量数据。
计算采样器
一个描述如何在内核中读取图像时进行采样的对象。图像读取函数接受一个采样器作为参数。采样器指定图像寻址模式(意味着如何处理超出范围的坐标)、过滤模式以及输入图像坐标是归一化还是未归一化的值。
计算设备
计算设备是一组计算单元。命令队列用于向设备排队命令。命令的例子包括执行内核或读取/写入内存对象。OpenCL 设备通常对应于 GPU、多核 CPU 以及其他处理器,如数字信号处理器(DSP)和 cell/B.E. 处理器。
计算资源
一个应用程序可以创建和删除的 OpenCL 资源。
计算对象
在 OpenCL 环境中通过其句柄识别的对象。
计算上下文
计算上下文是内核实际执行的环境以及定义同步和内存管理的域。
计算命令队列
命令队列是一个对象,它包含将在特定设备上执行的操作。命令队列在上下文中的特定设备上创建。对队列的命令按顺序排队,但可以按顺序或非顺序执行。
计算缓冲区
一个存储字节线性集合的内存对象。缓冲区对象可以通过在设备上执行的内核中的指针访问。
计算事件
事件封装了操作的状态,如命令。它可以用于在上下文中同步操作。
计算图像
一个存储 2D 或 3D 结构化数组的内存对象。图像数据只能通过读取和写入函数访问。读取函数使用采样器。
计算平台
主机加上由 OpenCL 框架管理的设备集合,允许应用程序共享资源并在平台上的设备上执行内核。
计算用户事件
这代表一个用户创建的事件。
Kelp.Net 框架
函数
函数是 Kelp.Net 神经网络的基本构建块。单个函数在函数栈中链在一起,以创建强大且可能复杂的网络链。你需要了解四种主要类型的函数,以及它们的目的,应该是显而易见的:
-
单输入函数
-
双输入函数
-
多输入函数
-
多输出函数
当从磁盘加载网络时,函数也会被链在一起。
每个函数都有一个正向和反向方法,你将在创建自己的函数时实现:
public abstract NdArray[] Forward(params NdArray[] xs);
public virtual void Backward([CanBeNull] params NdArray[] ys){}
函数栈
函数栈是在一个正向、反向或更新过程中同时执行的一组函数层。当你创建测试或从磁盘加载模型时,会创建函数栈。以下是一些函数栈的示例。
它们也可以很小且简单:
FunctionStack nn = new FunctionStack(
new Linear(2, 2, name: "l1 Linear"),
new Sigmoid(name: "l1 Sigmoid"),
new Linear(2, 2, name: "l2 Linear"));
它们可以稍微大一点:
FunctionStack nn = new FunctionStack(
new Convolution2D(1, 2, 3, name: "conv1", gpuEnable: true),// Do not forget the GPU flag if necessary
new ReLU(),
new MaxPooling(2, 2),
new Convolution2D(2, 2, 2, name: "conv2", gpuEnable: true),
new ReLU(),
new MaxPooling(2, 2),
new Linear(8, 2, name: "fl3"),
new ReLU(),
new Linear(2, 2, name: "fl4")
);
或者,它们也可以非常大:
FunctionStack nn = new FunctionStack(
new Linear(neuronCount * neuronCount, N, name: "l1 Linear"), // L1
new BatchNormalization(N, name: "l1 BatchNorm"),
new LeakyReLU(slope: 0.000001, name: "l1 LeakyReLU"),
new Linear(N, N, name: "l2 Linear"), // L2
new BatchNormalization(N, name: "l2 BatchNorm"),
new LeakyReLU(slope: 0.000001, name: "l2 LeakyReLU"),
new Linear(N, N, name: "l3 Linear"), // L3
new BatchNormalization(N, name: "l3 BatchNorm"),
new LeakyReLU(slope: 0.000001, name: "l3 LeakyReLU"),
new Linear(N, N, name: "l4 Linear"), // L4
new BatchNormalization(N, name: "l4 BatchNorm"),
new LeakyReLU(slope: 0.000001, name: "l4 LeakyReLU"),
new Linear(N, N, name: "l5 Linear"), // L5
new BatchNormalization(N, name: "l5 BatchNorm"),
new LeakyReLU(slope: 0.000001, name: "l5 LeakyReLU"),
new Linear(N, N, name: "l6 Linear"), // L6
new BatchNormalization(N, name: "l6 BatchNorm"),
new LeakyReLU(slope: 0.000001, name: "l6 LeakyReLU"),
new Linear(N, N, name: "l7 Linear"), // L7
new BatchNormalization(N, name: "l7 BatchNorm"),
new LeakyReLU(slope: 0.000001, name: "l7 ReLU"),
new Linear(N, N, name: "l8 Linear"), // L8
new BatchNormalization(N, name: "l8 BatchNorm"),
new LeakyReLU(slope: 0.000001, name: "l8 LeakyReLU"),
new Linear(N, N, name: "l9 Linear"), // L9
new BatchNormalization(N, name: "l9 BatchNorm"),
new PolynomialApproximantSteep(slope: 0.000001, name: "l9 PolynomialApproximantSteep"),
new Linear(N, N, name: "l10 Linear"), // L10
new BatchNormalization(N, name: "l10 BatchNorm"),
new PolynomialApproximantSteep(slope: 0.000001, name: "l10 PolynomialApproximantSteep"),
new Linear(N, N, name: "l11 Linear"), // L11
new BatchNormalization(N, name: "l11 BatchNorm"),
new PolynomialApproximantSteep(slope: 0.000001, name: "l11 PolynomialApproximantSteep"),
new Linear(N, N, name: "l12 Linear"), // L12
new BatchNormalization(N, name: "l12 BatchNorm"),
new PolynomialApproximantSteep(slope: 0.000001, name: "l12 PolynomialApproximantSteep"),
new Linear(N, N, name: "l13 Linear"), // L13
new BatchNormalization(N, name: "l13 BatchNorm"),
new PolynomialApproximantSteep(slope: 0.000001, name: "l13 PolynomialApproximantSteep"),
new Linear(N, N, name: "l14 Linear"), // L14
new BatchNormalization(N, name: "l14 BatchNorm"),
new PolynomialApproximantSteep(slope: 0.000001, name: "l14 PolynomialApproximantSteep"),
new Linear(N, 10, name: "l15 Linear") // L15
);
函数字典
函数字典是一个可序列化的函数字典(之前已描述)。当从磁盘加载网络模型时,将返回一个函数字典,可以像在代码中直接创建函数栈一样对其进行操作。函数字典主要用于与 Caffe 数据模型加载器一起使用。
Caffe1
Kelp.Net 在 Caffe 风格的开发基础上得到了强化,并支持其许多特性。
Caffe 为多媒体科学家和从业者提供了一个干净且可修改的框架,用于最先进的深度学习算法和一系列参考模型。该框架是一个带有 Python 和 MATLAB 绑定的 BSD 许可证 C++ 库,用于在通用架构上高效地训练和部署通用卷积神经网络和其他深度模型。通过 CUDA GPU 计算,Caffe 满足行业和互联网规模媒体需求,单个 K40 或 Titan GPU 每天处理超过 4000 万张图片(每张图片大约 2 毫秒)。通过分离模型表示和实际实现,Caffe 允许实验,并在平台之间无缝切换,便于开发和部署,从原型机到云环境。
Chainer
根据 Chainer 文档[2]:
"Chainer 是一个灵活的神经网络框架。一个主要目标是灵活性,因此它必须能够让我们简单直观地编写复杂的架构。"
Chainer 采用定义-运行方案,即网络通过实际的正向计算动态定义。更精确地说,Chainer 存储计算的历史而不是编程逻辑。例如,Chainer 不需要任何魔法就可以将条件语句和循环引入网络定义中。定义-运行方案是 Chainer 的核心概念。这种策略也使得编写多 GPU 并行化变得容易,因为逻辑更接近网络操作。
Kelp.Net 可以直接从磁盘加载 Chainer 模型。
损失
Kelp.Net 由一个单一的抽象 LossFunction 类组成,该类旨在实现你的特定实例,以确定你如何评估损失。
在机器学习中,损失函数或代价函数是一个将事件或一个或多个变量的值映射到实数的函数,直观地表示与事件相关的某些成本。Kelp.Net 提供了两种现成的损失函数:均方误差和 softmax 交叉熵。你可以轻松扩展这些函数以满足你的需求。
模型保存和加载
Kelp.Net 通过调用一个简单的类即可轻松保存和加载模型。ModelIO 类公开了 Save 和 Load 方法,以便轻松地将模型保存到磁盘。以下是一个在训练后保存模型、重新加载并随后对该模型进行测试的非常简单的示例:

优化器
优化算法根据模型的参数最小化或最大化误差函数。参数的例子可以是权重和偏差。它们帮助计算输出值,并通过最小化损失来更新模型,使其朝向最优解的位置。将 Kelp.Net 扩展以添加自己的优化算法是一个简单的过程,尽管添加 OpenCL 和资源方面需要协调努力。
Kelp.Net 随带许多预定义的优化器,例如:
-
AdaDelta
-
AdaGrad
-
Adam
-
GradientClipping
-
MomentumSGD
-
RMSprop
-
SGD
这些都是基于抽象优化器类。
数据集
Kelp.Net 本地支持以下数据集:
-
CIFAR
-
MNIST
CIFAR
CIFAR 数据集有两种风味,CIFAR-10 和 CIFAR 100,区别在于每个数据集中的类别数量。让我们简要讨论一下两者。
CIFAR-10
CIFAR-10 数据集由 10 个类别的 60,000 张 32 x 32 彩色图像组成,每个类别有 6,000 张图像。有 50,000 张训练图像和 10,000 张测试图像。数据集分为五个训练批次和一个测试批次,每个批次有 10,000 张图像。测试批次包含每个类别恰好 1,000 张随机选择的图像。训练批次包含剩余的图像,以随机顺序排列,但某些训练批次可能包含比另一个类别更多的图像。在这些批次之间,训练批次包含每个类别恰好 5,000 张图像。
CIFAR-100
CIFAR-100 数据集与 CIFAR-10 类似,但它有 100 个类别,每个类别包含 600 张图片。每个类别有 500 张训练图片和 100 张测试图片。CIFAR-100 的 100 个类别被分为 20 个超级类别。每张图片都附有一个精细标签(它所属的类别)和一个粗略标签(它所属的超级类别)。以下是 CIFAR-100 中类别的列表:
| 超级类别 | 类别 |
|---|---|
| 水生哺乳动物 | 海狸、海豚、水獭、海豹和鲸鱼 |
| 鱼类 | 水族馆鱼类、扁鱼、鳐鱼、鲨鱼和鲑鱼 |
| 花卉 | 兰花、罂粟花、玫瑰、向日葵和郁金香 |
| 食品容器 | 瓶子、碗、罐头、杯子和盘子 |
| 水果和蔬菜 | 苹果、蘑菇、橙子、梨和甜椒 |
| 家用电器 | 时钟、电脑键盘、灯、电话和电视 |
| 家具 | 床、椅子、沙发、桌子和衣柜 |
| 昆虫 | 蜜蜂、甲虫、蝴蝶、毛毛虫和蟑螂 |
| 大型食肉动物 | 熊、豹、狮子、老虎和狼 |
| 大型人造户外物品 | 桥梁、城堡、房屋、道路和摩天大楼 |
| 大型自然户外景观 | 云、森林、山脉、平原和海洋 |
| 大型杂食性和草食性动物 | 骆驼、牛、黑猩猩、大象和袋鼠 |
| 中型哺乳动物 | 狐狸、刺猬、负鼠、浣熊和臭鼬 |
| 非昆虫无脊椎动物 | 螃蟹、龙虾、蜗牛、蜘蛛和蠕虫 |
| 人群 | 婴儿、男孩、女孩、男人和女人 |
| 爬行动物 | 鳄鱼、恐龙、蜥蜴、蛇和乌龟 |
| 小型哺乳动物 | 仓鼠、老鼠、兔子、鼩鼱和松鼠 |
| 树木 | 榉树、橡树、棕榈、松树和柳树 |
| 车辆 1 | 自行车、公共汽车、摩托车、皮卡和火车 |
| 车辆 2 | 草割机、火箭、电车、坦克和拖拉机 |
MNIST
MNIST 数据库是一个包含大量手写数字的大型数据库,通常用于训练各种图像处理系统。该数据库在机器学习领域的训练和测试中也得到了广泛的应用。它包含 60,000 个示例的训练集和 10,000 个示例的测试集。数字已经被标准化到固定大小的图像中,并进行了居中处理,这使得它成为想要尝试各种学习技术而不需要预处理和格式化工作的人们的首选标准:

MNIST 示例
测试
测试是实际的执行事件,可以说是小型程序。由于使用了 OpenCL,这些程序在运行时进行编译。要创建一个测试,您只需要提供一个静态的Run函数,该函数封装了您的代码。Kelp.Net 附带了一个预配置的测试器,这使得添加您自己的测试变得非常简单。我们将在编写测试的章节中详细探讨这一点,现在,这里有一个简单的 XOR 测试程序的示例:
public static void Run()
{
const int learningCount = 10000;
Real[][] trainData =
{
new Real[] { 0, 0 },
new Real[] { 1, 0 },
new Real[] { 0, 1 },
new Real[] { 1, 1 }
};
Real[][] trainLabel =
{
new Real[] { 0 },
new Real[] { 1 },
new Real[] { 1 },
new Real[] { 0 }
};
FunctionStack nn = new FunctionStack(
new Linear(2, 2, name: "l1 Linear"),
new ReLU(name: "l1 ReLU"),
new Linear(2, 1, name: "l2 Linear"));
nn.SetOptimizer(new AdaGrad());
RILogManager.Default?.SendDebug("Training...");
for (int i = 0; i < learningCount; i++)
{
//use MeanSquaredError for loss function
Trainer.Train(nn, trainData[0], trainLabel[0], new MeanSquaredError(), false);
Trainer.Train(nn, trainData[1], trainLabel[1], new MeanSquaredError(), false);
Trainer.Train(nn, trainData[2], trainLabel[2], new MeanSquaredError(), false);
Trainer.Train(nn, trainData[3], trainLabel[3], new MeanSquaredError(), false);
//If you do not update every time after training, you can update it as a mini batch
nn.Update();
}
RILogManager.Default?.SendDebug("Test Start...");
foreach (Real[] val in trainData)
{
NdArray result = nn.Predict(val)[0];
RILogManager.Default?.SendDebug($"{val[0]} xor {val[1]} = {(result.Data[0] > 0.5 ? 1 : 0)} {result}");
}
}
监控 Kelp.Net
ReflectSoftware 的 ReflectInsight 无疑是当今最好的实时日志记录和丰富可视化框架。Kelp.Net 原生支持此框架,因此您很容易看到测试内部的运行情况。
下面是 ReflectInsight 主屏幕的样貌:

Reflect Insight 主屏幕的一个示例
表盘
监视器允许您在测试执行过程中关注特定的数据元素。在机器学习领域,理解和看到算法内部的确切运行情况至关重要,而监视面板正是实现这一目标的地方:

消息
消息面板是测试执行期间显示每个消息的地方。可用的信息完全取决于您。消息文本左侧显示的图像基于您发送的消息类型(信息、调试、警告、错误等):

属性
每个消息都有预定义的属性,可以通过属性面板查看。有标准属性,如下所示,适用于每个消息。然后还有可自定义的消息属性,可以应用:

消息属性示例
Weaver
Weaver 是 Kelp.Net 的关键组件,当你运行测试时,你将首先调用这个对象。这个对象包含各种 OpenCL 对象,例如:
-
计算上下文
-
一组计算设备
-
计算命令队列
-
一个布尔标志,指示是否启用 GPU
-
计算平台
-
核心源字典
Weaver 是你告诉程序是否将使用 CPU 或 GPU,以及你将使用哪个设备(如果你的系统具有多个设备)的地方。你只需要在程序开始时调用一次 weaver,就像你在这里看到的那样:
Weaver.Initialize(ComputeDeviceTypes.Gpu);
你还可以避免使用 weaver 的初始化调用,并允许它自动确定需要发生什么。
这里是 weaver 的基本内容。其目的是构建(在运行时动态编译)将要执行的程序:
/// <summary> The context. </summary>
internal static ComputeContext Context;
/// <summary> The devices. </summary>
private static ComputeDevice[] Devices;
/// <summary> Queue of commands. </summary>
internal static ComputeCommandQueue CommandQueue;
/// <summary> Zero-based index of the device. </summary>
private static int DeviceIndex;
/// <summary> True to enable, false to disable. </summary>
internal static bool Enable;
/// <summary> The platform. </summary>
private static ComputePlatform Platform;
/// <summary> The kernel sources. </summary>
private static readonly Dictionary<string, string> KernelSources = new Dictionary<string, string>();
编写测试
为 Kelp.Net 编写测试非常简单。你编写的每个测试只需要公开一个 Run 函数。其余的是你想要网络如何运行的逻辑。你的 Run 函数的一般指南将是:
- 加载数据(真实或模拟):
Real[][] trainData = new Real[N][];
Real[][] trainLabel = new Real[N][];
for (int i = 0; i < N; i++)
{
//Prepare Sin wave for one cycle
Real radian = -Math.PI + Math.PI * 2.0 * i / (N - 1);
trainData[i] = new[] { radian };
trainLabel[i] = new Real[] { Math.Sin(radian) };
}
- 创建你的函数堆栈:
FunctionStack nn = new FunctionStack(
new Linear(1, 4, name: "l1 Linear"),
new Tanh(name: "l1 Tanh"),
new Linear(4, 1, name: "l2 Linear")
);
- 选择你的优化器:
nn.SetOptimizer(new SGD());
- 训练你的数据:
for (int i = 0; i < EPOCH; i++)
{
Real loss = 0;
for (int j = 0; j < N; j++)
{
//When training is executed in the network, an error is returned to the return value
loss += Trainer.Train(nn, trainData[j], trainLabel[j], new MeanSquaredError());
}
if (i % (EPOCH / 10) == 0)
{
RILogManager.Default?.SendDebug("loss:" + loss / N);
RILogManager.Default?.SendDebug("");
}
}
- 测试你的数据:
RILogManager.Default?.SendDebug("Test Start...");
foreach (Real[] val in trainData)
{
RILogManager.Default?.SendDebug(val[0] + ":" + nn.Predict(val)[0].Data[0]);
}
基准测试函数
KelpNetTester 类中的 SingleBenchmark 类允许对各种激活、噪声和其他函数进行简单的基准测试。如果一个函数具有 GPU 功能,那么它将被基准测试,CPU 功能也是如此。时间精度在微秒级别,因为 ReLU 前向通常总是低于 1 毫秒的粒度。
启用 CPU
启用 GPU
现在我们来谈谈如何运行单个基准测试。
运行单个基准测试
当你运行 SingleBenchmark 类时,你将在即将出现的图像中看到的功能将被计时。将提供前向和反向 CPU 和 GPU 的时间(当适用时)。以下是基准测试的折叠视图:

这里是基准测试的展开视图:

摘要
在本章中,我们欢迎你进入直观深度学习的世界。我们展示了你如何使用 Kelp.Net 作为你的研究平台来测试几乎任何假设。我们还展示了 Kelp.Net 的强大功能和灵活性。在我们的下一章中,我们将进入量子计算的世界,并展示计算的一小部分未来。戴上你的帽子,这一章是不同的!
参考文献
-
快速特征嵌入的卷积架构,Y Jia, E Shelhamer, J Donahue, S Karayev, J Long,第 22 届 ACM 国际会议论文集,2014
-
Chainer 在
docs.chainer.org/en/stable/index.html -
从微小图像中学习多层特征,Alex Krizhevsky,2009,见
www.cs.toronto.edu/~kriz/learning-features-2009-TR.pdf -
原始 Kelp.Net 在
github.com/harujoh -
(Filice '15) Simone Filice, Giuseppe Castellucci, Danilo Croce, Roberto Basili,Kelp:一种基于核的自然语言处理学习平台,ACL 系统演示会议论文集,北京,中国(2015 年 7 月)
第十三章:量子计算 – 未来
我们将以另一个话题的开始来结束这个系列,量子计算。量子计算是未来,它正在到来,它是真实的。它使用量子力学现象,如叠加和纠缠,其计算基于称为量子比特(qubits)的东西。普通计算机是基于晶体管,并使用著名的 0 和 1,而量子计算使用量子比特,它们可以处于状态的叠加,而不仅仅是开或关。
在撰写本文时,量子计算仍处于起步阶段,但正在取得进展。因此,这一章将会很短,但我们希望让你了解未来,以便你有所了解。
在本章中,我们将涵盖:
-
叠加
-
传送
正在进行许多实验,微软发布了其量子计算软件开发工具包(SDK)。微软和 IBM 都在开发他们自己的量子计算机版本。但在我们到达那里之前,让我们回顾一下你需要了解的一些术语。这些将进入你的超级术语清单!
这里有一个布洛赫球面,它是希尔伯特空间中量子比特的表示,它是量子计算最基本的部分:

一个经典计算机的内存由比特组成——1 和 0。然而,量子计算机由一系列量子比特组成。单个量子比特可以代表一个 1、一个 0,或者这两个量子比特状态的任何量子叠加。单个量子比特可以处于两种状态中的任何一种。一对量子比特可以处于两种状态的任何叠加,三个量子比特可以处于八种状态的任何叠加。因此,量子计算机可以处于许多不同状态的叠加,而传统计算机在任何给定时刻只能处于这些状态中的任何一个。
量子计算机通过量子门和量子逻辑门(类似于传统计算机的逻辑门)运行,它试图解决的问题是通过设置量子比特的初始值来编码的,就像传统计算机一样。量子算法被认为是大多数概率性的,即在已知概率下提供正确解决方案:

量子比特由受控粒子和控制手段(例如,捕获粒子并将它们从一个状态切换到另一个状态的设备)组成。
叠加
维基百科将叠加定义为:
"...量子力学的一个基本原理。它指出,与经典物理学中的波类似,任何两个(或更多)量子状态可以相加(“叠加”),其结果将是另一个有效的量子状态;反之,每个量子状态都可以表示为两个或更多其他不同状态的和。在数学上,它指的是薛定谔方程解的性质;由于薛定谔方程是线性的,任何解的线性组合也将是解。"
这实际上意味着,当量子系统没有被观察时,它可以同时处于多个状态。它们可以同时存在于所有可能的状态中。想象一下风大的日子里池塘上有很多波浪。毫无疑问,你看到它们在某些时候是重叠的。这就是量子叠加的简单粗暴的解释。
超光速
在量子计算中,超光速指的是将量子状态从一个位置移动到另一个位置的方法,而不需要移动任何物理粒子。这个过程通常伴随着发送和接收位置之间的纠缠。量子状态的传输使用量子纠缠现象(我们下一个话题)作为手段。当两个或更多粒子纠缠在一起时,它们的量子状态是相互依赖的,无论它们相隔多远。实际上,它们作为一个单一的量子对象起作用。
量子纠缠
量子纠缠是一种物理现象,当成对或成组的粒子以某种方式生成或相互作用,使得每个粒子的状态不能独立于其他粒子的状态来描述,即使粒子之间相隔很远——相反,必须为整个系统描述一个量子状态。
或许需要一个更直观的例子。假设你有一份今天的报纸,它有 100 页长。如果你阅读了 10 页,你就知道了 10%的内容。如果你再阅读另外 10 页,你现在就知道了 20%的内容,以此类推。然而,如果报纸非常复杂,如果你阅读了 10 页,你几乎什么也不知道。为什么?因为信息分布在那些页面之间,而不是在页面上。所以你必须想出一个方法一次性阅读所有页面。
现在我们已经描述了这些术语,让我们向您展示一个来自微软量子计算 SDK 的快速示例。正如我们提到的,在撰写本文时,量子计算还处于起步阶段,所以我们能做的最好的就是向您展示它的方向。我们将用一个非常简短的例子来做到这一点,然后如果您愿意,您可以继续学习。
那么,量子计算程序看起来是什么样子呢?就像这样:
class Program
{
static void Main(string[] args)
{
using (var sim = new QuantumSimulator())
{
var rand = new System.Random();
foreach (var idxRun in Enumerable.Range(0, 8))
{
var sent = rand.Next(2) == 0;
var received = TeleportClassicalMessage.Run(sim, sent).Result;
System.Console.WriteLine($"Round {idxRun}:tSent {sent},tgot
{received}.");
System.Console.WriteLine(sent == received ? "Teleportation
successful!!n" : "n");
}
}
System.Console.WriteLine("nnPress Enter to continue...nn");
System.Console.ReadLine();
}
}
嗯,就是这样!好吧,差不多。你看,使用微软量子 SDK 的量子计算程序由两部分组成。第一部分是你在这里看到的 C#组件。实际上,前端可以是 C#、Python 以及几种其他语言。后端,我们稍后会看到,是量子部分,用 Q#编写;这是微软的新量子计算语言。每个 Q#操作都会生成一个同名的 C#类,该类将有一个Run方法。这个方法是异步的,因为操作将在量子计算机上异步运行。
根据微软关于 Q#的文档:
"Q#(Q-Sharp)是一种用于表达量子算法的领域特定编程语言。它用于编写在经典主机程序和计算机控制下运行的子程序,在辅助量子处理器上执行。
Q#提供了一组原始类型,以及两种创建新结构类型的方式(数组和解包)。它支持一个基本的程序模型来编写程序,包括循环和 if/then 语句。Q#中的顶级构造是用户定义的类型、操作和函数。
那么,让我们来谈谈我们的 C#代码做了什么。它只是通过量子传输(现在你知道我们为什么从术语开始!)发送一条消息。让我们看看后端,看看发生了什么:
operation Teleport(msg : Qubit, there : Qubit) : ()
{
body
{
using (register = Qubit[1])
{
// Ask for an auxillary qubit that we can use to prepare
// for teleportation.
let here = register[0];
// Create some entanglement that we can use to send our message.\
H(here);
CNOT(here, there);
// Move our message into the entangled pair.
CNOT(msg, here);
H(msg);
// Measure out the entanglement.
if (M(msg) == One) { Z(there); }
if (M(here) == One) { X(there); }
// Reset our "here" qubit before releasing it.
Reset(here);
}
}
}
现在许多想法在你的脑海中涌现。问题比答案多,对吧?别担心;这确实是一种更技术性的软件编写方法,但我们会给你一点启示,让你明白这一切。
H、CNOT、M,发生了什么?这些都是 Q#定义的函数,并将存在于你的项目中的 Q#组件文件中。让我们看看其中一个,并解释一下发生了什么。
CNOT
这将CNOT门(一个受控非门)应用于一个量子比特。CNOT 门是经典门的“量子化”,可以用来纠缠和解纠缠 EPR 状态。对于那些对 EPR 感兴趣的人,我建议阅读一下爱因斯坦-波多尔斯基-罗森佯谬(EPR)。
CNOT门是一组行和列的集合,类似于这样:

H
这个函数将哈达玛变换应用于单个量子比特。它基本上会翻转量子比特的一半,而不是全部。哈达玛变换用于数据加密,以及 JPEG XR 和 MPEG-4 音频视频编解码器等信号处理算法。在视频压缩中,它通常用于绝对变换差分的总和。哈达玛变换还用于质谱学、晶体学等科学方法中。
目前,哈达玛函数定义为:

M
这测量单个量子比特在泡利 Z基下的状态,输出结果由分布给出。M操作定义为:
Pr(Zero||ψ
⟩
)=
⟨
ψ|0
⟩⟨
0|ψ
⟩
Q#语言与 C#有何不同?以下是一些要点。
using语句与 C#中的不同。它用于为处理分配量子比特数组。与 C#中的using语句类似,量子比特在using语句结束时被释放。在整个应用程序的生命周期中,没有量子比特被使用。
Q#有一个不同的for循环,用于遍历范围。没有 C# for循环的直接等效。
默认情况下,Q#中的所有变量都是不可变的,这意味着一旦它们被分配,就不能更改。有一个let关键字可以用来绑定变量。操作参数始终是不可变的。尽管如此,(在撰写本文时)有声明变量并使用set语句稍后设置其值的能力。
摘要
好吧,我希望你们阅读这本书的乐趣和我写作时的乐趣一样。记住,还有很多东西要介绍,而且每天都有新的发展和变化!在系列书的下一本书中,我计划深入探索深度学习的世界,并真正探索一些有趣事物的内部。
现在,我希望你们已经找到了一种方法来接受这些宝贵的开源项目,并将它们融入到你们的日常生活中。无论你是机器学习开发者、数据科学家,还是对上述所有内容都感兴趣的普通 C#开发者,这本书中都有适合每个人的内容。探索开源项目、它们的示例和测试用例;构建一个框架,允许你将它们整合到你的日常生活中。
现在,非常感谢您阅读这本书,并祝您在未来的机器学习努力中一切顺利!我想感谢所有参与创建这本书并将其推向市场的人。Packt 团队非常乐于助人且礼貌,使整个过程变得简单而有趣。对所有校对者,非常感谢您的时间和努力。您的评论帮助使这本书变得更好。


浙公网安备 33010602011771号