ML-NET-机器学习实用指南-全-

ML.NET 机器学习实用指南(全)

原文:annas-archive.org/md5/33e1ce66b918ce1fb5ef8f09832c0dab

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

机器学习ML)在许多行业中得到广泛应用,如科学、医疗保健和研究,其受欢迎程度仍在不断增长。2018 年 3 月,微软推出了 ML.NET,以帮助.NET 爱好者与机器学习(ML)进行工作。通过这本书,你将探索如何使用 C#代码构建 ML.NET 应用程序,并使用各种 ML 模型。

本书首先为你概述了机器学习和使用的 ML 算法类型,同时介绍了 ML.NET 是什么以及为什么需要它来构建 ML 应用程序。然后,你将探索 ML.NET 框架、其组件和 API。本书将作为一本实用指南,帮助你使用 ML.NET 库构建智能应用程序。你将逐渐精通如何使用真实世界示例和数据集实现回归、分类和聚类等 ML 算法。每一章都将涵盖实际应用,展示如何在.NET 应用程序中实现 ML。你还将学习如何将 TensorFlow 集成到 ML.NET 应用程序中。稍后,你将发现如何将回归模型房价预测结果存储在数据库中,并使用 ASP.NET Core Blazor 和 SignalR 在 Web 应用程序中显示从数据库实时预测的结果。

在本书结束时,你将学会如何在 ML.NET 中自信地执行从基础到高级的机器学习任务。

本书面向对象

如果你是一名希望使用 ML.NET 实现机器学习模型的.NET 开发者,那么这本书适合你。这本书对寻找有效工具以实现各种机器学习算法的数据科学家和机器学习开发者也有益。为了有效地掌握本书中涵盖的概念,对 C#和.NET 的基本理解是必需的。

本书涵盖内容

第一章,开始使用机器学习和 ML.NET,讨论了机器学习是什么以及机器学习在我们当今社会中的重要性。它还介绍了 ML.NET,并在了解机器学习的概念及其相关性的基础上,更详细地介绍了如何开始使用它。

第二章,设置 ML.NET 环境,更详细地介绍了如何开始使用 ML.NET,继续概述机器学习以及 ML.NET 如何在新旧应用程序中帮助开发和运行模型。你将确保你的开发环境已设置好,章节结束时,将有一个简单的预训练模型在控制台应用程序中演示,表明你已准备好继续进行训练。

第三章,回归模型,讨论了在 ML.NET 中使用回归和逻辑回归模型,以及相关的数学知识以及这些模型可以帮助解决哪些问题。此外,本章提供了创建和使用 ML.NET 中的回归模型和逻辑回归模型的逐步说明。本章末尾详细介绍了使用数据集和 ML.NET 中的两个模型快速创建的命令行应用程序。

第四章,分类模型,讨论了在 ML.NET 中使用分类训练器模型以及分类模型可以帮助解决哪些问题。对于本章,我们将创建两个应用程序来展示 ML.NET 中的分类训练器支持。第一个应用程序使用 ML.NET 提供的 FastTree 训练器,根据几个属性和比较价格预测汽车是否具有良好价值。第二个应用程序使用 ML.NET 中的 SDCA 训练器对电子邮件数据(主题、正文、发件人)进行分类,将其分类为订单、垃圾邮件或朋友。通过这些应用程序,你还将学习如何评估分类模型。

第五章,聚类模型,讨论了在 ML.NET 中使用 k-means 聚类训练器,以及聚类模型可以帮助解决哪些问题。在本章中,我们将使用 ML.NET 提供的 k-means 聚类训练器来创建一个示例应用程序,该应用程序将文件分类为可执行文件、文档或脚本。此外,你还将学习如何在 ML.NET 中评估聚类模型。

第六章,异常检测模型,讨论了在 ML.NET 中使用异常检测模型以及异常检测模型可以帮助解决哪些问题。对于本章,我们将创建两个示例应用程序。第一个应用程序使用 ML.NET 与 SSA 检测网络流量异常,而第二个示例使用 ML.NET 与 PCA 检测一系列用户登录中的异常。通过这些应用程序,我们还将探讨如何在你训练了异常检测模型之后评估它。

第七章,矩阵分解模型,讨论了在 ML.NET 中使用矩阵分解模型,以及相关的数学知识以及矩阵分解模型可以帮助解决哪些问题。在本章中,我们将创建一个音乐推荐应用程序,使用 ML.NET 提供的矩阵分解训练器。利用几个数据点,这个推荐引擎将根据模型提供的训练数据推荐音乐。此外,在创建此应用程序之后,我们将学习如何在 ML.NET 中评估矩阵分解模型。

第八章,使用 ML.NET 与.NET Core 和预测,介绍了一个利用.NET Core 的实战应用,并使用回归和时间序列模型来演示股票价格的预测。

第九章,使用 ML.NET 与 ASP.NET Core,介绍了一个利用 ASP.NET 的实战应用,通过前端上传文件以确定其是否恶意。本章重点介绍如何使用二分类器并将其集成到 ASP.NET 应用程序中。

第十章,使用 ML.NET 与 UWP,介绍了一个利用 UWP 和 ML.NET 的实战应用。该应用将使用 ML.NET 来分类网页内容是否恶意。本章还将简要介绍 UWP 应用程序设计和 MVVM,以提供一个真正的生产就绪的示例应用程序,用于构建或适应其他应用程序以使用 ML.NET 与 UWP。

第十一章,训练和构建生产模型,涵盖了在规模上训练模型的所有考虑因素,以及使用 DMTP 项目正确训练生产模型。所学到的经验包括获取适当的训练集(多样性是关键)、适当的特征以及对你模型的真正评估。本章的重点是关于训练生产就绪模型的技巧、窍门和最佳实践。

第十二章,使用 TensorFlow 与 ML.NET,讨论了使用预训练的 TensorFlow 模型与 ML.NET 结合,通过 UWP 应用程序确定图片中是否有汽车。

第十三章,使用 ONNX 与 ML.NET,讨论了使用预训练的 ONNX 模型与 ML.NET 结合,以及将现有的 ONNX 格式模型直接带入 ML.NET 所增加的价值。

为了充分利用本书

您需要在计算机上安装 Angular 版本——如果可能的话,安装最新版本。所有代码示例都已使用 Windows OS 上的 Angular 9 进行测试。然而,它们也应该适用于未来的版本发布。

本书涵盖的软件/硬件 操作系统要求
Microsoft Visual Studio 2019 一个常见的 Windows 10 开发环境,具有 20-50 GB 的空闲空间(强烈推荐使用四核处理器和 8 GB 的 RAM)

如果您正在使用这本书的数字版,我们建议您亲自输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将帮助您避免与代码复制/粘贴相关的任何潜在错误。

下载示例代码文件

您可以从www.packt.com的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载。

  4. 在搜索框中输入书名,并遵循屏幕上的说明。

下载文件后,请确保使用最新版本解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

书籍的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Machine-Learning-with-ML.NET。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他代码包,这些代码包来自我们丰富的书籍和视频目录,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789801781_ColorImages.pdf

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“应用程序首次运行时,使用图像和tags.tsv文件(将在下一节中讨论)训练 ML.NET 模型版本。”

代码块设置如下:

public void Classify(string imagePath)
{
    var result = _prediction.Predict(imagePath);

    ImageClassification = $"Image ({imagePath}) is a picture of {result.PredictedLabelValue} with a confidence of {result.Score.Max().ToString("P2")}";
}

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

dotnet --version
3.0.100

粗体:表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的文字会以这种方式显示。以下是一个示例:“首先,确保已选中.NET 桌面开发通用 Windows 平台开发ASP.NET 和 Web 开发。”

警告或重要提示如下所示。

技巧和窍门如下所示。

联系我们

我们欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com给我们发邮件。

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告这一点,我们将不胜感激。请访问 www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上以任何形式发现了我们作品的非法副本,如果您能向我们提供位置地址或网站名称,我们将不胜感激。请通过copyright@packt.com与我们联系,并附上材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

评价

请留下您的评价。一旦您阅读并使用了这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,并且我们的作者可以查看他们对书籍的反馈。谢谢!

如需了解更多关于 Packt 的信息,请访问 packt.com

第一部分:机器学习和 ML.NET 的基础知识

本节概述了本书的读者对象,并对机器学习以及学习如何利用机器学习的重要性进行了简要介绍。此外,本节向读者介绍了 ML.NET。它还讨论了构建应用程序所需的工具和框架,并逐步解释了如何使用 ML.NET。

本节包括以下章节:

  • 第一章,开始使用机器学习和 ML.NET

  • 第二章,设置 ML.NET 环境

第一章:开始使用机器学习和 ML.NET

通过打开这本书,你正在通过使用微软的 ML.NET 框架,以机器学习的方法来接近解决复杂问题的解决方案,从而开始颠覆自己的知识。你将使用 Microsoft's ML.NET 框架来实现这一点。在将机器学习应用于网络安全领域度过了数年之后,我坚信,你从这本书中获得的知识不仅会为你打开职业机会,还会开启你的思维过程,改变你解决问题的方法。你将不再会面对复杂问题而不去思考机器学习如何可能解决它。

在本书的整个过程中,你将学习以下内容:

  • 如何以及何时使用 ML.NET 提供的五种不同算法

  • 展示 ML.NET 算法的实际应用端到端示例

  • 训练模型、构建训练集和特征工程的最佳实践

  • 在 TensorFlow 和 ONNX 格式中使用预训练模型

本书假设你有一个相当扎实的 C#理解。如果你有其他使用强类型面向对象编程语言(如 C++或 Java)的经验,语法和设计模式足够相似,不会阻碍你阅读本书。然而,如果你是第一次深入研究强类型语言(如 C#),我强烈建议你阅读 Gaurav Aroraa 所著的《Learn C# in 7 Days》,由 Packt Publishing 出版,以快速建立基础。此外,不需要或预期有先前的机器学习经验,尽管对机器学习有初步的了解将加速你的学习。

在本章中,我们将涵盖以下内容:

  • 了解机器学习的重要性

  • 模型构建过程

  • 探索学习类型

  • 探索各种机器学习算法

  • ML.NET 简介

到本章结束时,你应该对从头到尾构建模型所需的基本要素有一个基本的理解,这为本书的其余部分提供了基础。

了解机器学习的重要性

在最近几年中,机器学习和人工智能已经成为我们生活中许多领域的不可或缺的一部分,这些领域包括在 MRI 中寻找癌细胞以及职业篮球比赛中的面部和物体识别。仅在 2013 年至 2017 年的短短四年中,机器学习专利就增长了 34%,预计到 2021 年支出将增长到 576 亿美元(www.forbes.com/sites/louiscolumbus/2018/02/18/roundup-of-machine-learning-forecasts-and-market-estimates-2018/#794d6f6c2225)。

尽管机器学习是一个不断发展的技术,但“机器学习”这个术语是在 1959 年由亚瑟·塞缪尔提出的——那么是什么导致了 60 年的采用差距?可能最显著的两个因素是能够快速处理模型预测的技术可用性,以及每分钟以数字形式捕获的数据量。根据 DOMO Inc 的研究,2017 年的一项研究得出结论,每天产生 2.5 泽字节的数据,当时,2015 年至 2017 年间,全球 90%的数据被创造出来(www.domo.com/learn/data-never-sleeps-5?aid=ogsm072517_1&sf100871281=1)。到 2025 年,预计每天将产生 463 艾字节的数据,其中大部分将来自汽车、视频、图片、物联网设备、电子邮件,甚至尚未过渡到智能运动的设备(www.visualcapitalist.com/how-much-data-is-generated-each-day/)。

在过去十年中,数据量的增长引发了关于企业或公司如何利用这些数据来改善销售预测、预测客户需求或检测文件中的恶意字节的问题。传统的统计方法可能需要指数级增加员工人数才能跟上当前的需求,更不用说随着捕获的数据进行扩展了。以谷歌地图为例。2013 年,谷歌收购了 Waze,谷歌地图的用户因此得到了基于其用户匿名 GPS 数据的非常准确的路线建议。在这个模型下,数据点(在这种情况下是智能手机的 GPS 数据)越多,谷歌对你的旅行预测就越好。正如我们将在本章后面讨论的,高质量的数据库是机器学习的关键组成部分,尤其是在谷歌地图的情况下,如果没有合适的数据库,用户体验就会大打折扣。

此外,计算机硬件的速度,特别是专门为机器学习定制的专用硬件,也发挥了作用。应用特定集成电路ASICs)的使用呈指数增长。市场上最受欢迎的 ASIC 之一是谷歌的张量处理单元TPU)。它最初于 2016 年发布,此后已经经过了两次迭代,并为谷歌云平台上的机器学习任务提供了基于云的加速。其他云平台,如亚马逊的 AWS 和微软的 Azure,也提供了 FPGA。

此外,来自 AMD 和 NVIDIA 的 图形处理单元GPU)分别通过 ROCm 平台和 CUDA 加速库加速了基于云和本地的工作负载。除了加速工作负载外,AMD 和 NVIDIA 提供的典型专业 GPU 比仅采用 CPU 的传统方法提供了更高的处理器密度。例如,AMD Radeon Instinct MI60 提供了 4,096 个流处理器。虽然它不是一个完整的 x86 内核,但它也不是一对一的比较,双精度浮点任务的峰值性能被评为 7.373 TFLOPs,而 AMD 极其强大的 EPYC 7742 服务器 CPU 的性能为 2.3 TFLOPs。从成本和可扩展性的角度来看,即使在工作站配置中利用 GPU,如果算法加速以利用 AMD 和 NVIDIA 提供的更专业的核心,也会提供指数级的训练时间减少。幸运的是,ML.NET 提供了几乎不需要额外努力的 GPU 加速。

从软件工程职业的角度来看,随着这种增长和需求远远超过供应,作为软件工程师开发机器学习技能从未有过更好的时机。此外,软件工程师还拥有传统数据科学家不具备的技能——例如,能够自动化模型构建过程等任务,而不是依赖于手动脚本。软件工程师可以提供更多价值的另一个例子是在训练模型时,将单元测试和有效性测试作为完整流程的一部分。在一个大型生产应用中,拥有这些自动化测试对于避免生产问题至关重要。

最后,在 2018 年,数据首次被认为比石油更有价值。随着行业继续采用数据收集的使用,现有行业利用他们拥有的数据,机器学习将与数据交织在一起。机器学习对数据的重要性就像炼油厂对石油的重要性一样。

模型构建过程

在深入探讨 ML.NET 之前,了解核心机器学习概念是必要的。这些概念将为您构建基础,以便我们在本书的整个过程中开始构建模型并学习 ML.NET 提供的各种算法。从高层次来看,生成模型是一个复杂的过程;然而,它可以被分解为六个主要步骤:

图片

在接下来的几节中,我们将详细讨论这些步骤,以向您提供如何执行每个步骤以及每个步骤如何与整个机器学习过程相关的清晰理解。

定义你的问题陈述

实际上,你试图解决什么问题?在这个阶段保持具体至关重要,因为一个不够简洁的问题可能导致大量的返工。例如,考虑以下问题陈述:预测选举结果。当我听到这个问题陈述时,我的第一个问题会是,在哪个层面?县、州还是国家?每个层面可能需要比上一个层面更多的特征和数据来正确预测。在机器学习旅程的早期,一个更好的问题陈述可能是针对县一级的具体职位,例如预测 2020 年约翰·多伊县市长。有了这个更直接的问题陈述,你的特征和数据集将更加聚焦,并且更有可能实现。即使在机器学习方面有更多经验,正确界定你的问题陈述也是至关重要的。应遵循谁(Who)、什么(What)、何时(When)、何地(Where)和为什么(Why)的五个 W,以保持你的陈述简洁。

定义你的特征

机器学习的第二步是定义你的特征。将特征视为你希望解决的问题的组成部分或属性。在机器学习中——特别是在创建新模型时——特征对你的模型性能影响最大。正确思考你的问题陈述将促进一组初始特征,这将推动你的数据集和模型结果之间的差异化。回到前一部分的市长例子,你会考虑哪些数据点作为市民的特征?或许可以从市长竞选和他在问题上的立场与其他候选人不同的方式开始考虑。这些值可以转化为特征,然后制成民意调查供约翰·多伊县的市民回答。使用这些数据点将创建特征的第一阶段坚实基础。这里的一个方面也存在于模型构建中,即运行多个特征工程和模型训练迭代,尤其是在你的数据集增长时。在模型评估后,特征重要性用于确定哪些特征实际上在驱动你的预测。偶尔,你会发现,在经过几轮模型训练和特征工程后,直觉特征实际上可能无关紧要。

在第十一章《训练和构建生产模型》中,我们将深入探讨在定义特征和解决复杂问题的常见方法时的最佳实践,以获得特征工程的第一阶段坚实基础。

获取数据集

如你所想,模型构建过程中最重要的方面之一是获取高质量的数据集。数据集用于在监督学习的情况下训练模型,以确定输出应该是什么。在无监督学习的情况下,需要对数据集进行标记。在创建数据集时,一个常见的误解是“越大越好”。在很多情况下,这远远不是事实。继续前面的例子,如果所有的民意调查结果对每个问题都给出了相同的回答,那会怎样?到那时,你的数据集将只包含相同的数据点,你的模型将无法正确预测任何其他候选人。这种情况被称为 过度拟合。对于机器学习算法来说,需要一个多样化但具有代表性的数据集,才能正确构建一个生产就绪的模型。

在第十一章 训练和构建生产模型,我们将深入探讨获取高质量数据集的方法,查看有用的资源,管理你的数据集的方式,以及数据转换,通常称为数据清洗。

特征提取和管道

一旦你获得了特征和数据集,下一步就是进行特征提取。特征提取,根据你的数据集大小和特征,可能是模型构建过程中最耗时的元素之一。

例如,假设上述虚构的约翰·多伊县选举民意调查的结果有 40,000 个回复。每个回复都存储在一个从网页表单捕获的 SQL 数据库中。执行一个 SQL 查询,比如说你将所有数据返回到一个 CSV 文件中,然后使用这个文件来训练你的模型。从高层次来看,这就是你的特征提取和管道。对于更复杂的场景,例如预测恶意网络内容或图像分类,提取将包括文件中特定字节的二进制提取。正确存储这些数据以避免需要重新运行提取对于快速迭代至关重要(假设特征没有变化)。

在第十一章 [训练和构建生产模型] 中,我们将深入探讨如何对你的特征提取数据进行版本控制并保持对数据集的控制,特别是随着数据集规模的增加。

模型训练

在特征提取之后,你现在已经准备好训练你的模型了。幸运的是,使用 ML.NET 进行模型训练非常直接。根据特征提取阶段提取的数据量、管道的复杂性和主机机的规格,这一步可能需要几个小时才能完成。当你的管道变得非常大,你的模型变得更加复杂时,你可能发现自己需要比笔记本电脑或台式机能提供的更多计算资源;存在像 Spark 这样的工具来帮助你扩展到 n 个节点。

在第十一章“训练和构建生产模型”中,我们将讨论使用易于使用的开源项目来扩展这一步骤的工具和技巧。

模型评估

模型训练完成后,最后一步是评估模型。典型的模型评估方法是保留一部分数据集用于评估。其背后的想法是将已知数据提交给训练好的模型,并衡量模型的有效性。这一步骤的关键是保留一个具有代表性的数据集。如果你的保留集受到某种影响,那么你很可能会得到一个关于高绩效或低绩效的虚假印象。在下一章中,我们将深入探讨各种评分和评估指标。ML.NET 提供了一个相对简单的接口来评估模型;然而,每个算法都有独特的属性需要验证,我们将在深入研究各种算法时进行回顾。

探索学习类型

现在你已经了解了构成模型构建过程的步骤,接下来要介绍的是两种主要的学习类型。还有其他几种机器学习类型,例如强化学习。然而,在本书的范围内,我们将专注于 ML.NET 提供的两种类型——监督学习和无监督学习。如果你对其他类型的学习感兴趣,可以查看 Giuseppe Bonaccorso 所著的《机器学习算法》,Packt 出版社出版。

监督学习

监督学习是两种类型中更常见的一种,因此它也用于我们将在本书中涵盖的大多数算法。简单来说,监督学习意味着作为数据科学家,你将已知的输出作为训练的一部分传递给模型。以本章前面讨论的选举示例为例。在监督学习中,选举调查中用作特征的每个数据点,以及他们表示将投票给谁,都会在训练期间发送到模型。这一步骤在分类算法中传统上被称为标记,其输出值将是预训练标签之一。

无监督学习

相反,在无监督学习中,典型的用例是在确定输入和输出标签困难时。以选举场景为例,当你不确定哪些特征将真正为模型提供数据点以确定选民投票时,无监督学习可以提供价值和见解。这种方法的优点是,你选择的算法决定了驱动标签的特征。例如,使用 k-means 这样的聚类算法,你可以将所有选民数据点提交给模型。然后,算法能够将选民数据分组到簇中,并预测未见数据。我们将在第五章“聚类模型”中深入探讨无监督学习和聚类。

探索各种机器学习算法

机器学习的核心是用于解决复杂问题的各种算法。如引言中所述,本书将涵盖五种算法:

  • 二元分类

  • 回归

  • 异常检测

  • 聚类

  • 矩阵分解

每个算法将在本书后面的章节中详细介绍,但在此,我们先对这些算法进行简要概述。

二元分类

最容易理解的算法之一是二元分类。二元分类是一种监督式机器学习算法。正如其名,使用二元分类算法训练的模型将返回一个真或假的判断(如 0 或 1)。最适合二元分类模型的问题包括判断一个评论是否仇恨或一个文件是否恶意。ML.NET 提供了几个二元分类模型算法,我们将在第四章,分类模型中介绍,并附带一个判断文件是否恶意的示例。

回归

另一个强大且易于理解的算法是回归。回归是另一种监督式机器学习算法。与二元算法或返回特定值集合的算法不同,回归算法返回一个实数值。你可以将回归算法视为一个代数方程求解器,其中有许多已知值,目标是预测一个未知值。一些最适合回归算法的问题包括预测流失、天气预报、股市预测和房价等。

此外,还有一组称为逻辑回归模型的回归算法子集。与前面描述的传统线性回归算法不同,逻辑回归模型将返回结果发生的概率。

ML.NET 提供了几个回归模型算法,我们将在第三章,回归模型中介绍。

异常检测

异常检测,正如其名,是在提交给模型的 数据中寻找意外事件。这个算法的数据,你可能猜得到,需要一段时间内的数据。ML.NET 中的异常检测不仅考虑峰值,还考虑变化点。峰值,正如其名,是暂时的,而变化点是更长变化的开端。

ML.NET 提供了一个异常检测算法,我们将在第六章,异常检测模型中介绍。

聚类

聚类算法是无监督算法,为寻找相关项最接近匹配的问题提供了一种独特的解决方案。在数据训练过程中,数据根据特征分组,然后在预测过程中选择最接近的匹配项。聚类算法的用途示例包括文件类型分类和预测客户选择。

ML.NET 特别使用 k-means 算法,我们将在第五章“聚类模型”中深入探讨。

矩阵分解

最后但同样重要的是,矩阵分解算法提供了一个强大且易于使用的算法,用于提供推荐。此算法针对历史数据可用且需要解决的问题是从该数据中预测选择的情况,例如电影或音乐预测。Netflix 的电影推荐系统就使用了矩阵分解来提供他们认为你会喜欢的电影建议。

我们将在第七章“矩阵分解模型”中详细介绍矩阵分解。

什么是 ML.NET?

现在你对核心机器学习概念有了相当深入的理解,我们现在可以深入了解微软的 ML.NET 框架。ML.NET 是微软的顶级机器学习框架。它提供了一个易于使用的框架,在.NET 生态系统中轻松训练、创建和运行模型。

微软的 ML.NET 于 2018 年 5 月在华盛顿州西雅图的微软开发者大会 BUILD 上宣布并发布(版本 0.1)。该项目本身是开源的,在 GitHub 上拥有 MIT 许可证(github.com/dotnet/machinelearning),自首次发布以来,已经更新了 17 次。

在微软内部使用 ML.NET 的一些产品包括 Excel 中的 Chart Decisions、PowerPoint 中的幻灯片设计、Windows Hello 和 Azure 机器学习。这强调了 ML.NET 在生产部署中的生产就绪性。

ML.NET 从一开始就被设计和构建,以方便 C#和 F#开发者使用机器学习,其架构对于熟悉.NET Framework 的人来说是自然而然的。在 ML.NET 出现之前,没有一个完整的、受支持的框架,你不仅可以在.NET 生态系统中训练模型,还可以运行模型。例如,Google 的 TensorFlow 有一个由 Miguel de Icaza 编写的开源包装器,可在 GitHub 上找到(github.com/migueldeicaza/TensorFlowSharp);然而,在撰写本书时,大多数工作流程都需要使用 Python 来训练模型,然后由 C#包装器运行预测。

此外,微软还致力于支持所有.NET 开发者过去几年习惯于发布应用程序的主要平台。以下是一些平台的示例,括号中列出了它们的目标框架:

  • 网络端(ASP.NET)

  • 移动端(Xamarin)

  • 桌面端(UWP、WPF 和 WinForms)

  • 游戏开发(MonoGame 和 SharpDX)

  • 物联网(.NET Core 和 UWP)

在本书的后续部分,我们将在这大多数平台上实现几个现实世界的应用,以展示如何将 ML.NET 集成到各种应用类型和平台中。

ML.NET 的技术细节

随着 ML.NET 1.4 的发布,推荐将目标设置为.NET Core 3.0 或更高版本,以利用.NET Core 3.0 作为一部分添加的硬件内建函数。对于那些不熟悉的人,.NET Core 2.x(以及更早版本)以及.NET Framework 针对具有流式 单指令多数据SIMD扩展SSE)的 CPU 进行了优化。实际上,这些指令为在数据集上执行多个 CPU 指令提供了一条优化路径。这种方法被称为单指令多数据SIMD)。鉴于 SSE CPU 扩展最初是在 1999 年的 Pentium III 中添加的,后来 AMD 在 2001 年的 Athlon XP 中添加,这提供了一条极其向后兼容的路径。然而,这也并不允许代码利用过去 20 年中 CPU 扩展的所有进步。其中一项进步是大多数 2011 年或之后创建的 Intel 和 AMD CPU 上可用的高级向量扩展AVX)。

这在单条指令中提供了八个 32 位操作,而 SSE 提供的是四个。正如你可能猜到的,机器学习可以利用这种指令数量的加倍。对于.NET Core 3 中尚未支持(如 ARM)的 CPU,.NET Core 3 会自动回退到基于软件的实现。

ML.NET 组件

如前所述,ML.NET 被设计成对经验丰富的.NET 开发者直观。其架构和组件与 ASP.NET 和 WPF 中发现的模式非常相似。

ML.NET 的核心是MLContext对象。类似于.NET 应用程序中的AppContextMLContext是一个单例类。MLContext对象本身提供了对 ML.NET 提供的所有训练目录的访问(其中一些由额外的 NuGet 包提供)。你可以将 ML.NET 中的训练目录视为一个特定的算法,例如二元分类或聚类。

这里是 ML.NET 的一些目录:

  • 异常检测

  • 二元分类

  • 聚类

  • 预测

  • 回归

  • 时间序列

这六个算法组在本章中已进行了回顾,并将在本书的后续专门章节中更详细地介绍。

此外,ML.NET 1.4 最近添加了直接从数据库导入数据的功能。这个特性,尽管在撰写本文时处于预览状态,不仅可以简化特征提取过程,还可以扩展在现有应用程序或管道中进行实时预测的可能性。所有主要数据库都受到支持,包括 SQL Server、Oracle、SQLite、PostgreSQL、MySQL、DB2 和 Azure SQL。我们将在第四章“分类模型”中探索这个特性,使用 SQLite 数据库的控制台应用程序。

下图展示了 ML.NET 的高级架构:

图片

在这里,您可以看到几乎与传统的机器学习过程完全一致。这是有意为之,以降低熟悉其他框架的人的学习曲线。架构中的每一步可以总结如下:

  1. IDataView:这个接口用于将加载的训练数据存储到内存中。

  2. 创建管道:管道创建将 IDataView 对象属性映射到值,以便发送给模型进行训练。

  3. Fit():无论算法如何,在创建管道后,调用 Fit() 将启动实际的模型训练。

  4. Save():正如其名所示,这个接口用于将模型(以二进制格式)保存到文件。

  5. ITransformer:这个接口用于将模型重新加载到内存中以运行预测。

  6. Evaluate():正如其名所示,这个接口用于评估模型(第二章 设置 ML.NET 环境将进一步探讨评估架构)。

在本书的整个过程中,我们将更深入地探讨这些方法。

ML.NET 的可扩展性

最后,ML.NET,像大多数健壮的框架一样,提供了相当大的可扩展性。微软随后推出了额外的可扩展性支持,以便能够运行以下外部训练的模型类型,以及其他类型:

  • TensorFlow

  • ONNX

  • Infer.Net

  • CNTK

TensorFlow (www.tensorflow.org/),如前所述,是谷歌的机器学习框架,具有对 C++、Go、Java 和 JavaScript 的官方支持绑定。此外,TensorFlow 可以通过 GPU 加速,正如之前提到的,还有谷歌自己的 TPUs。此外,类似于 ML.NET,它还提供了在包括 iOS、Android、macOS、ARM、Linux 和 Windows 在内的广泛平台上运行预测的能力。谷歌提供了多个预训练模型。其中较受欢迎的一个模型是图像分类模型,它可以对提交的图像中的对象进行分类。ML.NET 近期的一些改进使得您可以根据该预训练模型创建自己的图像分类器。我们将在第十二章 使用 TensorFlow 与 ML.NET 中详细讨论这一场景。

ONNX (onnx.ai/),即 Open Neural Network Exchange Format 的缩写,由于能够导出为通用格式,在数据科学领域被广泛使用。ONNX 具有 XGBoost、TensorFlow、scikit-learn、LibSVM 和 CoreML 等转换器。ML.NET 对 ONNX 格式的原生支持不仅将允许与现有的机器学习管道更好地扩展,还将增加 ML.NET 在机器学习世界中的采用率。我们将在第十三章 使用 ONNX 与 ML.NET 中使用预训练的 ONNX 格式模型。

Infer.Net 是另一个专注于概率编程的微软开源机器学习框架。你可能想知道概率编程是什么。从高层次来看,概率编程处理传统变量类型确定性的灰色区域,例如布尔值或整数。概率编程使用具有一系列可能结果的随机变量,类似于数组。常规数组和概率编程中的变量之间的区别在于,对于每个值,都有一个特定值发生的概率。

Infer.Net 的一个很好的实际应用是微软 TrueSkill 背后的技术。TrueSkill 是一个评分系统,它为《光环》和《战争机器》中的匹配系统提供动力,玩家根据多种变量、游戏类型以及地图等因素进行匹配,甚至可以归因于两个玩家之间的匹配程度。虽然这超出了本书的范围,但一份深入探讨 Infer.Net 和概率编程(一般而言)的出色白皮书可以在这里找到:dotnet.github.io/infer/InferNet_Intro.pdf

CNTK,也来自微软,其缩写为认知工具包,是一个专注于神经网络的深度学习工具包。CNTK 的一个独特特性是它通过有向图来描述神经网络。虽然这超出了本书的范围(我们将在第十二章使用 TensorFlow 介绍神经网络),但前馈深度神经网络、卷积神经网络和循环神经网络的世界非常迷人。要更深入地了解神经网络,我建议阅读《使用 C#进行神经网络编程实践》,这也是 Packt 出版的一本书。

将 Azure 和其他模型支持(如 PyTorch [https://pytorch.org/](https://pytorch.org/))扩展到 Azure 的计划中,但在撰写本文时尚未确定时间表。

摘要

在本章中,你学习了发现机器学习的重要性。此外,你还学习了机器学习的核心概念,包括学习和本书后面将要介绍的算法之间的差异。你还接受了 ML.NET 的简介。本章的核心概念是本书其余部分的基础,我们将在后续章节中在此基础上进行构建。在下一章中,我们将设置你的环境并在 ML.NET 中训练你的第一个模型!

第二章:设置 ML.NET 环境

现在,您已经掌握了机器学习的基础知识,了解了 Microsoft 的 ML.NET 是什么以及它提供了什么,是时候训练并创建您的第一个机器学习模型了!我们将基于评论构建一个简单的餐厅情感分析模型,并将此模型集成到一个简单的 .NET Core 应用程序中。在我们开始训练和创建模型之前,我们首先需要配置开发环境。

在本章中,我们将涵盖以下主题:

  • 设置您的开发环境

  • 使用 ML.NET 从头到尾创建您的第一个模型

  • 评估模型

设置您的开发环境

幸运的是,为 ML.NET 配置环境相对简单。在本节中,我们将安装 Visual Studio 2019 和 .NET Core 3。如果您不确定是否已安装,请按照以下步骤操作。此外,在本书的后续章节中,您开始自己实验时,我们还需要尽早建立一些组织元素和流程。

安装 Visual Studio

ML.NET 开发的核心是 Microsoft Visual Studio。本书中使用的所有示例和截图均基于 Windows 10 19H2 上的 Microsoft Visual Studio 2019 Professional。在撰写本文时,16.3.0 是最新版本。请使用最新版本。如果您没有 Visual Studio 2019,可以在 www.visualstudio.com 免费获得功能齐全的社区版。

如 第一章 中所述,本书的范围是开始使用机器学习和 ML.NET,我们将创建各种应用程序类型,以展示 ML.NET 在特定应用平台上的各种问题领域。因此,我们将提前安装几个可用的工作负载,以避免在后续章节中需要返回安装程序:

  1. 首先,请确保已选中.NET 桌面开发通用 Windows 平台开发ASP.NET 和 Web 开发。这些工作负载将使您能够创建我们将在后续章节中使用的 UWP、WPF 和 ASP.NET 应用程序:

图片

  1. 此外,请确保已选中.NET Core 跨平台开发。这将使 .NET Core 能够为命令行和桌面应用程序进行开发,例如我们将在本章后面制作的程序:

图片

安装 .NET Core 3

如第一章“开始使用机器学习和 ML.NET”中所述,由于.NET Core 3 在开发过程中实现了优化工作,因此在针对多个平台时,.NET Core 3 是写作时的首选.NET 框架。在写作时,.NET Core 3 在版本 16.3.0 之前的 Visual Studio 安装程序中未捆绑,需要在此处单独下载:dotnet.microsoft.com/download/dotnet-core/3.0。本书中特定使用的下载版本是 3.0.100,但您阅读时可能已有更新的版本。对于好奇的读者,运行时与 SDK 捆绑在一起。

您可以通过打开 PowerShell 或命令提示符并执行以下命令来验证安装是否成功:

dotnet --version
3.0.100

输出应从3开始,如下所示。在写作时,3.0.100 是可用的最新生产版本。

确保安装 32 位和 64 位版本,以避免在本书后续部分和未来的实验中针对 32 位和 64 位平台时出现的问题。

创建一个流程

在本书的整个过程中以及您自己的探索中,您将收集样本数据,构建模型,并尝试各种应用程序。尽早建立一个流程来组织这些元素将使长期工作更加容易。以下是一些建议,供您参考:

  • 总是使用源控制来管理所有代码。

  • 确保测试集和训练集在其各自的文件夹中命名正确(如果可能的话,进行版本管理)。

  • 使用命名和源控制对模型进行版本管理。

  • 将评估指标和使用的参数保存在电子表格中。

随着您技能的提升和更复杂问题的创建,可能需要额外的工具,如 Apache Spark 或其他聚类平台。我们将在第十一章“训练和构建生产模型”中讨论这一点,以及其他关于大规模训练的建议。

创建您的第一个 ML.NET 应用程序

现在是时候开始创建您的第一个 ML.NET 应用程序了。对于这个第一个应用程序,我们将创建一个.NET Core 控制台应用程序。这个应用程序将根据提供的小样本数据集对单词句子进行分类,判断其为正面陈述还是负面陈述。对于这个项目,我们将使用随机对偶坐标上升法SDCA)的二进制逻辑回归分类模型。在第三章“回归模型”中,我们将更深入地探讨这种方法。

在 Visual Studio 中创建项目

打开时,根据你在 Visual Studio 中的配置,它将直接打开到项目创建屏幕,或者将是一个空的 Visual Studio 窗口。如果你的环境显示后者,只需点击 文件,然后 新建,然后 项目

  1. 当窗口打开时,在搜索字段中输入 console app 以找到 Console App (.NET Core)。确保语言类型是 C#(有相同名称的 Visual Basic 模板),突出显示此模板,然后点击 下一步

  1. 我建议给项目命名一个你可以回想起来的名字,比如 Chapter02,这样你以后就能找到这个项目:

  1. 到目前为止,你有一个 .NET Core 3 控制台应用程序,所以现在让我们添加 ML.NET NuGet 包。在项目上右键单击并点击 管理 NuGet 包

  1. 在搜索字段中输入 microsoft ml。你应该能看到可用的最新 Microsoft.ML 版本:

  1. 一旦找到,点击 安装 按钮。很简单!

在撰写本文时,1.3.1 是可用的最新版本,本书中的所有示例都将使用该版本。在 1.0 之前,语法变化很大,但自那时起一直保持一致,因此使用较新版本应该功能相同。

在这个阶段,项目已经配置为使用 ML.NET——所有未来的项目都将以此方式引用 ML.NET,并回指这些步骤。

项目架构

简单的项目将被分为两个主要功能:

  • 训练和评估

  • 模型运行

这种功能上的分割反映了现实世界的生产应用程序,这些应用程序通常使用机器学习,因为通常有专门的团队负责每个部分。

对于那些希望从完成的项目开始并跟随本节其余部分的人来说,你可以从这里获取代码:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter02

以下截图显示了 Visual Studio 解决方案资源管理器中的项目分解。如前所述,项目分为两个主要类——PredictorTrainer

Trainer 类包含所有模型构建和评估的代码,而 Predictor 类,正如其名所示,包含使用训练模型进行预测的代码。

BaseML类是我们将在后续章节中使用并在本书的其余部分进行扩展的类。这个类的想法是减少DRY不要 重复 自己)违规,并创建一个统一且易于迭代的框架。《Constants》类进一步帮助这个想法——在进入更复杂的应用时减少魔法字符串;这种设计将在所有未来的章节项目中使用。

最后,Program类是我们控制台应用程序的主要入口点。

运行代码

我们现在将深入探讨在这个项目中使用的各种类,包括以下类:

  • RestaurantFeedback

  • RestaurantPrediction

  • Trainer

  • Predictor

  • BaseML

  • Program

RestaurantFeedback

RestaurantFeedback类为我们模型提供输入类。在 ML.NET(和其他框架)中,传统的做法是有一个结构化输入来馈入您的数据管道,该管道随后被传递到训练阶段,最终传递到您的训练模型。

以下类定义了我们的容器类,用于存储预测值。这是我们将在本书的其余部分使用的方法:

using Microsoft.ML.Data;

namespace chapter02.ML.Objects
{
     public class RestaurantFeedback
     {
          [LoadColumn(0)]
          public bool Label { get; set; }

          [LoadColumn(1)]
          public string Text { get; set; }
     }
}

您可能想知道RestarauntFeedback类中的LabelText属性与源数据之间的相关性。在Data文件夹中,有一个名为sampledata.csv的文件。此文件包含以下内容:

0    "Great Pizza"
0    "Awesome customer service"
1    "Dirty floors"
1    "Very expensive"
0    "Toppings are good"
1    "Parking is terrible"
0    "Bathrooms are clean"
1    "Management is unhelpful"
0    "Lighting and atmosphere are romantic"
1    "Crust was burnt"
0    "Pineapple was fresh"
1    "Lack of garlic cloves is upsetting"
0    "Good experience, would come back"
0    "Friendly staff"
1    "Rude customer service"
1    "Waiters never came back"
1    "Could not believe the napkins were $10!"
0    "Supersized Pizza is a great deal"
0    "$5 all you can eat deal is good"
1    "Overpriced and was shocked that utensils were an upcharge"

第一列映射到Label属性。如您在第一章中回忆的那样,开始使用机器学习和 ML.NET,监督学习(如本示例中执行的那样)需要标记。在这个项目中,我们的标签是一个布尔值。数据集中的 False(0)表示正面反馈,而 True(1)表示负面反馈。

第二列映射到Text属性以传播情感(即要馈入模型的句子)。

RestaurantPrediction

RestaurantPrediction类包含模型运行将输出的属性。根据使用的算法,输出类(您将在未来的章节中找到)将包含更多的属性:

using Microsoft.ML.Data;

namespace chapter02.ML.Objects
{
    public class RestaurantPrediction
    {
        [ColumnName("PredictedLabel")]
        public bool Prediction { get; set; }

        public float Probability { get; set; }

        public float Score { get; set; }
    }
}

RestaurantFeedback Label属性类似,Prediction属性包含正面或负面反馈的整体结果。Probability属性包含我们对该决策的模型置信度。Score属性用于评估我们的模型。

Trainer

在以下内容中,您将找到Trainer类中的唯一方法。从高层次来看,Trainer方法执行以下操作:

  • 它将训练数据(在这种情况下是我们的 CSV 文件)加载到内存中。

  • 它构建了一个训练集和一个测试集。

  • 它创建了管道。

  • 它训练并保存模型。

  • 它对模型进行评估。

这就是我们将在本书余下部分遵循的结构和流程。现在,让我们深入到Train方法的代码背后:

  1. 首先,我们检查确保训练数据文件名存在:
if (!File.Exists(trainingFileName)) {
    Console.WriteLine($"Failed to find training data file ({trainingFileName}");

    return;
}

尽管这是一个简单的测试应用程序,但始终将其视为生产级应用程序是一种良好的实践。此外,由于这是一个控制台应用程序,你可能会错误地传递一个训练数据的路径,这可能导致方法中进一步出现异常。

  1. 使用 ML.NET 提供的LoadFromTextFile辅助方法来帮助将文本文件加载到IDataView对象中:
IDataView trainingDataView = MlContext.Data.LoadFromTextFile<RestaurantFeedback>(trainingFileName);

如你所见,我们传递了训练文件名和类型;在这种情况下,它是之前提到的RestaurantFeedback类。需要注意的是,此方法还有其他几个参数,包括以下内容:

  • separatorChar:这是列分隔符字符;默认为\t(换句话说,制表符)。

  • hasHeader:如果设置为true,数据集的第一行包含标题;默认为false

  • allowQuoting:这定义了源文件是否可以包含由引号字符串定义的列;默认为false

  • trimWhitespace:这会从行中移除尾随空格;默认为false

  • allowSparse:这定义了文件是否可以包含稀疏格式的数值向量;默认为false。稀疏格式需要一个新列来表示特征的数量。

对于本书中使用的多数项目,我们将使用默认设置。

  1. 给定我们之前创建的IDataView对象,使用 ML.NET 提供的TrainTestSplit方法从主要训练数据中创建一个测试集:
DataOperationsCatalog.TrainTestData dataSplit = MlContext.Data.TrainTestSplit(trainingDataView, testFraction: 0.2);

如第一章,“开始使用机器学习和 ML.NET”中提到的,样本数据被分为两个集合——训练集和测试集。参数testFraction指定了保留用于测试的数据集百分比,在我们的例子中是 20%。默认情况下,此参数设置为 0.2。

  1. 首先,我们创建管道:
TextFeaturizingEstimator dataProcessPipeline = MlContext.Transforms.Text.FeaturizeText(outputColumnName: "Features",
        inputColumnName: nameof(RestaurantFeedback.Text));

未来的示例将有一个更复杂的管道。在这个例子中,我们只是将之前讨论的Text属性映射到Features输出列。

  1. 然后,我们实例化我们的Trainer类:
SdcaLogisticRegressionBinaryTrainer sdcaRegressionTrainer = MlContext.BinaryClassification.Trainers.SdcaLogisticRegression(
        labelColumnName: nameof(RestaurantFeedback.Label),
        featureColumnName: "Features");

如你从第一章,“开始使用机器学习和 ML.NET”,中记得的那样,ML.NET 中找到的各种算法被称为训练器。在这个项目中,我们使用 SCDA 训练器。

  1. 然后,我们通过附加之前实例化的训练器来完成管道:
EstimatorChain<BinaryPredictionTransformer<CalibratedModelParametersBase<LinearBinaryModelParameters, PlattCalibrator>>> trainingPipeline = dataProcessPipeline.Append(sdcaRegressionTrainer);
  1. 接下来,我们使用本章之前创建的数据集来训练模型:
ITransformer trainedModel = trainingPipeline.Fit(dataSplit.TrainSet);
  1. 我们将新创建的模型保存到指定的文件名中,与训练集的架构相匹配:
MlContext.Model.Save(trainedModel, dataSplit.TrainSet.Schema, ModelPath);
  1. 现在,我们使用之前创建的测试集来转换新创建的模型:
IDataView testSetTransform = trainedModel.Transform(dataSplit.TestSet);
  1. 最后,我们将之前创建的testSetTransform函数传递给BinaryClassification类的Evaluate方法:
CalibratedBinaryClassificationMetrics modelMetrics = MlContext.BinaryClassification.Evaluate(
        data: testSetTransform,
        labelColumnName: nameof(RestaurantFeedback.Label),
        scoreColumnName: nameof(RestaurantPrediction.Score));

Console.WriteLine($"Area Under Curve: {modelMetrics.AreaUnderRocCurve:P2}{Environment.NewLine}" +
        $"Area Under Precision Recall Curve: {modelMetrics.AreaUnderPrecisionRecallCurve:P2}" +                    $"{Environment.NewLine}" +
        $"Accuracy: {modelMetrics.Accuracy:P2}{Environment.NewLine}" +
        $"F1Score: {modelMetrics.F1Score:P2}{Environment.NewLine}" +
        $"Positive Recall: {modelMetrics.PositiveRecall:#.##}{Environment.NewLine}" +
        $"Negative Recall: {modelMetrics.NegativeRecall:#.##}{Environment.NewLine}");

此方法允许我们生成模型度量。然后,我们使用训练模型和测试集打印主要度量。我们将在本章的评估模型部分具体探讨这些属性。

Predictor

如前所述,Predictor类是我们项目中提供预测支持的类。此方法背后的想法是提供一个简单的接口来运行模型,考虑到相对简单的输入。在未来的章节中,我们将扩展此方法结构以支持更复杂的集成,例如托管在 Web 应用程序中的集成:

  1. Trainer类中执行的操作类似,我们在读取模型之前验证模型是否存在:
if (!File.Exists(ModelPath)) {
    Console.WriteLine($"Failed to find model at {ModelPath}");

    return;
}
  1. 然后,我们定义ITransformer对象:
ITransformer mlModel;

using (var stream = new FileStream(ModelPath, FileMode.Open, FileAccess.Read, FileShare.Read)) {
    mlModel = MlContext.Model.Load(stream, out _);
}

if (mlModel == null) {
    Console.WriteLine("Failed to load model");

    return;
}

通过Model.Load方法加载模型后,此对象将包含我们的模型。此方法也可以直接接受文件路径。然而,流方法更适合支持我们在后续章节中使用的非磁盘方法。

  1. 接下来,根据我们之前加载的模型创建一个PredictionEngine对象:
var predictionEngine = MlContext.Model.CreatePredictionEngine<RestaurantFeedback,                        RestaurantPrediction>(mlModel);

我们传递了 TSrc 和 TDst,在我们的项目中,分别是RestaurantFeedbackRestaurantPrediction

  1. 然后,在PredictionEngine类上调用Predict方法:
var prediction = predictionEngine.Predict(new RestaurantFeedback { Text = inputData });

因为当我们使用 TSrc 创建对象时,类型被设置为RestaurantFeedback,所以我们有一个对模型的强类型接口。然后,我们使用包含将要运行模型的句子的字符串的inputData变量创建RestaurantFeedback对象。

  1. 最后,显示预测输出以及概率:
Console.WriteLine($"Based on \"{inputData}\", the feedback is predicted to be:{Environment.NewLine}" +
        "{(prediction.Prediction ? "Negative" : "Positive")} at a {prediction.Probability:P0}" +                 " confidence");

BaseML

如前所述,BaseML类将包含我们的TrainerPredictor类之间的公共代码,从本章开始。在本书的剩余部分,我们将在以下定义的BaseML类之上构建:

using System;
using System.IO;

using chapter02.Common;

using Microsoft.ML;

namespace chapter02.ML.Base
{
    public class BaseML
    {
        protected static string ModelPath => Path.Combine(AppContext.BaseDirectory,                                                           Constants.MODEL_FILENAME);

        protected readonly MLContext MlContext;

        protected BaseML()
        {
            MlContext = new MLContext(2020);
        }
    }
}

对于所有 ML.NET 应用,无论是训练还是预测,都需要一个MLContext对象。初始化对象时需要一个特定的种子值,以便在测试组件期间创建更一致的结果。一旦加载了模型,种子值(或其缺失)不会影响输出。

Program

对于那些创建过控制台应用程序的人来说,应该熟悉Program类及其内部的Main方法。在本书的剩余部分,我们将遵循此结构来处理其他基于控制台的应用程序。以下代码块包含程序类,应用程序将从该类开始执行:

using System;

using chapter02.ML;

namespace chapter02
{
    class Program
    {
        static void Main(string[] args)
        {
            if (args.Length != 2)
            {
                Console.WriteLine($"Invalid arguments passed in, exiting.{Environment.NewLine}" +                            $"{Environment.NewLine}Usage: {Environment.NewLine}" +
                    $"predict <sentence of text to predict against>{Environment.NewLine}" +
                    $"or {Environment.NewLine}" +
                    $"train <path to training data file>{Environment.NewLine}");

                return;
            }

            switch (args[0])
            {
                case "predict":
                    new Predictor().Predict(args[1]);
                    break;
                case "train":
                    new Trainer().Train(args[1]);
                    break;
                default:
                    Console.WriteLine($"{args[0]} is an invalid option");
                    break;
            }
        }
    }
}

对于熟悉解析命令行参数的人来说,这是一个相当直接的方法实现。使用简单的两个参数方法,如帮助文本所示。

当执行一个更复杂的命令行应用程序,该应用程序接受多个参数(可选和必需)时,微软提供了一个简单易用的 NuGet 包,可在以下链接找到:github.com/dotnet/command-line-api

运行示例

要运行训练和预测,只需构建项目,然后传入适当的数据。

对于训练,您可以使用包含的sampledata.csv文件或创建自己的。我们将通过打开 PowerShell 窗口并传入相对路径来完成此操作:

.\chapter02.exe train ..\..\..\Data\sampledata.csv
Area Under Curve: 100.00%
Area Under Precision Recall Curve: 100.00%
Accuracy: 100.00%
F1Score: 100.00%
Positive Recall: 1
Negative Recall: 1

一旦构建了模型,您可以按照以下方式运行预测:

.\chapter02.exe predict "bad"
Based on "bad", the feedback is predicted to be:
Negative at a 64% confidence

随意尝试各种短语来测试模型的功效,并祝贺您训练出了您的第一个模型!

评估模型

正如您在运行示例项目的训练组件时所见,模型评估有多种元素。对于每种模型类型,在分析模型性能时都有不同的指标要考虑。

在例如示例项目中找到的二分类模型中,我们在调用Evaluate方法后,在CalibratedBiniaryClassificationMetrics中公开了以下属性。然而,首先,我们需要在二分类中定义四种预测类型:

  • 真阴性:正确分类为负例

  • 真阳性:正确分类为正例

  • 假阴性:错误地分类为负例

  • 假阳性:错误地分类为正例

首先要了解的指标是准确度。正如其名所示,准确度是在评估模型时最常用的指标之一。该指标简单地计算为正确分类预测与总分类的比率。

接下来要了解的指标是精确度。精确度定义为模型中所有正例中真实结果的比率。例如,精确度为 1 表示没有假阳性,这是理想场景。如前所述,假阳性是指将某物错误地分类为正例,而它应该被分类为负例。一个常见的假阳性例子是将一个文件错误地分类为恶意文件,而实际上它是良性的。

接下来要了解的指标是召回率。召回率是模型返回的所有正确结果的比例。例如,召回率为 1 表示没有假阴性,这是另一个理想场景。假阴性是指将某物错误地分类为负例,而它应该被分类为正例。

接下来要了解的指标是F 分数,它同时利用了精确度和召回率,基于假阳性和假阴性产生一个加权平均值。F 分数提供了与仅查看准确度相比对模型性能的另一种视角。值的范围在 0 到 1 之间,理想值为 1。

曲线下面积,也称为 AUC,正如其名所示,是在 y 轴上绘制真实正例,x 轴上绘制假正例的曲线下的面积。对于本章中我们之前训练的模型等分类器,正如你所看到的,这返回了介于 0 和 1 之间的值。

最后,平均对数损失训练对数损失都用于进一步解释模型的性能。平均对数损失通过取真实分类与模型预测之间的差异,以一个数字有效地表达了错误结果的惩罚。训练对数损失表示模型的不确定性,使用概率与已知值进行比较。随着你训练模型,你将希望得到一个低数值(数值越低越好)。

关于其他模型类型,我们将在各自的章节中深入探讨如何评估它们,其中我们将涵盖回归和聚类指标。

摘要

在本章的整个过程中,我们设置了我们的开发环境,并了解了未来文件组织的正确方式。我们还创建了我们的第一个 ML.NET 应用程序,除了训练、评估和针对新模型运行预测之外。最后,我们探讨了如何评估模型以及各种属性的含义。

在下一章中,我们将深入探讨逻辑回归算法。

第二部分:ML.NET 模型

本节讨论了截至版本 1.1,ML.NET 中可用的各种训练器。在每个章节中,将详细介绍训练器、数学原理以及如何使用 ML.NET 进行操作。

本节包含以下章节:

  • 第三章,回归模型

  • 第四章,分类模型

  • 第五章,聚类模型

  • 第六章,异常检测模型

  • 第七章,矩阵分解模型

第三章:回归模型

在我们的开发环境配置完成并且我们的第一个 ML.NET 应用完成后,现在是时候深入研究回归模型了。在本章中,我们将深入研究回归模型背后的数学原理,以及回归模型的各种应用。我们还将构建两个额外的 ML.NET 应用,一个使用线性回归模型,另一个使用逻辑回归模型。线性回归应用将根据各种员工属性预测员工流失。逻辑回归应用将对文件进行基本静态文件分析,以确定其是恶意还是良性。最后,我们将探讨如何使用 ML.NET 在回归模型中公开的特性来评估回归模型。

在本章中,我们将涵盖以下主题:

  • 分解各种回归模型

  • 创建线性回归应用

  • 创建逻辑回归应用

  • 评估回归模型

分解回归模型

尽管机器学习生态系统中提供了多种回归模型类型,但主要有两种主要的回归模型组:线性回归和逻辑回归,它们在 ML.NET 中都有丰富的实现。

ML.NET 提供了以下线性回归训练器:

  • FastTreeRegressionTrainer

  • FastTreeTweedieTrainer

  • FastForestRegressionTrainer

  • GamRegressionTrainer

  • LbfgsPoissonRegressionTrainer

  • LightGbmRegressionTrainer

  • OlsTrainer

  • OnlineGradientDescentTrainer

  • SdcaRegressionTrainer

我们将在本章后面创建的员工流失应用将使用线性回归 SDCA 训练器。

此外,ML.NET 还提供了以下二元逻辑回归训练器:

  • LbfgsLogisticRegressionBinaryTrainer

  • SdcaLogisticRegressionBinaryTrainer

  • SdcaNonCalibratedBinaryTrainer

  • SymbolicSgdLogisticRegressionBinaryTrainer

对于文件分类应用,我们将使用SDCALogisticRegressionBinaryTrainer模型。

选择回归模型类型

在所有这些选项中,你如何选择正确的回归模型类型?

你选择的回归模型类型取决于你期望的输出。如果你只希望得到一个布尔值(即,0 或 1),那么应该使用逻辑回归模型,就像我们在本章后面将要编写的文件分类应用中那样。此外,如果你希望返回一个特定的预定义值范围,比如汽车类型,如敞篷车、敞篷车或掀背车,那么逻辑回归模型是正确的选择。

相反,线性回归模型返回一个数值,例如我们在本章后面将要探讨的就业时长示例。

因此,总结如下:

  • 如果你的输出是布尔值,请使用逻辑回归模型。

  • 如果你的输出由预设的范围类型值组成(类似于枚举),请使用逻辑回归模型。

  • 如果你的输出是一个数值未知值,请使用线性回归模型。

选择线性回归训练器

当查看 ML.NET 中九个线性回归训练器的列表时,可能会感到有些令人畏惧,不知道哪一个才是最好的。

对于 ML.NET 线性回归训练器来说,总体而言,最受欢迎的是 FastTree 和 LightGBM。三个 FastTree 算法利用邻接和启发式方法快速识别候选连接来构建决策树。LightGBM 是一个非常流行的线性回归算法,它利用 基于梯度的单边采样GOSS)来过滤数据实例以找到分割值。这两个训练器都提供了快速的训练和预测时间,同时提供了非常准确的模式性能。此外,这两个算法都有更多的文档、论文和研究资料。

剩下的五个训练器很有用,值得深入实验,但总体来说,你可能会发现 LightGBM 和 FastTree 的效果同样好或更好。

选择逻辑回归训练器

在 ML.NET 中提供的四个逻辑回归训练器中,哪一个最适合你的问题?虽然所有四个回归训练器都返回二元分类,但它们针对不同的数据集和工作负载进行了优化。

你是否在寻找一个低内存环境下的训练和预测?如果是这样,考虑到它是为了处理内存受限环境而创建的,L-BFGS 逻辑回归训练器(LbfgsLogisticRegressionBinaryTrainer)是一个合理的选择。

基于 SDCA 的两个训练器——SdcaLogisticRegressionBinaryTrainerSdcaNonCalibratedBinaryTrainer——已经针对训练的可扩展性进行了优化。如果你的训练集很大,并且你在寻找二元分类,那么任一 SDCA 训练器都是一个不错的选择。

SymbolicSgdLogisticRegressionBinaryTrainer 模型与其他三个不同,因为它基于随机梯度下降算法。这意味着该算法不是试图最大化误差函数,而是试图最小化误差函数。

如果你好奇想扩展你对 SCDAs 的知识,特别是微软研究如何扩展 SCDAs 的实验,请阅读这篇白皮书:www.microsoft.com/en-us/research/wp-content/uploads/2016/06/main-3.pdf

创建线性回归应用程序

如前所述,我们将要创建的应用程序是一个员工流失预测器。给定一组与员工相关的属性,我们可以预测他们将在当前工作中停留多久。本例中包含的属性并不是属性的确切列表,也不应直接在生产环境中使用;然而,我们可以将其作为基于多个属性预测单个数值输出的起点。

与 第一章,“使用 ML.NET 开始机器学习”,完成的项目代码、样本数据集和项目文件可以在此处下载:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter03_linear_regression

深入了解训练器

如前所述,对于这个线性回归应用,我们将使用 SDCA 训练器。SDCA 代表 Stochastic Dual Coordinate Ascent,如果你还记得,我们在 第二章,“设置 ML.NET 环境”的例子中使用了这个训练器的逻辑回归版本。

对于普通读者来说,构成 SDCA 的四个单词可能都是未知的,所以让我们逐一解释每个单词的含义,以便更好地理解当您使用 SDCA 训练器时会发生什么。首先,Stochastic,换句话说,就是不可预测性。在机器学习的情况下,这意味着尝试以概率预测误差函数,并将训练集中的随机样本输入到优化器中。使用 Dual Coordinate 意味着在训练模型时两个变量是耦合的。正如你可能猜到的,这使得模型变得更加复杂,但不需要任何额外的工作就可以使用。最后,Ascent 指的是最大化误差函数的值。

探索项目架构

在 第二章,“设置 ML.NET 环境”中创建的项目架构和代码的基础上,这个例子在架构上的主要变化是输入机制。在 第二章,“设置 ML.NET 环境”中,使用简单的字符串通过命令行参数提供情感分析。在这个应用中,有几个属性需要传递给模型;因此,对于这个应用,我们现在使用 JSON 文件来包含我们的输入数据。随着这个添加,我们现在包括流行的 Newtonsoft.Json NuGet 包(在撰写本文时,最新版本是 12.0.2,并在包含的示例中使用)。如果您是从头开始构建此项目并且不记得如何添加 NuGet 引用,请参阅 第二章,“设置 ML.NET 环境”。

以下截图显示了项目的 Visual Studio Solution Explorer 视图。解决方案的新增内容是 ExtensionMethods 类文件,我们将在下一节中对其进行回顾:

图片

sampledata.csv 文件包含 40 行随机数据;您可以随意调整数据以适应自己的观察或调整训练模型。以下是数据的片段:

16,1,1,0,20,38,1,1,1
23,1,1,1,17,36,0,1,0
6,1,1,0,10,30,1,0,1
4,0,1,0,6,26,1,0,1
14,0,0,0,4,27,1,0,1
24,1,1,1,14,30,1,0,1
5,1,1,0,8,31,0,1,1
12,1,1,0,20,50,0,1,1
12,1,1,0,12,50,1,0,1
6,1,1,0,10,52,0,1,1

这些每一行都包含新创建的 EmploymentHistory 类中属性的值,我们将在本章后面进行回顾。

如果你想使用更大的数据集来训练并扩展此示例,Kaggle 网站提供了由 IBM 数据科学家创建的数据集。此数据集在此处可用:www.kaggle.com/pavansubhasht/ibm-hr-analytics-attrition-dataset

深入代码

正如所述,对于此应用程序,我们是在完成第二章,设置 ML.NET 环境的工作基础上构建的。对于这次深入研究,我们将专注于此应用程序更改的代码。

被更改或添加的课程如下:

  • ExtensionMethods

  • EmploymentHistory

  • EmploymentHistoryPrediction

  • Predictor

  • Trainer

  • Program

ExtensionMethods

这个新添加的类提供了一个易于使用的扩展方法,可以返回类中除标签外的所有属性。如果你不熟悉扩展方法,这些方法提供了一种非常简单的语法,可以潜在地对单个对象执行复杂操作,就像在这个例子中,我们取一个任意类型并返回它包含的所有属性(除了 labelName):

using System;
using System.Linq;

namespace chapter03.Common
{
    public static class ExtensionMethods
    {
        public static string[] ToPropertyList<T>(this Type objType, string labelName) =>                     objType.GetProperties().Where(a => a.Name != labelName).Select(a =>                             a.Name).ToArray();
    }
}

EmploymentHistory

EmploymentHistory 类是包含预测和训练模型所需数据的容器类。这些列按照先前审查的样本数据顺序映射。如果你开始尝试新的功能并向此列表添加内容,请确保适当地增加数组索引:

using Microsoft.ML.Data;

namespace chapter03.ML.Objects
{
    public class EmploymentHistory
    {
        [LoadColumn(0)]
        public float DurationInMonths { get; set; }

        [LoadColumn(1)]
        public float IsMarried { get; set; }

        [LoadColumn(2)]
        public float BSDegree { get; set; }

        [LoadColumn(3)]
        public float MSDegree { get; set; }

        [LoadColumn(4)]
        public float YearsExperience { get; set; }

        [LoadColumn(5)]
        public float AgeAtHire { get; set; }

        [LoadColumn(6)]
        public float HasKids { get; set; }

        [LoadColumn(7)]
        public float WithinMonthOfVesting { get; set; }

        [LoadColumn(8)]
        public float DeskDecorations { get; set; }

        [LoadColumn(9)]
        public float LongCommute { get; set; }
    }
}

EmploymentHistoryPrediction

EmploymentHistoryPrediction 类仅包含在 DurationInMonths 属性中预测的员工预计在其职位上工作多少个月的预测值:

using Microsoft.ML.Data;

namespace chapter03.ML.Objects
{
    public class EmploymentHistoryPrediction
    {
        [ColumnName("Score")]
        public float DurationInMonths;
    }
}

Predictor

在此类中有一两个更改,用于处理就业预测场景:

  1. 首先,在对此文件进行预测之前,验证输入文件是否存在:
if (!File.Exists(inputDataFile))
{
    Console.WriteLine($"Failed to find input data at {inputDataFile}");

    return;
}
  1. 另一个更改是在预测调用本身。正如你可能猜到的,TSrc 和 TDst 参数需要调整以利用我们创建的两个新类,EmploymentHistoryEmploymentHistoryPrediction
var predictionEngine = MlContext.Model.CreatePredictionEngine<EmploymentHistory, EmploymentHistoryPrediction>(mlModel);
  1. 由于我们不再只是传递字符串并在飞行中构建对象,我们需要首先以文本形式读取文件。然后,将 JSON 反序列化为我们的 EmploymentHistory 对象:
var json = File.ReadAllText(inputDataFile);

var prediction = predictionEngine.Predict(JsonConvert.DeserializeObject<EmploymentHistory>(json));
  1. 最后,我们需要调整预测的输出以匹配我们新的 EmploymentHistoryPrediction 属性:
Console.WriteLine(
 $"Based on input json:{System.Environment.NewLine}" +
 $"{json}{System.Environment.NewLine}" + 
 $"The employee is predicted to work {prediction.DurationInMonths:#.##} months");

Trainer

Trainer 类中,大部分代码被重写,以处理使用的扩展功能,并提供回归算法评估,而不是我们在第二章,设置 ML.NET 环境中查看的二分类。

第一个更改是使用逗号分隔数据,而不是我们像在第二章设置 ML.NET 环境中使用的默认制表符:

var trainingDataView = MlContext.Data.LoadFromTextFile<EmploymentHistory>(trainingFileName, ',');

下一个更改是在管道创建本身。在我们的第一个应用程序中,我们有一个标签并将其直接输入到管道中。在这个应用程序中,我们有九个特征用于预测 DurationInMonths 属性中一个人的就业时长,并使用 C# 6.0 特性 nameof 将每个特征附加到管道中。你可能已经注意到在 GitHub 和 MSDN 上的各种代码示例中使用了魔法字符串来将类属性映射到特征;我个人认为,与强类型方法相比,这种方法更容易出错。

对于每个属性,我们调用 NormalizeMeanVariance 转换方法,正如其名称所暗示的,该方法在均值和方差上对输入数据进行归一化。ML.NET 通过从输入数据的均值中减去并除以输入数据的方差来计算这一点。这样做背后的目的是消除输入数据中的异常值,以便模型不会偏向处理边缘情况,而不是正常范围。例如,假设就业历史样本数据集有 20 行,其中除了有一行外,其他所有行都有一个有 50 年经验的人。那个不符合的行将被归一化,以便更好地适应模型中输入的值范围。

此外,请注意使用前面提到的扩展方法来帮助简化以下代码,当我们连接所有特征列时:

var dataProcessPipeline = MlContext.Transforms.CopyColumns("Label", nameof(EmploymentHistory.DurationInMonths))
 .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(EmploymentHistory.IsMarried)))
 .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(EmploymentHistory.BSDegree)))
 .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(EmploymentHistory.MSDegree)))
 .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(EmploymentHistory.YearsExperience))
 .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(EmploymentHistory.AgeAtHire)))
 .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(EmploymentHistory.HasKids)))
 .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(EmploymentHistory.WithinMonthOfVesting)))
 .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(EmploymentHistory.DeskDecorations)))
 .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(EmploymentHistory.LongCommute)))
 .Append(MlContext.Transforms.Concatenate("Features",
 typeof(EmploymentHistory).ToPropertyList<EmploymentHistory>(nameof(EmploymentHistory.DurationInMonths)))));

我们可以使用默认参数("Label""Features")创建 Sdca 训练器:

var trainer = MlContext.Regression.Trainers.Sdca(labelColumnName: "Label", featureColumnName: "Features");

最后,我们调用 Regression.Evaluate 方法来提供回归特定的度量,然后通过 Console.WriteLine 调用来将这些度量输出到控制台。我们将在本章的最后部分详细介绍这些度量分别代表什么:

var modelMetrics = MlContext.Regression.Evaluate(testSetTransform);

Console.WriteLine($"Loss Function: {modelMetrics.LossFunction:0.##}{Environment.NewLine}" +
 $"Mean Absolute Error: {modelMetrics.MeanAbsoluteError:#.##}{Environment.NewLine}" +
 $"Mean Squared Error: {modelMetrics.MeanSquaredError:#.##}{Environment.NewLine}" +
 $"RSquared: {modelMetrics.RSquared:0.##}{Environment.NewLine}" +
 $"Root Mean Squared Error: {modelMetrics.RootMeanSquaredError:#.##}");

程序类

Program 类的唯一更改是帮助文本,用于指示预测时需要一个文件名而不是字符串:

if (args.Length != 2)
{
    Console.WriteLine($"Invalid arguments passed in, exiting.{Environment.NewLine}                    {Environment.NewLine}Usage:{Environment.NewLine}" +
        $"predict <path to input json file>{Environment.NewLine}" +
        $"or {Environment.NewLine}" +
        $"train <path to training data file>{Environment.NewLine}");

        return;
}

运行应用程序

运行应用程序的过程几乎与第二章的示例应用程序相同。为了更快地迭代,调试配置自动将包含的 sampledata.csv 文件作为命令行参数传入:

由于应用程序的复杂性不断增加,未来的所有示例应用程序都将具有以下预设:

  1. 要像在第一章开始使用机器学习和 ML.NET 中那样在命令行上运行训练,只需传递以下命令(假设您正在使用包含的样本数据集):
PS chapter03\bin\Debug\netcoreapp3.0> .\chapter03.exe train ..\..\..\Data\sampledata.csv 
Loss Function: 324.71
Mean Absolute Error: 12.68
Mean Squared Error: 324.71
RSquared: 0.14
Root Mean Squared Error: 18.02

注意扩展的输出包括几个度量数据点——我们将在本章的末尾解释这些数据点各自代表什么。

  1. 训练模型后,构建一个示例 JSON 文件并将其保存为 input.json
{
  "durationInMonths": 0.0,
  "isMarried": 0,
  "bsDegree": 1,
  "msDegree": 1,
  "yearsExperience": 2,
  "ageAtHire": 29,
  "hasKids": 0,
  "withinMonthOfVesting": 0,
  "deskDecorations": 1,
  "longCommute": 1
}
  1. 要使用此文件运行模型,只需将文件名传递给构建的应用程序,预测输出将显示:
PS chapter03\bin\Debug\netcoreapp3.0> .\chapter03.exe predict input.json 
Based on input json:
{
 "durationInMonths": 0.0,
 "isMarried": 0,
 "bsDegree": 1,
 "msDegree": 1,
 "yearsExperience": 2,
 "ageAtHire": 29,
 "hasKids": 0,
 "withinMonthOfVesting": 0,
 "deskDecorations": 1,
 "longCommute": 1
}

The employee is predicted to work 22.82 months

随意修改值,看看基于模型训练的数据集,预测如何变化。从这个点开始,一些实验性的区域可能包括以下内容:

  • 根据你自己的经验添加一些额外的功能。

  • 修改sampledata.csv以包含你团队的经验。

  • 修改示例应用程序以具有 GUI,使运行预测更容易。

创建逻辑回归应用程序

如前所述,我们将创建的应用程序来展示逻辑回归是一个文件分类器。给定一个文件(任何类型),我们从文件中提取字符串。这是一种非常常见的文件分类方法,尽管,就像前面的例子一样,这通常只是文件分类的一个元素,而不是唯一的组成部分。因此,不要期望它能找到下一个零日恶意软件!

完成的项目代码、样本数据集和项目文件可以在此处下载: github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter03_logistic_regression.

本应用程序中使用的训练器也使用 SDCA,但使用本章前面讨论的逻辑回归变体。

与前面的例子一样,我们将首先探索项目架构,深入代码,然后展示如何运行示例以进行训练和预测。

探索项目架构

在前面例子中创建的项目架构和代码的基础上,本例在架构上的主要变化是特征提取。在本例中,我们将在FeatureExtractor类中添加新输入和预测类。这样做的原因是回到将事物保持分离和明确定义的想法,如第二章中讨论的,设置 ML.NET 环境。对于这个示例应用程序和未来你可能编写的应用程序,它们很可能会有输入文件需要转换为数据行。通过有一个单独的类来处理这个管道的部分,你可以干净地封装这个功能。

以下截图显示了项目的 Visual Studio 解决方案资源管理器视图。解决方案的新增内容是FeatureExtractor类文件,我们将在下一节中对其进行审查:

sampledata.csv 文件包含八行随机数据。请随意调整数据以适应您的观察或调整训练模型。以下是包含的样本数据:

False !This program cannot be run in DOS mode.L$ SUVWH\$ UVWAVAWH\$ VWAVHWATAUAVAWHA_AA]A\_l$ VWAVHt
False !This program cannot be run in DOS mode.L$ SUVWH\$ VWAVHUVWAVAWHUVWATAUAVAWHA_AA]A\_]UVWAVAWHU
False !This program cannot be run in DOS mode.$7ckw7ckw7ckw>jv$ckw7cjwiv6ckwRich7ckw9A98u6A9xx ATAVA
False !This program cannot be run in DOS mode.EventSetInformationmshelp URL calledLaunchFwLink"mshelp
True !This program cannot be run in DOS mode.Fm;Ld &~_New_ptrt(M4_Alloc_max"uJIif94H3"j?TjV*?invalid
True <</Length 17268/Type/EmbeddedFile/Filter/FlateDecode/Params<</ModDate(D:20191003012641+00'00'/Size
True !This program cannot be run in DOS mode._New_ptr7(_MaskQAlloc_maxtEqx?$xjinvalid argumC:\Program F
True __gmon_startN_easy_cKcxa_amxBZNSt8ios_bEe4IeD1Evxxe6naDtqv_Z<4endlIcgLSaQ6appw3d_ResumeCXXABI_1.3%d

每一行包含两列数据。第一列是分类,其中 true 表示恶意,false 表示良性。这些属性映射到我们稍后在本章中将要审查的新创建的 FileInput 类中。

深入代码

如前所述,对于这个应用程序,我们正在构建本章中之前完成的工作。同样,对于这次深入分析,我们将仅关注为这个应用程序更改的代码。

被更改或添加的类如下:

  • FeatureExtractor

  • FileInput

  • FilePrediction

  • BaseML

  • Predictor

  • Trainer

  • Program

FeatureExtractor 类

这个新添加的类为我们提供了给定文件夹中文件的特性提取提取完成后,分类和字符串数据将被写入到 sampledata 文件中:

using System;
using System.IO;

using chapter03_logistic_regression.Common;
using chapter03_logistic_regression.ML.Base;

namespace chapter03_logistic_regression.ML
{
    public class FeatureExtractor : BaseML
    {
        public void Extract(string folderPath)
        {
            var files = Directory.GetFiles(folderPath);

            using (var streamWriter =
                new StreamWriter(Path.Combine(AppContext.BaseDirectory, $"../../../Data/{Constants.SAMPLE_DATA}")))
            {
                foreach (var file in files)
                {
                    var strings = GetStrings(File.ReadAllBytes(file));

                    streamWriter.WriteLine($"{file.ToLower().Contains("malicious")}\t{strings}");
                }
            }

            Console.WriteLine($"Extracted {files.Length} to {Constants.SAMPLE_DATA}");
        }
    }
}

FileInput 类

FileInput 类提供了我们提取的训练分类和字符串数据的容器:

using Microsoft.ML.Data;

namespace chapter03_logistic_regression.ML.Objects
{
    public class FileInput
    {
        [LoadColumn(0)]
        public bool Label { get; set; }

        [LoadColumn(1)]
        public string Strings { get; set; }
    }
}

FilePrediction 类

FilePrediction 类提供了分类、概率和得分的容器:

using Microsoft.ML.Data;

namespace chapter03_logistic_regression.ML.Objects
{
    public class FilePrediction
    {
        [ColumnName("PredictedLabel")]
        public bool IsMalicious { get; set; }

        public float Probability { get; set; }

        public float Score { get; set; }
    }
}

BaseML 类

对于 BaseML 类,我们对它进行了几项增强,从构造函数开始。在构造函数中,我们将 stringRex 变量初始化为我们将用于提取字符串的正则表达式。Encoding.RegisterProvider 对于使用 Windows-1252 编码至关重要。这种编码是 Windows 可执行文件使用的编码:

private static Regex _stringRex;

protected BaseML()
{
    MlContext = new MLContext(2020);

    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

    _stringRex = new Regex(@"[ -~\t]{8,}", RegexOptions.Compiled);
}

下一个主要新增功能是 GetStrings 方法。该方法接收字节,运行之前创建的编译后的正则表达式,并提取字符串匹配项:

  1. 首先,我们定义方法定义并初始化 stringLines 变量以保存字符串:
protected string GetStrings(byte[] data)
{
    var stringLines = new StringBuilder();
  1. 接下来,我们将对输入数据进行合理性检查,确保它不是 null 或空的:
if (data == null || data.Length == 0)
{
    return stringLines.ToString();
}
  1. 在接下来的代码块中,我们打开一个 MemoryStream 对象和一个 StreamReader 对象:
 using (var ms = new MemoryStream(data, false))
 {
     using (var streamReader = new StreamReader(ms, Encoding.GetEncoding(1252), false, 2048, false))
     {
  1. 然后,我们将遍历 streamReader 对象,直到达到 EndOfStream 条件,逐行读取:
while (!streamReader.EndOfStream)
{
    var line = streamReader.ReadLine();
  1. 然后,我们将对数据进行一些字符串清理,并优雅地处理行是否为空的情况:
if (string.IsNullOrEmpty(line))
{
    continue;
}

line = line.Replace("^", "").Replace(")", "").Replace("-", "");
  1. 然后,我们将正则表达式匹配项追加到之前定义的 stringLines 变量中:
stringLines.Append(string.Join(string.Empty,
                    _stringRex.Matches(line).Where(a => !string.IsNullOrEmpty(a.Value) && !string.IsNullOrWhiteSpace(a.Value)).ToList()));
  1. 最后,我们将使用 string.Join 方法将 stringLines 变量转换为单个字符串:
    return string.Join(string.Empty, stringLines);
}

Predictor 类

Predictor 类,就像在线性回归示例中更改的那样,只是进行了修改以支持新模型并返回分类:

  1. 我们首先将两个新类 FileInputFilePrediction 传递给 CreatePredictionEngine 方法:
var predictionEngine = MlContext.Model.CreatePredictionEngine<FileInput, FilePrediction>(mlModel);
  1. 接下来,我们创建 FileInput 对象,将 Strings 属性设置为之前编写的 GetStrings 方法的返回值:
var prediction = predictionEngine.Predict(new FileInput
{
    Strings = GetStrings(File.ReadAllBytes(inputDataFile))
});
  1. 最后,我们将输出调用更新到 Console 对象,包括我们的文件分类和概率:
Console.WriteLine(
                    $"Based on the file ({inputDataFile}) the file is classified as {(prediction.IsMalicious ? "malicious" : "benign")}" + 
                    $" at a confidence level of {prediction.Probability:P0}");

Trainer

Trainer 类中,我们将构建一个新的管道来训练我们的模型。FeaturizeText 转换从我们之前从文件中提取的字符串数据构建 NGrams。NGrams 是一种从字符串创建向量的流行方法,进而将数据输入模型。你可以将 NGrams 视为根据 NGram 参数值将较长的字符串分解为字符范围。例如,一个二元组会将以下句子 ML.NET is great 转换为 ML-.N-ET-is-gr-ea-t。最后,我们构建了 SdcaLogisticRegression 训练器对象:

var dataProcessPipeline = MlContext.Transforms.CopyColumns("Label", nameof(FileInput.Label))
 .Append(MlContext.Transforms.Text.FeaturizeText("NGrams", nameof(FileInput.Strings)))
 .Append(MlContext.Transforms.Concatenate("Features", "NGrams"));

var trainer = MlContext.BinaryClassification.Trainers.SdcaLogisticRegression(labelColumnName: "Label", featureColumnName: "Features");

对于想要深入了解 Transforms 目录 API 的人来说,请查看微软的以下文档:docs.microsoft.com/en-us/dotnet/api/microsoft.ml.transformscatalog?view=ml-dotnet

Program

Program 类中,我们添加了第三个选项来提取特征并创建样本数据 .tsv 文件:

  1. 首先,我们修改帮助文本以指示新的提取选项,该选项接受训练文件夹的路径:
if (args.Length != 2)
{
    Console.WriteLine($"Invalid arguments passed in, exiting.{Environment.NewLine}{Environment.NewLine}Usage:{Environment.NewLine}" +
                      $"predict <path to input file>{Environment.NewLine}" +
                      $"or {Environment.NewLine}" +
                      $"train <path to training data file>{Environment.NewLine}" + 
                      $"or {Environment.NewLine}" +
                      $"extract <path to folder>{Environment.NewLine}");

    return;
}
  1. 此外,我们还需要修改主开关/情况以支持 extract 参数:
switch (args[0])
{
    case "extract":
        new FeatureExtractor().Extract(args[1]);
        break;
    case "predict":
        new Predictor().Predict(args[1]);
        break;
    case "train":
        new Trainer().Train(args[1]);
        break;
    default:
        Console.WriteLine($"{args[0]} is an invalid option");
        break;
}

运行应用程序

在我们的管道中添加了特征提取后,我们首先需要对文件执行特征提取:

  1. 假设名为 temp_data 的文件文件夹存在,请执行以下命令:
PS chapter03-logistic-regression\bin\Debug\netcoreapp3.0> .\chapter03-logistic-regression.exe extract temp_data                                                
Extracted 8 to sampledata.csv

输出显示了提取的文件数量和输出样本文件。

  1. 要使用包含的 sampledata.csv 或你自己训练的数据训练模型,请执行以下命令:
PS chapter03-logistic-regression\bin\Debug\netcoreapp3.0> .\chapter03-logistic-regression.exe train ..\..\..\Data\sampledata.csv

完成后,chapter3.mdl 模型文件应存在于执行文件夹中。

  1. 要运行针对现有文件(如编译的 chapter3 可执行文件)的新训练模型,请运行以下命令:
PS chapter03-logistic-regression\bin\Debug\netcoreapp3.0> .\chapter03-logistic-regression.exe predict .\chapter03-logistic-regression.exe                      
Based on the file (.\chapter03-logistic-regression.exe) the file is classified as benign at a confidence level of 8%

如果你在寻找示例文件,c:\Windowsc:\Windows\System32 文件夹包含大量的 Windows 可执行文件和 DLL。此外,如果你想要创建看起来恶意但实际上是干净的文件,你可以在 cwg.io 上动态创建各种格式的文件。这是网络安全领域的一个有用工具,在开发机器上测试新功能比在真实环境中引爆零日威胁要安全得多!

评估回归模型

如前几章所述,评估模型是整个模型构建过程中的关键部分。一个训练不良的模型只会提供不准确的预测。幸运的是,ML.NET 提供了许多流行的属性,可以根据训练时的测试集计算模型精度,从而让你了解你的模型在生产环境中将如何表现。

在 ML.NET 中,如前所述,线性回归示例应用中有五个属性构成了RegressionMetrics类对象。这些包括以下内容:

  • 损失函数

  • 均方绝对误差

  • 均方误差

  • R 平方

  • 根号均方误差

在下一节中,我们将分解这些值的计算方法和理想值。

损失函数

此属性使用在回归训练器初始化时设置的损失函数。在我们的线性回归示例应用中,我们使用了默认构造函数,对于 SDCA 而言,默认为SquaredLoss类。

ML.NET 提供的其他回归损失函数如下:

  • TweedieLoss(用于 Tweedie 回归模型)

  • PoissonLoss(用于泊松回归模型)

此属性背后的想法是在评估模型时提供一些灵活性,与其他四个使用固定算法进行评估的属性相比。

均方误差

均方误差,也称为MSE,定义为误差平方的平均值。简单来说,请参考以下图表:

图片

点代表我们模型中的数据点,而蓝色线是预测线。红色点与预测线之间的距离是误差。对于 MSE,值是基于这些点和它们到线的距离计算的。从这个值中,计算平均值。对于 MSE,值越小,你的模型拟合得越好,预测越准确。

MSE 最好用于评估模型,当异常值对预测输出至关重要时。

均方绝对误差

均方绝对误差,也称为MAE,与 MSE 相似,关键区别在于它将点与预测线之间的距离求和,而不是计算平均值。需要注意的是,MAE 在计算总和时不考虑方向。例如,如果你有两个等距离于线的点,一个在上面,一个在下面,实际上,这将通过正负值相互抵消。在机器学习中,这被称为平均偏差误差,然而,ML.NET 在撰写本文时并未将其作为RegressionMetrics类的一部分提供。

MAE(均方绝对误差)最好用于评估模型,当异常值被视为简单的异常,不应计入评估模型性能时。

R 平方

R 平方,也称为确定系数,是另一种表示预测相对于测试集准确性的方法。R 平方是通过计算每个数据点到均方距离的总和,然后减去这些距离并平方来计算的。

R-平方值通常在 0 到 1 之间,表示为浮点值。当拟合模型被评估为比平均拟合更差时,可能会出现负值。然而,低数值并不总是反映模型不好。本章中我们查看的基于预测人类行为的预测,通常发现低于 50%。

相反,较高的值并不一定是模型性能的可靠标志,因为这可能是模型过拟合的迹象。这种情况发生在向模型提供大量特征时,使得模型比例如我们在第一章“使用机器学习和 ML.NET 入门”中构建的模型更复杂,或者训练集和测试集的多样性不足。例如,如果所有员工的价值大致相同,并且测试集保留集由相同范围的价值组成,这将被认为是过拟合。

均方根误差

均方根 误差,也称为 RMSE,鉴于之前的方法,可以说是最容易理解的。以下是一个示例图:

图片

在测试模型的情况下,正如我们之前使用保留集所做的,红色点代表测试集的实际值,而蓝色点代表预测值。图中描绘的 X 是预测值和实际值之间的距离。RMSE 简单地取所有这些距离的平均值,然后平方该值,最后取平方根。

低于 180 的值通常被认为是良好的模型。

摘要

在本章中,我们探讨了线性回归模型和逻辑回归模型之间的差异。此外,我们还回顾了何时选择线性或逻辑模型以及 ML.NET 提供的培训器。我们还使用 SDCA 和 ML.NET 创建并训练了我们的第一个线性回归应用程序,以预测员工流失。我们还创建了一个逻辑回归应用程序,使用 SDCA 和 ML.NET 提供文件分类。最后,我们还深入探讨了如何评估回归模型以及 ML.NET 提供的各种属性,以正确评估您的回归模型。

在下一章中,我们将深入探讨二元分类算法。

第四章:分类模型

在我们了解了回归模型之后,现在是时候深入到分类模型中去了。在本章中,我们将探讨分类模型背后的数学原理,以及分类模型的各种应用。此外,我们将构建两个新的 ML.NET 分类应用程序:第一个是一个二元分类示例,它将预测汽车价格是否为好交易,类似于你在汽车购买网站上找到的内容;另一个应用程序是一个多类分类应用程序,用于对电子邮件进行分类。最后,我们将探讨如何使用 ML.NET 在分类模型中公开的特性来评估分类模型。

在本章中,我们将涵盖以下主题:

  • 解构分类模型

  • 创建一个二元分类应用程序

  • 创建一个多类分类应用程序

  • 评估分类模型

解构分类模型

如第一章中所述,开始使用机器学习和 ML.NET,分类被分为两大类——二类和多类。在二类分类器中,也称为二元分类器,预测结果简单地返回 0 或 1。在多类问题中,返回预选的标签范围,例如病毒类型或汽车类型。

在机器学习生态系统中,有几种二元分类模型类型可供选择,如下所示:

  • AveragedPerceptronTrainer

  • SdcaLogisticRegressionBinaryTrainer

  • SdcaNonCalibratedBinaryTrainer

  • SymbolicSgdLogisticRegressionBinaryTrainer

  • LbfgsLogisticRegressionBinaryTrainer

  • LightGbmBinaryTrainer

  • FastTreeBinaryTrainer

  • FastForestBinaryTrainer

  • GamBinaryTrainer

  • FieldAwareFactorizationMachineTrainer

  • PriorTrainer

  • LinearSvmTrainer

我们将在本章后面创建的汽车价值应用程序将使用FastTreeBinaryTrainer模型。

ML.NET 还提供了以下多类分类器:

  • LightGbmMulticlassTrainer

  • SdcaMaximumEntropyMulticlassTrainer

  • SdcaNonCalibratedMulticlassTrainer

  • LbfgsMaximumEntropyMulticlassTrainer

  • NaiveBayesMulticlassTrainer

  • OneVersusAllTrainer

  • PairwiseCouplingTrainer

对于多类分类器示例应用程序,我们将使用SdcaMaximumEntropyMulticlassTrainer模型。这样做的原因是随机双坐标上升法SDCAs)可以在不调整的情况下提供良好的默认性能。

选择分类训练器

针对两种分类类型,你应该选择哪一种?如本章前面所述,与回归模型相比,你的预测输出类型将决定是二分类还是多分类分类。你的问题仅仅是预测一个真或假的值,还是基于预定义的值集提供更丰富的输出?如果你的答案是前者,你需要使用二分类。如果是后者,你需要使用多分类分类。在本章中,我们将演示这两种模型预测类型。

对于特定的二分类训练器,SDCA、LightGBM 和 FastTree 是最受欢迎的选项,同时也是文档最完善的。

对于特定的多分类分类训练器,LightGBM 和 SDCA 是最受欢迎且文档最完善的选项。

创建一个二分类应用

如前所述,我们将创建的应用是一个汽车价值预测器。给定一组与汽车相关的属性,可以预测价格是否为好交易。本例中包含的属性并不是属性列表的最终版本,也不应在生产环境中直接使用。然而,可以用这个作为预测基于几个属性简单真或假答案的起点。

与前几章一样,完整的项目代码、样本数据集和项目文件可以在此处下载:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter04

深入了解训练器

如前所述,对于这个二分类应用,我们将使用 FastTree 训练器。

FastTree 基于多重加性回归树(MART)梯度提升算法。梯度提升是一种非常流行的技术,它通过逐步构建一系列树,最终选择最佳树。MART 通过学习使用标量值在其叶子节点上的回归树集合,将这种方法进一步发展。

FastTree 训练器不需要归一化,但要求所有特征列使用float变量类型,标签列使用bool变量类型。

如果你对 MART 感兴趣,康奈尔大学有一篇 2015 年的论文讨论了该主题:arxiv.org/abs/1505.01866

探索项目架构

在第三章中创建的项目架构和代码的基础上,即 回归模型,在此示例中,主要的架构变化是输入机制。对于此应用程序,由于我们使用的是 FastTree 算法,这需要引用 Microsoft.ML.FastTree NuGet 包(在撰写本文时,最新版本为 1.3.1)。如果您从头开始构建此项目并且不记得如何添加 NuGet 引用,请参阅第二章,即 设置 ML.NET 环境

在下面的屏幕截图中,您将找到项目的 Visual Studio 解决方案资源管理器视图。解决方案的新增内容是 testdata.csv 文件,我们将在下面进行回顾:

sampledata.csv 文件包含 18 行随机数据。您可以随意调整数据以适应您的观察或调整训练好的模型。以下是数据的片段:

0,0,0,4000,0
1,1,1,4500,1
0,1,0,5000,0
0,0,1,4500,0
0,0,0,3000,1
0,1,0,3100,1
0,1,1,3500,1
1,1,1,5500,0
1,1,1,4200,1

每一行都包含我们将在此章后面回顾的全新 CarInventory 类中属性的值。

此外,在本章中,我们添加了 testdata.csv 文件,其中包含额外的数据点,以测试新训练的模型并评估。以下是 testdata.csv 内部的数据片段:

0,0,0,2010,1
1,0,0,2600,1
1,0,0,3700,0
1,1,0,3100,1
1,1,0,3600,0
0,1,0,3500,0
0,0,1,3400,1
0,0,1,5100,0

深入代码

如前节所述,对于此应用程序,我们正在基于第三章中完成的工作构建,即 回归模型。对于这次深入研究,我们将专注于此应用程序更改的代码。

以下是被更改或添加的类:

  • CarInventory

  • CarInventoryPrediction

  • Predictor

  • 训练器

  • 程序

CarInventory 类

CarInventory 类是包含预测和训练模型所需数据的容器类。这些列按照之前审查的样本数据的顺序映射。如果您开始尝试新的功能并向以下类添加内容,请确保适当地增加数组索引,如下所示:

using Microsoft.ML.Data;

namespace chapter04.ML.Objects
{
    public class CarInventory
    {
        [LoadColumn(0)]
        public float HasSunroof { get; set; }

        [LoadColumn(1)]
        public float HasAC { get; set; }

        [LoadColumn(2)]
        public float HasAutomaticTransmission { get; set; }

        [LoadColumn(3)]
        public float Amount { get; set; }

        [LoadColumn(4)]
        public bool Label { get; set; }
    }
}

CarInventoryPrediction 类

CarInventoryPrediction 类包含映射到我们预测输出的属性,以及用于模型评估的 ScoreProbability 属性。PredictedLabel 属性包含我们的分类结果,而不是像前几章中的标签,如下面的代码块所示:

namespace chapter04.ML.Objects
{
    public class CarInventoryPrediction
    {
        public bool Label { get; set; }

        public bool PredictedLabel { get; set; }

        public float Score { get; set; }

        public float Probability { get; set; }
    }
}

预测器类

在此类中进行了几处更改以处理就业预测场景,如下所示:

  1. 第一个更改是在预测调用本身。正如您可能猜到的,TSrcTDst 参数需要调整以利用我们创建的两个新类,即 CarInventoryCarInventoryPrediction,如下所示:
var predictionEngine = MlContext.Model.CreatePredictionEngine<CarInventory, CarInventoryPrediction>(mlModel);            
  1. 由于我们不再只是传递字符串并在运行时构建对象,我们需要首先以文本形式读取文件。然后,我们将 JSON 反序列化到我们的CarInventory对象中,如下所示:
var prediction = predictionEngine.Predict(JsonConvert.DeserializeObject<CarInventory>(json));
  1. 最后,我们需要调整我们的预测输出以匹配新的CarInventoryPrediction属性,如下所示:
Console.WriteLine(
    $"Based on input json:{System.Environment.NewLine}" +
    $"{json}{System.Environment.NewLine}" + 
    $"The car price is a {(prediction.PredictedLabel ? "good" : "bad")} deal, with a {prediction.Probability:P0} confidence");

Trainer

Trainer类内部,需要做出一些修改以支持二元分类,如下所示:

  1. 第一个更改是检查测试文件名是否存在,如下面的代码块所示:
if (!File.Exists(testFileName))
{
    Console.WriteLine($"Failed to find test data file ({testFileName}");

    return;
}
  1. 然后,我们使用在第三章中使用的NormalizeMeanVariance转换方法构建数据处理管道,该方法用于输入值,如下所示:
IEstimator<ITransformer> dataProcessPipeline = MlContext.Transforms.Concatenate("Features",
 typeof(CarInventory).ToPropertyList<CarInventory>(nameof(CarInventory.Label)))
 .Append(MlContext.Transforms.NormalizeMeanVariance(inputColumnName: "Features",
 outputColumnName: "FeaturesNormalizedByMeanVar"));
  1. 然后,我们可以使用来自CarInventory类的标签和归一化均值方差创建FastTree训练器,如下所示:
var trainer = MlContext.BinaryClassification.Trainers.FastTree(
    labelColumnName: nameof(CarInventory.Label),
    featureColumnName: "FeaturesNormalizedByMeanVar",
    numberOfLeaves: 2,
    numberOfTrees: 1000,
    minimumExampleCountPerLeaf: 1,
    learningRate: 0.2);

之后,在您运行应用程序之后,考虑调整叶子和树的数量,以查看模型指标和您的预测概率百分比如何变化。

  1. 最后,我们调用Regression.Evaluate方法提供回归特定的指标,然后通过Console.WriteLine调用将这些指标输出到控制台。我们将在本章的最后部分详细说明这些指标的含义,但现在,代码如下所示:
var trainingPipeline = dataProcessPipeline.Append(trainer);

var trainedModel = trainingPipeline.Fit(trainingDataView);

MlContext.Model.Save(trainedModel, trainingDataView.Schema, ModelPath);

现在,我们评估我们刚刚训练的模型,如下所示:

var evaluationPipeline = trainedModel.Append(MlContext.Transforms
 .CalculateFeatureContribution(trainedModel.LastTransformer)
 .Fit(dataProcessPipeline.Fit(trainingDataView).Transform(trainingDataView)));

var testDataView = MlContext.Data.LoadFromTextFile<CarInventory>(testFileName, ',', hasHeader: false);

var testSetTransform = evaluationPipeline.Transform(testDataView);

var modelMetrics = MlContext.BinaryClassification.Evaluate(data: testSetTransform,
 labelColumnName: nameof(CarInventory.Label),
 scoreColumnName: "Score");

最后,我们输出所有的分类指标。我们将在下一节详细说明每个指标,但现在,代码如下所示:

Console.WriteLine($"Accuracy: {modelMetrics.Accuracy:P2}");
Console.WriteLine($"Area Under Curve: {modelMetrics.AreaUnderRocCurve:P2}");
Console.WriteLine($"Area under Precision recall Curve: {modelMetrics.AreaUnderPrecisionRecallCurve:P2}");
Console.WriteLine($"F1Score: {modelMetrics.F1Score:P2}");
Console.WriteLine($"LogLoss: {modelMetrics.LogLoss:#.##}");
Console.WriteLine($"LogLossReduction: {modelMetrics.LogLossReduction:#.##}");
Console.WriteLine($"PositivePrecision: {modelMetrics.PositivePrecision:#.##}");
Console.WriteLine($"PositiveRecall: {modelMetrics.PositiveRecall:#.##}");
Console.WriteLine($"NegativePrecision: {modelMetrics.NegativePrecision:#.##}");
Console.WriteLine($"NegativeRecall: {modelMetrics.NegativeRecall:P2}");

Program

Program类中唯一的更改是帮助文本,用于指示训练者接受测试文件的使用方法,如下面的代码块所示:

if (args.Length < 2)
{
    Console.WriteLine($"Invalid arguments passed in, exiting.{Environment.NewLine}        {Environment.NewLine}Usage:{Environment.NewLine}" +
 $"predict <path to input json file>{Environment.NewLine}" +
 $"or {Environment.NewLine}" +
 $"train <path to training data file> <path to test data file>{Environment.NewLine}");

    return;
}

最后,我们修改switch/case语句以支持Train方法的附加参数,如下所示:

switch (args[0])
{
    case "predict":
        new Predictor().Predict(args[1]);
        break;
    case "train":
        new Trainer().Train(args[1], args[2]);
        break;
    default:
        Console.WriteLine($"{args[0]} is an invalid option");
        break;
}

运行应用程序

要运行应用程序,过程几乎与第三章中的示例应用程序相同,只是在训练时添加了传递测试数据集,如下所述:

  1. 要在命令行上运行训练,就像我们在第一章中做的那样,开始使用机器学习和 ML.NET,我们只需传递以下命令(假设您正在使用包含的示例数据集和测试数据集):
PS chapter04\bin\Debug\netcoreapp3.0> .\chapter04.exe train ..\..\..\Data\sampledata.csv ..\..\..\Data\testdata.csv
Accuracy: 88.89%
Area Under Curve: 100.00%
Area under Precision recall Curve: 100.00%
F1Score: 87.50%
LogLoss: 2.19
LogLossReduction: -1.19
PositivePrecision: 1
PositiveRecall: .78
NegativePrecision: .82
NegativeRecall: 100.00%

注意扩展的输出包括几个指标数据点——我们将在本章末尾解释这些数据点各自的意义。

  1. 训练模型后,构建一个示例 JSON 文件,并将其保存为input.json,如下所示:
{
    "HasSunroof":0,
    "HasAC":0,
    "HasAutomaticTransmission":0,
    "Amount":1300
}
  1. 要使用此文件运行模型,只需将文件名传递给构建的应用程序,预测输出将如下所示:
PS chapter04\bin\Debug\netcoreapp3.0> .\chapter04.exe predict .\input.json
Based on input json:
{
"HasSunroof":0,"HasAC":0,"HasAutomaticTransmission":0,"Amount":1300
}
The car price is a good deal, with a 100% confidence

随意修改这些值,并查看基于模型训练数据集的预测如何变化。从这个点开始,一些实验区域可能如下:

  • 根据您自己的购车经验添加一些额外特征

  • 修改sampledata.csv文件以包含您自己的购车经验

  • 修改示例应用程序以具有图形用户界面GUI),以便更容易运行预测

创建一个多类分类应用程序

如前所述,我们现在将创建一个多类分类应用程序,将电子邮件分类为以下三个类别之一:

  • 订单

  • 垃圾邮件

  • 朋友

将此示例应用于生产应用程序可能会包含更多类别以及更多特征。然而,这是一个很好的起点,用于演示多类分类用例。

与其他示例一样,完整的项目代码、样本数据集和项目文件可以在此处下载:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter04-multiclass

深入了解训练器

如前所述,对于这个多类分类应用程序,我们将使用SdcaMaximumEntropy训练器。

如其名称所示,SdcaMaximumEntropy类基于我们在第三章中深入探讨的 SDCA,即回归模型,并使用经验风险最小化,根据训练数据进行优化。这确实留下了异常值或异常对预测性能产生重大影响的潜在可能性。因此,当使用此训练器时,请向训练器提供足够的预期数据样本,以避免过拟合以及在预测数据时可能出现的错误。

与之前的二分类示例不同,SdcaMaximumEntropy训练器确实需要归一化。此外,不需要缓存;然而,我们在构建管道时确实使用了缓存。

探索项目架构

在本章前面创建的项目架构和代码的基础上,本项目不需要包含任何新的 NuGet 包,因为 SDCA 训练器被视为核心训练器。主要变化在于Training管道,我们将在本节后面的部分进一步详细说明。

在下面的屏幕截图中,您将找到项目的 Visual Studio 解决方案资源管理器视图:

图片

sampledata.csv文件包含六行随机数据。请随意调整数据以适应您的观察或调整训练模型。以下是数据的片段:

"Order #1234","Thank you for ordering a new CPU","order@cpulandia.com","orders"
"Get Free Free","Click here for everything free","freefree@asasdasd.com","spam"
"Checking in","How is it going?","johndough@gmail.com","friend"
"Order 4444","Thank you for ordering a pizza","order@pizzalandia.com","orders"
"Unlock Free","Click here to unlock your spam","spammer@asasdasd.com","spam"
"Hello","Did you see my last message?","janedough@gmail.com","friend"

每一行都包含我们将在本章后面审查的新创建的Email类中属性的值。

此外,在本章中,我们添加了包含额外数据点的testdata.csv文件,以便对新训练的模型进行测试。以下是数据的片段:

"Order 955","Thank you for ordering a new gpu","order@gpulandia.com","orders"
"Win Free Money","Lottery winner, click here","nowfree@asasdasd.com","spam"
"Yo","Hey man?","john@gmail.com","friend"

深入代码

对于这个应用,正如之前提到的,我们是在 第三章 完成的作品基础上进行构建的,回归模型。对于这次深入探讨,我们将专注于为这个应用更改的代码。

被更改或添加的类如下:

  • Email

  • EmailPrediction

  • Predictor

  • Trainer

  • Program

Email

Email 类是包含用于预测和训练模型数据的容器类。这些列按照之前审查的样本数据的顺序映射。如果你开始尝试新的特征并添加到这个列表中,确保适当地增加数组索引,如下面的代码块所示:

using Microsoft.ML.Data;

namespace chapter04_multiclass.ML.Objects
{
    public class Email
    {
        [LoadColumn(0)]
        public string Subject { get; set; }

        [LoadColumn(1)]
        public string Body { get; set; }

        [LoadColumn(2)]
        public string Sender { get; set; }

        [LoadColumn(3)]
        public string Category { get; set; }
    }
}

EmailPrediction

EmailPrediction 类包含映射到预测输出并用于模型评估的属性。在下面的代码块中,我们返回 Category 值(字符串值):

using Microsoft.ML.Data;

namespace chapter04_multiclass.ML.Objects
{
    public class EmalPrediction
    {
        [ColumnName("PredictedLabel")]
        public string Category;
    }
}

Predictor

在这个类中有一两个更改来处理电子邮件分类预测场景,如下所示:

  1. 第一个更改是在预测调用本身。正如你可能猜到的,TSrcTDst 参数需要调整以利用我们创建的两个新类,EmailEmailPrediction,如下所示:
var predictionEngine = MlContext.Model.CreatePredictionEngine<Email, EmailPrediction>(mlModel);            
  1. 由于我们不再只是传递字符串并在运行时构建对象,我们首先需要以文本形式读取文件。然后,我们将 JSON 反序列化到我们的 Email 对象中,如下所示:
var prediction = predictionEngine.Predict(JsonConvert.DeserializeObject<Email>(json));
  1. 最后,我们需要调整预测输出的结果以匹配我们新的 EmailPrediction 属性,如下所示:
Console.WriteLine(
    $"Based on input json:{System.Environment.NewLine}" +
    $"{json}{System.Environment.NewLine}" + 
    $"The email is predicted to be a {prediction.Category}");

Trainer

在这个类中有一两个更改来处理电子邮件分类预测场景,如下所示:

  1. 首先,我们读取 trainingFileName 字符串并将其转换为 Email 对象,如下所示:
var trainingDataView = MlContext.Data.LoadFromTextFile<Email>(trainingFileName, ',', hasHeader: false);           
  1. 接下来,我们将创建我们的管道,将输入属性映射到 FeaturizeText 转换,然后再附加我们的 SDCA 训练器,如下所示:
var dataProcessPipeline = MlContext.Transforms.Conversion.MapValueToKey(inputColumnName: nameof(Email.Category), outputColumnName: "Label")
    .Append(MlContext.Transforms.Text.FeaturizeText(inputColumnName: nameof(Email.Subject), outputColumnName: "SubjectFeaturized"))
    .Append(MlContext.Transforms.Text.FeaturizeText(inputColumnName: nameof(Email.Body), outputColumnName: "BodyFeaturized"))
    .Append(MlContext.Transforms.Text.FeaturizeText(inputColumnName: nameof(Email.Sender), outputColumnName: "SenderFeaturized"))
    .Append(MlContext.Transforms.Concatenate("Features", "SubjectFeaturized", "BodyFeaturized", "SenderFeaturized"))
    .AppendCacheCheckpoint(MlContext);

var trainingPipeline = dataProcessPipeline
    .Append(MlContext.MulticlassClassification.Trainers.SdcaMaximumEntropy("Label", "Features"))
    .Append(MlContext.Transforms.Conversion.MapKeyToValue("PredictedLabel"));
  1. 最后,我们加载测试数据,运行 MultiClassClassification 评估,然后输出四个模型评估属性,如下所示:
var testDataView = MlContext.Data.LoadFromTextFile<Email>(testFileName, ',', hasHeader: false);

var modelMetrics = MlContext.MulticlassClassification.Evaluate(trainedModel.Transform(testDataView));

Console.WriteLine($"MicroAccuracy: {modelMetrics.MicroAccuracy:0.###}");
Console.WriteLine($"MacroAccuracy: {modelMetrics.MacroAccuracy:0.###}");
Console.WriteLine($"LogLoss: {modelMetrics.LogLoss:#.###}");
Console.WriteLine($"LogLossReduction: {modelMetrics.LogLossReduction:#.###}");

运行应用程序

要运行应用程序,过程几乎与 第三章 回归模型 中的示例应用程序相同,只是在训练时添加了测试数据集:

  1. 要在命令行上运行训练,就像我们在 第一章 使用机器学习和 ML.NET 入门 中做的那样,只需传递以下命令(假设你正在使用包含的样本数据集和测试数据集):
PS chapter04-multiclass\bin\Debug\netcoreapp3.0> .\chapter04-multiclass.exe train ..\..\..\Data\sampledata.csv ..\..\..\Data\testdata.csv
MicroAccuracy: 1
MacroAccuracy: 1
LogLoss: .1
LogLossReduction: .856

注意输出已扩展以包含几个指标数据点——我们将在本章末尾解释每个数据点的含义。

  1. 在训练好模型后,创建一个示例 JSON 文件并将其保存为 input.json,如下所示:
{
    "Subject":"hello",
    "Body":"how is it?",
    "Sender":"joe@gmail.com"
}
  1. 要使用此文件运行模型,只需将文件名传递给构建的应用程序,预测输出将显示如下:
PS chapter04-multiclass\bin\Debug\netcoreapp3.0> .\chapter04-multiclass.exe predict .\input.json
Based on input json:
{
"Subject":"hello",
"Body":"how is it?",
"Sender":"joe@gmail.com"
}
The email is predicted to be a "friend"

随意修改这些值,看看基于模型训练数据集的预测如何变化。从这个点开始,一些实验领域可能包括:

  • 根据你自己的电子邮件添加更多样本和测试数据。

  • 根据你自己的电子邮件添加更多类别。

  • 扩展特征,例如发送日期和发送者的 IP 地址。

评估分类模型

如前几章所述,评估模型是整个模型构建过程中的关键部分。一个训练不当的模型只会提供不准确的预测。幸运的是,ML.NET 提供了许多流行的属性来计算模型精度,基于训练时的测试集,以给你一个关于模型在生产环境中表现如何的印象。

在 ML.NET 中,如前所述的示例应用程序中提到的,有几个属性构成了CalibratedBinaryClassificationMetrics类对象。在第二章《设置 ML.NET 环境》中,我们回顾了这些属性的一些内容。然而,现在我们有一个更复杂的示例,并且已经学会了如何评估回归模型,让我们深入了解以下属性:

  • 精度

  • ROC 曲线下的面积

  • F1 分数

  • 精确率-召回率曲线下的面积

此外,我们还将查看在多分类分类应用程序中使用的MulticlassClassificationMetrics对象返回的以下四个指标:

  • 微精度

  • 宏精度

  • 对数损失

  • 对数损失减少

在下一节中,我们将分解这些值的计算方法,并详细说明要寻找的理想值。

精度

精度是测试数据集中正确预测与错误预测的比例。

你希望尽可能接近 100%的值,但不是正好 100%。正如我们在二分类示例中所看到的,我们得到了 88.89%——接近 100%,但并不完全。如果你在实验中看到 100%的分数,你很可能遇到了过拟合的情况。

ROC 曲线下的面积

罗马曲线下的面积,通常也称为 AUC,是曲线下面积的度量。

与精度一样,接近 100%的值是理想的。如果你看到低于 50%的值,你的模型可能需要更多的特征和/或更多的训练数据。

F1 分数

F1 分数是精确率和召回率的调和平均值。

值接近或等于 100%更受欢迎。0 的值表示你的精确率完全不准确。正如我们在二分类示例中所看到的,我们得到了 87.50%。

精确率-召回率曲线下的面积

精确率-召回率曲线下的面积,通常也称为 AUPRC,是成功预测的衡量标准。当你的数据集不平衡到某一分类时,应检查此值。

与 AUC 和精度一样,更倾向于接近 100%的值,因为这表明你有很高的召回率。正如我们在二分类示例中所看到的,我们得到了 100%的 AUPRC 值。

微精度

微精度评估每个样本-类别对是否对准确度指标贡献相等。

值接近或等于 1 更受欢迎。正如我们在示例应用中使用样本和测试数据集所示,达到了 1 的值。

宏观准确度

宏观准确度评估每个类别对是否对准确度指标贡献相等。

值接近或等于 1 更受欢迎。正如我们在示例应用中使用样本和测试数据集所示,达到了 1 的值。

对数损失

对数损失是描述分类器准确度的评估指标。对数损失考虑了模型预测与实际分类之间的差异。

值接近 0 更受欢迎,因为 0 表示模型在测试集上的预测是完美的。正如我们在示例应用中使用样本和测试数据集所示,达到了 0.1 的值。

对数损失降低

对数损失降低是一个简单的评估指标,描述了分类器的准确度与随机预测相比。

值接近或等于 1 更受欢迎,因为随着值的接近 1,模型的相对准确度提高。正如我们在示例应用中使用样本和测试数据集所示,达到了 0.856 的值,这意味着猜测正确答案的概率是 85.6%。

摘要

在本章中,我们深入探讨了分类模型。我们还创建并训练了我们的第一个二分类应用,使用 FastTree 和 ML.NET 来预测汽车价格的好坏。我们还创建了我们第一个多分类应用,使用 SDCA 训练器对电子邮件进行分类。最后,我们还深入探讨了如何评估分类模型以及 ML.NET 公开的各种属性,以正确评估您的分类模型。

在下一章中,我们将深入探讨 ML.NET 中的聚类算法以及创建文件类型分类器。

第五章:聚类模型

在分类模型之后,现在是时候深入探讨聚类模型了。目前,在 ML.NET 中只有一个聚类算法,即 k-means。在本章中,我们将深入探讨 k-means 聚类以及最适合使用聚类算法的各种应用。此外,我们将构建一个新的 ML.NET 聚类应用程序,该程序仅通过查看内容就能确定文件的类型。最后,我们将探讨如何使用 ML.NET 公开的属性来评估 k-means 聚类模型。

在本章中,我们将涵盖以下主题:

  • 分析 k-means 算法

  • 创建聚类应用程序

  • 评估 k-means 模型

分析 k-means 算法

如第一章中所述,“使用 ML.NET 开始机器学习之旅”,根据定义,k-means 聚类是一种无监督学习算法。这意味着数据根据提供给模型进行训练的数据被分组到各个簇中。在本节中,我们将深入探讨聚类的多种用例以及 k-means 训练器。

聚类的用例

正如你可能开始意识到的,聚类有众多应用,其输出将相似输出分类为相似数据点的组。

它的一些潜在应用包括以下内容:

  • 跟踪自然灾害,如地震或飓风,并创建高风险区域的簇

  • 根据作者、主题和来源对书籍或文档进行分组

  • 将客户数据分组以进行目标市场营销预测

  • 搜索结果分组,将其他用户认为有用的相似结果分组在一起

此外,它还有许多其他应用,如预测恶意软件家族或癌症研究的医疗用途。

深入了解 k-means 训练器

ML.NET 中使用的 k-means 训练器是基于阴阳方法,而不是经典的 k-means 实现。像我们在前几章中查看的一些训练器一样,所有输入都必须是 Float 类型。此外,所有输入都必须归一化到一个单一的特征向量中。幸运的是,k-means 训练器包含在主要的 ML.NET NuGet 包中;因此,不需要额外的依赖项。

要了解更多关于阴阳实现的信息,微软研究院在此发布了白皮书:www.microsoft.com/en-us/research/wp-content/uploads/2016/02/ding15.pdf

看一下以下图表,展示了三个簇和一个数据点:

图片

在聚类中,每个这些簇代表了一组相似数据点的分组。对于 k-means 聚类(以及其他聚类算法),数据点与每个簇之间的距离是模型将返回哪个簇的度量。对于 k-means 聚类特别来说,它使用每个簇的中心点(也称为质心)并计算到数据点的距离。这些值中最小的是预测的簇。

对于 k-means 训练器,它可以以三种方式之一进行初始化。一种方式是使用随机初始化——正如你可能猜到的,这可能导致随机的预测结果。另一种方式是使用 k-means++,它力求产生 O(log K) 的预测。最后,ML.NET 的默认方法 k-means|| 使用并行方法来减少初始化所需的遍历次数。

关于 k-means|| 的更多信息,你可以参考斯坦福大学发表的一篇论文,其中对其进行了详细解释:theory.stanford.edu/~sergei/papers/vldb12-kmpar.pdf

关于 k-means++ 的更多信息,你可以参考斯坦福大学在 2006 年发表的一篇论文,其中对其进行了详细解释:ilpubs.stanford.edu:8090/778/1/2006-13.pdf

我们将在下一节中演示这个训练器。

创建聚类应用程序

如前所述,我们将创建的应用程序是一个文件类型分类器。给定从文件中静态提取的一组属性,预测将返回它是一个文档、可执行文件还是脚本。对于那些使用过 Linux file 命令的人来说,这是一个简化的版本,但基于机器学习。本例中包含的属性不是属性的确切列表,也不应在生产环境中直接使用;然而,你可以将其用作创建基于机器学习的 Linux file 命令替代品的起点。

与前面的章节一样,完整的项目代码、样本数据集和项目文件可以在此处下载:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter05

探索项目架构

在前面章节中创建的项目架构和代码的基础上,主要的架构变化是在训练集和测试集上进行的特征提取。

在这里,你可以找到项目的 Visual Studio Solution Explorer 视图。解决方案中的新增文件是 FileTypesFileDataFilePrediction 文件,我们将在本节稍后进行回顾:

图片

sampledata.csv 文件包含了我系统上的 80 行随机文件,包括 30 个 Windows 可执行文件,20 个 PowerShell 脚本和 20 个 Word 文档。请随意调整数据以适应您的观察或调整训练好的模型。以下是数据的一个片段:

0,1,1,0
0,1,1,0
0,1,1,0
2,0,0,0
2,0,0,0
2,0,0,0
2,0,0,0
2,0,0,0
2,0,0,0
2,0,0,0
2,0,0,0
2,0,0,0
2,0,0,0
2,0,0,0
1,1,0,1
1,1,0,1
1,1,0,1
1,1,0,1

每一行都包含新创建的 FileData 类中属性的值,我们将在本章稍后进行回顾。

此外,我们还添加了 testdata.csv 文件,其中包含额外的数据点,用于测试新训练的模型并评估。分布是均匀的,包括 10 个 Windows 可执行文件,10 个 PowerShell 脚本和 10 个 Word 文档。以下是 testdata.csv 内部数据的一个片段:

0,1,1,0
0,1,1,0
2,0,0,0
2,0,0,0
2,0,0,0
2,0,0,0
2,0,0,0
2,0,0,0
1,1,0,1

深入代码

如前节所述,对于这个应用,我们正在构建在 第四章 完成的作品之上,即 分类模型。对于这次深入探讨,我们将专注于为这个应用更改的代码。

被更改或添加的类如下:

  • Constants

  • `BaseML`

  • FileTypes

  • FileData

  • FileTypePrediction

  • FeatureExtractor

  • Predictor

  • Trainer

  • Program

Constants

Constants 类已被更改,以将模型保存到 chapter5.mdl,同时支持提取特征的 testdata.csv 变量。以下代码块反映了这些更改:

namespace chapter05.Common
{
    public class Constants
    {
        public const string MODEL_FILENAME = "chapter5.mdl";

        public const string SAMPLE_DATA = "sampledata.csv";

        public const string TEST_DATA = "testdata.csv";
    }
}

BaseML

BaseML 类中唯一的更改是添加了 FEATURES 变量。通过在这里使用变量,我们可以移除在 Trainer 类中使用魔法字符串的需求(我们将在本节稍后讨论这一点):

protected const string FEATURES = "Features";

FileTypes 枚举

FileTypes 枚举包含一个强类型方法,用于映射我们的分类和数值。正如我们在先前的示例中所发现的那样,使用枚举而不是魔法或常量值提供了更好的灵活性,正如这里和剩余类中所示:

namespace chapter05.Enums
{
    public enum FileTypes
    {
        Executable = 0,
        Document = 1,
        Script = 2
    }
}

FileData

FileData 类是包含用于预测和训练我们的模型的数据的容器类:

  1. 首先,我们添加了 TrueFalse 的常量值,因为 k-means 需要浮点数值:
public class FileData
{
    private const float TRUE = 1.0f;
    private const float FALSE = 0.0f;
  1. 接下来,我们创建了一个构造函数,它支持我们的预测和训练。我们可以选择性地传递训练的文件名以提供标签,在这种情况下,对于脚本、可执行文件和文档,分别是 ps1exedoc。我们还调用辅助方法来确定文件是否为二进制文件,或者它是否以 MZ 或 PK 开头:
public FileData(Span<byte> data, string fileName = null)
{
    // Used for training purposes only
    if (!string.IsNullOrEmpty(fileName))
    {
        if (fileName.Contains("ps1"))
        {
            Label = (float) FileTypes.Script;
        } else if (fileName.Contains("exe"))
        {
            Label = (float) FileTypes.Executable;
        } else if (fileName.Contains("doc"))
        {
            Label = (float) FileTypes.Document;
        }
    }

    IsBinary = HasBinaryContent(data) ? TRUE : FALSE;

    IsMZHeader = HasHeaderBytes(data.Slice(0, 2), "MZ") ? TRUE : FALSE;

    IsPKHeader = HasHeaderBytes(data.Slice(0, 2), "PK") ? TRUE : FALSE;
}

MZ 和 PK 被认为是 Windows 可执行文件和现代 Microsoft Office 文件的魔法数字。魔法数字是位于每个文件开头的唯一字节字符串。在这种情况下,两者都是两个字节。在分析文件时,快速确定对于性能至关重要。对于细心的读者,PK 也是 ZIP 的魔法数字。现代 Microsoft Office 文档实际上是 ZIP 存档。为了简化本例,我们使用 PK 而不是执行额外的检测级别。

  1. 接下来,我们还添加了一个额外的构造函数来支持值的硬真设置。我们将在本节稍后深入了解此添加的目的:
/// <summary>
/// Used for mapping cluster ids to results only
/// </summary>
/// <param name="fileType"></param>
public FileData(FileTypes fileType)
{
    Label = (float)fileType;

    switch (fileType)
    {
        case FileTypes.Document:
            IsBinary = TRUE;
            IsMZHeader = FALSE;
            IsPKHeader = TRUE;
            break;
        case FileTypes.Executable:
            IsBinary = TRUE;
            IsMZHeader = TRUE;
            IsPKHeader = FALSE;
            break;
        case FileTypes.Script:
            IsBinary = FALSE;
            IsMZHeader = FALSE;
            IsPKHeader = FALSE;
            break;
    }
}
  1. 接下来,我们实现我们的两个辅助方法。第一个,HasBinaryContent,正如其名所示,接受原始二进制数据并搜索非文本字符以确保它是一个二进制文件。其次,我们定义HasHeaderBytes;此方法接受一个字节数组,将其转换为UTF8字符串,然后检查该字符串是否与传入的字符串匹配:
private static bool HasBinaryContent(Span<byte> fileContent) =>
            System.Text.Encoding.UTF8.GetString(fileContent.ToArray()).Any(a => char.IsControl(a) && a != '\r' && a != '\n');

private static bool HasHeaderBytes(Span<byte> data, string match) => System.Text.Encoding.UTF8.GetString(data) == match;
  1. 接下来,我们添加用于预测、训练和测试的属性:
[ColumnName("Label")]
public float Label { get; set; }

public float IsBinary { get; set; }

public float IsMZHeader { get; set; }

public float IsPKHeader { get; set; }
  1. 最后,我们重写ToString方法以用于特征提取:
public override string ToString() => $"{Label},{IsBinary},{IsMZHeader},{IsPKHeader}";

FileTypePrediction

FileTypePrediction类包含映射到我们的预测输出的属性。在 k-means 聚类中,PredictedClusterId属性存储找到的最近簇。此外,Distances数组包含数据点到每个簇的距离:

using Microsoft.ML.Data;

namespace chapter05.ML.Objects
{
    public class FileTypePrediction
    {
        [ColumnName("PredictedLabel")]
        public uint PredictedClusterId;

        [ColumnName("Score")]
        public float[] Distances;
    }
}

FeatureExtractor

我们在第三章的“回归模型”示例中使用的FeatureExtractor类已被调整以支持测试和训练数据提取:

  1. 首先,我们将提取泛化到接受文件夹路径和输出文件。如前所述,我们还传递了文件名,以确保LabelingFileData类内部干净地发生:
private void ExtractFolder(string folderPath, string outputFile)
{
    if (!Directory.Exists(folderPath))
    {
        Console.WriteLine($"{folderPath} does not exist");

        return;
    }

    var files = Directory.GetFiles(folderPath);

    using (var streamWriter =
        new StreamWriter(Path.Combine(AppContext.BaseDirectory, $"../../../Data/{outputFile}")))
    {
        foreach (var file in files)
        {
            var extractedData = new FileData(File.ReadAllBytes(file), file);

            streamWriter.WriteLine(extractedData.ToString());
        }
    }

    Console.WriteLine($"Extracted {files.Length} to {outputFile}");
}
  1. 最后,我们从命令行中获取两个参数(由Program类调用)并简单地再次调用前面的方法:
public void Extract(string trainingPath, string testPath)
{
    ExtractFolder(trainingPath, Constants.SAMPLE_DATA);
    ExtractFolder(testPath, Constants.TEST_DATA);
}

Predictor

在此类中有一两个更改来处理文件类型预测场景:

  1. 首先,我们添加一个辅助方法GetClusterToMap,它将已知值映射到预测簇。注意这里使用Enum.GetValues;随着你添加更多文件类型,此方法不需要修改:
private Dictionary<uint, FileTypes> GetClusterToMap(PredictionEngineBase<FileData, FileTypePrediction> predictionEngine)
{
    var map = new Dictionary<uint, FileTypes>();

    var fileTypes = Enum.GetValues(typeof(FileTypes)).Cast<FileTypes>();

    foreach (var fileType in fileTypes)
    {
        var fileData = new FileData(fileType);

        var prediction = predictionEngine.Predict(fileData);

        map.Add(prediction.PredictedClusterId, fileType);
    }

    return map;
}         
  1. 接下来,我们将FileDataFileTypePrediction类型传递给CreatePredictionEngine方法以创建我们的预测引擎。然后,我们将文件作为二进制文件读取,并在运行预测和映射初始化之前将这些字节传递给FileData构造函数:
var predictionEngine = MlContext.Model.CreatePredictionEngine<FileData, FileTypePrediction>(mlModel);

var fileData = new FileData(File.ReadAllBytes(inputDataFile));

var prediction = predictionEngine.Predict(fileData);

var mapping = GetClusterToMap(predictionEngine);
  1. 最后,我们需要调整输出以匹配 k-means 预测返回的输出,包括欧几里得距离:
Console.WriteLine(
    $"Based on input file: {inputDataFile}{Environment.NewLine}{Environment.NewLine}" +
    $"Feature Extraction: {fileData}{Environment.NewLine}{Environment.NewLine}" +
    $"The file is predicted to be a {mapping[prediction.PredictedClusterId]}{Environment.NewLine}");

Console.WriteLine("Distances from all clusters:");

for (uint x = 0; x < prediction.Distances.Length; x++) { 
    Console.WriteLine($"{mapping[x+1]}: {prediction.Distances[x]}");
}

Trainer

Trainer类内部,需要进行一些修改以支持 k-means 分类:

  1. 第一个变化是添加了一个GetDataView辅助方法,它从FileData类中先前定义的列构建IDataView对象:
private IDataView GetDataView(string fileName)
{
    return MlContext.Data.LoadFromTextFile(path: fileName,
        columns: new[]
        {
            new TextLoader.Column(nameof(FileData.Label), DataKind.Single, 0),
            new TextLoader.Column(nameof(FileData.IsBinary), DataKind.Single, 1),
            new TextLoader.Column(nameof(FileData.IsMZHeader), DataKind.Single, 2),
            new TextLoader.Column(nameof(FileData.IsPKHeader), DataKind.Single, 3)
        },
        hasHeader: false,
        separatorChar: ',');
}
  1. 我们接下来构建数据处理管道,将列转换为一个单独的Features列:
var trainingDataView = GetDataView(trainingFileName);

var dataProcessPipeline = MlContext.Transforms.Concatenate(
    FEATURES,
    nameof(FileData.IsBinary),
    nameof(FileData.IsMZHeader),
    nameof(FileData.IsPKHeader));
  1. 然后,我们可以创建一个具有 3 个聚类的 k-means 训练器并创建模型:
var trainer = MlContext.Clustering.Trainers.KMeans(featureColumnName: FEATURES, numberOfClusters: 3);
var trainingPipeline = dataProcessPipeline.Append(trainer);
var trainedModel = trainingPipeline.Fit(trainingDataView);

MlContext.Model.Save(trainedModel, trainingDataView.Schema, ModelPath);

聚类数量的默认值为 5。一个有趣的实验是基于这个数据集或您修改后的数据集,看看通过调整这个值预测结果如何变化。

  1. 现在,我们使用测试数据集评估我们刚刚训练的模型:
var testingDataView = GetDataView(testingFileName);

IDataView testDataView = trainedModel.Transform(testingDataView);

ClusteringMetrics modelMetrics = MlContext.Clustering.Evaluate(
    data: testDataView,
    labelColumnName: "Label",
    scoreColumnName: "Score",
    featureColumnName: FEATURES);
  1. 最后,我们输出所有分类度量,我们将在下一节中详细介绍每个度量。
Console.WriteLine($"Average Distance: {modelMetrics.AverageDistance}");
Console.WriteLine($"Davies Bould Index: {modelMetrics.DaviesBouldinIndex}");
Console.WriteLine($"Normalized Mutual Information: {modelMetrics.NormalizedMutualInformation}");

Program

如前几章所述,Program类是我们应用程序的主要入口点。Program类中唯一的变化是帮助文本,用于指示如何使用extract方法接受提取测试文件夹路径:

if (args.Length < 2)
{
    Console.WriteLine($"Invalid arguments passed in, exiting.{Environment.NewLine}{Environment.NewLine}Usage:{Environment.NewLine}" +
                      $"predict <path to input file>{Environment.NewLine}" +
                      $"or {Environment.NewLine}" +
                      $"train <path to training data file> <path to test data file>{Environment.NewLine}" +
                      $"or {Environment.NewLine}" + $"extract <path to training folder> <path to test folder>{Environment.NewLine}");

    return;
}

最后,我们修改switch/case语句以支持extract方法的附加参数,以支持训练和测试数据集:

switch (args[0])
{
    case "extract":
        new FeatureExtractor().Extract(args[1], args[2]);
        break;
    case "predict":
        new Predictor().Predict(args[1]);
        break;
    case "train":
        new Trainer().Train(args[1], args[2]);
        break;
    default:
        Console.WriteLine($"{args[0]} is an invalid option");
        break;
}

运行应用程序

要运行应用程序,过程几乎与第三章中回归模型的示例应用程序相同,只是在训练时传递测试数据集:

  1. 要在命令行上运行训练,就像我们在前几章中所做的那样,只需传递以下命令(假设您已添加两组文件;一组用于您的训练集,另一组用于测试集):
PS chapter05\bin\Debug\netcoreapp3.0> .\chapter05.exe extract ..\..\..\TrainingData\ ..\..\..\TestData\
Extracted 80 to sampledata.csv
Extracted 30 to testdata.csv

代码库中包含两个预特征提取文件(sampledata.csvtestdata.csv),以便您可以在不执行自己的特征提取的情况下训练模型。如果您想执行自己的特征提取,创建一个TestDataTrainingData`文件夹。在这些文件夹中填充PowerShellPS1)、Windows 可执行文件EXE)和Microsoft Word 文档DOCX)的样本。

  1. 在提取数据后,我们必须通过传递新创建的sampledata.csvtestdata.csv文件来训练模型:
PS chapter05\bin\Debug\netcoreapp3.0> .\chapter05.exe train ..\..\..\Data\sampledata.csv ..\..\..\Data\testdata.csv 
Average Distance: 0
Davies Bould Index: 0
Normalized Mutual Information: 1
  1. 要使用此文件运行模型,只需将文件名传递给构建的应用程序(在这种情况下,使用编译的chapter05.exe)即可,预测输出将显示:
PS chapter05\bin\Debug\netcoreapp3.0> .\chapter05.exe predict .\chapter05.exe
Based on input file: .\chapter05.exe

Feature Extraction: 0,1,1,0

The file is predicted to be a Executable

Distances from all clusters:
Executable: 0
Script: 2
Document: 2

注意输出已扩展以包括几个度量数据点——我们将在本章末尾详细介绍每个数据点的含义。

随意修改值,并查看基于模型训练数据集的预测如何变化。从这个点开始的一些实验领域可能包括以下内容:

  • 添加一些额外的功能以提高预测准确性

  • 向聚类中添加额外的文件类型,如视频或音频

  • 添加新的文件范围以生成新的样本和测试数据

评估 k-means 模型

如前几章所述,评估模型是整个模型构建过程中的关键部分。一个训练不当的模型只会提供不准确的预测。幸运的是,ML.NET 提供了许多流行的属性,可以根据训练时的测试集计算模型精度,从而让你了解你的模型在生产环境中将如何表现。

在 ML.NET 中,正如示例应用程序中提到的,有三个属性构成了ClusteringMetrics类对象。让我们深入了解ClusteringMetrics对象公开的属性:

  • 平均距离

  • Davies-Bouldin 指数

  • 标准化互信息

在接下来的几节中,我们将分解这些值的计算方法以及理想值。

平均距离

也被称为平均得分的是簇中心到测试数据的距离。该值,类型为 double,随着簇数量的增加而减小,有效地为边缘情况创建簇。此外,当你的特征创建出独特的簇时,可能存在一个值为 0 的情况,就像我们在示例中看到的那样。这意味着,如果你发现自己看到较差的预测性能,你应该增加簇的数量。

Davies-Bouldin 指数

Davies-Bouldin 指数是衡量聚类质量的另一个指标。具体来说,Davies-Bouldin 指数衡量簇分离的散布,值范围从 0 到 1(类型为 double),值为 0 是理想的(正如我们的示例所示)。

关于 Davies-Bouldin 指数的更多细节,特别是算法背后的数学,可以在以下资源中找到:en.wikipedia.org/wiki/Davies%E2%80%93Bouldin_index

标准化互信息

标准化互信息度量用于衡量特征变量的相互依赖性。

值的范围是从 0 到 1(类型为 double),越接近或等于 1 越理想,类似于本章早期训练的模型。

关于标准化互信息的更多细节以及算法背后的数学,请阅读en.wikipedia.org/wiki/Mutual_information#Normalized_variants

摘要

在本章的整个过程中,我们深入探讨了 ML.NET 通过 k-means 聚类算法提供的聚类支持。我们还创建并训练了我们的第一个聚类应用程序,使用 k-means 来预测文件类型。最后,我们探讨了如何评估 k-means 聚类模型以及 ML.NET 公开的各种属性,以实现 k-means 聚类模型的正确评估。

在下一章中,我们将通过创建登录异常预测器来深入探讨 ML.NET 中的异常检测算法。

第六章:异常检测模型

在我们完成了 k-means 聚类模型之后,现在是时候深入异常检测模型了。异常检测是 ML.NET 中较新的功能之一,特别是时间序列转换。在本章中,我们将深入探讨异常检测及其最适合利用异常检测的各种应用。此外,我们将构建两个新的示例应用程序:一个用于确定登录尝试是否异常的异常检测应用程序,展示了随机 PCA 训练器,另一个演示了在网络安全异常检测应用程序中的时间序列。最后,我们将探讨如何使用 ML.NET 公开的属性来评估异常检测模型。

在本章中,我们将涵盖以下主题:

  • 分析异常检测

  • 创建时间序列应用程序

  • 创建异常检测应用程序

  • 评估异常检测模型

分析异常检测

如第一章“开始使用机器学习和 ML.NET”中所述,根据定义,异常检测是一种无监督学习算法。这意味着该算法将在数据上训练并寻找不符合正常数据的数据。在本节中,我们将深入了解异常检测的应用案例以及 ML.NET 中可用的各种异常检测训练器。

异常检测的应用案例

如您可能已经意识到的,异常检测在数据可用但未知数据中是否存在异常的众多应用中都有。无需进行手动抽查,异常检测算法会在这些数据上训练并确定是否存在任何异常。ML.NET 提供了各种异常检测值,以便在您的应用程序中程序化地查看。我们将在本章后面回顾这些值,以确保任何检测都不是假阳性。

一些最适合用于异常检测的潜在应用包括以下内容:

  • 销售预测

  • 股市

  • 欺诈检测

  • 预测设备因各种因素而发生的故障

  • 针对远程连接和网络流量登录历史记录的网络安全应用程序,例如我们将在后面深入研究的示例应用程序

深入随机 PCA 训练器

随机 PCA 训练器是 ML.NET 在编写时发现的唯一传统异常检测训练器。随机 PCA 训练器需要归一化值;然而,缓存不是必需的,也不需要额外的 NuGet 包来使用训练器。

与其他算法类似,输入是一个已知的Float类型向量大小。输出包含两个属性:ScorePredictedLabelScore的值是Float类型,非负且无界。相比之下,PredictedLabel属性根据设定的阈值指示一个有效的异常;true 值表示异常,而 false 值表示不是异常。ML.NET 的默认阈值为 0.5,可以通过ChangeModelThreshold方法进行调整。实际上,高于阈值的值返回 true,低于阈值的返回 false。

在幕后,该算法使用特征向量来估计包含正常类的子空间,然后计算实际特征向量在该子空间中的投影特征向量之间的归一化差异。简单来说,如果计算出的误差不接近 0,算法会找到边缘情况。如果它发现误差接近 0,则被认为是正常数据点(即非异常)。

我们将在本章后面的第二个示例应用中通过检测登录异常来演示这个训练器。

如果你想要深入了解随机 PCA,以下论文是一个很好的资源:web.stanford.edu/group/mmds/slides2010/Martinsson.pdf

深入了解时间序列转换

与本书中和其他 ML.NET 本身找到的算法不同,时间序列支持是通过一系列应用于你的训练和测试数据的转换来添加的。如前所述,时间序列也是 ML.NET 的新增功能之一,添加于 1.2.0 版本。

在 ML.NET 中,时间序列转换被分组到TimeSeriesCatalog类中。这个类内部有六个不同的方法:

  • DetectAnomalyBySrCnn:使用 SRCNN 算法检测异常

  • DetectChangePointBySsa:使用奇异谱分析SSA)算法在变化点检测异常

  • DetectIidChangePoint:使用独立同分布i.i.d)算法检测变化以预测变化点

  • DetectIidSpike:使用 i.i.d 算法检测变化,但预测尖峰而不是变化点

  • DetectSpikeBySsa:使用 SSA 算法检测尖峰

  • ForecastBySsa:使用 SSA 算法进行基于单一变量(通常称为单变量)的时间序列预测

根据应用的不同,你可能想要寻找数据变化的尖峰或变化点(上升或下降螺旋)。在本章关于时间序列的示例中,我们将利用DetectSpikeBySsa寻找网络传输随时间变化的尖峰。

关于使用 SSA 进行预测的更多信息,这里有一个很好的资源:arxiv.org/pdf/1206.6910.pdf

创建时间序列应用

如前所述,我们将创建的应用是一个网络流量异常检测器。给定一组与网络流量量(以字节为单位)相关的属性,该应用将使用这些数据来查找给定检查点的流量异常。与其他应用一样,这并不是为了推动下一个机器学习网络流量异常检测产品的开发;然而,它将向您展示如何在 ML.NET 中使用时间序列,特别是如何使用 SSA 检测峰值。

如前几章所述,完成的项目代码、样本数据集和项目文件可以在此下载:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter06-time-series

探索项目架构

在前几章中创建的项目架构和代码的基础上,大部分的更改集中在模型的训练上,因为时间序列需要从我们在前几章中回顾的内容进行相当大的范式转变。此外,当使用时间序列转换时,如果您是从零开始创建项目,您需要添加 Microsoft.ML.TimeSeries NuGet 包。GitHub 仓库中提供的示例应用已经包含了这个包。

在下面的屏幕截图中,您将找到项目的 Visual Studio 解决方案资源管理器视图。解决方案中的新添加项是 NetworkTrafficHistoryNetworkTrafficPrediction 文件,我们将在本节后面进行回顾:

图片

sampledata.csv 文件包含八行网络流量数据。请随意调整数据以适应您的观察或调整训练模型。以下是数据的片段:

laptop,2019-11-14T11:13:23,1500
laptop,2019-11-15T11:13:23,1000
laptop,2019-11-16T11:13:23,1100
laptop,2019-11-17T11:13:23,1600
laptop,2019-11-18T11:13:23,1000
laptop,2019-11-19T11:13:23,1100
laptop,2019-11-20T11:13:23,1000
laptop,2019-11-21T11:13:23,1000

每一行都包含新创建的 NetworkTrafficHistory 类中属性的值,我们将在本章后面进行回顾。

此外,我们还添加了 testdata.csv 文件,其中包含额外的数据点,用于测试新训练的模型并评估。以下是 testdata.csv 内的数据片段:

laptop,2019-11-22T11:13:23,1000
laptop,2019-11-23T11:13:23,1100
laptop,2019-11-24T11:13:23,1200
laptop,2019-11-25T11:13:23,1300
laptop,2019-11-26T11:13:23,1400
laptop,2019-11-27T11:13:23,3000
laptop,2019-11-28T11:13:23,1500
laptop,2019-11-29T11:13:23,1600

深入代码

如前所述,对于这个应用,我们是在第五章,“聚类模型”的基础上进行工作的。对于这次深入研究,我们将专注于为这个应用更改的代码。

被更改或添加的类如下:

  • NetworkTrafficHistory

  • NetworkTrafficPrediction

  • Predictor

  • Trainer

  • Program

NetworkTrafficHistory 类

NetworkTrafficHistory 类是包含用于预测和训练我们的模型的数据的容器类。如前几章所述,LoadColumn 装饰器中的数字映射到 CSV 文件中的索引。如前所述,ML.NET 中的异常检测需要使用单个浮点值;在这种情况下,是 BytesTransferred 属性:

using System;

using Microsoft.ML.Data;

namespace chapter06.ML.Objects
{
    public class NetworkTrafficHistory
    {
        [LoadColumn(0)]
        public string HostMachine { get; set; }

        [LoadColumn(1)]
        public DateTime Timestamp { get; set; }

        [LoadColumn(2)] 
        public float BytesTransferred { get; set; }
    }
}

NetworkTrafficPrediction

NetworkTrafficPrediction 类包含映射到我们的预测输出的属性。VectorType(3) 函数包含警报、得分和 p 值。我们将在本节稍后回顾这些值:

using Microsoft.ML.Data;

namespace chapter06.ML.Objects
{
    public class NetworkTrafficPrediction
    {
        [VectorType(3)]
        public double[] Prediction { get; set; }
    }
}

Predictor

为了处理网络流量预测场景,需要对此类进行一些修改:

  1. 首先,我们使用 NetworkTrafficHistoryNetworkHistoryPrediction 类型创建我们的预测引擎:
var predictionEngine = MlContext.Model.CreatePredictionEngine<NetworkTrafficHistory, NetworkTrafficPrediction>(mlModel);
  1. 接下来,我们将输入文件读取到 IDataView 变量中(注意使用逗号作为 separatorChar 的重写):
var inputData = MlContext.Data.LoadFromTextFile<NetworkTrafficHistory>(inputDataFile, separatorChar: ',');
  1. 接下来,我们获取基于新创建的 IDataView 变量的可枚举对象:
var rows = MlContext.Data.CreateEnumerable<NetworkTrafficHistory>(inputData, false);
    1. 最后,我们需要运行预测并输出模型运行的输出结果:
    Console.WriteLine($"Based on input file ({inputDataFile}):");
    
    foreach (var row in rows)
    {
        var prediction = predictionEngine.Predict(row);
    
        Console.Write($"HOST: {row.HostMachine} TIMESTAMP: {row.Timestamp} TRANSFER: {row.BytesTransferred} ");
        Console.Write($"ALERT: {prediction.Prediction[0]} SCORE: {prediction.Prediction[1]:f2} P-VALUE: {prediction.Prediction[2]:F2}{Environment.NewLine}");
    }
    

    由于 Transform 只返回三个元素的向量,原始行数据被输出以提供上下文。

    Trainer

    Trainer 类内部,需要做出一些修改以支持时间序列转换。在许多方面,需要简化。执行了评估和测试数据加载的移除:

    1. 首先是向转换发送的四个变量的添加:
    private const int PvalueHistoryLength = 3;
    private const int SeasonalityWindowSize = 3;
    private const int TrainingWindowSize = 7;
    private const int Confidence = 98;
    

    由于在编写 ML.NET 库时的一个约束,训练窗口大小必须大于 p 值历史长度的两倍。

    1. 然后,我们从 CSV 训练文件构建 DataView 对象:
    var trainingDataView = GetDataView(trainingFileName);
    
    1. 然后,我们可以创建 SSA 脉冲检测:
    var trainingPipeLine = MlContext.Transforms.DetectSpikeBySsa(
        nameof(NetworkTrafficPrediction.Prediction),
        nameof(NetworkTrafficHistory.BytesTransferred),
        confidence: Confidence,
        pvalueHistoryLength: PvalueHistoryLength,
        trainingWindowSize: TrainingWindowSize,
        seasonalityWindowSize: SeasonalityWindowSize);
    
    1. 现在,我们在训练数据上拟合模型并保存模型:
    ITransformer trainedModel = trainingPipeLine.Fit(trainingDataView);
    
    MlContext.Model.Save(trainedModel, trainingDataView.Schema, ModelPath);
    
    Console.WriteLine("Model trained");
    

    Program

    由于训练只需要训练数据,因此需要对 Program 类进行一些修改:

    1. 帮助文本需要更新以反映新的用法:
    if (args.Length < 2)
    {
        Console.WriteLine($"Invalid arguments passed in, exiting.{Environment.NewLine}{Environment.NewLine}Usage:{Environment.NewLine}" +
                          $"predict <path to input file>{Environment.NewLine}" +
                          $"or {Environment.NewLine}" +
                          $"train <path to training data file>{Environment.NewLine}");
    
        return;
    }
    
    1. 此外,需要更新 switch-case 语句以反映预测传递的单个参数:
    switch (args[0])
    {
        case "predict":
            new Predictor().Predict(args[1]);
            break;
        case "train":
            new Trainer().Train(args[1]);
            break;
        default:
            Console.WriteLine($"{args[0]} is an invalid option");
            break;
    }
    

    运行应用程序

    要运行应用程序,我们使用的流程几乎与 第三章 中“回归模型”的示例应用程序相同:

    1. 准备好数据后,我们必须通过传递新创建的 sampledata.csv 文件来训练模型:
    PS chapter06-time-series\bin\Debug\netcoreapp3.0> .\chapter06-time-series.exe train ..\..\..\Data\sampledata.csv
    Model trained
    
    1. 要使用此文件运行模型,只需将之前提到的 testdata.csv 文件传递到新构建的应用程序中,预测输出将显示以下内容:
    PS bin\debug\netcoreapp3.0> .\chapter06-time-series.exe predict ..\..\..\Data\testdata.csv
    Based on input file (..\..\..\Data\testdata.csv):
    HOST: laptop TIMESTAMP: 11/22/2019 11:13:23 AM TRANSFER: 1000 ALERT: 0 SCORE: 46.07 P-VALUE: 0.50
    HOST: laptop TIMESTAMP: 11/23/2019 11:13:23 AM TRANSFER: 1100 ALERT: 0 SCORE: 131.36 P-VALUE: 0.00
    HOST: laptop TIMESTAMP: 11/24/2019 11:13:23 AM TRANSFER: 1200 ALERT: 0 SCORE: 180.44 P-VALUE: 0.06
    HOST: laptop TIMESTAMP: 11/25/2019 11:13:23 AM TRANSFER: 1300 ALERT: 0 SCORE: 195.42 P-VALUE: 0.17
    HOST: laptop TIMESTAMP: 11/26/2019 11:13:23 AM TRANSFER: 1400 ALERT: 0 SCORE: 201.15 P-VALUE: 0.22
    HOST: laptop TIMESTAMP: 11/27/2019 11:13:23 AM TRANSFER: 3000 ALERT: 1 SCORE: 1365.42 P-VALUE: 0.00
    HOST: laptop TIMESTAMP: 11/28/2019 11:13:23 AM TRANSFER: 1500 ALERT: 0 SCORE: -324.58 P-VALUE: 0.11
    HOST: laptop TIMESTAMP: 11/29/2019 11:13:23 AM TRANSFER: 1600 ALERT: 0 SCORE: -312.93 P-VALUE: 0.25
    

    输出包括三个数据点:HOSTTIMESTAMPTRANSFER。新增的是ALERTSCOREP-VALUEALERT的值不为零表示存在异常。SCORE是异常得分的数值表示;值越高表示峰值越大。P-VALUE,介于 0 和 1 之间的值,是当前点与平均点之间的距离。值接近或等于 0 是另一个表示峰值的指示。在评估模型和有效性时,结合这三个数据点可以确保真正的峰值,从而有效减少潜在的误报数量。

    您可以随意修改值,并探索基于模型训练数据集的预测如何变化。从这个点开始,一些实验区域可能包括以下内容:

    • 添加更具体的数据点,如 IP 地址

    • 在训练和测试数据中添加多样化和更多数据点

    创建一个异常检测应用

    如前所述,我们将创建的应用是一个登录异常检测器。给定一组与登录相关的属性,该应用将使用这些数据来查找异常,例如不寻常的登录时间。与其他应用一样,这并不是为了推动下一个机器学习登录异常检测产品的开发;然而,它将向您展示如何在 ML.NET 中使用异常检测。

    与前几章一样,完成的项目代码、示例数据集和项目文件可以在此处下载:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter06

    探索项目架构

    在前几章中创建的项目架构和代码的基础上,本例中的大部分更改都在模型的训练上。

    在下面的屏幕截图中,您将找到项目的 Visual Studio 解决方案资源管理器视图。解决方案的新增内容包括LoginHistoryLoginPrediction文件,我们将在本节后面进行回顾:

    图片

    sampledata.csv文件包含 10 行登录数据。您可以随意调整数据以适应您的观察或调整训练好的模型。以下是数据的一个片段:

    0,1,0,1,1,0
    0,1,0,1,1,0
    0,0,1,0,1,0
    0,0,1,0,1,0
    0,0,1,1,0,1
    1,1,0,1,1,0
    1,1,0,1,1,0
    1,0,1,0,1,0
    1,0,1,0,1,1
    1,0,1,1,0,0
    

    每一行都包含新创建的LoginHistory类中属性的值,我们将在本章后面进行回顾。

    此外,我们还添加了testdata.csv文件,其中包含额外的数据点,用于测试新训练的模型并评估。以下是testdata.csv中的数据片段:

    0,1,0,1,1,0
    0,1,0,1,1,0
    0,0,1,0,1,0
    0,0,1,0,1,0
    0,0,1,1,0,1
    1,1,0,1,1,0
    1,1,0,1,1,0
    1,0,1,0,1,0
    1,0,1,0,1,1
    1,0,1,1,0,0
    

    深入代码分析

    对于这个应用,正如前文所述,我们是在第五章“聚类模型”的基础上进行工作的。对于这次深入探讨,我们将专注于为这个应用更改的代码。

    已更改或添加的类如下:

    • Constants

    • LoginHistory

    • LoginPrediction

    • 预测器

    • 训练器

    Constants

    Constants 类已更改以将模型保存到 chapter6.mdl。以下代码块反映了这些更改:

    namespace chapter06.Common
    {
        public class Constants
        {
            public const string MODEL_FILENAME = "chapter6.mdl";
    
            public const string SAMPLE_DATA = "sampledata.csv";
    
            public const string TEST_DATA = "testdata.csv";
        }
    }
    

    LoginHistory

    LoginHistory 类是包含用于预测和训练我们的模型的数据的容器类。如前几章所述,LoadColumn 装饰器中的数字映射到 CSV 文件中的索引。每个属性映射到一个将发送到模型进行异常检测的值:

    using Microsoft.ML.Data;
    
    namespace chapter06.ML.Objects
    {
        public class LoginHistory
        {
            [LoadColumn(0)]
            public float UserID { get; set; }
    
            [LoadColumn(1)]
            public float CorporateNetwork { get; set; }
    
            [LoadColumn(2)] 
            public float HomeNetwork { get; set; }
    
            [LoadColumn(3)] 
            public float WithinWorkHours { get; set; }
    
            [LoadColumn(4)] 
            public float WorkDay { get; set; }
    
            [LoadColumn(5)] 
            public float Label { get; set; }
        }
    }
    

    LoginPrediction

    LoginPrediction 类包含映射到我们的预测输出的属性。以下 PredictedLabel 属性将保存我们的预测,而 LabelScore 属性用于评估:

    namespace chapter06.ML.Objects
    {
        public class LoginPrediction
        {
            public float Label;
    
            public float Score;
    
            public bool PredictedLabel;
        }
    }
    

    预测器

    为了处理 Login 异常检测场景,需要对此类进行一些更改:

    1. 首先,我们使用 LoginHistoryLoginPrediction 类型创建我们的预测引擎:
    var predictionEngine = MlContext.Model.CreatePredictionEngine<LoginHistory, LoginPrediction>(mlModel);     
    
    1. 然后,我们将输入文件读取到一个字符串变量中:
    var json = File.ReadAllText(inputDataFile);
    
    1. 最后,我们运行预测并输出模型运行的输出结果:
    var prediction = predictionEngine.Predict(JsonConvert.DeserializeObject<LoginHistory>(json));
    
    Console.WriteLine(
                        $"Based on input json:{System.Environment.NewLine}" +
                        $"{json}{System.Environment.NewLine}" + 
                        $"The login history is {(prediction.PredictedLabel ? "abnormal" : "normal")}, with a {prediction.Score:F2} outlier score");
    

    训练器

    Trainer 类内部,需要做出一些修改以支持使用随机 PCA 训练器的异常检测分类:

    1. 第一个更改是添加了一个 GetDataView 辅助方法,它从 LoginHistory 类中先前定义的列构建 IDataView 数据视图:
    private (IDataView DataView, IEstimator<ITransformer> Transformer) GetDataView(string fileName, bool training = true)
    {
        var trainingDataView = MlContext.Data.LoadFromTextFile<LoginHistory>(fileName, ',');
    
        if (!training)
        {
            return (trainingDataView, null);
        }
    
        IEstimator<ITransformer> dataProcessPipeline = MlContext.Transforms.Concatenate(
            FEATURES, 
            typeof(LoginHistory).ToPropertyList<LoginHistory>(nameof(LoginHistory.Label)));
    
        return (trainingDataView, dataProcessPipeline);
    }
    
    1. 然后,我们构建训练数据视图和 RandomizedPcaTrainer.Options 对象:
    var trainingDataView = GetDataView(trainingFileName);
    
    var options = new RandomizedPcaTrainer.Options
    {
        FeatureColumnName = FEATURES,
        ExampleWeightColumnName = null,
        Rank = 5,
        Oversampling = 20,
        EnsureZeroMean = true,
        Seed = 1
    };
    
    

    注意,Rank 属性必须等于或小于特征数:

    1. 然后,我们可以创建随机 PCA 训练器,将其附加到训练数据视图中,拟合我们的模型,然后保存它:
    IEstimator<ITransformer> trainer = MlContext.AnomalyDetection.Trainers.RandomizedPca(options: options);
    
    EstimatorChain<ITransformer> trainingPipeline = trainingDataView.Transformer.Append(trainer);
    
    TransformerChain<ITransformer> trainedModel = trainingPipeline.Fit(trainingDataView.DataView);
    
    MlContext.Model.Save(trainedModel, trainingDataView.DataView.Schema, ModelPath);
    
    1. 现在我们使用测试数据集评估我们刚刚训练的模型:
    var testingDataView = GetDataView(testingFileName, true);
    
    var testSetTransform = trainedModel.Transform(testingDataView.DataView);
    
    var modelMetrics = MlContext.AnomalyDetection.Evaluate(testSetTransform);
    
    1. 最后,我们输出所有分类指标。这些将在下一节中详细介绍:
    Console.WriteLine($"Area Under Curve: {modelMetrics.AreaUnderRocCurve:P2}{Environment.NewLine}" +
                      $"Detection at FP Count: {modelMetrics.DetectionRateAtFalsePositiveCount}");
    

    运行应用程序

    要运行应用程序,我们使用的流程几乎与 第三章 中 回归模型 的示例应用程序相同,只是在训练时添加了传入测试数据集:

    1. 提取数据后,我们必须通过传入新创建的 sampledata.csvtestdata.csv 文件来训练模型:
    PS chapter06\bin\Debug\netcoreapp3.0> .\chapter06.exe train ..\..\..\Data\sampledata.csv ..\..\..\Data\testdata.csv 
    Area Under Curve: 78.12%
    Detection at FP Count: 1
    
    1. 要使用此文件运行模型,只需传入一个构造的 JSON 文件(在这种情况下为 input.json)和预测输出将显示:
    PS chapter06\bin\Debug\netcoreapp3.0> .\chapter06.exe predict input.json 
    Based on input json:
    {
     "UserID": 0, "CorporateNetwork": 1, "HomeNetwork": 0, "WithinWorkHours": 1, "WorkDay": 1
    }
    The login history is normal, with a 0% score
    

    注意模型训练输出的扩展,包括两个指标数据点。我们将在本章末尾解释这些数据点的含义:

    随意修改值并探索基于模型训练的数据集,预测如何变化。从这个点开始的一些实验领域可能包括以下内容:

    • 在生产场景中添加一些额外的属性以提高预测精度,例如登录发生的小时:

    • 在训练和测试数据中增加多样性

    评估随机 PCA 模型

    如前几章所述,评估模型是整个模型构建过程中的关键部分。一个训练不良的模型只会提供不准确的预测。幸运的是,ML.NET 提供了许多流行的属性,可以根据训练时的测试集计算模型精度,以给你一个关于你的模型在生产环境中表现如何的印象。

    在 ML.NET 中,正如示例应用中提到的,有两个属性构成了AnomalyDetectionMetrics类对象。让我们深入了解AnomalyDetectionMetrics对象公开的属性:

    • ROC 曲线下的面积

    • 假阳性计数中的检测率

    在接下来的几节中,我们将分解这些值的计算方法和理想值。

    ROC 曲线下的面积

    如第三章中提到的 ROC 曲线下的面积,顾名思义,是接收者操作特征ROC)曲线下的面积。可能有人会问这样一个问题:这与评估异常检测模型有什么关系?

    这个计算出的面积等于算法(在我们的案例中是随机 PCA)随机选择一个正实例比一个负实例得分更高的概率,这两个实例都是随机选择的,以更好地评估数据。返回的数字越接近 100%是理想值,而如果它接近 0%,你很可能会有显著的假阳性。你可能还记得我们之前的应用示例得到了 78%。这意味着有 22%的假阳性概率;以下概述了一些改进模型的建议,应该会减少这个数字。

    下图直观地反映了随机猜测线和任意数据曲线。随机猜测线之间的数据曲线下的面积是 ROC 曲线数据度量下的面积:

    图片

    假阳性计数中的检测率

    在假阳性计数属性中的检测率是K个假阳性的检测率。在异常检测场景中,一个假阳性是将数据点视为异常,而实际上它并不是。这个比率是这样计算的:

    K 个假阳性的检测率 = X / Y

    在这里,X是根据异常检测示例中先前描述的分数计算出的顶级测试样本(按降序排序)。这些被认为是顶级真实正例(即更有可能是实际异常)。

    Y 被计算为测试数据中异常的总数,无论分数值如何(不是过滤看起来可疑或不可疑的点)。从理论上讲,如果训练数据中的假阳性数量很高,这个数字可能会非常高。随着你使用随机 PCA 构建生产模型,确保你的数据尽可能接近生产环境,以避免过度拟合或欠拟合到异常。

    摘要

    在本章的整个过程中,我们讨论了 ML.NET 通过随机 PCA 算法提供的异常检测支持。我们还创建并训练了我们的第一个异常检测应用程序,使用随机 PCA 算法来预测异常登录。除此之外,我们还创建了一个时间序列应用程序,观察网络流量并寻找传输数据量的峰值。最后,我们还探讨了如何评估异常检测模型以及 ML.NET 公开的各种属性,以实现异常检测模型的适当评估。

    在下一章中,我们将深入探讨 ML.NET 中的矩阵分解,以创建一个音乐偏好预测器。

第七章:矩阵分解模型

在异常检测模型之后,现在是时候深入研究矩阵分解模型了。矩阵分解是 ML.NET 中较新的添加之一,具有相同的转换名称。在本章中,我们将深入研究矩阵分解,以及最适合利用矩阵分解的各种应用。此外,我们将构建一个新的示例应用程序,根据样本训练数据预测音乐推荐。最后,我们将探讨如何使用 ML.NET 公开的特性来评估矩阵分解模型。

在本章中,我们将涵盖以下主题:

  • 破解矩阵分解

  • 创建矩阵分解应用程序

  • 评估矩阵分解模型

破解矩阵分解

如第一章中所述,《开始使用机器学习和 ML.NET》,矩阵分解,按定义,是一种无监督学习算法。这意味着该算法将在数据上训练并构建用户评分的模式矩阵,在预测调用期间,将尝试根据提供的数据找到类似评分。在本节中,我们将深入研究矩阵分解的应用案例,并查看 ML.NET 中的矩阵分解训练器。

矩阵分解的应用案例

正如你可能开始意识到的,矩阵分解在数据可用但基于先前未选择的数据提出其他匹配建议的许多应用中都有用。无需进行手动检查,矩阵分解算法在未选择的数据上训练,并使用键值对组合确定模式。ML.NET 提供了各种矩阵分解值,可以在你的应用程序中以编程方式查看。我们将在本章后面回顾这些值,以确保推荐不是假阳性。

一些最适合矩阵分解的应用包括:

  • 音乐推荐

  • 产品推荐

  • 电影推荐

  • 书籍推荐

实际上,任何可以追溯到单个用户的数据,并且随着更多数据的输入而构建的情况都可以利用矩阵分解。这个问题被称为冷启动问题。以一个旨在帮助你发现新乐队听的音乐平台为例。当你第一次到达网站并创建个人资料时,没有先前数据可用。作为最终用户,你必须告诉系统你喜欢什么,不喜欢什么。由于算法的性质,矩阵分解比我们在前几章中探讨的直接回归或二元分类算法更适合这个应用。

深入了解矩阵分解训练器

到目前为止,在 ML.NET 中找到的唯一传统训练器是矩阵分解训练器。矩阵分解训练器需要值的归一化和缓存。此外,如果您是从零开始创建项目,则要利用 ML.NET 中的矩阵分解,需要Microsoft.ML.Recommender NuGet 包。GitHub 仓库中包含的示例包括此包。

与其他算法类似,需要归一化,但矩阵分解是独特的。我们之前看到的二进制分类或回归算法,有多种值可以进行归一化。在矩阵分解中,只有三个值:LabelRowColumn值。输出由两个属性组成:ScoreLabelScore值是Float类型,非负且无界。

应注意,在 2018 年 7 月的 ML.NET 0.3 更新中,添加了字段感知因子机。然而,此类训练器仅提供二进制推荐(例如喜欢或不喜欢),而与支持任何范围的浮点值的矩阵分解不同。这提供了相当大的使用灵活性,例如获得更细粒度的预测。例如,如果矩阵分解推荐在 0 到 100 的范围内返回 30,则推荐引擎很可能返回一个负面的推荐。而仅仅是一个二进制响应,应用程序以及最终用户都没有看到推荐的强度。

我们将在下一节通过提供音乐推荐来演示此训练器。

创建矩阵分解应用程序

如前所述,我们将创建的应用程序是用于音乐预测。给定 UserID、MusicID 和评分,算法将使用这些数据创建推荐。与其他应用程序一样,这并不是要推动下一个类似 Spotify 的机器学习产品;然而,它将向您展示如何在 ML.NET 中使用矩阵分解。

与前几章一样,完整的项目代码、示例数据集和项目文件可以在此处下载:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter07.

探索项目架构

在前几章中创建的项目架构和代码的基础上,大部分更改都在模型的训练中,因为矩阵分解需要从我们之前章节中回顾的内容进行相当大的范式转变。

在下面的屏幕截图中,您将找到项目的 Visual Studio 解决方案资源管理器视图。解决方案的新增内容包括MusicRatingMusicPrediction文件,我们将在本节稍后进行回顾:

图片

sampledata.csv 文件包含 10 行随机的音乐评分。您可以随意调整数据以适应您的观察,或者调整训练模型。以下是数据的一个片段:

1,1000,4
1,1001,3.5
1,1002,1
1,1003,2
2,1000,1.5
2,1001,2
2,1002,4
2,1003,4
3,1000,1
3,1001,3

每一行都包含我们将在此章节稍后审查的 MusicRating 类中属性的价值。

此外,我们还添加了 testdata.csv 文件,其中包含额外的数据点以测试新训练的模型并评估。以下是 testdata.csv 内的数据片段:

1,1000,4
1,1001,3.5
2,1002,1
2,1003,2
3,1000,1.5
3,1001,2
4,1002,4
4,1003,4

深入代码分析

对于这个应用,正如前一个章节所述,我们是在第六章完成的工作基础上构建的,异常检测模型。对于这次深入研究,我们将专注于这个应用中更改的代码。

修改或添加的类如下:

  • MusicRating

  • MusicPrediction

  • Predictor

  • Trainer

  • Constants

MusicRating

MusicRating 类是包含预测和训练模型所需数据的容器类。如前几章所述,LoadColumn 装饰器中的数字映射到 CSV 文件中的索引。正如前面章节所述,ML.NET 中的矩阵分解需要使用归一化,如下面的代码块所示:

using Microsoft.ML.Data;

namespace chapter07.ML.Objects
{
    public class MusicRating
    {
        [LoadColumn(0)]
        public float UserID { get; set; }

        [LoadColumn(1)]
        public float MovieID { get; set; }

        [LoadColumn(2)]
        public float Label { get; set; }
    }
}

MusicPrediction

MusicPrediction 类包含映射到预测输出的属性。Score 包含预测准确的概率。我们将在本节稍后审查这些值,但现在它们可以在以下代码块中看到:

namespace chapter07.ML.Objects
{
    public class MusicPrediction
    {
        public float Label { get; set; }

        public float Score { get; set; }
    }
}

Predictor

在这个类中有一两个更改来处理音乐预测场景,如下所示:

  1. 首先,我们使用 MusicRatingMusicPrediction 类型创建我们的预测引擎,如下所示:
var predictionEngine = MlContext.Model.CreatePredictionEngine<MusicRating, MusicPrediction>(mlModel);
  1. 接下来,我们将输入文件读取到字符串对象中,如下所示:
var json = File.ReadAllText(inputDataFile);
  1. 接下来,我们将字符串反序列化为 MusicRating 类型的对象,如下所示:
var rating = JsonConvert.DeserializeObject<MusicRating>(json);
    1. 最后,我们需要运行预测,然后输出模型运行的结果,如下所示:
    var prediction = predictionEngine.Predict(rating);
    
    Console.WriteLine(
        $"Based on input:{System.Environment.NewLine}" +
        $"Label: {rating.Label} | MusicID: {rating.MusicID} | UserID: {rating.UserID}{System.Environment.NewLine}" +
        $"The music is {(prediction.Score > Constants.SCORE_THRESHOLD ? "recommended" : "not recommended")}");
    

    由于转换只返回三个元素的向量,原始行数据被输出以提供上下文。

    Trainer

    Trainer 类中,需要做出一些修改以支持矩阵分解。由于只有三个输入的性质,在很多方面都需要简化:

    1. 第一个增加的是两个用于变量编码的常量变量,如下面的代码块所示:
    private const string UserIDEncoding = "UserIDEncoding";
    private const string MovieIDEncoding = "MovieIDEncoding";
    
    1. 然后我们构建 MatrixFactorizationTrainer 选项。RowColumn 属性设置为之前定义的列名。Quiet 标志在每次迭代时显示额外的模型构建信息,如下面的代码块所示:
    var options = new MatrixFactorizationTrainer.Options
    {
        MatrixColumnIndexColumnName = UserIDEncoding,
        MatrixRowIndexColumnName = MovieIDEncoding,
        LabelColumnName = "Label",
        NumberOfIterations = 20,
        ApproximationRank = 10,
        Quiet = false
    };
    
    1. 我们可以创建矩阵分解训练器,如下所示:
    var trainingPipeline = trainingDataView.Transformer.Append(MlContext.Recommendation().Trainers.MatrixFactorization(options));
    
    1. 现在,我们将模型拟合到训练数据中并保存模型,如下所示:
    ITransformer trainedModel = trainingPipeLine.Fit(trainingDataView.DataView);
    
    MlContext.Model.Save(trainedModel, trainingDataView.DataView.Schema, ModelPath);
    
    Console.WriteLine($"Model saved to {ModelPath}{Environment.NewLine}");
    
    1. 最后,我们加载测试数据并将数据传递给矩阵分解评估器,如下所示:
    var testingDataView = GetDataView(testingFileName, true);
    
    var testSetTransform = trainedModel.Transform(testingDataView.DataView);
    
    var modelMetrics = MlContext.Recommendation().Evaluate(testSetTransform);
    
    Console.WriteLine($"matrix factorization Evaluation:{Environment.NewLine}{Environment.NewLine}" +
                      $"Loss Function: {modelMetrics.LossFunction}{Environment.NewLine}" +
                      $"Mean Absolute Error: {modelMetrics.MeanAbsoluteError}{Environment.NewLine}" +
                      $"Mean Squared Error: {modelMetrics.MeanSquaredError}{Environment.NewLine}" +
                      $"R Squared: {modelMetrics.RSquared}{Environment.NewLine}" +
                      $"Root Mean Squared Error: {modelMetrics.RootMeanSquaredError}");
    

    常数类

    此外,由于训练过程仅需要训练数据,因此需要对Program类进行一些修改,如下所示:

    namespace chapter07.Common
    {
        public class Constants
        {
            public const string MODEL_FILENAME = "chapter7.mdl";
    
            public const float SCORE_THRESHOLD = 3.0f;
        }
    }
    

    运行应用程序

    要运行应用程序,过程几乎与第六章的示例应用程序相同,如下所示:

    1. 准备好数据后,我们必须通过传递新创建的sampledata.csv文件来训练模型,如下所示:
    PS Debug\netcoreapp3.0> .\chapter07.exe train ..\..\..\Data\sampledata.csv ..\..\..\Data\testdata.csv
    iter tr_rmse obj
       0 2.4172 9.6129e+01
       1 1.9634 6.6078e+01
       2 1.5140 4.2233e+01
       3 1.3417 3.5027e+01
       4 1.2860 3.2934e+01
       5 1.1818 2.9107e+01
       6 1.1414 2.7737e+01
       7 1.0669 2.4966e+01
       8 0.9819 2.2615e+01
       9 0.9055 2.0387e+01
      10 0.8656 1.9472e+01
      11 0.7534 1.6725e+01
      12 0.6862 1.5413e+01
      13 0.6240 1.4311e+01
      14 0.5621 1.3231e+01
      15 0.5241 1.2795e+01
      16 0.4863 1.2281e+01
      17 0.4571 1.1938e+01
    
      18 0.4209 1.1532e+01
      19 0.3975 1.1227e+01
    
    Model saved to Debug\netcoreapp3.0\chapter7.mdl
    
    1. 要使用此文件运行模型,只需将前面提到的testdata.csv文件传递给新构建的应用程序,预测输出将显示以下内容:
    matrix factorization Evaluation:
    
    Loss Function: 0.140
    Mean Absolute Error: 0.279
    Mean Squared Error: 0.140
    R Squared: 0.922
    Root Mean Squared Error: 0.375
    

    在运行预测之前,在记事本中创建一个包含以下文本的 JSON 文件:

    { "UserID": 10, "MusicID": 4, "Label": 3 }
    

    然后将文件保存到你的输出文件夹。

    1. 然后,运行预测,如下所示:
    PS Debug\netcoreapp3.0> .\chapter07.exe predict input.json
    Based on input:
    Label: 3 | MusicID: 4 | UserID: 10
    The music is not recommended
    

    随意修改这些值,并查看基于模型训练的数据集,预测如何变化。从这个点开始,一些实验的领域可能包括:

    • 修改Trainer类深入探讨中提到的超参数。

    • 向训练和测试数据添加多样化和更多数据点。

    评估矩阵分解模型

    如前几章所述,评估模型是整个模型构建过程中的关键部分。一个训练不良的模型只会提供不准确的预测。幸运的是,ML.NET 在训练时提供了许多流行的属性来计算模型准确度,基于测试集来给你一个关于你的模型在生产环境中表现如何的直观印象。

    如前所述,在 ML.NET 中评估矩阵分解模型时,有五个属性构成了RegressionMetrics类对象。让我们深入了解RegressionMetrics对象中公开的属性:

    • 损失函数

    • 均方误差MSE

    • 平均绝对误差MAE

    • R 平方

    • 均方根误差RMSE

    在接下来的章节中,我们将分解这些值的计算方法,并详细说明理想值。

    损失函数

    此属性使用在矩阵分解训练器初始化时设置的损失函数。在我们的矩阵分解示例应用程序中,我们使用了默认构造函数,它默认为SquaredLossRegression类。

    ML.NET 提供的其他回归损失函数包括:

    • 平方损失单类

    • 平方损失回归

    这个属性的目的是在评估模型时提供一些灵活性,与其他四个属性相比,这四个属性使用固定的算法进行评估。

    均方误差(MSE)

    均方误差(MSE)定义为误差平方的平均值。简单来说,就是查看以下截图所示的图表:

    图片

    这些点代表我们模型的数据点,而蓝色线是预测线。红色点与预测线之间的距离是误差。对于均方误差(MSE),该值是基于这些点及其到线的距离计算的。从这个值中,计算平均值。对于 MSE,值越小,拟合越好,使用你的模型得到的预测将越准确。

    MSE 最适合在异常值对预测输出至关重要时评估模型。

    MAE

    MAE 与 MSE 相似,关键区别在于它是对点与预测线之间的距离求和,而不是计算平均值。需要注意的是,MAE 在计算总和时不考虑方向。例如,如果你有两个与线等距离的数据点,一个在上面,一个在下面,实际上这将通过正负值相互抵消。在机器学习中,这被称为平均偏差误差MBE)。然而,ML.NET 在撰写本文时并未提供作为RegressionMetrics类的一部分。

    MAE 最适合在将异常值视为简单异常时评估模型,并且不应计入评估模型性能。

    R-squared

    R-squared,也称为确定系数,是另一种表示预测与测试集比较效果的方法。R-squared 是通过取每个预测值与其对应实际值之间的差值,平方该差值,然后对每对点的平方和进行求和来计算的。

    R-squared 的值通常在 0 到 1 之间,表示为浮点数。当拟合模型被评估为比平均拟合更差时,可能会出现负值。然而,低数值并不总是反映模型不好。基于预测人类行为的预测,如我们在本章中看到的,通常发现其值低于 50%。

    相反,高值并不一定是模型性能的可靠指标,因为这可能是模型过拟合的迹象。这种情况发生在向模型提供大量特征时,与我们在第二章的“创建第一个 ML.NET 应用程序”部分中构建的模型相比,模型变得更加复杂。在“设置 ML.NET 环境”部分中,训练集和测试集的多样性不足。例如,如果所有员工的价值大致相同,并且测试集保留组由相同范围的价值组成,这将被视为过拟合。

    RMSE

    RMSE 可以说是最容易理解的性质,鉴于前面的方法。以下截图显示了以下截图中的图:

    图片

    在测试模型的情况下,正如我们之前使用保留集所做的,红色点代表测试集的实际值,而蓝色点代表预测值。X 所表示的是预测值和实际值之间的距离。RMSE 简单地取所有这些距离的平均值,然后平方该值,最后取平方根。

    低于 180 的值通常被认为是一个好的模型。

    摘要

    在本章的整个过程中,我们深入探讨了 ML.NET 的矩阵分解支持。我们还创建并训练了我们的第一个矩阵分解应用程序,用于预测音乐推荐。最后,我们还深入了解了如何评估矩阵分解模型,并查看了 ML.NET 提供的各种属性,以实现矩阵分解模型的正确评估。

    随着本章的结束,我们也完成了对 ML.NET 提供的各种模型的初步调查。在下一章中,我们将创建完整的应用程序,基于前几章所获得的知识,第一个将是一个完整的 .NET Core 应用程序,提供股票预测。

第三部分:ML.NET 的实战集成

本节深入探讨了使用在第二部分,“ML.NET 模型”中获取的知识,来创建.NET Core 控制台应用程序、Web 应用程序和 Windows 10 桌面应用程序的完整应用。

本节包含以下章节:

  • 第八章,“使用 ML.NET 与.NET Core 和预测”

  • 第九章,“使用 ML.NET 与 ASP.NET Core”

  • 第十章,“使用 ML.NET 与 UWP”

第八章:在 .NET Core 和预测中使用 ML.NET

现在我们已经深入了解了 ML.NET 提供的各种算法组,接下来我们将开始探索在接下来的几章中将 ML.NET 集成到生产应用程序中。在本章中,我们将深入探讨一个基于前几章定义的结构构建的 .NET Core 控制台应用程序,重点关注加固和错误处理。我们将构建的应用程序使用预测来根据一系列趋势预测股价。到本章结束时,您应该能够熟练地设计和编码一个具有 ML.NET 的生产级 .NET Core 应用程序。

在本章中,我们将涵盖以下主题:

  • 分析 .NET Core 应用程序架构

  • 创建预测应用程序

  • 探索额外的生产应用程序增强功能

分析 .NET Core 应用程序架构

如 第一章 中所述,开始使用机器学习和 ML.NET,.NET Core 3.x 由于 3.0 版本中的优化,是使用 ML.NET 的首选平台。此外,.NET Core 提供了一个统一的编码框架,以针对 Linux、macOS 和 Windows,如下面的图所示:

图片

.NET Core 架构

自 2016 年成立以来,.NET Core 的基本目标是为用户提供快速更新和与(之前仅限 Windows 的)Microsoft .NET Framework 的功能一致性。随着时间的推移和版本的更新,通过简单地添加缺失的 API、使用额外的 NuGet 包,差距已经缩小。其中一个例子是 Microsoft.Windows.Compatibility,它提供了 20,000 个在 Core 框架中找不到的 API,包括注册表访问、绘图和 Windows 权限模型访问。这种方法保持了框架的轻量级和跨平台性,但确实引入了一些设计模式来帮助您开发特定平台的应用程序。

以一个使用 ML.NET 提供入侵检测系统(IDS)的 Windows 桌面应用程序为例。一个简单的方法是将所有代码都写在一个 .NET Core Windows Presentation FoundationWPF)应用程序中。然而,这将使您仅限于 Windows 而无法进行重大重构。更好的方法是创建一个包含所有平台无关代码的 .NET Core 类库,然后创建抽象类或接口,在您的平台应用程序中实现特定平台的代码。

.NET Core 目标

如前所述,.NET Core 提供了一个单一框架来针对 Windows、macOS 和 Linux。然而,这不仅仅适用于我们在这本书中使用的控制台应用程序。.NET Core 3 最近的工作提供了将现有的.NET Framework WPF 和 Windows Forms 应用程序迁移到.NET Core 3 的能力,从而使得依赖于可能已经存在多年的框架的应用程序能够使用最新的.NET Core 进步。此外,之前使用 ASP.NET 的 Web 应用程序也可以迁移到 ASP.NET Core(目前 ASP.NET WebForms 没有迁移路径)。

.NET Core 的目标之一是能够使用--self-contained标志进行编译。这个标志编译你的应用程序或库,然后将所有必要的.NET Core 框架文件捆绑在一起。这允许你在安装时无需.NET 先决条件即可部署你的应用程序。这确实会使你的整体构建输出更大,但在客户场景中,大约 100MB 的增加远远超过了先决条件的部署障碍。

.NET Core 的未来

你可能会想知道.NET Framework、Mono 和.NET Core 的未来是什么。幸运的是,在撰写本文时,微软已经确认所有现有框架都将迁移到一个单一的框架,这个框架简单地被称为.NET 5。在此之前,在决定使用哪个框架时,某些权衡是不可避免的。因此,将每个框架的优点结合起来并首次实现统一,将完全消除这些权衡。例如,Mono 的即时编译AOT)或 Xamarin 的跨平台 UI 支持,这些都可以根据已发布的信息在现有的基于.NET Core 3.x 的应用程序中使用。

预计.NET 5 的预览版将在 2020 年上半年发布,正式版将在 2020 年 11 月发布。

创建股价估算应用程序

如前所述,我们将要创建的应用程序是一个股价估算器。给定一系列跨越几天、几周或几年的股价,预测算法将内部识别趋势模式。与之前的章节不同,该应用程序将被设计成可以插入到生产流程中。

与前几章一样,完成的项目代码、示例数据集和项目文件可以从以下链接下载:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter08

探索项目架构

在前几章中创建的项目架构和代码的基础上,本章将要探索的架构将进一步增强架构,使其更加结构化,从而更适合最终用户使用。

如同前几章中的一些章节,需要额外的 NuGet 包——Microsoft.ML.TimeSeries——来利用 ML.NET 中的预测功能。GitHub 上包含的示例和本章的深入探讨中都使用了 1.3.1 版本。

在下面的屏幕截图中,您将找到项目的 Visual Studio 解决方案资源管理器视图。为了便于我们针对的目标生产用例,解决方案中添加了几个新功能。我们将在本章后面详细审查解决方案屏幕截图中所显示的每个新文件:

图片

sampledata.csv文件包含 24 行股票价格。请随意调整数据以适应您的观察或调整训练模型。以下是数据的片段:

33
34
301
33
44
299
40
50
400
60
76
500

这些每一行都包含我们将填充到StockPrices类对象中的股票价格值,我们将在本章后面进行审查。

此外,我们还添加了包含用于测试新训练模型并评估它的额外数据点的testdata.csv文件。以下是testdata.csv内部数据的片段:

10
25
444
9
11
333
4
3
500

深入代码

如前节所述,我们是在前几章完成的工作基础上构建的。然而,对于本章,我们将更改每个文件以支持生产用例。对于从前几章更改的每个文件,我们将审查所做的更改及其背后的原因。

以下是被更改或添加的类和枚举:

  • ProgramActions

  • CommandLineParser

  • BaseML

  • StockPrediction

  • StockPrices

  • Predictor

  • Trainer

  • ProgramArguments

  • Program

程序动作枚举

以下ProgramActions枚举已被添加到解决方案中,以方便使用强类型和结构化的路径来处理程序执行的各种操作:

namespace chapter08.Enums
{
    public enum ProgramActions
    {
        TRAINING,
        PREDICT
    }
}

在本应用的情况下,我们只有两个动作——“训练”和“预测”。然而,如前几章所示,您可能还有一个特征提取步骤或可能提供一个评估步骤。这种设计模式既提供了灵活性,又消除了本章开头提到的“魔法字符串”问题。

CommandLineParser

CommandLineParser类提供了一个程序无关的解析器,用于处理命令行参数。在前几章中,我们手动解析索引并将这些值映射到参数上。另一方面,这种方法创建了一个灵活、易于维护且结构化的响应对象,它将参数直接映射到属性。现在让我们深入了解这个类:

  1. 首先,我们定义函数原型:
public static T ParseArguments<T>(string[] args) 

使用泛型(即T)创建了一种灵活的方法,使此方法不受限于仅此应用。

  1. 接下来,我们测试null参数:
if (args == null)
{
    throw new ArgumentNullException(nameof(args));
}
  1. 然后,我们测试空参数,并告知用户将使用默认值而不是失败,如前几章所述:
if (args.Length == 0)
{
    Console.WriteLine("No arguments passed in - using defaults");

    return Activator.CreateInstance<T>();
}
  1. 在执行空和空检查之后,我们执行多个检查,因为所有参数都是成对的:
if (args.Length % 2 != 0)
{
    throw new ArgumentException($"Arguments must be in pairs, there were {args.Length} passed in");
}
  1. 继续进行,然后我们使用 Activator.CreateInstance 方法创建 T 类型的对象:
var argumentObject = Activator.CreateInstance<T>();

确保在创建类对象时,构造函数没有参数,因为这个调用如果没有无参构造函数将会抛出异常。如果你创建了一个带有构造函数参数的对象而没有无参构造函数,请使用 Activator.CreateInstance 的重载版本并传递所需的参数。

  1. 接下来,我们使用反射来获取 T 类型的所有属性:
var properties = argumentObject.GetType().GetProperties();
  1. 现在我们已经创建了通用对象及其属性,然后我们遍历每个参数键/值对,并在对象中设置属性:
for (var x = 0; x < args.Length; x += 2)
{
    var property = properties.FirstOrDefault(a => a.Name.Equals(args[x], StringComparison.CurrentCultureIgnoreCase));

    if (property == null)
    {
        Console.WriteLine($"{args[x]} is an invalid argument");

        continue;
    }

    if (property.PropertyType.IsEnum)
    {
        property.SetValue(argumentObject, Enum.Parse(property.PropertyType, args[x + 1], true));
    }
    else
    {
        property.SetValue(argumentObject, args[x + 1]);
    }
}

注意 IsEnum 函数的特殊情况,用于处理之前提到的 ProgramActions 枚举。由于字符串值不能自动转换为枚举,我们需要使用 Enum.Parse 方法专门处理字符串到枚举的转换。按照目前的写法,如果添加更多的枚举到 T 类型,枚举处理器是通用的。

BaseML

为此应用创建的 BaseML 类已经精简,只需实例化 MLContext 对象:

using Microsoft.ML;

namespace chapter08.ML.Base
{
    public class BaseML
    {
        protected readonly MLContext MlContext;

        protected BaseML()
        {
            MlContext = new MLContext(2020);
        }
    }
}

StockPrediction

StockPrediction 类是我们预测值的容器,如这里定义的:

namespace chapter08.ML.Objects
{
    public class StockPrediction
    {
        public float[] StockForecast { get; set; }

        public float[] LowerBound { get; set; }

        public float[] UpperBound { get; set; }
    }
}

StockForecast 属性将保存基于模型训练和提交给预测引擎的预测股票值。LowerBoundUpperBound 值分别保存最低和最高估计值。

StockPrices

StockPrices 类包含我们的单个浮点值,该值持有股票价格。为了在填充值时保持代码的整洁,添加了一个接受股票价格值的构造函数:

using Microsoft.ML.Data;

namespace chapter08.ML.Objects
{
    public class StockPrices
    {
        [LoadColumn(0)]
        public float StockPrice;

        public StockPrices(float stockPrice)
        {
            StockPrice = stockPrice;
        }
    }
}

Predictor

与前几章相比,Predictor 类已经精简并适应了预测功能:

  1. 首先,调整 Predict 方法以接受新定义的 ProgramArguments 类对象:
public void Predict(ProgramArguments arguments)   
  1. 接下来,我们更新模型 file.Exists 检查以利用 arguments 对象:
if (!File.Exists(arguments.ModelFileName))
{
    Console.WriteLine($"Failed to find model at {arguments.ModelFileName}");

    return;
}
  1. 类似地,我们还更新了预测文件名引用,以利用 arguments 对象:
if (!File.Exists(arguments.PredictionFileName))
{
    Console.WriteLine($"Failed to find input data at {arguments.PredictionFileName}");

    return;
}
  1. 接下来,我们还修改了模型打开调用以利用 arguments 对象:
using (var stream = new FileStream(Path.Combine(AppContext.BaseDirectory, arguments.ModelFileName), FileMode.Open, FileAccess.Read, FileShare.Read))
{
    mlModel = MlContext.Model.Load(stream, out _);
}
  1. 然后,我们使用 StockPricesStockPrediction 类型创建时间序列引擎对象:
var predictionEngine = mlModel.CreateTimeSeriesEngine<StockPrices, StockPrediction>(MlContext);
  1. 接下来,我们将股票价格预测文件读入一个字符串数组:
var stockPrices = File.ReadAllLines(arguments.PredictionFileName);
  1. 最后,我们遍历每个输入,调用预测引擎,并显示估计值:
foreach (var stockPrice in stockPrices)
{
    var prediction = predictionEngine.Predict(new StockPrices(Convert.ToSingle(stockPrice)));

    Console.WriteLine($"Given a stock price of ${stockPrice}, the next 5 values are predicted to be: " +
                      $"{string.Join(", ", prediction.StockForecast.Select(a => $"${Math.Round(a)}"))}");
}

Trainer

Trainer 类,类似于 Predictor 类,对 ML.NET 预测算法进行了精简和修改:

  1. 首先,更新函数原型以接受 ProgramArguments 对象:
public void Train(ProgramArguments arguments)     
  1. 接下来,我们更新训练文件检查以利用 argument 对象:
if (!File.Exists(arguments.TrainingFileName))
{
    Console.WriteLine($"Failed to find training data file ({arguments.TrainingFileName})");

    return;
}
  1. 同样地,我们随后更新测试文件检查以利用 argument 对象:
if (!File.Exists(arguments.TestingFileName))
{
    Console.WriteLine($"Failed to find test data file ({arguments.TestingFileName})");

    return;
}
  1. 接下来,我们从训练文件中加载 StockPrices 值:
var dataView = MlContext.Data.LoadFromTextFile<StockPrices>(arguments.TrainingFileName);
  1. 然后,我们创建 Forecasting 对象并利用 C# 的 nameof 特性来避免魔法字符串引用:
var model = MlContext.Forecasting.ForecastBySsa(
    outputColumnName: nameof(StockPrediction.StockForecast),
    inputColumnName: nameof(StockPrices.StockPrice), 
    windowSize: 7, 
    seriesLength: 30, 
    trainSize: 24, 
    horizon: 5,
    confidenceLevel: 0.95f,
    confidenceLowerBoundColumn: nameof(StockPrediction.LowerBound),
    confidenceUpperBoundColumn: nameof(StockPrediction.UpperBound));

输入和输出列名引用与我们之前在章节中看到的一样。windowSize 属性是训练集中数据点之间的持续时间。对于这个应用程序,我们使用 7 来表示一周的持续时间。seriesLength 属性表示数据集的总持续时间。horizon 属性表示在运行模型时应该计算多少个预测值。在我们的例子中,我们请求 5 个预测值。

  1. 最后,我们使用训练数据转换模型,调用 CreateTimeSeriesEngine 方法,并将模型写入磁盘:
var transformer = model.Fit(dataView);

var forecastEngine = transformer.CreateTimeSeriesEngine<StockPrices, StockPrediction>(MlContext);

forecastEngine.CheckPoint(MlContext, arguments.ModelFileName);

Console.WriteLine($"Wrote model to {arguments.ModelFileName}");

ProgramArguments

如本节前面所述,这个新类提供了应用程序中参数到属性的一对一映射:

  1. 首先,我们定义直接映射到命令行参数的属性:
public ProgramActions Action { get; set; }

public string TrainingFileName { get; set; }

public string TestingFileName { get; set; }

public string PredictionFileName { get; set; }

public string ModelFileName { get; set; }
  1. 最后,我们为属性填充默认值:
public ProgramArguments()
{
    ModelFileName = "chapter8.mdl";

    PredictionFileName = @"..\..\..\Data\predict.csv";

    TrainingFileName = @"..\..\..\Data\sampledata.csv";

    TestingFileName = @"..\..\..\Data\testdata.csv";
}

与前几章不同,如果任何属性没有按预期设置,程序将失败。这对于开发者体验来说是可以接受的;然而,在现实世界中,最终用户更有可能尝试在没有任何参数的情况下运行应用程序。

Program

Program 类中,代码已被简化以利用本章前面讨论的新 CommandLineParser 类。使用 CommandLineParser 类,所有操作都已切换到使用强类型枚举:

  1. 首先,虽然相对简单,清除屏幕上的任何先前运行数据是一个改进的用户体验:
Console.Clear();
  1. 我们随后使用我们新的 CommandLineParser 类及其相关的 ParseArguments 方法来创建一个强类型参数对象:
var arguments = CommandLineParser.ParseArguments<ProgramArguments>(args);
  1. 然后,我们可以使用简化和强类型的 switch case 来处理我们的两个操作:
switch (arguments.Action)
{
    case ProgramActions.PREDICT:
        new Predictor().Predict(arguments);
        break;
    case ProgramActions.TRAINING:
        new Trainer().Train(arguments);
        break;
    default:
        Console.WriteLine($"Unhandled action {arguments.Action}");
        break;
}

运行应用程序

要运行应用程序,过程几乎与第三章中示例应用程序的“回归模型”相同,只是在训练时传递测试数据集:

  1. 在没有参数的情况下运行应用程序以训练模型,我们使用以下步骤:
PS chapter08\bin\Debug\netcoreapp3.0> .\chapter08.exe
No arguments passed in - using defaults
Wrote model to chapter8.mdl
  1. 基于包含的预测数据运行应用程序以进行预测,我们使用以下步骤:
PS chapter08\bin\Debug\netcoreapp3.0> .\chapter08.exe action predict
Given a stock price of $101, the next 5 values are predicted to be: $128, $925, $140, $145, $1057
Given a stock price of $102, the next 5 values are predicted to be: $924, $138, $136, $1057, $158
Given a stock price of $300, the next 5 values are predicted to be: $136, $134, $852, $156, $150
Given a stock price of $40, the next 5 values are predicted to be: $133, $795, $122, $149, $864
Given a stock price of $30, the next 5 values are predicted to be: $767, $111, $114, $837, $122
Given a stock price of $400, the next 5 values are predicted to be: $105, $102, $676, $116, $108
Given a stock price of $55, the next 5 values are predicted to be: $97, $594, $91, $103, $645
Given a stock price of $69, the next 5 values are predicted to be: $557, $81, $87, $605, $90
Given a stock price of $430, the next 5 values are predicted to be: $76, $78, $515, $84, $85

随意修改值并查看基于模型训练数据集的预测如何变化。从这个点开始,一些实验性的区域可能包括以下内容:

  • 调整 Trainer 类中审查的超参数,如 windowSizeseriesLengthhorizon 属性,以查看精度如何受到影响。

  • 添加显著更多的数据点——这可能需要使用你关注的股票的数据源。

探索额外的生产应用程序增强

现在我们已经完成了深入探讨,还有一些额外的元素可能可以进一步增强应用程序。这里讨论了一些想法。

日志记录

随着应用程序复杂性的增加,强烈建议使用 NLog(nlog-project.org/)或类似的开源项目进行日志记录。这将允许你以不同的级别记录到文件、控制台或第三方日志解决方案,如 Loggly。例如,如果你将此应用程序部署给客户,将错误级别至少分解为 Debug、Warning 和 Error,在远程调试问题时将非常有帮助。

进一步利用反射

如前所述,为了创建灵活性和适应性,我们使用了Reflection来解析命令行参数。你可以更进一步,将Program类中的 switch case 语句/标准流程替换为完全基于反射的方法,这意味着对于应用程序中定义的每个操作,它都可以继承自一个抽象的BaseAction类,并在运行时根据参数调用适当的类。对于每个新的操作,只需向ProgramActions枚举中添加一个新条目,然后定义一个具有该枚举的类即可。

利用数据库

在实际场景中,用于运行预测的数据很可能来自数据库。这个数据库,无论是 Postgres、SQL Server 还是 SQLite 数据库(仅举几个例子),可以使用 Microsoft 的 Entity Framework Core 或 ML.NET 内置的数据库加载方法CreateDatabaseLoader访问。这个加载器类似于我们如何从可枚举或文本文件中加载数据,只是增加了注入 SQL 查询的额外步骤。

在生产场景中,鉴于 Entity Framework Core 的性能和能够使用 LINQ 而不是 ML.NET 实现(在撰写本文时)的能力,如果使用了数据库源,我建议使用 Entity Framework。

摘要

在本章中,我们深入探讨了如何使用前几章的工作作为基础,构建一个生产就绪的.NET Core 应用程序架构。我们还使用 ML.NET 中的预测算法创建了一个全新的股票价格估算器。最后,我们讨论了一些增强.NET Core 应用程序(以及一般的生产应用程序)的方法。

在下一章中,我们将深入探讨使用 ML.NET 的二进制分类和 ASP.NET Core 框架创建一个生产级文件分类 Web 应用程序。

第九章:在 ASP.NET Core 中使用 ML.NET

现在我们已经了解了如何创建一个生产级别的 .NET Core 控制台应用程序,在本章中,我们将深入探讨创建一个功能齐全的 ASP.NET Core Blazor 网络应用程序。这个应用程序将利用 ML.NET 二元分类模型对 Windows 可执行文件(可移植可执行文件PE)文件)进行文件分类,以确定文件本身是干净的还是恶意的。此外,我们将探索将我们的应用程序代码分解为基于组件的架构,使用 .NET Core 库在 web 应用程序和将训练我们的模型的控制台应用程序之间共享。到本章结束时,你应该能够熟练地设计和编码带有 ML.NET 的生产级别 ASP.NET Core Blazor 网络应用程序。

在本章中,我们将涵盖以下主题:

  • 拆解 ASP.NET Core

  • 创建文件分类网络应用程序

  • 探索额外的生产应用增强功能

拆解 ASP.NET Core

基于第八章中讨论的相同 .NET Core 技术,在 .NET Core 和预测中使用 ML.NET,ASP.NET Core 添加了一个强大的网络框架。这个网络框架包括一个强大的渲染引擎 Razor,以及支持可扩展的 表示状态转移REST)服务。本章的示例将使用这项技术来创建我们的文件分类前端。在接下来的两个部分中,我们将深入研究 ASP.NET Core 架构并讨论微软的新网络框架 Blazor。

理解 ASP.NET Core 架构

从高层次来看,ASP.NET Core 建立在 .NET Core 之上,提供了一个功能齐全的网络框架。与 .NET Core 一样,ASP.NET Core 在 Windows、Linux 和 macOS 上运行,同时还允许部署到 x86、x64 和 高级精简指令集机器ARM)CPU 架构。

一个典型的 ASP.NET Core 应用程序包括以下内容:

  • 模型

  • 视图

  • 控制器

这些组件构成了一个常见的网络架构原则 模型-视图-控制器MVC)。

控制器

控制器为处理网络应用程序和 REST 服务的业务逻辑提供服务器端代码。控制器可以在同一个控制器中包含 web 和 REST 调用,尽管我建议将它们分开,以确保代码组织得干净整洁。

模型

模型为从控制器到视图以及相反方向提供数据容器。例如,考虑一个从数据库中获取数据的列表页面。控制器将返回一个包含该数据的模型,如果相同的数据被用于过滤,它也将序列化为 JavaScript 对象表示法JSON)并发送回控制器。

视图

视图提供了前端视图的模板,并支持模型绑定。模型绑定允许将绑定到各种领域对象模型DOM)对象的属性(如文本框、复选框和下拉列表)干净地映射到和从。这种模型绑定的方法具有支持强类型引用的附加好处,这对于你有一个具有数十个绑定到模型的属性的复杂视图来说非常有用。

使用模型绑定处理表单提供了与我们在第十章“使用 ML.NET 与 UWP”中将要深入探讨的模型-视图-视图模型MVVM)方法类似的模型,这是一个通用 Windows 平台UWP)应用程序。

如果你想进一步深入了解 ASP.NET,微软的 Channel 9 有一个名为 ASP.NET Core 101 的系列,涵盖了 ASP.NET 的所有主要方面,请访问channel9.msdn.com/Series/ASPNET-Core-101

Blazor

建立在 ASP.NET Core 基础设施之上,Blazor 专注于消除复杂 Web 应用程序中最大的障碍之一——JavaScript。Blazor 允许你编写 C#代码而不是 JavaScript 代码来处理客户端任务,如表单处理、HTTP 调用和异步加载数据。在底层,Blazor 使用WebAssemblyWasm),这是一个由所有当前浏览器(Edge、Safari、Chrome 和 Firefox)支持的高性能 JavaScript 框架。

与其他框架类似,Blazor 也支持并推荐使用模块化组件来促进重用。这些被称为Blazor 组件

此外,在创建 Blazor 应用程序时,有三种项目类型:

  • 仅使用 Blazor 客户端,这对于更多静态页面来说非常理想。

  • 一个 Blazor(ASP.NET Core 托管)客户端应用程序,它托管在 ASP.NET Core 内部(这是我们将在下一节中要审查的项目类型)。

  • 一个 Blazor 服务器端应用程序,用于更新 DOM。这对于与 SignalR(微软的实时 Web 框架,支持聊天、实时股票行情和地图等)一起使用非常理想。

如果你想进一步深入了解 Blazor,微软已经在微软开发者网络MSDN)上编写了大量关于 Blazor 的文档,请访问:docs.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-3.1

创建文件分类网络应用程序

如前所述,我们将创建的应用程序是一个文件分类网络应用程序。使用 第四章 中“创建二进制分类应用程序”部分的知识,即 分类模型,我们将更进一步,查看在分类之前向文件添加更多属性。此外,我们还将机器学习与 ML.NET 集成到网络应用程序中,用户可以上传文件进行分类,返回清洁或恶意文件,以及预测的置信度。

与前面的章节一样,完整的项目代码、样本数据集和项目文件可以在以下位置下载:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter09

探索项目架构

由于之前的应用都是命令行应用,因此本例的项目架构相当不同。

与一些前面的章节一样,为了在 ML.NET 中使用 FastTree 算法,需要额外的 ML.NET NuGet 包——Microsoft.ML.FastTree。GitHub 中的示例和本章的深入探讨中都使用了 1.3.1 版本。

在以下屏幕截图中,您将找到示例解决方案的 Visual Studio 解决方案资源管理器视图。鉴于本例包含三个独立的项目(更类似于生产场景),新文件和显著修改的文件数量相当大。我们将在本节末尾运行应用程序之前,在以下部分详细审查以下解决方案屏幕截图中的每个新文件:

图片

sampledata.csv 文件包含从 Windows 可执行文件中提取的 14 行特征(我们将在下一节中详细介绍这些特征)。请随意调整数据以适应您的观察或使用不同的样本文件调整训练模型。以下是从 sampledata.data 文件中找到的一行示例:

18944 0 7 0 0 4 True "!This program cannot be run in DOS mode.Fm;Ld &~_New_ptrt(M4_Alloc_max"uJIif94H3"j?TjV*?invalid argum_~9%sC:\Program Files (x86\Microsoft Visu Studio\20cl4e\xomory"/Owneby CWGnkno excepti & 0xFF;b?eCErr[E7XE#D%d3kRegO(q/}nKeyExWa!0 S=+,H}Vo\DebugPE.pdbC,j?_info ByteToWidendled=aekQ3V?$buic_g(@1@A8?5/wQAEAAV0;AH@Z?flush@Co12@XcCd{(kIN<7BED!?rdbufPA[Tght_tDB.0J608(:6<?xml version='1.0' encoding='UTF8' standalone='yes'?><assembly xmlns='urn:schemasmicrosoftcom:asm.v1' manifestVersion='1.0'> <trustInfo > <security> <requestedPrivileges> <requestedExecutionLevel level='asInvoker' uiAccess='false' /> </requestedPrivileges> </security> </trustInfo></assembly>KERNEL32.DLLMSVCP140D.dllucrtbased.dllVCRUNTIME140D.dllExitProcessGetProcAddressLoadLibraryAVirtualProtect??1_Lockit@std@@QAE@XZ"

此外,我们还添加了 testdata.data 文件,其中包含额外的数据点,用于测试新训练的模型并对其进行评估。以下是 testdata.data 文件中的数据样本行:

1670144 1 738 0 0 24 False "!This program cannot be run in DOS mode.WATAUAVAWH A_AA]A\_t$ UWAVHx UATAUAVAWHA_AA]A\]UVWATAUAVAWH|$@H!t$0HA_AA]A\_]VWATAVAWHSUVWATAUAVAWH(A_AA]A\_][@USVWATAVAWHA_AA\_[]UVWATAUAVAWHA_AA]A\_]@USVWAVH` UAUAVHWATAUAVAWH A_AA]A\_x ATAVAWHUSVWATAUAVAWHA_AA]A\_[]UVWATAUAVAWHA_AA]A\_]\$ UVWATAUAVAWHA_AA]A\_]x UATAUAVAWHA_AA]A\]@USVWAVHUVWATAUAVAWHA_AA]A\_]UVWATAUAVAWHA_AA]A\_]@USVWATAVAWHA_AA\_[]t$ UWAVH@USVWAVHUVWAVAWHh VWATAVAWHUVWAVAWHUVWATAUAVAWHpA_AA]A\_]WATAUAVAWH0A_AA]A\_L$ UVWATAUAVAWH@A_AA]A\_]UVWATAUAVAWH`A_AA]A\_]UVWATAUAVAWHpA_AA]A\_]@USVWATAVAWHD$0fD9 tA_AA\_[]"

由于示例项目的规模,我们将在本节末尾运行应用程序之前,按照以下顺序深入了解每个不同组件的代码:

  • 两个应用程序之间的常见代码的 .NET Core 库

  • 用于运行预测的 ASP.NET Blazor 网络应用程序

  • 用于特征提取和训练的 .NET Core 控制台应用程序

深入了解库

以下是被更改或添加的类和枚举:

  • 文件分类响应项

  • 转换器

  • 扩展方法

  • 哈希扩展

  • 文件数据

  • 文件数据预测

  • FileClassificationFeatureExtractor

  • FileClassificationPredictor

  • FileClassificationTrainer

ConstantsBaseML 类与第八章 第八章,使用 ML.NET 与.NET Core 和预测 中保持未修改。

由于本应用程序和生产应用程序的性质,其中存在多个平台和/或执行共享代码的方式,本章示例应用程序中使用了库。使用库的好处是所有常用代码都可以以便携和无需依赖的方式存在。将此示例应用程序的功能扩展到包括桌面或移动应用程序,将比复制代码或保留在实际应用程序中要容易得多。

FileClassificationResponseItem

FileClassificationResponseItem 类是包含用于向我们的模型提供属性的共同类,同时也用于在 Web 应用程序中返回给最终用户。

  1. 首先,我们定义 TRUEFALSE 映射到 1.0f0.0f,如下所示:
private const float TRUE = 1.0f;
private const float FALSE = 0.0f;
  1. 接下来,我们添加所有要用于向我们的模型提供并显示给 Web 应用程序的最终用户的属性。FileSizeIs64BitNumImportsNumImportFunctionsNumExportFunctionsIsSignedStrings 属性被专门用作模型中的特征。SHA1SumConfidenceIsMaliciousErrorMessage 属性用于将我们的分类返回给最终用户,如下面的代码块所示:
public string SHA1Sum { get; set; }

public double Confidence { get; set; }

public bool IsMalicious { get; set; }

public float FileSize { get; set; }

public float Is64Bit { get; set; }

public float NumImports { get; set; }

public float NumImportFunctions { get; set; }

public float NumExportFunctions { get; set; }

public float IsSigned { get; set; }

public string Strings { get; set; }

public string ErrorMessage { get; set; }
  1. 接下来,我们有构造函数方法。如您所见,构造函数有一个字节数组作为参数。这样做是为了方便两个应用程序中的训练和预测路径,想法是原始文件字节将从 File.ReadAllBytes 调用或其他机制进入构造函数,以提供灵活性。从那里,我们使用 PeNet NuGet 包。此包提供了一个易于使用的接口,用于从 Windows 可执行文件(也称为 PE 文件)中提取特征。对于本应用程序的范围,选择了一些特征进行提取并存储到相应的属性中,如下面的代码块所示:
public FileClassificationResponseItem(byte[] fileBytes)
{
    SHA1Sum = fileBytes.ToSHA1();
    Confidence = 0.0;
    IsMalicious = false;
    FileSize = fileBytes.Length;

    try
    {
        var peFile = new PeNet.PeFile(fileBytes);

        Is64Bit = peFile.Is64Bit ? TRUE : FALSE;

        try
        {
            NumImports = peFile.ImageImportDescriptors.Length;
        }
        catch
        {
            NumImports = 0.0f;
        }

        NumImportFunctions = peFile.ImportedFunctions.Length;

        if (peFile.ExportedFunctions != null)
        {
            NumExportFunctions = peFile.ExportedFunctions.Length;
        }

        IsSigned = peFile.IsSigned ? TRUE : FALSE;

        Strings = fileBytes.ToStringsExtraction();
    }
    catch (Exception)
    {
        ErrorMessage = $"Invalid file ({SHA1Sum}) - only PE files are supported";
    }
}

FileData

与之前的预测数据容器一样,FileData 类为我们提供必要的字段,以提供文件分类。此外,我们重写了 ToString 方法,以便在特征提取步骤中将此数据轻松导出到 逗号分隔值 (CSV) 文件,如下所示:

public class FileData
{
    [LoadColumn(0)]
    public float FileSize { get; set; }

    [LoadColumn(1)]
    public float Is64Bit { get; set; }

    [LoadColumn(2)]
    public float NumberImportFunctions { get; set; }

    [LoadColumn(3)]
    public float NumberExportFunctions { get; set; }

    [LoadColumn(4)]
    public float IsSigned { get; set; }

    [LoadColumn(5)]
    public float NumberImports { get; set; }

    [LoadColumn(6)]
    public bool Label { get; set; }

    [LoadColumn(7)]
    public string Strings { get; set; }

    public override string ToString() => $"{FileSize}\t{Is64Bit}\t{NumberImportFunctions}\t" +
                                         $"{NumberExportFunctions}\t{IsSigned}\t{NumberImports}\t" +
                                         $"{Label}\t\"{Strings}\"";
}

FileDataPrediction

FileDataPrediction 类包含预测的分类和概率属性,以便在我们的 Web 应用程序中返回给最终用户,如下面的代码块所示:

public class FileDataPrediction
{
    public bool Label { get; set; }

    public bool PredictedLabel { get; set; }

    public float Score { get; set; }

    public float Probability { get; set; }
}

Converters

Converters 类提供了一个扩展方法,用于将前面在本节中审查过的 FileClassificationResponseItem 类转换为 FileData 类。通过创建扩展方法,如以下代码块所示,我们可以快速且干净地在应用程序容器和我们的仅模型容器之间进行转换:

public static class Converters
{
    public static FileData ToFileData(this FileClassificationResponseItem fileClassification)
    {
        return new FileData
        {
            Is64Bit = fileClassification.Is64Bit,
            IsSigned = fileClassification.IsSigned,
            NumberImports = fileClassification.NumImports,
            NumberImportFunctions = fileClassification.NumImportFunctions,
            NumberExportFunctions = fileClassification.NumExportFunctions,
            FileSize = fileClassification.FileSize,
            Strings = fileClassification.Strings
        };
    }
}

ExtensionMethods

如前几章所示,ExtensionMethods 类包含辅助扩展方法。在本例中,我们将添加 ToStrings 扩展方法。字符串是在对文件进行分类时非常受欢迎的第一遍扫描,并且易于捕获的特征。让我们深入了解该方法,如下所示:

  1. 首先,我们定义了两个新的常量来处理缓冲区大小和编码。如前所述,1252 是 Windows 可执行文件所使用的编码,如下面的代码块所示:
private const int BUFFER_SIZE = 2048;
private const int FILE_ENCODING = 1252;
  1. 下一个更改是添加了 ToStringsExtraction 方法本身以及定义我们的正则表达式,如下所示:
public static string ToStringsExtraction(this byte[] data)
{
     var stringRex = new Regex(@"[ -~\t]{8,}", RegexOptions.Compiled);

这个正则表达式是我们将用来遍历文件字节的。

  1. 接下来,我们初始化 StringBuilder 类并检查传入的字节数组是否为空或为空(如果是,我们无法处理它),如下所示:
var stringLines = new StringBuilder();

if (data == null || data.Length == 0)
{
     return stringLines.ToString();
}
  1. 既然我们已经确认传入的数组中有字节,我们只想取最多 65536 字节。这样做的原因是,如果文件是 100 MB,这个操作可能需要很长时间。您可以随意调整这个数字并查看效果。代码如下所示:
var dataToProcess = data.Length > 65536 ? data.Take(65536).ToArray() : data;
  1. 现在我们有了将要分析的字节,我们将遍历并提取字节中找到的文本行,如下所示:
using (var ms = new MemoryStream(dataToProcess, false))
{
    using (var streamReader = new StreamReader(ms, Encoding.GetEncoding(FILE_ENCODING), false, BUFFER_SIZE, false))
    {
        while (!streamReader.EndOfStream)
        {
            var line = streamReader.ReadLine();

            if (string.IsNullOrEmpty(line))
            {
                continue;
            }

            line = line.Replace("^", "").Replace(")", "").Replace("-", "");

            stringLines.Append(string.Join(string.Empty,
                stringRex.Matches(line).Where(a => !string.IsNullOrEmpty(a.Value) && !string.IsNullOrWhiteSpace(a.Value)).ToList()));
        }
    }
}
  1. 最后,我们只需将行连接成一个单独的字符串,如下所示:
return string.Join(string.Empty, stringLines);

HashingExtensions

新的 HashingExtensions 类将我们的字节数组转换为 SHA1 字符串。之所以没有将其放在我们的其他扩展方法中,是为了提供一个通用的类,可能包含 SHA256、ssdeep 或其他哈希(特别是鉴于最近的 SHA1 冲突,证明了 SHA1 的不安全性)。

对于这个方法,我们使用内置的 .NET Core SHA1 类,然后通过调用 ToBase64String 方法将其转换为 Base64 字符串,如下所示:

public static class HashingExtension
{
    public static string ToSHA1(this byte[] data)
    {
        var sha1 = System.Security.Cryptography.SHA1.Create();

        var hash = sha1.ComputeHash(data);

        return Convert.ToBase64String(hash);
    }
}

FileClassificationFeatureExtractor

FileClassificationFeatureExtractor 类包含我们的 ExtractExtractFolder 方法:

  1. 首先,我们的 ExtractFolder 方法接收文件夹路径和将包含我们的特征提取的输出文件,如下面的代码块所示:
private void ExtractFolder(string folderPath, string outputFile)
{
    if (!Directory.Exists(folderPath))
    {
        Console.WriteLine($"{folderPath} does not exist");

        return;
    }

    var files = Directory.GetFiles(folderPath);

    using (var streamWriter =
        new StreamWriter(Path.Combine(AppContext.BaseDirectory, $"../../../../{outputFile}")))
    {
        foreach (var file in files)
        {
            var extractedData = new FileClassificationResponseItem(File.ReadAllBytes(file)).ToFileData();

            extractedData.Label = !file.Contains("clean");

            streamWriter.WriteLine(extractedData.ToString());
        }
    }

    Console.WriteLine($"Extracted {files.Length} to {outputFile}");
}     
  1. 接下来,我们使用 Extract 方法调用训练和测试提取,如下所示:
public void Extract(string trainingPath, string testPath)
{
    ExtractFolder(trainingPath, Constants.SAMPLE_DATA);
    ExtractFolder(testPath, Constants.TEST_DATA);
}

FileClassificationPredictor

FileClassificationPredictor 类为我们的命令行和 Web 应用程序提供接口,使用重载的 Predict 方法:

  1. 第一个 Predict 方法是为我们的命令行应用程序准备的,它简单地接收文件名,并在加载字节后调用 步骤 2 中的重载,如下所示:
public FileClassificationResponseItem Predict(string fileName)
{
    var bytes = File.ReadAllBytes(fileName);

    return Predict(new FileClassificationResponseItem(bytes));
}
  1. 第二种实现是为我们的 Web 应用程序,它接收FileClassificationResponseItem对象,创建我们的预测引擎,并返回预测数据,如下所示:
public FileClassificationResponseItem Predict(FileClassificationResponseItem file)
{
    if (!File.Exists(Common.Constants.MODEL_PATH))
    {
        file.ErrorMessage = $"Model not found ({Common.Constants.MODEL_PATH}) - please train the model first";

        return file;
    }

    ITransformer mlModel;

    using (var stream = new FileStream(Common.Constants.MODEL_PATH, FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        mlModel = MlContext.Model.Load(stream, out _);
    }

    var predictionEngine = MlContext.Model.CreatePredictionEngine<FileData, FileDataPrediction>(mlModel);

    var prediction = predictionEngine.Predict(file.ToFileData());

    file.Confidence = prediction.Probability;
    file.IsMalicious = prediction.PredictedLabel;

    return file;
}

FileClassificationTrainer 类

在库中最后添加的类是FileClassificationTrainer类。此类支持使用FastTree ML.NET 训练器,以及利用我们从文件中提取的特征:

  1. 第一项更改是使用FileData类将 CSV 文件读取到dataView属性中,如下面的代码块所示:
var dataView = MlContext.Data.LoadFromTextFile<FileData>(trainingFileName, hasHeader: false);
  1. 接下来,我们将我们的FileData特征映射以创建我们的流水线,如下所示:
var dataProcessPipeline = MlContext.Transforms.NormalizeMeanVariance(nameof(FileData.FileSize))
    .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(FileData.Is64Bit)))
    .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(FileData.IsSigned)))
    .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(FileData.NumberImportFunctions)))
    .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(FileData.NumberExportFunctions)))
    .Append(MlContext.Transforms.NormalizeMeanVariance(nameof(FileData.NumberImports)))
    .Append(MlContext.Transforms.Text.FeaturizeText("FeaturizeText", nameof(FileData.Strings)))
    .Append(MlContext.Transforms.Concatenate(FEATURES, nameof(FileData.FileSize), nameof(FileData.Is64Bit),
        nameof(FileData.IsSigned), nameof(FileData.NumberImportFunctions), nameof(FileData.NumberExportFunctions),
        nameof(FileData.NumberImports), "FeaturizeText"));
  1. 最后,我们初始化我们的FastTree算法,如下所示:
var trainer = MlContext.BinaryClassification.Trainers.FastTree(labelColumnName: nameof(FileData.Label),
    featureColumnName: FEATURES,
    numberOfLeaves: 2,
    numberOfTrees: 1000,
    minimumExampleCountPerLeaf: 1,
    learningRate: 0.2);

该方法的其他部分与我们在第五章中讨论的二元分类Train方法类似,聚类模型

深入 Web 应用程序

在审查了库代码之后,下一个组件是 Web 应用程序。如开篇部分所述,我们的 Web 应用程序是一个 ASP.NET Core Blazor 应用程序。在本例的范围内,我们使用标准方法来处理后端和前端。该应用程序的架构结合了 Blazor 和 ASP.NET Core——具体来说,使用 ASP.NET Core 来处理应用程序的 REST 服务组件。

我们将在本节中深入研究的文件如下:

  • UploadController

  • Startup

  • Index.razor

UploadController 类

UploadController类的目的是处理文件提交后的服务器端处理。对于那些以前使用过 ASP.NET MVC 或 Web API 的人来说,这个控制器看起来应该非常熟悉:

  1. 需要注意的第一件事是装饰类的属性标记。ApiController属性配置控制器以处理 HTTP API,而Route标记表示控制器将监听/Upload路径,如下面的代码块所示:
[ApiController]
[Route("[controller]")]
public class UploadController : ControllerBase
  1. 需要注意的下一件事是在UploadController构造函数中使用依赖注入DI)传递预测对象。DI 是一种强大的方法,用于提供对单例对象(如FileClassificationPredictor或数据库)的访问,如下面的代码块所示:
private readonly FileClassificationPredictor _predictor;

public UploadController(FileClassificationPredictor predictor)
{
    _predictor = predictor;
}
  1. 接下来,我们创建一个辅助方法来处理从 HTTP POST 中获取IFormFile并返回所有字节,如下所示:
private static byte[] GetBytesFromPost(IFormFile file)
{
    using (var ms = new BinaryReader(file.OpenReadStream()))
    {
        return ms.ReadBytes((int)file.Length);
    }
}
  1. 最后,我们创建Post方法。HttpPost属性告诉路由引擎仅监听HttpPost调用。该方法处理GetBytesFromPost方法调用的输出,创建FileClassificationResponseItem对象,然后返回预测,如下面的代码块所示:
[HttpPost]
public FileClassificationResponseItem Post(IFormFile file)
{
    if (file == null)
    {
        return null;
    }

    var fileBytes = GetBytesFromPost(file);

    var responseItem = new FileClassificationResponseItem(fileBytes);

    return _predictor.Predict(responseItem);
}

Startup 类

在 ASP.NET Core 和 Blazor 应用程序中,Startup类控制 Web 应用程序中使用的各种服务的初始化。对 Visual Studio 附带的Startup模板进行了两项主要更改,如下所示:

  1. 首个改动发生在 ConfigureServices 方法中。因为这是一个同时应用了 ASP.NET Core 和 Blazor 的组合应用程序,我们需要调用 AddControllers 方法。此外,我们打算利用依赖注入(DI)并一次性初始化预测器对象,然后再将其作为单例添加,如下面的代码块所示:
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddControllers();
    services.AddServerSideBlazor();

    services.AddSingleton<FileClassificationPredictor>();
    services.AddSingleton<HttpClient>();
}
  1. 第二个改动发生在 Configure 方法中。首先,我们需要注册 CodePages 实例。如果没有这个调用,对 Windows-1252 编码的引用将导致异常(我们将在下一节中将此调用添加到训练应用程序中)。其次,是配置 MapControllerRoute 的使用,如下面的代码块所示:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });
}

Index.razor 文件

Index.razor 文件包含我们文件分类 Web 应用程序的前端。此外,它还包含对之前在本节中描述的 UploadController 类的 REST 调用。对于这次深入分析,我们将特别查看以下 Blazor 代码块:

  1. 首先要注意的是我们 FileClassificationResponseItem 类的声明。我们在这个块中定义变量,因为它将允许在整个页面中访问。第二个元素是 HandleSelection 方法的声明,如下面的代码块所示:
FileClassificationResponseItem _classificationResponseItem;

async Task HandleSelection(IEnumerable<IFileListEntry> files) {
  1. 接下来,我们将第一个文件转换为字节数组,并创建 MultipartFormdataContent 对象,以便将其 POST 到之前描述的 Post 方法,如下所示:
var file = files.FirstOrDefault();

if (file != null)
{
    var ms = new MemoryStream();
    await file.Data.CopyToAsync(ms);

    var content = new MultipartFormDataContent {
        {
            new ByteArrayContent(ms.GetBuffer()), "file", file.Name
        }
    };
  1. 最后,我们将文件 POST 到我们的 UploadController 端点,并异步等待来自 ML.NET 预测的响应,然后将响应分配给我们的响应变量 _classificationResponseItem,如下所示:
var response = await client.PostAsync("http://localhost:5000/upload/", content);

var jsonResponse = await response.Content.ReadAsStringAsync();

_classificationResponseItem = JsonSerializer.Deserialize<FileClassificationResponseItem>(jsonResponse, new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true
});

深入了解训练应用程序

现在我们已经回顾了共享库和 Web 应用程序,让我们深入到训练应用程序中。

我们将回顾以下文件:

  • ProgramArguments

  • ProgramActions

  • Program

ProgramArguments

基于 第八章 中 ProgramArguments 类的详细工作,使用 ML.NET 与 .NET Core 和预测,我们只对该类进行了一项添加。这次改动为类添加了存储 TestingTraining 文件夹路径的属性,如下面的代码块所示:

public string TestingFolderPath { get; set; }

public string TrainingFolderPath { get; set; }

与上一章不同,特征提取基于多个 Windows 可执行文件,而不是仅仅包含一个 CSV 文件。

ProgramActions 枚举

首个改动发生在 ProgramActions 枚举中。在 第八章 的 使用 ML.NET 与 .NET Core 和预测 中,我们只有训练和预测。然而,正如本章前面提到的,我们现在还有 FeatureExtraction 需要执行。为了添加支持,我们只需将 FEATURE_EXTRACTOR 添加到枚举中,如下所示:

public enum ProgramActions
{
    FEATURE_EXTRACTOR,
    TRAINING,
    PREDICT
}

Program

Program类中,与上一章对命令行参数解析的重构相比,只有两个更改,如下所示:

  1. 首先,我们需要注册CodePages编码器实例,以便正确读取文件中的 Windows-1252 编码,就像我们在 Web 应用程序中所做的那样,如下所示:
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
  1. 我们可以使用一个简化和强类型化的 switch case 来处理我们的三个动作,如下所示:
switch (arguments.Action)
{
    case ProgramActions.FEATURE_EXTRACTOR:
        new FileClassificationFeatureExtractor().Extract(arguments.TrainingFolderPath,
            arguments.TestingFolderPath);
        break;
    case ProgramActions.PREDICT:
        var prediction = new FileClassificationPredictor().Predict(arguments.PredictionFileName);

        Console.WriteLine($"File is {(prediction.IsMalicious ? "malicious" : "clean")} with a {prediction.Confidence:P2}% confidence");
        break;
    case ProgramActions.TRAINING:
        new FileClassificationTrainer().Train(arguments.TrainingFileName, arguments.TestingFileName);
        break;
    default:
        Console.WriteLine($"Unhandled action {arguments.Action}");
        break;
}

运行训练应用程序

首先,我们需要先运行chapter09.trainer应用程序以执行模型的特征提取和训练。要运行训练应用程序,过程几乎与第三章中展示的示例应用程序相同,即回归模型,但需要添加在训练时传递测试数据集文件夹路径,我们将遵循以下步骤:

  1. 我们将运行训练应用程序,传递训练和测试文件夹的路径以执行特征提取,如下所示:
PS chapter09\chapter09.trainer\bin\Debug\netcoreapp3.1> .\chapter09.trainer.exe trainingfolderpath ..\..\..\..\TrainingData\ testingfolderpath ..\..\..\..\TestData\
Extracted 14 to sampledata.data
Extracted 14 to testdata.data

代码仓库中包含两个预特征提取的文件(sampledata.csvtestdata.csv),以便您可以在不执行自己的特征提取的情况下训练模型。如果您想执行自己的特征提取,创建一个TestDataTrainingData文件夹。将这些文件夹填充为PowerShellPS1)、Windows 可执行文件EXE)和Microsoft Word 文档DOCX)的样本。

  1. 现在,我们将再次运行应用程序,根据步骤 1样本和测试数据导出训练模型。生成的模型(fileclassification.mdl)将与可执行文件在同一文件夹中,如下所示:
PS chapter09\chapter09.trainer\bin\Debug\netcoreapp3.1> .\chapter09.trainer.exe action training trainingfilename ..\..\..\..\sampledata.data testingfilename ..\..\..\..\testdata.data
Entropy: 0.5916727785823275
Log Loss: 12.436063032030377
Log Loss Reduction: -20.018480961432264

随意修改值,看看基于模型训练数据集的预测如何变化。从这个点开始,一些实验性的区域可能包括以下内容:

  • 调整Trainer类中审查的超参数——如numberOfLeavesnumberOfTreeslearningRate——以查看精度如何受到影响。

  • FileData类添加新功能,例如特定的导入,而不仅仅是使用计数。

  • 向训练和样本集添加更多变化,以获得更好的数据采样。

为了方便,GitHub 仓库包含了testdata.csvsampledata.csv两个文件。

运行 Web 应用程序

现在我们已经训练了模型,我们可以运行我们的 Web 应用程序并测试文件提交。如果您还没有构建 Web 应用程序,您必须首先构建它。这将创建bin\debug\netcoreapp3.1文件夹。构建 Web 应用程序后,复制上一节中训练的模型。此时,启动 Web 应用程序。启动后,您应该在默认浏览器中看到以下内容:

图片

继续点击选择文件按钮,选择一个.exe.dll文件,您应该看到我们的模型以下列结果:

图片

随意尝试在您的机器上使用各种文件来查看置信度分数,如果您收到错误阳性结果,可能需要向模型添加更多功能以纠正分类。

探索改进的额外想法

现在我们已经完成了深入探讨,还有一些额外的元素可能有助于进一步增强应用程序。下面将讨论一些想法。

记录日志

就像我们在上一章深入探讨日志记录时一样,添加日志记录对于远程了解 Web 应用程序上何时发生错误可能是至关重要的。随着应用程序复杂性的增加,强烈建议使用 NLog (nlog-project.org/) 或类似的开源项目进行日志记录。这将允许您以不同的级别将日志记录到文件、控制台或第三方日志解决方案——如 Loggly。

利用缓存层

想象一下将此应用程序部署到面向公众的 Web 服务器上,并拥有数百个并发用户。很可能会发生用户上传相同的文件——在内存中缓存结果可以避免每次预测时进行不必要的 CPU 处理。一些缓存选项包括利用 ASP.NET 内存缓存,或外部缓存数据库,如 Redis。这两个都可通过 NuGet 包获得。

利用数据库

与缓存建议类似,将结果记录在数据库中可以避免不必要的 CPU 处理。一个合理的选择是使用 NoSQL 数据库,例如 MongoDB。使用 SHA1 哈希作为键,将完整 JSON 响应作为值可以显著提高高流量场景下的性能。MongoDB 在 NuGet 上有一个名为 MongoDB.Driver 的 .NET 接口。在撰写本文时,2.10.0 是最新的版本。

摘要

在本章的讨论过程中,我们探讨了构建一个生产就绪的 ASP.NET Core Blazor 网络应用程序架构所需的内容,并以前几章的工作为基础。我们还创建了一个全新的文件分类网络应用程序,利用了 ML.NET 的 FastTree 二进制分类器。最后,我们还讨论了一些进一步增强 ASP.NET Core 应用程序(以及一般的生产应用程序)的方法。

在下一章中,我们将深入探讨创建一个生产级网络浏览器,使用网页内容来确定内容是否恶意,利用 ML.NET 的情感分析和 UWP 框架。

第十章:在 UWP 中使用 ML.NET

现在我们已经建立了如何创建生产级的 .NET Core 控制台应用程序,在本章中,我们将深入探讨使用 通用 Windows 平台UWP)框架创建一个功能齐全的 Windows 10 应用程序。此应用程序将利用 ML.NET 二元分类模型来对网页内容进行分类,以确定内容是良性的还是恶性的。此外,我们将探讨将代码分解为基于组件的架构,使用 .NET Standard 库在桌面应用程序和将训练我们的模型的控制台应用程序之间共享。到本章结束时,您应该能够熟练地设计和编码带有 ML.NET 的生产级 UWP 桌面应用程序。

本章将涵盖以下主题:

  • 分析 UWP 应用程序

  • 创建网络浏览器分类应用程序

  • 探索额外的生产级应用程序增强功能

分析 UWP 架构

从高层次来看,UWP 提供了一个易于使用的框架来创建 Windows 10 的丰富桌面应用程序。正如所讨论的,使用 .NET Core,UWP 允许针对 x86、x64 和 高级精简指令集机器ARM)。在撰写本文时,ARM 不支持 ML.NET。此外,UWP 应用程序也可以使用 JavaScript 和 HTML 编写。

一个典型的 UWP 桌面应用程序包括以下核心代码元素:

  • 视图

  • 模型

  • 视图模型

这些组件构成了 模型-视图-视图模型MVVM)这一通用应用程序架构原则。除了代码组件外,图像和音频也是常见的,这取决于您应用程序或游戏的本质。

与 Android 和 iOS 平台上的移动应用程序类似,每个应用程序在安装时都会被沙盒化,以特定的权限运行,这些权限由您,即开发者,在安装时请求。因此,在您开发自己的 UWP 应用程序时,只请求您的应用程序绝对需要的访问权限。

对于本章我们将创建的示例应用程序,我们只需要作为客户端访问互联网,正如在标记为“互联网(客户端)”的“功能”选项卡中所示,以下截图所示:

图片

互联网(客户端)和其他权限定义在位于 UWP 应用程序根目录下的 Package.appxmanifest 文件中,在“功能”选项卡下。此文件在后续的“探索项目架构”部分中的 Visual Studio 解决方案资源管理器截图中有展示。

为了准备我们深入探讨在 UWP 应用程序中集成 ML.NET,让我们深入了解 UWP 应用程序中发现的三个核心组件。

视图

视图,正如我们在上一章的 Blazor 讨论中所定义的,包含了应用程序的用户界面(UI)组件。在 UWP 开发中,如 Windows Presentation Foundation(WPF)和 Xamarin.Forms 中的视图,使用可扩展应用程序标记语言(XAML)语法。那些熟悉使用 Bootstrap 的 Grid 模式进行现代 Web 开发的人,在我们深入本章内容时,将能够迅速看到其中的相似之处。

Web 开发和 UWP 开发之间最大的区别在于,当与 MVVM 原则一起使用时,XAML 视图具有强大的双向绑定功能。正如你将在深入探讨中看到的那样,XAML 绑定消除了在代码后手动设置和获取值的需求,就像你可能在之前的 Windows Forms 或 WebForms 项目中执行的那样。

对于采用 Web 方法的程序,HTML 将定义你的视图,就像我们在第九章使用 ML.NET 与 ASP.NET Core 中提到的 Blazor 项目一样。

模型

模型提供了视图和视图模型之间的数据容器。将模型视为纯粹是视图和视图模型之间包含数据的传输工具。例如,如果你有一个电影列表,你的MovieListingModel类中将定义一个List集合的MovieItems。这个容器类将在视图模型中实例化和填充,然后绑定到你的视图中。

视图模型

视图模型提供了填充你的模型以及间接地你的视图的业务逻辑层。正如之前提到的,UWP 开发中提供的 MVVM 绑定简化了触发点的管理,以确保你的 UI 层是最新的。这是通过在我们的视图模型中实现INotifyPropertyChanged接口来实现的。对于我们要绑定到 UI 的每个属性,我们只需调用OnPropertyChanged。其背后的力量在于,你可以在其他属性的设置器中拥有复杂的表单和触发器,而不需要条件语句和无尽的代码来处理复杂性。

如果你想要进一步深入 UWP 开发,微软的 Channel9 有一个名为Windows 10 Development for Absolute Beginners的系列,涵盖了 UWP 开发的各个方面:channel9.msdn.com/Series/Windows-10-development-for-absolute-beginners

创建网络浏览器分类应用程序

如前所述,我们将创建的应用程序是一个网页浏览器分类应用程序。利用逻辑分类章节中获得的知识,我们将使用 SdcaLogisticRegression 算法来获取网页的文本内容,对文本进行特征化,并提供恶意程度的置信度。此外,我们将把这个技术集成到一个模拟网页浏览器的 Windows 10 UWP 应用程序中——在导航到页面时运行模型,并判断页面是否恶意。如果发现页面是恶意的,我们将重定向到警告页面。虽然在现实场景中,这可能证明在每个页面上运行太慢,但高度安全的网页浏览器的优势,根据环境要求,可能远远超过运行我们的模型所造成的轻微开销。

与前几章一样,完成的项目代码、样本数据集和项目文件可以从github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter10下载。

探索项目架构

在本章中,我们将深入探讨一个本地的 Windows 10 桌面应用程序。如本章第一部分所述,我们将使用 UWP 框架来创建我们的应用程序。

对于这个示例应用程序,不需要额外的 ML.NET NuGet 包。然而,我们将使用 HtmlAgilityPack NuGet 包提供一个快速的方法来从给定的网页中提取文本。在撰写本文时,版本 1.11.18 是最新版本,也是本例中使用的版本。

在以下屏幕截图中,您将找到解决方案的 Visual Studio 解决方案资源管理器视图。鉴于这个示例包含三个独立的项目(更类似于生产场景),新增和显著修改的文件数量相当大。我们将在本节稍后详细审查解决方案屏幕截图中显示的每个新文件:

图片

sampledata.csv 文件(位于代码存储库中的 Data 文件夹中)包含从 trainingURLList.csv 文件(也位于 Data 文件夹中)中找到的 URL 提取的八行文本。您可以随意调整 URL 列表文件以测试您经常访问的网站。以下是此类行的一个样本:

False|BingImagesVideosMapsNewsShoppingMSNOfficeOutlookWordExcelPowerPointOneNoteSwayOneDriveCalendarPeopleSigninRewardsDownloadtoday’simagePlaytoday'squizTheTajMahalinAgraforIndia'sRepublicDay©MicheleFalzone/plainpictureIt'sRepublicDayinIndiaImageofthedayJan26,2020It'sRepublicDayinIndia©MicheleFalzone/plainpictureForIndia's70thRepublicDay

除了 sampledata.csv 文件外,我们还添加了 testdata.csv 文件,其中包含额外的数据点,用于测试新训练的模型并评估。以下是 testdata.csv 文件中数据的一个样本行:

True|USATODAY:LatestWorldandUSNews-USATODAY.comSUBSCRIBENOWtogethomedeliveryNewsSportsEntertainmentLifeMoneyTechTravelOpinionWeatherIconHumidityPrecip.WindsOpensettingsSettingsEnterCityNameCancelSetClosesettingsFullForecastCrosswordsInvestigationsAppsBest-SellingBooksCartoons

由于示例项目的规模,我们将在本节末尾运行应用程序之前,按照以下顺序深入到每个不同组件的代码中:

  • .NET 标准库,用于两个应用程序之间的通用代码

  • Windows 10 UWP 浏览器应用程序

  • .NET Core console application for feature extraction and training

Diving into the library

Due to the nature of this application and that of production applications where there are multiple platforms and/or ways to execute shared code, a library is being used in this chapter's example application. The benefit of using a library is that all common code can reside in a portable and dependency-free manner. Expanding the functionality in this sample application to include other platforms such as Linux or Mac applications with Xamarin would be a much easier lift than having the code either duplicated or kept in the actual applications.

Classes and enumerations that were changed or added in the library are as follows:

  • Constants

  • WebPageResponseItem

  • Converters

  • ExtensionMethods

  • WebPageInputItem

  • WebPagePredictionItem

  • WebContentFeatureExtractor

  • WebContentPredictor

  • WebContentTrainer

The Classification, TrainerActions,and BaseML classes remain unmodified from Chapter 9, Using ML.NET with ASP.NET Core.

The Constants class

The Constants class, as used in all of our examples to this point, is the common class that contains our constant values used in our library, trainer, and UWP applications. For this chapter, the MODEL_NAME and MALICIOUS_THRESHOLD properties were added to hold our model's name and an arbitrary threshold for when we should decide to classify our prediction as malicious or not, respectively. If you find your model too sensitive, try adjusting this threshold, like this:

public static class Constants
{
    public const string MODEL_NAME = "webcontentclassifier.mdl";

    public const string SAMPLE_DATA = "sampledata.csv";

    public const string TEST_DATA = "testdata.csv";

    public const double MALICIOUS_THRESHOLD = .5;
}

The WebPageResponseItem class

The WebPageResponseItem class is our container class between our predictor and application. This class contains the properties we set after running the predictor and then use to display in our desktop application, as shown in the following code block:

public class WebPageResponseItem
{
    public double Confidence { get; set; }

    public bool IsMalicious { get; set; }

    public string Content { get; set; }

    public string ErrorMessage { get; set; }

    public WebPageResponseItem()
    {
    }

    public WebPageResponseItem(string content)
    {
        Content = content;
    }
}

The Converters class

The Converters class has been adjusted to provide an extension method to convert our container class into the type our model expects. In this example, we have the Content property, which simply maps to the HTMLContent variable in the WebPageInputItem class, as follows:

public static WebPageInputItem ToWebPageInputItem(this WebPageResponseItem webPage)
{
    return new WebPageInputItem
    {
        HTMLContent = webPage.Content
    };
}

The ExtensionMethods class

如前所述,在第九章中讨论的ExtensionMethods类,已被扩展以包括ToWebContentString扩展方法。在这个方法中,我们传入我们想要检索网页内容的 URL。使用之前提到的HtmlAgilityPack,我们创建一个HtmlWeb对象并调用Load方法,在遍历文档对象模型DOM)之前。鉴于大多数网站都有大量的脚本和样式表,在这个例子中,我们的目的是检查页面中的文本,因此我们在代码中过滤了脚本和样式节点。一旦节点被遍历并添加到StringBuilder对象中,我们就返回该对象的类型转换字符串,如下面的代码块所示:

public static string ToWebContentString(this string url)
{
    var web = new HtmlWeb();

    var htmlDoc = web.Load(url);

    var sb = new StringBuilder();

    htmlDoc.DocumentNode.Descendants().Where(n => n.Name == "script" || n.Name == "style").ToList().ForEach(n => n.Remove());

    foreach (var node in htmlDoc.DocumentNode.SelectNodes("//text()[normalize-space(.) != '']"))
    {
        sb.Append(node.InnerText.Trim().Replace(" ", ""));
    }

    return sb.ToString();
}

WebPageInputItem

WebPageInputItem类是我们模型的输入对象,包含我们网页的标签和提取内容,如下面的代码块所示:

public class WebPageInputItem
{
    [LoadColumn(0), ColumnName("Label")]
    public bool Label { get; set; }

    [LoadColumn(1)]
    public string HTMLContent { get; set; }
}

WebPagePredictionItem

WebPagePredictionItem类是我们模型的输出对象,包含对网页是否恶意或良性的预测,以及预测准确的概率分数和我们在模型创建评估阶段使用的Score值,如下面的代码块所示:

public class WebPagePredictionItem
{
    public bool Prediction { get; set; }

    public float Probability { get; set; }

    public float Score { get; set; }
}

WebContentFeatureExtractor

WebContentFeatureExtractor类包含我们的GetContentFileExtract方法,它们的工作方式如下:

  1. 首先,我们的GetContentFile方法接受inputFileoutputFile值(分别是 URL 列表 CSV 和特征提取 CSV)。然后,它读取每个 URL,获取内容,然后输出到outputFile字符串,如下所示:
private static void GetContentFile(string inputFile, string outputFile)
{
    var lines = File.ReadAllLines(inputFile);

    var urlContent = new List<string>();

    foreach (var line in lines)
    {
        var url = line.Split(',')[0];
        var label = Convert.ToBoolean(line.Split(',')[1]);

        Console.WriteLine($"Attempting to pull HTML from {line}");

        try
        {
            var content = url.ToWebContentString();

            content = content.Replace('|', '-');

            urlContent.Add($"{label}|{content}");
        }
        catch (Exception)
        {
            Console.WriteLine($"Failed to pull HTTP Content from {url}");
        }
    }

    File.WriteAllText(Path.Combine(AppContext.BaseDirectory, outputFile), string.Join(Environment.NewLine, urlContent));
}     
  1. 接下来,我们使用Extract方法调用训练和测试提取,传递两个输出文件的名称,如下所示:
public void Extract(string trainingURLList, string testURLList, string trainingOutputFileName, string testingOutputFileName)
{
    GetContentFile(trainingURLList, trainingOutputFileName);

    GetContentFile(testURLList, testingOutputFileName);
}

WebContentPredictor

WebContentPredictor类为我们提供命令行和桌面应用程序的接口,使用重载的Predict方法,如下所述:

  1. 第一个Predict方法是为我们的命令行应用程序设计的,它简单地接受 URL 并调用在步骤 3中的重载,在调用ToWebContentString扩展方法之后,如下所示:
public WebPageResponseItem Predict(string url) => Predict(new WebPageResponseItem(url.ToWebContentString()));
  1. 然后,我们创建Initialize方法,在其中我们从嵌入式资源中加载我们的模型。如果成功,该方法返回true;否则,它返回false,如下面的代码块所示:
public bool Initialize()
{
    var assembly = typeof(WebContentPredictor).GetTypeInfo().Assembly;

    var resource = assembly.GetManifestResourceStream($"chapter10.lib.Model.{Constants.MODEL_NAME}");

    if (resource == null)
    {
        return false;
    }

    _model = MlContext.Model.Load(resource, out _);

    return true;
}
  1. 最后,我们调用我们的Predict方法来创建预测引擎。然后,我们调用预测器的Predict方法,然后更新ConfidenceIsMalicious属性,在返回更新后的WebPageResponseItem对象之前,如下所示:
public WebPageResponseItem Predict(WebPageResponseItem webPage)
{
    var predictionEngine = MlContext.Model.CreatePredictionEngine<WebPageInputItem, WebPagePredictionItem>(_model);

    var prediction = predictionEngine.Predict(webPage.ToWebPageInputItem());

    webPage.Confidence = prediction.Probability;
    webPage.IsMalicious = prediction.Prediction;

    return webPage;
}

WebContentTrainer

WebContentTrainer类包含所有用于训练和评估我们模型的代码。与之前的示例一样,这个功能包含在一个名为Train的方法中:

  1. 第一个更改是使用WebPageInputItem类将 CSV 读取到以|分隔的dataView对象中,如下面的代码块所示:
var dataView = MlContext.Data.LoadFromTextFile<WebPageInputItem>(trainingFileName, hasHeader: false, separatorChar: '|');
  1. 接下来,我们将文件数据特征映射到创建我们的管道。在本例中,我们简单地特征化HTMLContent属性,并将其传递给SdcaLogisticRegression训练器,如下所示:
var dataProcessPipeline = MlContext.Transforms.Text
    .FeaturizeText(FEATURES, nameof(WebPageInputItem.HTMLContent))
    .Append(MlContext.BinaryClassification.Trainers.SdcaLogisticRegression(labelColumnName: "Label", featureColumnName: FEATURES));
  1. 然后,我们调整模型,并将模型保存到磁盘,如下所示:
var trainedModel = dataProcessPipeline.Fit(dataView);

MlContext.Model.Save(trainedModel, dataView.Schema, Path.Combine(AppContext.BaseDirectory, modelFileName));
  1. 最后,我们加载测试文件,并调用BinaryClassification评估,如下所示:
var testingDataView = MlContext.Data.LoadFromTextFile<WebPageInputItem>(testingFileName, hasHeader: false, separatorChar: '|');

IDataView testDataView = trainedModel.Transform(testingDataView);

var modelMetrics = MlContext.BinaryClassification.Evaluate(
    data: testDataView);

Console.WriteLine($"Entropy: {modelMetrics.Entropy}");
Console.WriteLine($"Log Loss: {modelMetrics.LogLoss}");
Console.WriteLine($"Log Loss Reduction: {modelMetrics.LogLossReduction}");

深入 UWP 浏览器应用程序

在审查了库代码之后,下一个组件是桌面应用程序。正如开篇部分所讨论的,我们的桌面应用程序是一个 UWP 应用程序。在本例的范围内,我们使用标准的处理应用程序架构的方法,遵循本章开篇部分讨论的 MVVM 方法。

我们将在本节中深入探讨的文件如下:

  • MainPageViewModel

  • MainPage.xaml

  • MainPage.xaml.cs

UWP 项目内部的其他文件,例如磁贴图像和应用程序类文件,都未从默认的 Visual Studio UWP 应用程序模板中进行修改。

MainPageViewModel

MainPageViewModel类的目的是包含我们的业务逻辑并控制视图:

  1. 我们首先实例化之前讨论的WebContentPredictor类,用于运行预测,如下所示:
private readonly WebContentPredictor _prediction = new WebContentPredictor();
  1. 下面的代码块处理了 MVVM 为我们GO按钮、web 服务 URL 字段和 web 分类属性提供的功能。对于这些属性中的每一个,我们在值发生变化时调用OnPropertyChanged,这会触发视图与任何绑定到这些属性的域的绑定刷新,如下面的代码块所示:
private bool _enableGoButton;

public bool EnableGoButton
{
    get => _enableGoButton;

    private set
    {
        _enableGoButton = value;
        OnPropertyChanged();
    }
}

private string _webServiceURL;

public string WebServiceURL
{
    get => _webServiceURL;

    set
    {
        _webServiceURL = value;

        OnPropertyChanged();

        EnableGoButton = !string.IsNullOrEmpty(value);
    }
}

private string _webPageClassification;

public string WebPageClassification
{
    get => _webPageClassification;

    set
    {
        _webPageClassification = value;
        OnPropertyChanged();
    }
}
  1. 接下来,我们定义Initialize方法,它调用预测器的Initialize方法。如果模型无法加载或找到,该方法将返回 false,如下所示:
public bool Initialize() => _prediction.Initialize();
  1. 然后,我们获取用户通过WebServiceURL属性输入的 URL。从这个值中,我们验证是否以httphttps开头。如果没有,则在将其转换为 URI 之前,在 URL 前添加http://,如下所示:
public Uri BuildUri()
{
    var webServiceUrl = WebServiceURL;

    if (!webServiceUrl.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) &&
        !webServiceUrl.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
    {
        webServiceUrl = $"http://{webServiceUrl}";
    }

    return new Uri(webServiceUrl);
}
  1. 现在,让我们看看我们的Classify方法,该方法接受用户输入的 URL。此方法调用我们的Predict方法,构建状态栏文本,如果发现是恶意内容,则构建发送回WebView对象的 HTML 响应,如下所示:
public (Classification ClassificationResult, string BrowserContent) Classify(string url)
{
    var result = _prediction.Predict(url);

    WebPageClassification = $"Webpage is considered {result.Confidence:P1} malicious";

    return result.Confidence < Constants.MALICIOUS_THRESHOLD ? 
        (Classification.BENIGN, string.Empty) : 
        (Classification.MALICIOUS, $"<html><body bgcolor=\"red\"><h2 style=\"text-align: center\">Machine Learning has found {WebServiceURL} to be a malicious site and was blocked automatically</h2></body></html>");
}
  1. 最后,我们实现了OnPropertyChanged事件处理程序和方法,这是INotifyPropertyChanged接口的标准实现,正如本章开篇部分所讨论的,并在以下代码块中展示:
public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

MainPage.xaml

如开篇部分所述的 UWP 开发描述,XAML 标记用于定义你的 UI。在本应用的范围内,我们的 UI 相对简单:

  1. 我们首先定义的是我们的 Grid。在 XAML 中,Grid 是一个类似于网络开发中 div 元素的容器。然后我们定义我们的行。与 Bootstrap 类似(但在我看来更容易理解),是预先定义每行的长度。将行设置为 Auto 将自动调整高度以适应内容的高度,而星号则表示使用基于主要容器高度的所有剩余高度,如下面的代码块所示:
<Grid>
  <Grid.RowDefinitions>
     <RowDefinition Height="Auto" />
     <RowDefinition Height="*" />
     <RowDefinition Height="Auto" />
  </Grid.RowDefinitions>
  1. 步骤 1 中的行定义类似,我们预先定义了列。"Auto""*" 与行中的用法相同,只是关于宽度而不是高度,如下面的代码块所示:
<Grid.ColumnDefinitions>
    <ColumnDefinition Width="*" />
    <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
  1. 我们然后定义我们的 TextBox 对象用于 URL 输入。注意 Text 值中的 Binding 调用。这将文本框的文本字段绑定到视图模型中的 WebServiceURL 属性,如下所示:
<TextBox Grid.Row="0" Grid.Column="0" KeyUp="TxtBxUrl_KeyUp" Text="{Binding WebServiceURL, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
  1. 然后,我们添加按钮来模拟浏览器的 GO 按钮,该按钮触发导航。同时,注意使用 Binding 来启用或禁用按钮本身(它基于输入到 URL 文本框中的文本进行绑定),如下面的代码块所示:
<Button Grid.Row="0" Grid.Column="1" Content="GO" Click="BtnGo_Click" IsEnabled="{Binding EnableGoButton}" />
  1. 我们然后添加了 UWP 中的 WebView 控件,如下所示:
<WebView Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" x:Name="wvMain" NavigationStarting="WvMain_OnNavigationStarting" />
  1. 最后,我们添加我们的状态栏网格和 TextBlock 控件,以在窗口底部显示分类,如下所示:
<Grid Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2" Background="#1e1e1e" Height="30">
    <TextBlock Text="{Binding WebPageClassification, Mode=OneWay}" Foreground="White" Margin="10,0,0,0" />
</Grid>

MainPage.xaml.cs

MainPage.xaml.cs 文件包含之前讨论的 XAML 视图的代码:

  1. 我们首先定义的是一个包装属性,围绕基 Page 类中内置的 DataContext 属性构建,如下所示:
private MainPageViewModel ViewModel => (MainPageViewModel) DataContext;
  1. 接下来,我们定义 MainPage 的构造函数,以便将 DataContext 初始化为我们的 MainPageViewModel 对象,如下所示:
public MainPage()
{
    InitializeComponent();

    DataContext = new MainPageViewModel();
}
  1. 我们然后重写基类的 OnNavigatedTo 方法来初始化我们的视图模型,并验证模型是否正确加载,如下所示:
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
    var initialization = ViewModel.Initialize();

    if (initialization)
    {
        return;
    }

    await ShowMessage("Failed to initialize model - verify the model has been created");

    Application.Current.Exit();

    base.OnNavigatedTo(e);
}
  1. 接下来,我们添加我们的 ShowMessage 包装器,以提供一个易于在应用程序中调用的单行代码,如下所示:
public async Task<IUICommand> ShowMessage(string message)
{
    var dialog = new MessageDialog(message);

    return await dialog.ShowAsync();
}
  1. 然后,我们通过调用 Navigate 方法来处理 GO 按钮的点击,如下所示:
private void BtnGo_Click(object sender, RoutedEventArgs e) => Navigate();
  1. 然后我们创建我们的 Navigate 包装方法,该方法构建 URI 并将其传递给 WebView 对象,如下所示:
private void Navigate()
{
    wvMain.Navigate(ViewModel.BuildUri());
}
  1. 我们还希望处理键盘输入,以便在用户输入 URL 后监听用户按下 Enter 键,使用户能够按下 Enter 或点击 GO 按钮,如下所示:
private void TxtBxUrl_KeyUp(object sender, KeyRoutedEventArgs e)
{
    if (e.Key == VirtualKey.Enter && ViewModel.EnableGoButton)
    {
        Navigate();
    }
}
  1. 最后,我们通过挂钩到 WebView 的 OnNavigationStarting 事件来阻塞导航,直到可以获取分类,如下所示:
private void WvMain_OnNavigationStarting(WebView sender, WebViewNavigationStartingEventArgs args)
{
    if (args.Uri == null)
    {
        return;
    }

    var (classificationResult, browserContent) = ViewModel.Classify(args.Uri.ToString());

    switch (classificationResult)
    {
        case Classification.BENIGN:
            return;
        case Classification.MALICIOUS:
            sender.NavigateToString(browserContent);
            break;
    }
}

深入到训练应用程序

现在我们已经回顾了共享库和桌面应用程序,让我们深入到训练应用程序。由于在第八章的示例中进行了主要的架构变更,按照设计,训练应用程序只进行了最小变更来处理本章示例中使用的特定类对象。

我们将回顾以下文件:

  • ProgramArguments

  • Program

ProgramArguments

在第九章的ProgramArguments类的工作基础上,我们对这个类只做了三个添加。前两个添加是将TrainingTesting输出文件名包括在内,以提供更好的灵活性。此外,URL属性包含你可以通过命令行传递给训练应用程序以获取预测的 URL,如下面的代码块所示:

public string TrainingOutputFileName { get; set; }

public string TestingOutputFileName { get; set; }

public string URL { get; set; }

程序类

Program类内部,我们现在将修改switch case语句,以使用来自第十章,“使用 ML.NET 与 UWP”的类/方法,如下所示:

switch (arguments.Action)
{
    case ProgramActions.FEATURE_EXTRACTOR:
        new WebContentFeatureExtractor().Extract(arguments.TrainingFileName, arguments.TestingFileName, 
            arguments.TrainingOutputFileName, arguments.TestingOutputFileName);
        break;
    case ProgramActions.PREDICT:
        var predictor = new WebContentPredictor();

        var initialization = predictor.Initialize();

        if (!initialization)
        {
            Console.WriteLine("Failed to initialize the model");

            return;
        }

        var prediction = predictor.Predict(arguments.URL);

        Console.WriteLine($"URL is {(prediction.IsMalicious ? "malicious" : "clean")} with a {prediction.Confidence:P2}% confidence");
        break;
    case ProgramActions.TRAINING:
        new WebContentTrainer().Train(arguments.TrainingFileName, arguments.TestingFileName, arguments.ModelFileName);
        break;
    default:
        Console.WriteLine($"Unhandled action {arguments.Action}");
        break;
}

运行训练应用程序

要开始运行训练应用程序,我们首先需要运行chapter10.trainer应用程序,在训练我们的模型之前执行特征提取。要运行训练应用程序,过程几乎与第九章的示例应用程序相同,只是在训练时添加了传递测试数据集文件路径:

  1. 运行训练应用程序,传递训练和测试 URL 列表 CSV 文件的路径以执行特征提取,如下所示:
PS chapter10\trainer\bin\Debug\netcoreapp3.0> .\chapter10.trainer.exe TrainingFileName ..\..\..\..\Data\trainingURLList.csv TestingFileName ..\..\..\..\Data\testingURLList.csv
Attempting to pull HTML from https://www.google.com, false
Attempting to pull HTML from https://www.bing.com, false
Attempting to pull HTML from https://www.microsoft.com, false
Attempting to pull HTML from https://www8.hp.com/us/en/home.html, false
Attempting to pull HTML from https://dasmalwerk.eu, true
Attempting to pull HTML from http://vxvault.net, true
Attempting to pull HTML from https://www.tmz.com, true
Attempting to pull HTML from http://openmalware.org, true
Failed to pull HTTP Content from http://openmalware.org
Attempting to pull HTML from https://www.dell.com, false
Attempting to pull HTML from https://www.lenovo.com, false
Attempting to pull HTML from https://www.twitter.com, false
Attempting to pull HTML from https://www.reddit.com, false
Attempting to pull HTML from https://www.tmz.com, true
Attempting to pull HTML from https://www.cnn.com, true
Attempting to pull HTML from https://www.usatoday.com, true
  1. 根据第 1 步的样本和测试数据导出,运行应用程序以训练模型,如下所示:
PS chapter10\trainer\bin\Debug\netcoreapp3.0> .\chapter10.trainer.exe ModelFileName webcontentclassifier.mdl Action TRAINING TrainingFileName ..\..\..\..\Data\sampledata.csv TestingFileName ..\..\..\..\Data\testdata.csv
Entropy: 0.9852281360342516
Log Loss: 0.7992317560011841
Log Loss Reduction: 0.18878508766684401

随意修改值并查看预测如何根据模型训练的数据集而变化。从这个点开始,一些实验的领域可能包括:

  • 调整在随机双坐标上升SDCA)算法的Trainer类上审查的超参数,例如MaximumNumberOfIterations,以查看精度如何受到影响。

  • 除了简单地使用 HTML 内容外,还可以添加新的特征——例如连接类型或脚本数量。

  • 在训练和样本集中添加更多变化,以获得良性和恶意内容的更好采样。

为了方便,GitHub 仓库在Data文件夹中包含了以下所有数据文件:

  • testdata.csvsampledata.csv特征提取的 CSV 文件

  • testingURLList.csvtrainingURLList.csv URL 列表 CSV 文件

运行浏览器应用程序

现在我们已经训练了模型,我们可以运行我们的桌面应用程序并测试模型的功效。要运行示例,确保chapter10_app是启动应用程序,然后按F5。在启动我们的浏览器应用程序后,输入www.google.com,如下面的截图所示:

图片

注意前一个截图下方网页内容下的状态栏,它显示了运行模型后的恶意百分比。接下来,在浏览器中输入dasmalwerk.eu(这是一个默认训练 URL 列表预先分类为恶意网站的网站),注意强制重定向,如下面的截图所示:

图片

随意尝试你机器上的各种文件以查看置信度分数,如果你收到一个误报,可能需要向模型添加更多功能以纠正分类。

改进想法的额外建议

现在我们已经完成了深入探讨,还有一些额外的元素可能可以进一步增强应用程序。这里讨论了一些想法。

单次下载优化

目前,当在WebView UWP 控件中输入新 URL 或更改页面时,导航会暂停,直到可以进行分类。当这种情况发生时——正如我们之前详细说明的——使用HtmlAgilityPack库,我们会下载并提取文本。如果页面被认为很干净(正如你可能会在大多数情况下遇到的那样),我们实际上会下载内容两次。这里的优化是将文本存储在应用程序的沙盒存储中,一旦完成分类,然后将WebView对象指向该存储内容。此外,如果采用这种方法,添加一个清除后台工作线程以删除旧数据,这样你的最终用户就不会最终拥有几个 GB 的网页内容。

日志记录

就像我们在上一章深入探讨日志记录时一样,添加日志对于远程了解桌面应用程序发生错误的时间可能是至关重要的。与上一章中的 Web 应用程序不同,你的错误更有可能是服务器端的,并且可以远程访问,而你的桌面应用程序可以安装在 Windows 10 的任何配置上,几乎有无限多的排列组合。正如之前提到的,使用 NLog (nlog-project.org/) 或类似的开源项目进行日志记录,并配合远程日志解决方案如 Loggly,以便你能从用户的机器上获取错误数据。鉴于通用数据保护条例GDPR)和最近的加州消费者隐私法案CCPA),确保传达这一数据正在离开最终用户的机器的事实,并且不要将这些日志中包含个人信息。

利用数据库

用户通常会频繁访问相同的网站,因此将特定网站的 URL 的分类存储在本地数据库中,如 LiteDB (www.litedb.org/),将显著提高最终用户的性能。一种实现方法是将 URL 的 SHA256 哈希值作为键存储在本地,将分类作为值。从长远来看,你可以提供一个 Web URL 声誉数据库,将 URL 的 SHA256 哈希值发送到可扩展的云存储解决方案,如微软的 Cosmos DB。存储 URL 的 SHA256 哈希值可以避免你的最终用户对个人可识别信息和匿名性的任何疑问。

摘要

在本章的讨论过程中,我们深入探讨了构建一个生产就绪的 Windows 10 UWP 应用程序架构需要哪些要素,并以前几章的工作为基础。我们还创建了一个全新的网页分类 Windows 10 应用程序,利用了 ML.NET 中的SdcaLogisticRegression算法。最后,我们还讨论了一些进一步改进示例应用程序(以及一般的生产应用程序)的方法。

随着本章的结束,现实世界的应用部分也随之结束。本书的下一部分将包括敏捷生产团队中的通用机器学习实践,以及使用 TensorFlow 和Open Neural Network ExchangeONNX)模型扩展 ML.NET。在下一章中,我们将重点关注前者。

第四部分:扩展 ML.NET

本节解释了如何使用其他格式的预训练模型与 ML.NET 结合使用,包括 TensorFlow 和 ONNX。此外,一些章节还将涵盖如何进行大规模训练以及使用 DMTP 项目所获得的经验教训。

本节包含以下章节:

  • 第十一章, 训练和构建生产模型

  • 第十二章, 使用 TensorFlow 与 ML.NET

  • 第十三章, 使用 ONNX 与 ML.NET

第十一章:训练和构建生产模型

随着我们进入本书的最后一部分,本章提供了在生产环境中使用机器学习的概述。在本书的这一部分,你已经学习了 ML.NET 提供的各种算法,并且已经创建了一套三个生产级应用。在积累了所有这些知识之后,你可能会想:我如何立即创建下一个杀手级机器学习应用?在直接回答这个问题之前,本章将帮助你为这一旅程的下一步做好准备。正如在前几章中讨论和使用的,训练模型有三个主要组成部分:特征工程、样本收集和创建训练管道。在本章中,我们将重点关注这三个组成部分,扩展你如何成功创建生产级模型的思考过程,并提供一些建议工具,以便能够通过生产级训练管道重复这一成功。

在本章中,我们将讨论以下内容:

  • 调查特征工程

  • 获取训练和测试数据集

  • 创建你的模型构建管道

调查特征工程

正如我们在前几章中讨论的,特征是模型构建过程中最重要的组成部分——客观上也是最重要的组成部分。在处理一个新问题时,出现的主要问题是:你将如何解决这个问题?例如,在网络安全领域的一个常见攻击手段是使用隐写术。隐写术可以追溯到公元前 440 年,是一种在容器中隐藏数据的行为。这个容器包括绘画、填字游戏、音乐、图片等。在网络安全领域,隐写术用于在通常会被忽略的文件中隐藏恶意负载,例如图像、音频和视频文件。

查看以下食物篮子的图像。这张图像——使用在线隐写术工具创建——其中包含一个嵌入的消息;看看你是否能在以下图像中找到任何异常模式:

目前大多数工具都可以在复杂和纯色图像中隐藏内容,以至于作为最终用户的你甚至都不会注意到——正如前一个示例所示。

在这个场景继续进行的情况下,你现在可能需要回答的一个快速问题是:文件中是否包含其他文件格式?另一个需要考虑的因素是问题的范围。尝试回答上述问题可能会导致对每个使用递归解析器分析的文件格式进行耗时深入分析——这并不是一开始就要解决的问题。更好的选择是将问题范围缩小到可能只分析音频文件或图像文件。进一步思考这个思路,让我们将问题范围缩小到特定的图像类型和有效载荷类型。

嵌入可执行文件的 PNG 图像文件

让我们深入探讨这个更具体的问题:我们如何在便携式网络图形PNG)文件中检测 Windows 可执行文件?对于那些好奇的人来说,选择 PNG 文件的具体原因在于,由于它们具有极高的图像质量与文件大小的比率,PNG 文件是一种非常常见的无损图像格式,被广泛应用于视频游戏和互联网。这种广泛的使用为攻击者提供了一个机会,他们可以在你的机器上获取 PNG 文件,而你作为最终用户甚至不会多想,这与专有格式或 Windows可执行文件EXE)不同,这很可能会引起最终用户的警觉。

在下一节中,我们将把 PNG 文件分解成以下步骤:

图片

要进一步深入了解 PNG 文件格式,PNG 的规范可以在以下链接找到:libpng.org/pub/png/spec/1.2/PNG-Contents.html

创建一个 PNG 解析器

现在,让我们深入探讨将 PNG 文件格式分解成特性,以便为检测隐藏的有效载荷驱动潜在的模型。PNG 文件由连续的 chunks 组成。每个 chunk 由一个头部描述字段和一个数据有效载荷组成。PNG 文件所需的 chunks 包括IHDRIDATIEND。根据规范,这些部分必须按此顺序出现。以下将解释每个部分。

在 chunks 之前的第一要素是实施检查,以确保文件实际上是一个 PNG 图像文件。这个检查通常被称为文件魔数检查。在我们数字世界的绝大多数文件都有一个独特的签名,这使得这些文件的解析和保存变得更加容易。

对于那些对其他文件格式签名好奇的人来说,一个详尽的列表可以在以下链接找到:www.garykessler.net/library/file_sigs.html

PNG 文件特别以以下字节开始:

137, 80, 78, 71, 13, 10, 26, 10

通过使用这些文件魔数字节,我们可以利用.NET 的SequenceEqual方法来比较文件数据的第一序列字节,如下面的代码所示:

using var ms = new MemoryStream(data);

byte[] fileMagic = new byte[FileMagicBytes.Length];

ms.Read(fileMagic, 0, fileMagic.Length);

if (!fileMagic.SequenceEqual(FileMagicBytes))
{
     return (string.Empty, false, null);
}

如果SequenceEqual方法与FileMagicBytes属性比较不匹配,我们返回 false。在这种情况下,该文件不是 PNG 文件,因此我们想要停止进一步解析文件。

从这个点开始,我们现在将遍历文件的块。在任何时候,如果字节没有正确设置,应该注意这一点,因为 Microsoft Paint 或 Adobe PhotoShop 会按照 PNG 文件格式的规范保存文件。另一方面,恶意生成者可能会像这里展示的那样,扭曲遵守 PNG 文件格式规范的规则:

while (ms.Position != data.Length)
{
    byte[] chunkInfo = new byte[ChunkInfoSize];

    ms.Read(chunkInfo, 0, chunkInfo.Length);

    var chunkSize = chunkInfo.ToInt32();

    byte[] chunkIdBytes = new byte[ChunkIdSize];

    ms.Read(chunkIdBytes, 0, ChunkIdSize);

    var chunkId = Encoding.UTF8.GetString(chunkIdBytes);

    byte[] chunk = new byte[chunkSize];

    ms.Read(chunk, 0, chunkSize);

    switch (chunkId)
    {
        case nameof(IHDR):
            var header = new IHDR(chunk);

            // Payload exceeds length
            if (data.Length <= (header.Width * header.Height * MaxByteDepth) + ms.Position)
            {
                break;
            }

            return (FileType, false, new[] { "SUSPICIOUS: Payload is larger than what the size should be" });
        case nameof(IDAT):
            // Build Embedded file from the chunks
            break;
        case nameof(IEND):
            // Note that the PNG had an end
            break;
    }
}

对于每个块,我们读取ChunkInfoSize变量,它定义为 4 字节。一旦读取了这个ChunkInfoSize数组,它就包含了要读取的块的大小。除了确定我们要读取的块类型外,我们还读取了 4 字节的块,用于 4 个字符的字符串(IHDRIDATIEND)。

一旦我们有了块大小和类型,我们就构建每个类的对象表示。在这个代码示例的范围内,我们将只查看 IHDR 类的片段,它包含高级图像属性,如尺寸、位深度和压缩:

public class IHDR
{
    public Int32 Width;

    public Int32 Height;

    public byte BitDepth;

    public byte ColorType;

    public byte Compression;

    public byte FilterMethod;

    public byte Interlace;

    public IHDR(byte[] data)
    {
        Width = data.ToInt32();

        Height = data.ToInt32(4);
    }
}

我们只提取WidthHeight属性,它们是前 8 个字节(每个 4 字节)。在这个例子中,我们还使用了一个扩展方法将字节数组转换为Int32数组。在大多数情况下,BitConverter会是理想的选择,然而,在这个代码示例中,我想简化数据的顺序访问,例如在检索前面提到的Height属性时偏移 4 字节。

之前提到的 IDAT 块是实际图像数据——以及可能包含嵌入有效载荷的块。IEND,正如其名所示,只是告诉 PNG 解析器文件已完整,也就是说,IEND 块中没有有效载荷。

一旦文件被解析,我们返回文件类型(PNG)——无论它是否是一个有效结构的 PNG 文件——并且我们注意任何可疑之处,例如如果文件大小远远大于根据宽度、高度和最大位深度(24)应有的大小。对于这些注释中的每一个,它们都可以在生产模型中与有效/无效标志一起标准化。此外,这些可以用简单的枚举表示数字。

对于那些对完整应用程序的源代码感到好奇的人,请参阅github.com/jcapellman/virus-tortoise,它利用了在第九章“使用 ML.NET 与 ASP.NET Core”部分中展示的创建文件分类应用程序中展示的许多相同原则。

将这个例子进一步扩展,迭代包含实际图像数据——以及可能的可执行有效载荷的 IDAT 块,将完成生产应用程序中的提取器。

现在我们已经看到了构建生产级功能所需的工作量,让我们深入探讨构建生产级训练数据集。

获取训练和测试数据集

现在我们已经完成了关于特征工程讨论,下一步是获取一个数据集。对于某些问题,这可能非常困难。例如,当试图预测别人没有做过的事情,或者在一个新兴领域时,拥有一个用于训练的训练集会比我们之前的例子中寻找恶意文件更困难。

另一个需要考虑的方面是多样性和数据是如何划分的。例如,考虑您如何使用 ML.NET 提供的异常检测训练器根据行为分析预测恶意 Android 应用程序。当思考构建您的数据集时,我敢说,大多数 Android 用户,他们的应用程序中恶意软件的比例不会超过一半。因此,训练和测试集的恶意和良性(50/50)划分可能会过度拟合恶意应用程序。弄清楚和分析您的目标用户将遇到的实际代表性是至关重要的,否则您的模型可能会倾向于假阳性或假阴性,这两种情况您的最终用户都不会满意。

在训练和测试数据集时需要考虑的最后一个元素是如何获取您的数据集。由于您的模型主要基于训练和测试数据集,因此找到代表您问题集的真实数据集至关重要。以之前的隐写术为例,如果您没有验证就随机抽取 PNG 文件,那么训练一个基于不良数据的模型是有可能的。对此的一种缓解措施是检查 IDAT 块中的隐藏有效载荷。同样,在 PNG 示例中对实际文件进行验证也是至关重要的。当您只针对生产应用中的 PNG 文件运行时,在 PNG 文件中混合训练 JPG、BMP 或 GIF 文件可能会导致假阳性或假阴性,因为其他图像格式的二进制结构不同于 PNG,这种非代表性数据将使训练集偏向于不受支持的格式。

对于网络安全领域的人来说,VirusTotal (www.virustotal.com) 和 Reversing Labs (www.reversinglabs.com) 提供了广泛的文件数据库,您可以付费下载,如果难以获取各种文件类型的数据源。

创建您的模型构建管道

一旦创建了特征提取器并获取了数据集,接下来要建立的是模型构建管道。模型构建管道的定义可以在以下图中更好地展示:

在下一节中,我们将讨论每个步骤如何与您选择的管道相关联。

讨论在管道平台中需要考虑的属性

有很多管道工具可用于本地部署,无论是在云中还是在SaaS软件即服务)服务中。我们将审查行业中一些更常用的平台。然而,以下是一些无论你选择哪个平台都需要记住的要点:

  • 速度对于多个原因来说都很重要。在构建你的初始模型时,迭代的速度非常重要,因为你很可能会调整你的训练集和超参数,以测试各种组合。在流程的另一端,当你处于预生产或生产阶段时,与测试人员或客户(他们正在等待新模型以解决问题或添加功能)迭代的速度在大多数情况下是关键的。

  • 可重复性同样重要,以确保在给定相同的训练数据集、特征和超参数的情况下,每次都能重建一个完美的模型。尽可能多地利用自动化是避免模型训练中人为错误的一个方法,同时也有助于提高可重复性。下一节将要审查的所有平台都提倡在启动新的训练会话后,无需任何人工输入即可定义流程。

  • 版本控制和比较跟踪对于确保在做出更改时能够进行比较非常重要。例如,无论是超参数——比如 FastTree 模型中树的深度——还是你添加的额外样本,在迭代过程中跟踪这些变化至关重要。假设你进行了一次文档化的更改,而你的效果显著下降,你总是可以回过头来评估这一变化。如果你没有为比较版本化或记录你的个人更改,这种简单的更改可能很难确定效果下降的原因。跟踪的另一个要素是跟踪一段时间内的进度,比如按季度或按年。这种级别的跟踪可以帮助描绘一幅图景,也可以帮助推动下一步行动或跟踪效果的趋势,以便获取更多样本或添加更多特征。

  • 最后,质量保证对于多个原因来说都很重要,在几乎所有情况下,对项目的成功或失败都至关重要。想象一下,一个模型直接部署到生产环境中,没有任何由专门的质量保证团队进行的额外检查,包括手动和自动测试。自动测试——就像一组单元测试,确保在发布和进入生产之前,样本在模型之间测试相同或更好——可以是一个好的临时解决方案,而不是一个完整的自动测试套件,该套件具有特定的效果范围,需要保持在其中。

在执行上一节中讨论的模型构建管道的每个步骤时,应考虑这四个要素。交付的最后一步取决于前三个要素的正确完成。实际的交付取决于您的应用程序。例如,如果您正在创建一个 ASP.NET 应用程序,例如我们在第九章中创建的应用程序,使用 ML.NET 与 ASP.NET Core,将 ML.NET 模型作为 Jenkins 管道的一部分添加——这样它就会自动与您的部署捆绑在一起——将是一个好的方法。

探索机器学习平台

以下是我个人使用过,以及/或者同事使用过以解决各种问题的平台。每个平台都有其优缺点,尤其是在我们试图解决的每个问题的独特性方面。

Azure Machine Learning

微软的 Azure 云平台提供了一个完整的平台,包括 Kubernetes、虚拟机和数据库,以及提供机器学习平台。该平台提供了直接连接到 Azure SQL 数据库、Azure 文件存储和公共 URL 等,仅举几个例子,用于训练和测试集。在 Visual Studio Community 2019 中提供了一个免费的不具备扩展性的轻量级版本。以下截图显示了完整的用户界面:

图片

此外,还完全支持非.NET 技术,如 TensorFlow、PyTorch 和 scikit-learn。流行的 Jupyter Notebook 和 Azure Notebook 等工具也完全支持。

与 Apache Airflow 类似,在 Azure 机器学习中比较运行历史记录以比较版本也很容易做到。

所有上述模型构建管道的阶段都得到了支持。以下是 Azure 机器学习的优缺点:

优点:

  • 与多个数据源广泛集成

  • ML.NET 原生支持

  • 根据您的需求进行扩展和缩减

  • 无需基础设施设置

缺点:

  • 训练时可能很昂贵

Apache Airflow

Apache Airflow,一款开源软件,提供了创建几乎无限复杂性的管道的能力。虽然不是原生支持的框架,但.NET Core 应用程序——例如本书中创建的那些——可以在安装.NET Core 运行时或简单地使用自包含标志编译的情况下运行。虽然学习曲线高于微软的 Azure 机器学习平台,但在某些场景下免费,尤其是在仅仅进行实验时,可能更有益。以下截图显示了 Airflow 的用户界面:

图片

与 Azure 机器学习类似,管道的可视化确实使得特定管道的配置比 Apache Spark 更容易。然而,与 Apache Spark 类似,设置和配置(取决于你的技能水平)可能相当令人望而却步,尤其是在 pip 安装之后。一个更容易上手的方法是使用预构建的 Docker 容器,例如 Puckel 的 Docker 容器(hub.docker.com/r/puckel/docker-airflow)。

下面是 Apache Airflow 的一些优缺点:

优点:

  • 免费且开源

  • 提供了 4 年以上的文档和示例

  • 在 Windows、Linux 和 macOS 上运行

缺点:

  • 设置复杂(尤其是使用官方 pip 说明)

  • .NET 不是原生支持的

Apache Spark

Apache Spark,作为另一个开源工具,虽然通常用于大数据管道,但也可以配置用于特征提取、训练和大规模模型的生成。例如,当内存和 CPU 限制阻碍你构建模型时,比如使用大规模数据集进行训练,我亲眼看到 Apache Spark 可以扩展到利用多个 64C/128T AMD 服务器,并最大化超过 1TB 的 RAM。我发现这个平台比 Apache Airflow 或 Azure 的机器学习平台设置起来更困难,但一旦设置好,它就可以非常强大。以下截图显示了 Apache Spark 的用户界面:

图片

可以在微软的 Apache Spark 页面(dotnet.microsoft.com/learn/data/spark-tutorial/intro)上找到优秀的分步安装指南,适用于 Windows 和 Linux。这个指南确实消除了许多未知因素,然而,与 Azure 或 Airflow 相比,它仍然远非容易上手。以下是 Apache Spark 的一些优缺点:

优点:

  • 免费且开源

  • 来自微软的.NET 绑定

  • 由于其悠久的历史(超过 5 年),有大量的文档

  • 在 Windows、macOS 和 Linux 上运行

缺点:

  • 配置和启动可能比较困难

  • 对 IT 基础设施变化敏感

微软为 Apache Spark 编写了.NET 绑定,并将其免费发布:dotnet.microsoft.com/apps/data/spark。这些绑定适用于 Windows、macOS 和 Linux。

摘要

在本章的整个过程中,我们深入探讨了从原始目的问题到训练模型的生产就绪模型训练所涉及的内容。通过这次深入探讨,我们检查了创建详细特征所需的工作量,这些特征是通过生产思维过程和特征工程实现的。然后,我们回顾了挑战、解决训练的方法以及如何测试数据集问题。最后,我们还深入探讨了实际模型构建管道的重要性,使用完全自动化的流程。

在下一章中,我们将在一个 WPF 应用程序中使用预构建的 TensorFlow 模型来确定提交的图像是否包含某些对象。这次深入探讨将介绍 ML.NET 如何为 TensorFlow 模型提供一个易于使用的接口。

第十二章:使用 TensorFlow 与 ML.NET

在本章中,我们将使用预训练的 TensorFlow 模型,特别是 Inception 模型,并将其集成到Windows Presentation Foundation(WPF)应用程序中。我们将使用预训练的模型并应用迁移学习,通过添加一些食物和水的图片。在迁移学习完成后,我们允许用户选择他们自己的图片。到本章结束时,你应该对将 TensorFlow 模型集成到你的 ML.NET 应用程序中所需的内容有一个牢固的掌握。

本章将涵盖以下主题:

  • 拆解谷歌的 Inception 模型

  • 创建图像分类桌面应用程序

  • 探索额外的生产应用程序增强功能

拆解谷歌的 Inception 模型

谷歌的 Inception 模型(github.com/google/inception)已经在数百万张图像上进行了训练,以帮助解决我们社会中日益增长的问题之一——我的图像中有什么?想要回答这个问题的应用程序类型范围很广,从匹配人脸、自动检测武器或不希望的对象、游戏图片中的运动品牌(如运动鞋品牌),到为用户提供无需手动标签即可搜索的支持的图像归档器,仅举几例。

这类问题通常用物体识别来回答。你可能已经熟悉的一种物体识别应用是光学字符识别(OCR)。OCR 是指字符图像可以被解释为文本,例如在微软的 OneNote 手写笔迹到文本功能中,或在读取车牌的收费站中。我们将特别关注的物体识别的特定应用称为图像分类

Inception 模型通过使用深度学习来分类图像来帮助解决这个问题。该模型在数百万张图像上以监督方法进行了训练,输出是一个神经网络。这种方法的优点是,预构建的模型可以通过添加较小子集的图像来增强,这就是我们在本章下一节将要做的。这种添加额外数据和标签的方法称为迁移学习。当创建特定于客户模型的时,这种方法也可能很有帮助。

想象一下就像在 GitHub 的 master 分支中创建一个分支;你可能只想添加一个类或修改一个元素,而不必重新创建整个代码库。至于模型,以汽车图像分类器为例。假设你获得了数百万张涵盖美国和外国汽车、卡车、面包车等图片。一位新客户来找你,要求你创建一个模型来帮助监控进入政府设施的车辆。之前的模型不应该被丢弃,也不需要完全重新训练,只需添加更多带有标签的商业(或可能是军事)车辆即可。

对于更深入地了解 Google 的图像分类,一个很好的资源是他们的开发者文档,可以在developers.google.com/machine-learning/practica/image-classification/找到。

创建 WPF 图像分类应用程序

如前所述,我们将要创建的应用程序是一个图像分类应用程序,具体允许用户选择一张图片并确定它是食物还是水。这是通过上述和包含的预训练 TensorFlow Inception 模型实现的。当应用程序第一次运行时,ML.NET 版本的模型使用图片和tags.tsv文件(将在下一节中讨论)进行训练。

与前几章一样,完成的项目代码、样本数据集和项目文件可以在此处下载:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter12.

探索项目架构

在本章中,我们将深入探讨一个 WPF 桌面应用程序。正如本章第一部分所提到的,我们将使用 WPF 框架来创建我们的应用程序。你可能会问,为什么不使用 UWP 应用程序,比如我们在第十章中创建的浏览器应用程序?至少在撰写本文时,原因在于 TensorFlow 在 UWP 应用程序中,特别是对于图像分类,并不完全受支持。也许在 ML.NET 的未来版本中,这将被添加。对于其他非图像类应用程序,你可能在 UWP 应用程序中使用 TensorFlow。

那些之前进行过 WPF 开发并且仔细观察的人会注意到,该项目使用了.NET Core 3.1。在.NET Core 3.0 中,Microsoft 添加了对 WPF 和 WinForms 的支持,因此,你不再仅限于 Windows 的.NET Framework 进行 GUI 开发。相反,这种支持是通过Microsoft.WindowsDesktop.App.WPF NuGet 包添加的。

在这个例子中,我们将使用Microsoft.ML (1.3.1) NuGet 包——以及几个额外的 NuGet 包——以便在我们的.NET 应用程序中利用 TensorFlow。以下包括以下内容:

  • Microsoft.ML.ImageAnalytics (1.3.1)

  • Microsoft.ML.TensorFlow (1.3.1)

  • SciSharp.TensorFlow.Redist (1.14.0)

到你阅读这个的时候,可能已经有这些包的新版本,它们应该可以工作,然而,上面提到的版本是我们将在这次深入研究中使用,以及 GitHub 仓库中可用的版本。

在以下屏幕截图中,您将找到解决方案的 Visual Studio Solution Explorer 视图。由于 TensorFlow 对项目类型和 CPU 目标的要求更为严格,我们回到了一个单一的项目,而不是在前面几章中使用过的三个项目架构:

图片

tags.tsv文件(位于代码仓库中的assets\images文件夹中)包含八行,这些行将包含的图像映射到预分类:

ChickenWings.jpg food
Steak.jpg food
Pizza.jpg food
MongolianGrill.jpg food
Bay.jpg water
Bay2.jpg water
Bay3.jpg water
Beach.jpg water

如果你想尝试自己的分类,请删除包含的图像,复制你的图像,并更新tags.tsv文件以包含标签。我应该注意的是,所有包含的图像都是我在各种加州度假时拍摄的——请随意使用。

assets/inception文件夹中的文件包含所有 Google 预训练文件(以及许可文件)。

深入 WPF 图像分类应用程序

如在开头部分所述,我们的桌面应用程序是一个 WPF 应用程序。在本例的范围内,如第十章中所述,使用 ML.NET 与 UWP,我们通过遵循模型-视图-视图模型MVVM)设计模式来使用标准方法处理应用程序架构。

在本节中我们将深入研究的文件如下:

  • MainWindowViewModel

  • MainWindow.xaml

  • MainWindow.xaml.cs

  • BaseML

  • ImageDataInputItem

  • ImageDataPredictionItem

  • ImageClassificationPredictor

WPF 项目中的其余文件都未从默认的 Visual Studio .NET Core 3.1 WPF 应用程序模板中进行修改;例如,App.xamlAssemblyInfo.cs文件。

MainWindowViewModel

MainWindowViewModel类的目的是包含我们的业务逻辑并控制视图,如下所示:

  1. 我们首先实例化之前讨论过的ImageClassificationPredictor类,以便它可以用于运行预测:
private readonly ImageClassificationPredictor _prediction = new ImageClassificationPredictor();
  1. 下一个代码块处理了 MVVM 对分类字符串的强大功能,并存储选定的图像。对于这些属性中的每一个,当值发生变化时,我们调用OnPropertyChanged,这触发了视图与这些属性绑定的任何字段的绑定刷新:
private string _imageClassification;

public string ImageClassification
{
    get => _imageClassification;

    set
    {
        _imageClassification = value;
        OnPropertyChanged();
    }
}

private ImageSource _imageSource;

public ImageSource SelectedImageSource
{
    get => _imageSource;

    set
    {
        _imageSource = value;
        OnPropertyChanged();
    }
}
  1. 接下来,我们定义 Initialize 方法,它调用预测器的 Initialize 方法。该方法将返回一个元组,表示模型无法加载或找不到,以及异常(如果抛出):
public (bool Success, string Exception) Initialize() => _prediction.Initialize();
  1. 然后,我们处理用户点击选择图片按钮时会发生什么。该方法打开一个对话框,提示用户选择一个图片。如果用户取消对话框,则方法返回。否则,我们调用两个辅助方法将图片加载到内存中并对图片进行分类:
public void SelectFile()
{
    var ofd = new OpenFileDialog
    {
        Filter = "Image Files(*.BMP;*.JPG;*.PNG)|*.BMP;*.JPG;*.PNG"
    };

    var result = ofd.ShowDialog();

    if (!result.HasValue || !result.Value)
    {
        return;
    }

    LoadImageBytes(ofd.FileName);

    Classify(ofd.FileName);
}
  1. LoadImageBytes 方法接受文件名并将图片加载到我们的基于 MVVM 的 ImageSource 属性中,因此选择后,图片控件会自动更新为所选图片的视图:
private void LoadImageBytes(string fileName)
{
    var image = new BitmapImage();

    var imageData = File.ReadAllBytes(fileName);

    using (var mem = new MemoryStream(imageData))
    {
        mem.Position = 0;

        image.BeginInit();

        image.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
        image.CacheOption = BitmapCacheOption.OnLoad;
        image.UriSource = null;
        image.StreamSource = mem;

        image.EndInit();
    }

    image.Freeze();

    SelectedImageSource = image;
}
  1. 最后,Classify 方法接受路径并将其传递给 Predictor 类。在返回预测后,分类和置信度将集成到我们的 MVVM ImageClassification 属性中,因此 UI 会自动更新:
public void Classify(string imagePath)
{
 var result = _prediction.Predict(imagePath);

 ImageClassification = $"Image ({imagePath}) is a picture of {result.PredictedLabelValue} with a confidence of {result.Score.Max().ToString("P2")}";
}

MainWindowViewModel 类的最后一个元素是我们之前在第十章 使用 ML.NET 与 UWP 中定义的相同的 OnPropertyChanged 方法,它允许 MVVM 魔法发生。定义了我们的 ViewModel 类后,让我们继续到 MainWindow XAML 文件。

MainWindow.xaml

如在第十章 分解 UWP 架构 部分的 使用 ML.NET 与 UWP 节中所述,当描述开发时,XAML 标记用于定义用户界面。对于本应用程序的范围,我们的 UI 相对简单:ButtonImage ControlTextBlock

现在我们来看看代码:

  1. 我们首先定义的是我们的网格。在 XAML 中,网格是一个类似于网络开发中的 <div> 的容器。然后我们定义我们的行。与 Bootstrap 类似(但在我看来更容易理解),是预先定义每行的高度。将行设置为 Auto 将自动调整高度以适应内容的高度,而星号则表示根据主容器的高度使用所有剩余的高度:
<Grid.RowDefinitions>
    <RowDefinition Height="Auto" />
    <RowDefinition Height="*" />
    <RowDefinition Height="Auto" />
</Grid.RowDefinitions>
  1. 我们首先定义我们的 Button 对象,它将在我们的 ViewModel 类中触发上述的 SelectFile 方法:
<Button Grid.Row="0" Margin="0,10,0,0" Width="200" Height="35" Content="Select Image File" HorizontalAlignment="Center" Click="btnSelectFile_Click" />
  1. 我们接着定义我们的 Image 控件,它绑定到我们之前审查过的 SelectedImageSource 属性,该属性位于我们的 ViewModel 类中:
<Image Grid.Row="1" Margin="10,10,10,10" Source="{Binding SelectedImageSource}" />
  1. 我们接着添加将显示我们分类的 TextBlock 控件:
<TextBlock Grid.Row="2" Text="{Binding ImageClassification, Mode=OneWay}" TextWrapping="Wrap" Foreground="White" Margin="10,10,10,10" HorizontalAlignment="Center" FontSize="16" />

在定义了视图的 XAML 方面之后,现在让我们深入了解 MainWindow 类的代码部分。

MainWindow.xaml.cs 文件

MainWindow.xaml.cs 文件包含 XAML 视图的代码,相关内容在此讨论:

  1. 我们首先定义的是围绕 DataContext 属性的包装属性,该属性是基 Window 类中内置的:
private MainWindowViewModel ViewModel => (MainWindowViewModel) DataContext;
  1. 接下来,我们定义MainWindow的构造函数,以便将DataContext属性初始化为我们的MainWindowViewModel对象。如果初始化失败,我们不希望应用程序继续运行。此外,我们需要使用MessageBox对象让用户知道为什么它失败了:
public MainWindow()
{
    InitializeComponent();

    DataContext = new MainWindowViewModel();

    var (success, exception) = ViewModel.Initialize();

    if (success)
    {
        return;
    }

    MessageBox.Show($"Failed to initialize model - {exception}");

    Application.Current.Shutdown();
}
  1. 最后,我们调用 ViewModel 的SelectFile方法来处理图像选择和分类:
private void btnSelectFile_Click(object sender, RoutedEventArgs e) => ViewModel.SelectFile();

在我们完成了MainWindow类的代码之后,这就完成了 WPF 组件。现在让我们专注于示例中的机器学习部分。

基础 ML 类

BaseML类,在大多数之前的示例中都被使用,暴露了我们的 ML.NET 类的基础类。在这个示例中,由于使用了预训练模型,我们实际上简化了类。现在这个类只是初始化了MLContext属性:

public class BaseML
{
    protected MLContext MlContext;

    public BaseML()
    {
        MlContext = new MLContext(2020);
    }
}

在审查了简化的BaseML类之后,让我们深入到ImageDataInputItem类。

图像数据输入项类

ImageDataInputItem类包含了传递给模型的类;基本属性是ImagePath属性:

public class ImageDataInputItem
{
    [LoadColumn(0)]
    public string ImagePath;

    [LoadColumn(1)]
    public string Label;
}

尽管比我们的大多数输入类小,Inception 模型只需要两个属性。现在,让我们深入到被称为ImageDataPredictionItem的输出类。

图像数据预测项类

ImageDataPredictionItem类包含了预测响应,包括预测值字符串的置信度(在包含的图像中包含WaterFood):

public class ImageDataPredictionItem : ImageDataInputItem
{
    public float[] Score;

    public string PredictedLabelValue;
}

与输入类类似,输出类也只有两个属性,类似于之前的示例。在处理完输入和输出类之后,让我们深入到使用这些类进行迁移学习和预测的ImageClassificationPredictor类。

图像分类预测器类

ImageClassificationPredictor类包含了加载和针对 Inception TensorFlow 模型进行预测所需的所有代码:

  1. 首先,我们需要定义几个辅助变量来访问图像和.tsv文件:
// Training Variables
private static readonly string _assetsPath = Path.Combine(Environment.CurrentDirectory, "assets");
private static readonly string _imagesFolder = Path.Combine(_assetsPath, "images");
private readonly string _trainTagsTsv = Path.Combine(_imagesFolder, "tags.tsv");
private readonly string _inceptionTensorFlowModel = Path.Combine(_assetsPath, "inception", "tensorflow_inception_graph.pb");

private const string TF_SOFTMAX = "softmax2_pre_activation";
private const string INPUT = "input";

private static readonly string ML_NET_MODEL = Path.Combine(Environment.CurrentDirectory, "chapter12.mdl");
  1. 接下来,我们定义预训练的 Inception 模型所需的设置:
private struct InceptionSettings
{
    public const int ImageHeight = 224;
    public const int ImageWidth = 224;
    public const float Mean = 117;
    public const float Scale = 1;
    public const bool ChannelsLast = true;
}
  1. 接下来,我们创建我们的Predict方法,并重载它,它简单地接受图像文件路径。像之前的示例一样,我们通过调用我们的MLContext对象创建PredictionEngine,传入我们的输入类(ImageDataInputItem)和输出类(ImageDataPredictionItem),然后调用Predict方法来获取我们的模型预测:
public ImageDataPredictionItem Predict(string filePath) => 
    Predict(new ImageDataInputItem 
        {
            ImagePath = filePath 
        }
    );

public ImageDataPredictionItem Predict(ImageDataInputItem image)
{
    var predictor = MlContext.Model.CreatePredictionEngine<ImageDataInputItem, ImageDataPredictionItem>(_model);

    return predictor.Predict(image);
}

最后,我们使用自己的样本初始化并扩展我们的预训练模型:

public (bool Success, string Exception) Initialize()
{
    try
    {
        if (File.Exists(ML_NET_MODEL))
        {
            _model = MlContext.Model.Load(ML_NET_MODEL, out DataViewSchema modelSchema);

            return (true, string.Empty);
        }

       ...
    }
    catch (Exception ex)
    {
        return (false, ex.ToString());
    }
} 

对于完整代码,请参阅以下 GitHub 仓库链接:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/blob/master/chapter12/chapter12.wpf/ML/ImageClassificationPredictor.cs。完成 Initialize 方法后,代码深入探讨就结束了。现在让我们运行应用程序!

运行图像分类应用程序

由于我们使用的是预训练模型,因此可以直接从 Visual Studio 运行应用程序。运行应用程序后,您将看到一个几乎为空窗口:

图片

点击“选择图像文件”按钮并选择一个图像文件将触发模型运行。在我的情况下,我选择了一张最近去德国度假的照片,置信度得分为 98.84%:

图片

随意尝试您机器上的各种文件以查看置信度得分和分类——如果您开始注意到问题,请按照前面章节所述,向图像文件夹和 tags.tsv 文件中添加更多样本。在做出这些更改之前,请务必删除 chapter12.mdl 文件。

改进建议的额外想法

现在我们已经完成了深入探讨,还有一些额外的元素可能会进一步增强应用程序。这里讨论了一些想法。

基于最终用户输入的自训练

如本章开头所述,其中一个优点是能够在动态应用中利用迁移学习。与本书中已审查的先前示例应用不同,此应用实际上允许最终用户选择一系列(或文件夹)图像,并且只需进行少量代码更改,就可以构建新的 .tsv 文件并训练新的模型。对于 Web 应用或商业产品,这将提供很高的价值,同时也会减轻您获取各种类型图像的负担——这是一个令人畏惧的、很可能徒劳的目标。

日志记录

如第十章中提到的使用 ML.NET 与 UWP 的日志记录部分所述,桌面应用程序有其优点和缺点。最大的缺点是需要日志记录,因为您的桌面应用程序可以安装在从 Windows 7 到 Windows 10 的任何配置上,几乎有无限多的排列组合。如前所述,强烈建议使用 NLog (nlog-project.org/)或类似的开源项目进行日志记录,并结合远程日志解决方案,如 Loggly,以便从用户的机器上获取错误数据。鉴于 GDPR 和最近的 CCPA,我们需要确保离开最终用户机器的数据得到传达,并且这些日志不包含个人数据(或通过日志记录机制上传到远程服务器的实际图像)。

使用数据库

与第十章中性能优化建议使用 ML.NET 与 UWP 类似,如果用户多次选择相同的图像,尤其是在这个应用程序被用于自助终端或转换为 Web 应用程序的情况下,存储分类的性能优势可能相当显著。实现这一点的快速简单方法可能是对图像执行 SHA256 哈希,并检查该哈希值与数据库。根据用户数量以及他们是否将并发,我建议两种选择之一:

  • 如果用户逐个使用,并且应用程序将保持为 WPF 应用程序,那么推荐使用之前提到的轻量级数据库——LiteDB (www.litedb.org/)。

  • 如果您正在使用生产环境启动大型 Web 应用程序,那么为了确保数据库查找不会比重新执行模型预测慢,建议使用 MongoDB 或水平可扩展的数据库,如微软的 CosmosDB。

摘要

在本章的整个过程中,我们深入探讨了如何使用预训练的 TensorFlow 模型创建一个 WPF 应用程序。我们还回顾并仔细研究了谷歌的图像分类 Inception 模型。此外,我们学习了如何将这个模型集成到应用程序中,以便对用户选择的图像进行图像分类。最后,我们还讨论了一些进一步改进示例应用程序的方法。

在下一章和最后一章中,我们将专注于在 WPF 应用程序中使用预训练的 ONNX 模型进行目标检测。

第十三章:使用 ML.NET 与 ONNX

现在我们已经完成了使用 TensorFlow 和 ML.NET 的深入探索,现在是时候深入使用 Open Neural Network eXchangeONNX)与 ML.NET 的结合了。具体来说,在本章的最后一部分,我们将回顾 ONNX 是什么,以及创建一个名为 YOLO 的预训练 ONNX 模型的新示例应用程序。这个应用程序将基于上一章的内容,并显示模型检测到的对象的边界框。此外,我们将在本章结束时提出改进示例的建议,使其成为生产级应用程序或集成到生产应用程序中。

在本章中,我们将涵盖以下主题:

  • 解构 ONNX 和 YOLO

  • 创建 ONNX 物体检测应用程序

  • 探索额外的生产应用增强功能

解构 ONNX 和 YOLO

如 第一章 中所述,使用 ML.NET 开始机器学习之旅,ONNX 标准在业界被广泛认为是一个真正通用的格式,适用于各种机器学习框架。在接下来的两节中,我们将回顾 ONNX 提供的内容,以及将在本章示例中驱动的 YOLO 模型。

介绍 ONNX

ONNX 的创建是为了在处理预训练模型或跨框架训练模型时提供一个更开放和自由流动的过程。通过为框架提供一个开放的导出格式,ONNX 允许互操作性,从而促进了由于几乎每个框架中使用的专有格式的性质而可能难以进行的实验。

目前,支持的平台包括 TensorFlow、XGBoost 和 PyTorch——当然,还包括 ML.NET。

如果你想进一步深入了解 ONNX,请访问他们的网站:onnx.ai/index.html

YOLO ONNX 模型

基于 第十二章 中进行的操作,使用 ML.NET 的 TensorFlow,其中我们使用了预训练的 Inception 模型,在本章中,我们将使用预训练的 YOLO 模型。这个模型提供了非常快速和准确的对象检测,这意味着它可以在图像中找到多个对象,并具有一定的置信度。这与上一章提供的纯图像分类模型不同,例如水或食物。

为了帮助可视化两种模型之间的差异,可以将上一章的 TensorFlow 模型(用于分类水)与本章的汽车对象检测进行比较,如下面的截图所示:

图片

由于互联网上图像数量的显著增加以及安全需求,图像(和视频)中的目标检测需求不断增加。想象一下拥挤的环境,比如足球场,尤其是前门附近。保安巡逻并监控这个区域;然而,就像你一样,他们也是凡人,只能以一定程度的准确性瞥见那么多人。将机器学习中的目标检测实时应用于检测武器或大包,然后可以用来提醒保安追捕嫌疑人。

YOLO 模型本身有两种主要形式——小型和完整模型。在本例的范围内,我们将使用较小的模型(约 60 MB),它可以对图像中发现的 20 个对象进行分类。小型模型由九个卷积层和六个最大池化层组成。完整模型可以分类数千个对象,并且,在适当的硬件(特别是图形处理单元GPU))的支持下,可以比实时运行得更快。

以下图表展示了 YOLO 模型的工作原理(以及在一定程度上,神经网络):

图片

实际上,图像(或图像)被转换为 3 x 416 x 416 的图像。3 个组件代表红-绿-蓝RGB)值。416 个值代表调整大小后的图像的宽度和高度。这个输入层随后被输入到模型的隐藏层。对于我们在本章中使用的 Tiny YOLO v2 模型,在输出层之前共有 15 层。

要深入了解 YOLO 模型,请阅读这篇论文:arxiv.org/pdf/1612.08242.pdf

创建 ONNX 目标检测应用程序

如前所述,我们将创建的应用程序是一个使用预训练 ONNX 模型的目标检测应用程序。以我们在第十二章中开发的应用程序为起点,即使用 ML.NET 与 TensorFlow,我们将添加对模型分类已知对象时图像上叠加的边界框的支持。这种对公众的实用性在于图像目标检测提供的各种应用。想象一下,你正在为警察或情报社区的项目工作,他们有图像或视频,并希望检测武器。正如我们将展示的,利用 YOLO 模型与 ML.NET 将使这个过程变得非常简单。

与前几章一样,完整的项目代码、预训练模型和项目文件可以在此处下载:github.com/PacktPublishing/Hands-On-Machine-Learning-With-ML.NET/tree/master/chapter13

探索项目架构

在前几章中创建的项目架构和代码的基础上,我们将审查的架构得到了增强,使其更加结构化和便于最终用户使用。

如同一些前几章一样,如果您想利用 ONNX 模型进行对象检测,以下两个额外的 NuGet 包是必需的:

  • Microsoft.ML.ImageAnalytics

  • Microsoft.ML.OnnxTransformer

这些 NuGet 包已在包含的示例代码中引用。这些包的 1.3.1 版本在 GitHub 中的示例和本章的深入探讨中均使用。

在下面的屏幕截图中,您将找到项目的 Visual Studio 解决方案资源管理器视图。解决方案中添加了几个新内容,以方便我们针对的生产用例。我们将在本章后面的部分详细审查以下解决方案截图中的每个新文件:

由于当前 ML.NET 的限制,截至本文撰写时,ONNX 支持仅限于使用预存模型进行评分。本例中包含的预训练模型可在 assets/model 文件夹中找到。

深入代码

如前所述,对于这个应用程序,我们正在构建在 第十二章 完成的作品之上,使用 TensorFlow 与 ML.NET。虽然 用户界面UI) 没有太大变化,但运行 ONNX 模型的底层代码已经改变。对于每个更改的文件——就像前几章一样——我们将审查所做的更改及其背后的原因。

已更改或添加的类如下:

  • DimensionsBase

  • BoundingBoxDimensions

  • YoloBoundingBox

  • MainWindow.xaml

  • ImageClassificationPredictor

  • MainWindowViewModel

还有一个额外的文件,其中包含 YoloOutputParser 类。这个类是从 麻省理工学院MIT)许可的接口派生出来的,用于 TinyYOLO ONNX 模型。由于这个类的长度,我们不会对其进行审查;然而,代码易于阅读,如果您想逐步进行预测,流程将很容易跟随。

DimensionsBase

DimensionsBase 类包含坐标以及 HeightWidth 属性,如下面的代码块所示:

public class DimensionsBase
{
    public float X { get; set; }

    public float Y { get; set; }

    public float Height { get; set; }

    public float Width { get; set; }
}

这个基类被 YoloOutputParserBoundingBoxDimensions 类使用,以减少代码重复。

YoloBoundingBox 类

YoloBoundingBox 类提供了用于在生成叠加时填充边界框的容器类,如下面的代码块所示:

public class YoloBoundingBox
{
    public BoundingBoxDimensions Dimensions { get; set; }

    public string Label { get; set; }

    public float Confidence { get; set; }

    public RectangleF Rect => new RectangleF(Dimensions.X, Dimensions.Y, Dimensions.Width, Dimensions.Height);

    public Color BoxColor { get; set; }
}

此外,在同一个类文件中定义了我们的 BoundingBoxDimensions 类,如下面的代码块所示:

public class BoundingBoxDimensions : DimensionsBase { }

再次强调,这种继承用于减少代码重复。

MainWindow.xaml 文件

我们应用程序的可扩展应用程序标记语言XAML)视图已被简化为仅包含按钮和图像控件,如下面的代码块所示:

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <Button Grid.Row="0" Margin="0,10,0,0" Width="200" Height="35" Content="Select Image File" HorizontalAlignment="Center" Click="btnSelectFile_Click" />

    <Image Grid.Row="1" Margin="10,10,10,10" Source="{Binding SelectedImageSource}" />
</Grid>

此外,由于所选定的外接矩形和图像的性质,窗口默认设置为最大化,如下面的代码块所示:

<Window x:Class="chapter13.wpf.MainWindow"

        mc:Ignorable="d"
        ResizeMode="NoResize"
        WindowStyle="SingleBorderWindow"
        WindowState="Maximized"
        WindowStartupLocation="CenterScreen"
        Background="#1e1e1e"
        Title="Chapter 13" Height="450" Width="800">

在 XAML 更改完成后,现在让我们深入探讨修订后的ImageClassificationPredictor类。

ImageClassificationPredictor 类

ImageClassificationPredictor类,与第十二章中提到的类似,即使用 ML.NET 与 TensorFlow 结合使用,其中包含了运行图像预测的方法。在本章中,我们需要创建几个额外的类对象来支持 ONNX 模型的运行,具体如下:

  1. 首先,我们定义ImageNetSettings结构体,它定义了网络的宽度和高度。YOLO 模型需要使用 416 像素×416 像素,如下面的代码块所示:
public struct ImageNetSettings
{
    public const int imageHeight = 416;
    public const int imageWidth = 416;
}   
  1. 接下来,我们定义TinyYoloModelSettings结构体,用于与 ONNX 模型一起使用,如下所示:
public struct TinyYoloModelSettings
{
    public const string ModelInput = "image";

    public const string ModelOutput = "grid";
}
  1. 与前一章不同,在前一章中,TensorFlow 模型在第一次运行时被导入并导出为 ML.NET 模型,但截至本文写作时,ONNX 不支持该路径。因此,我们必须在Initialize方法中每次都加载 ONNX 模型,如下面的代码块所示:
public (bool Success, string Exception) Initialize()
{
    try
    {
        if (File.Exists(ML_NET_MODEL))
        {
            var data = MlContext.Data.LoadFromEnumerable(new List<ImageDataInputItem>());

            var pipeline = MlContext.Transforms.LoadImages(outputColumnName: "image", imageFolder: "", 
                    inputColumnName: nameof(ImageDataInputItem.ImagePath))
                .Append(MlContext.Transforms.ResizeImages(outputColumnName: "image", 
                    imageWidth: ImageNetSettings.imageWidth, 
                    imageHeight: ImageNetSettings.imageHeight, 
                    inputColumnName: "image"))
                .Append(MlContext.Transforms.ExtractPixels(outputColumnName: "image"))
                .Append(MlContext.Transforms.ApplyOnnxModel(modelFile: ML_NET_MODEL, 
                    outputColumnNames: new[] { TinyYoloModelSettings.ModelOutput }, 
                    inputColumnNames: new[] { TinyYoloModelSettings.ModelInput }));

            _model = pipeline.Fit(data);

            return (true, string.Empty);
        }

        return (false, string.Empty);
    }
    catch (Exception ex)
    {
        return (false, ex.ToString());
    }
}
  1. 接下来,我们广泛修改Predict方法以支持YoloParser调用,调用DrawBoundingBox方法来叠加外接矩形,然后返回更新后的图像的字节,如下所示:
public byte[] Predict(string fileName)
{
    var imageDataView = MlContext.Data.LoadFromEnumerable(new List<ImageDataInputItem> { new ImageDataInputItem { ImagePath = fileName } });

    var scoredData = _model.Transform(imageDataView);

    var probabilities = scoredData.GetColumn<float[]>(TinyYoloModelSettings.ModelOutput);

    var parser = new YoloOutputParser();

    var boundingBoxes =
        probabilities
            .Select(probability => parser.ParseOutputs(probability))
            .Select(boxes => parser.FilterBoundingBoxes(boxes, 5, .5F));

    return DrawBoundingBox(fileName, boundingBoxes.FirstOrDefault());
}

为了简洁起见,这里没有展示DrawBoundingBox方法。从高层次来看,原始图像被加载到内存中,然后模型的外接矩形被绘制在图像上,包括标签和置信度。然后,这个更新后的图像被转换为字节数组并返回。

MainWindowViewModel 类

MainWindowViewModel类内部,由于示例的性质,需要进行一些更改。我们在这里看看它们:

  1. 首先,LoadImageBytes方法现在只需将解析后的图像字节转换为Image对象,如下所示:
private void LoadImageBytes(byte[] parsedImageBytes)
{
    var image = new BitmapImage();

    using (var mem = new MemoryStream(parsedImageBytes))
    {
        mem.Position = 0;

        image.BeginInit();

        image.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
        image.CacheOption = BitmapCacheOption.OnLoad;
        image.UriSource = null;
        image.StreamSource = mem;

        image.EndInit();
    }

    image.Freeze();

    SelectedImageSource = image;
}
  1. 最后,我们修改Classify方法,在成功运行模型后调用LoadImageBytes方法,如下所示:
public void Classify(string imagePath)
{
    var result = _prediction.Predict(imagePath);

    LoadImageBytes(result);
}

针对Classify方法的更改已经实施,这标志着本章示例所需的代码更改已经完成。现在,让我们运行应用程序吧!

运行应用程序

运行应用程序的过程与第十二章中的示例应用程序相同,即使用 ML.NET 与 TensorFlow 结合使用。要从 Visual Studio 内部运行应用程序,只需单击工具栏中的播放图标,如图下所示:

图片

启动应用程序后,就像在第十二章使用 TensorFlow 与 ML.NET 中一样,选择一个图像文件,模型就会运行。例如,我选择了一张我在德国度假时拍摄的图片(注意汽车边界框),如下面的截图所示:

图片

随意尝试选择您硬盘上的图像,以查看检测的置信水平和边界框围绕对象的形成情况。

探索额外的生产应用增强

现在我们已经完成了深入探讨,还有一些额外的元素可以进一步增强应用程序。一些想法将在接下来的章节中讨论。

日志记录

如前所述,在桌面应用程序中强调日志记录的重要性是至关重要的。随着应用程序复杂性的增加,强烈建议使用 NLog (nlog-project.org/)或类似的开源项目进行日志记录。这将允许您以不同的级别将日志记录到文件、控制台或第三方日志解决方案,如 Loggly。例如,如果您将此应用程序部署给客户,将错误级别至少分解为调试、警告和错误,将有助于远程调试问题。

图像缩放

如您可能已注意到,对于相当大的图像(那些超出您的屏幕分辨率),在图像预览中对边界框进行文本标注和调整大小并不像 640 x 480 这样的图像那样容易阅读。在此方面的一个改进点可能是提供悬停功能,将图像调整到窗口的尺寸或动态增加字体大小。

利用完整的 YOLO 模型

此外,对于此示例的另一个改进点是在应用程序中使用完整的 YOLO 模型。正如之前在示例应用程序中使用的 Tiny YOLO 模型一样,只提供了 20 个标签。在生产应用程序或您希望构建的应用程序中,使用更大、更复杂的模型是一个不错的选择。

您可以在此处下载完整的 YOLO 模型:github.com/onnx/models/tree/master/vision/object_detection_segmentation/yolov3

摘要

在本章的整个过程中,我们深入探讨了 ONNX 格式的内容以及它为社区提供的功能。此外,我们还使用 ML.NET 中的预训练 Tiny YOLO 模型创建了一个全新的检测引擎。

伴随着这一点,您对 ML.NET 的深入研究也就此结束。从这本书的第一页到这一页,您可能已经逐渐了解到 ML.NET 提供的非常直接且功能丰富的抽象能力。由于 ML.NET(就像 .NET 一样)不断进化,ML.NET 的功能集和部署目标的发展也将毫无疑问,从嵌入式 物联网IoT)设备到移动设备。我希望这本书对您深入理解 ML.NET 和机器学习有所帮助。此外,我希望在您未来遇到问题时,您首先会考虑这个问题是否可以通过利用 ML.NET 来更高效甚至更好地解决问题。鉴于世界上的数据持续以指数级增长,使用非暴力/传统方法的需求只会继续增长,因此,从这本书中获得的知识和技能将帮助您多年。

posted @ 2025-09-03 10:09  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报