原文:annas-archive.org/md5/441a7eca6cdbd075d6fb97fab4a6bbb6

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在数据时代,很难忽视机器学习(ML)和数据科学的重要性。机器学习已经在许多行业中得到广泛应用,其采用率正在以比以往任何时候都要快的速度增长。不仅像谷歌、微软和苹果这样的大科技公司,而且像彭博社和高盛这样的非科技公司也在大量投资机器学习。从在搜索引擎上搜索今晚要吃什么,到获取新信用卡的批准,机器学习的应用无处不在,渗透到我们日常生活的方方面面。作为一名数据科学家和机器学习实践者,我无法强调数据时代,尤其是大数据时代,机器学习的重要性。

如果你正在寻找学习应用机器学习(ML)的资源,你来到了正确的地点。对于许多有志于成为数据科学家和机器学习实践者的人来说,在现有的机器学习书籍中,关于 C#应用机器学习的资源相对较少。你可以轻松找到详细解释机器学习背后理论的书籍。你也能轻松找到涉及不同编程语言(如 Python)中机器学习实际应用的书籍。然而,正如你可能已经注意到的,关于如何使用 C#构建实际机器学习模型和应用的书籍并不多。

在这本书中,我们将专注于机器学习的实际应用,并直接深入到构建各种现实世界项目的机器学习模型和应用。通过分析带有现实世界数据集的机器学习真实案例,你将了解其他数据科学家和机器学习实践者实际上是如何为他们的生产系统构建机器学习模型和应用的。这本书的独特之处在于,每一章都是一个具有现实世界商业用例的独立机器学习项目。

在这本书中,我们将选择 C#作为将要工作的机器学习项目的编程语言。你可能会问,“为什么是 C#?”答案其实非常简单。正如你可能已经知道的,C#是行业中最受欢迎和最广泛使用的语言之一。特别是在金融公司中,C#是少数几种被普遍接受和用于生产应用的编程语言之一。

个人而言,当我刚开始在数据科学领域职业生涯时,我非常需要像这样一本书。当时,学校里教的内容和现实生活中真正有效的方法(以及如何有效)之间存在差异。在这本书中,我想分享我不得不艰难学习到的知识和经验。在这本书中,我们将讨论一些不常被提及的话题,例如 ML 项目通常是如何开始的,ML 模型在不同行业中是如何构建和测试的,ML 应用是如何在生产系统中部署的,以及那些在生产系统中运行的 ML 模型是如何被监控和评估的。我们将在这本书的整个过程中一起努力,帮助你为未来可能遇到的任何 ML 项目做好准备。到这本书结束时,你将能够使用 C#构建稳健且性能良好的 ML 模型和应用。

这本书面向的对象

这本书是为那些知道如何使用 C#编写代码并且对 ML 有基本了解的人而写的。即使你对 ML 算法背后的理论没有深入了解,也不要担心!这是可以的。这本书将帮助你理解如何根据不同的用例使用不同的学习算法。如果你已经学习了 ML,也许是在学校、在线课程或数据科学训练营,那么这本书对你来说将是非常好的。这本书将通过九个真实的 ML 项目和使用真实数据集,向你展示如何实际应用你学到的 ML 理论和概念。如果你已经是 ML 从业者,你仍然可以从这本书中受益良多!通过研究各种实际应用的 ML 案例,这本书将帮助你扩展将 ML 应用于各种其他商业案例的知识和经验。

这本书实际上是为任何对应用 ML 有热情的人而写的。如果你希望能够从第一天开始就能构建可以在生产系统中使用的 ML 模型和应用,那么这本书就是为你准备的!

这本书涵盖的内容

第一章,机器学习建模基础,讨论了我们周围可以轻松找到的一些 ML 应用的真实案例。它还涵盖了构建 ML 模型的基本步骤以及如何为即将到来的真实 ML 项目设置 C#开发环境。

第二章,垃圾邮件过滤,涵盖了文本数据集的特征工程技术和如何使用逻辑回归和朴素贝叶斯学习算法构建分类模型。本章还讨论了一些分类模型的基本验证方法。

第三章,推特情感分析,描述了一些常用的自然语言处理NLP)技术用于特征工程以及如何构建多类分类模型。本章还涵盖了如何在 C#中构建朴素贝叶斯和随机森林分类器,以及用于分类模型的更高级模型评估指标。

第四章,外汇汇率预测,探讨了回归问题,其中目标变量是连续变量。本章讨论了一些在外汇市场中经常使用的技术指标,以及如何将它们用作构建外汇汇率预测模型的特征。它还涵盖了如何构建用于外汇汇率预测的线性回归和支持向量机SVMs)。

第五章,房屋和财产公允价值,涉及数据集中具有混合类型特征回归问题。本章讨论了为 SVM 模型使用不同的核方法。它还描述了一些回归模型的基本模型评估指标以及如何使用它们来比较构建的模型。

第六章,客户细分,描述了一个无监督学习问题,其中没有标记的目标变量。它讨论了如何使用 k-means 聚类算法从电子商务数据集中提取客户行为的见解。本章还讨论了一个可以用来评估每个聚类或细分形成得有多好的指标。

第七章,音乐流派推荐,介绍了一个排名问题,其中机器学习模型的输出数量不止一个。本章涵盖了如何构建用于推荐音乐流派的人工智能模型以及如何评估这些模型的推荐结果。

第八章,手写数字识别,讨论了一个图像识别问题,其目标是构建机器学习模型来识别手写数字。它涵盖了一种降维技术以及它如何用于图像数据集。本章介绍了用于图像识别的神经网络模型。

第九章,网络攻击检测,深入探讨了异常检测问题。在本章中,我们将尝试构建机器学习模型来检测网络攻击。它涵盖了如何使用称为主成分分析PCA)的降维技术来构建一个可以识别网络攻击的异常检测模型。

第十章,信用卡欺诈检测,继续讨论异常检测问题。本章讨论了如何构建机器学习模型来检测信用卡欺诈。它介绍了一种新的机器学习算法,单类 SVM,用于异常检测模型。

第十一章,接下来是什么?,是本书的最后一章。它回顾了本书中讨论的所有内容。然后,它涵盖了现实生活中机器学习项目中经常出现的挑战。本章还讨论了一些用于数据科学任务的一些常用技术和工具。

为了充分利用这本书

为了充分利用这本书,我建议您彻底遵循每一章中概述的每个步骤。通过代码示例并在自己的环境中运行它们,将有助于您更好地理解并更快地熟悉构建机器学习模型。我还建议您勇于尝试,将不同章节中讨论的技术和学习算法混合起来。完成这本书后,如果您能再次从头开始浏览项目,并开始为个别项目构建自己的机器学习模型版本,那就更好了。

下载示例代码文件

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

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

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

  2. 选择“支持”标签。

  3. 点击“代码下载与勘误表”。

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

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

  • 适用于 Windows 的 WinRAR/7-Zip

  • 适用于 Mac 的 Zipeg/iZip/UnRarX

  • 适用于 Linux 的 7-Zip/PeaZip

本书代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/CSharp-Machine-Learning-Projects。如果代码有更新,它将在现有的 GitHub 仓库中更新。

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

下载彩色图像

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

使用的约定

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

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“打开您的 Visual Studio,在 Visual C#类别下创建一个新的 Console Application。使用前面的命令通过 NuGet 安装 Deedle 库,并将引用添加到您的项目中。”

代码块设置如下:

var barChart = DataBarBox.Show(
    new string[] { "Ham", "Spam" },
    new double[] {
        hamEmailCount,
        spamEmailCount
    }
);
barChart.SetTitle("Ham vs. Spam in Sample Set");

任何命令行输入或输出都应如下所示:

PM> Install-Package Deedle

粗体:表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的文字会像这样显示。以下是一个示例:“打开包管理器(工具 | NuGet 包管理器 | 包管理器控制台),并使用以下命令安装 Deedle。”

警告或重要提示看起来像这样。

小贴士和技巧看起来像这样。

联系我们

我们始终欢迎读者的反馈。

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

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

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

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

评论

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

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

第一章:机器学习建模基础

要看到机器学习(ML)如何影响普通人的日常生活可能很困难。实际上,机器学习无处不在!在寻找晚餐餐厅的过程中,你几乎肯定使用了机器学习。在寻找晚宴时穿的连衣裙时,你也会使用机器学习。在你前往晚餐约会的过程中,如果你使用了共享出行应用,你很可能也使用了机器学习。机器学习已经如此广泛地被使用,以至于它已经成为我们生活中不可或缺的一部分,尽管它通常不易察觉。随着数据的不断增长及其可访问性,机器学习的应用和需求在各个行业中迅速增长。然而,训练有素的科学家和机器学习工程师的增长速度尚未满足企业对机器学习增长的需求,尽管有丰富的资源和软件库使构建机器学习模型变得更加容易,这是因为数据科学家和机器学习工程师掌握这些技能集需要时间和经验。本书将通过基于真实世界数据集的实际项目来为这样的人做好准备。

在本章中,我们将了解一些机器学习的实际例子和应用、构建机器学习模型的基本步骤,以及如何为机器学习设置我们的 C# 环境。在本章简短的介绍之后,我们将立即进入使用文本数据集构建分类机器学习模型,第二章 垃圾邮件过滤 和 第三章 Twitter 情感分析。然后,我们将使用金融和房地产数据在 第四章 外汇汇率预测 和 第五章 房屋和财产的公允价值 中构建回归模型。在 第六章 客户细分 中,我们将使用聚类算法通过电子商务数据深入了解客户行为。在 第七章 音乐流派推荐 和 第八章 手写数字识别 中,我们将使用音频和图像数据构建推荐和图像识别模型。最后,我们将在 第九章 网络攻击检测 和 第十章 信用卡欺诈检测 中使用半监督学习技术来检测异常。

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

  • 关键机器学习任务和应用

  • 构建机器学习模型的步骤

  • 为机器学习设置 C# 环境

关键机器学习任务和应用

在我们的日常生活中,有许多地方使用机器学习,而我们并未意识到。媒体公司使用机器学习为您推荐最相关的内容,如新闻文章、电影或音乐,供您阅读、观看或收听。电子商务公司使用机器学习来建议您可能感兴趣且最有可能购买的商品。游戏公司使用机器学习来检测您的运动和关节运动,以用于他们的动作感应游戏。机器学习在行业中的其他一些常见用途包括相机上的面部检测以实现更好的对焦、自动问答,其中聊天机器人或虚拟助手与客户互动以回答问题和请求,以及检测和预防欺诈交易。在本节中,我们将探讨一些我们在日常生活中使用且高度依赖机器学习的应用:

  • 谷歌新闻动态:谷歌新闻动态使用机器学习根据用户的兴趣和其他个人资料数据生成个性化的文章流。协同过滤算法常用于此类推荐系统,并基于其用户群体的查看历史数据构建。媒体公司使用此类个性化推荐系统来吸引更多流量到他们的网站并增加订阅者数量。

  • 亚马逊产品推荐:亚马逊利用用户浏览和订单历史数据来训练一个机器学习模型,推荐用户最有可能购买的产品。这是电子商务行业中监督学习的良好用例。这些推荐算法帮助电子商务公司通过显示与每个用户兴趣最相关的商品来最大化其利润。

  • Netflix 电影推荐:Netflix 使用电影评分、观看历史和偏好配置文件来推荐用户可能喜欢的其他电影。他们使用数据训练协同过滤算法以做出个性化推荐。根据 Wired 杂志上的一篇文章(www.wired.co.uk/article/how-do-netflixs-algorithms-work-machine-learning-helps-to-predict-what-viewers-will-like),超过 80%的 Netflix 用户观看的电视节目是通过平台的推荐系统发现的,这是一个非常有用且有利可图的媒体公司机器学习用例。

  • 相机上的面部检测:相机通过检测面部来实现更好的对焦和曝光测量。这是计算机视觉和分类中最常用的例子。此外,一些照片管理软件使用聚类算法将图像中的相似面部分组在一起,以便您可以稍后通过图像中的特定人物搜索照片。

  • Alexa 虚拟助手:虚拟助手系统,如 Alexa,可以回答诸如纽约的天气如何? 或完成某些任务,如打开客厅的灯。这类虚拟助手系统通常使用语音识别、自然语言理解(NLU)、深度学习和各种其他机器学习技术构建。

  • 微软 Xbox Kinect:Kinect 可以感知每个物体与传感器的距离并检测关节位置。Kinect 使用随机决策森林算法进行训练,从深度图像中构建大量单个决策树。

以下截图展示了使用机器学习构建的不同推荐系统示例:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00005.jpeg

左:谷歌新闻推送,右上:亚马逊产品推荐,右下:Netflix 电影推荐

以下截图展示了几个其他机器学习应用的例子:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00006.jpeg

左:谷歌新闻推送,右上:亚马逊产品推荐,右下:Netflix 电影推荐

构建机器学习模型的步骤

现在我们已经看到了一些现有的机器学习应用的例子,问题是,我们如何着手构建这样的机器学习应用和系统? 有关机器学习的书籍和大学中教授的机器学习课程通常首先介绍机器学习算法背后的数学和理论,然后将这些算法应用于给定的数据集。这种方法对于对这个主题完全陌生且希望学习机器学习基础的人来说是很好的。然而,那些有一定先验知识和经验,并希望将他们的知识应用于实际机器学习项目的有志数据科学家往往在如何开始以及如何处理一个特定的机器学习项目上感到困惑。在本节中,我们将讨论构建机器学习应用的典型工作流程,我们将在本书中遵循这个流程。以下图总结了我们的使用机器学习开发应用的方法,我们将在接下来的小节中详细讨论:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00007.jpeg

构建机器学习模型的步骤

如前图所示,构建学习模型的步骤如下:

  • 问题定义:开始任何项目的第一步不仅是理解问题,还要定义你试图用机器学习解决的问题。问题定义不明确会导致构建的机器学习系统没有意义,因为模型已经被训练和优化用于你实际上并不试图解决的问题。这一步无疑是构建有用的机器学习模型和应用中最重要的一步。在开始构建机器学习模型之前,你应该至少回答以下四个问题:

    • 问题是?这是你描述和声明你试图用机器学习解决的问题的地方。例如,问题描述可能为需要一个系统来评估小型企业主偿还贷款的能力(针对小型企业贷款项目)。

    • 为什么这是一个问题?定义为什么这样的问题实际上是一个问题,以及为什么新的机器学习模型将会是有用的,这是非常重要的。也许你已经有一个正在工作的模型,并且你注意到它的表现不如以前;你可能已经获得了可以用于构建新预测模型的新数据源;或者你可能希望你的现有模型能够更快地产生预测结果。可能有多个原因让你认为这是一个问题,以及为什么你需要一个新的模型。定义为什么这是一个问题将帮助你保持正确的方向,在你构建新的机器学习模型时。

    • 解决这个问题的方法有哪些?这就是你构思解决给定问题方法的地方。你应该考虑这个模型将要如何被使用(你需要这是一个实时系统,还是作为批处理运行?),它是什么类型的问题(是分类问题、回归、聚类还是其他什么?),以及你需要为你的模型准备哪些类型的数据。这将为你构建机器学习模型未来的步骤提供一个良好的基础。

    • 成功的标准是什么?这是你定义检查点的地方。你应该考虑你将查看哪些指标,以及你的目标模型性能应该是什么样的。如果你正在构建一个将在实时系统中使用的模型,那么你还可以将目标执行速度和数据可用性作为运行时的成功标准。设定这些成功标准将帮助你避免在某个步骤上停滞不前。

  • 数据收集:拥有数据是构建机器学习模型最基本和关键的部分,最好是大量的数据。没有数据,就没有模型。根据你的项目,你收集数据的方法可能会有所不同。你可以从其他供应商那里购买现有的数据源,你可以抓取网站并从中提取数据,你可以使用公开可用的数据,或者你也可以收集自己的数据。你可以用多种方式收集你需要的机器学习模型数据,但当你处于数据收集过程中时,你需要记住这两个数据要素——目标变量和特征变量。目标变量是预测的答案,特征变量是模型将用来学习如何预测目标变量的因素。通常,目标变量不会以标记的形式出现。例如,当你处理 Twitter 数据以预测每条推文的情感时,你可能没有每条推文的标记情感数据。在这种情况下,你将不得不额外一步来标记你的目标变量。一旦你收集了数据,你就可以继续到数据准备步骤。

  • 数据准备:一旦你收集了所有输入数据,你需要将其准备成可用的格式。这一步比你想象的更重要。如果你有杂乱的数据,并且没有为你的学习算法清理它们,你的算法将无法从你的数据集中很好地学习,并且不会按预期表现。此外,即使你拥有高质量的数据,如果你的数据格式不适合你的算法进行训练,那么拥有高质量数据也是没有意义的。数据差,模型差。你应该至少处理以下列出的常见问题,以便为下一步做好准备:

    • 文件格式:如果你从多个数据源获取数据,你很可能会遇到每个数据源都有不同的格式。一些数据可能以 CSV 格式存储,而其他数据可能以 JSON 或 XML 格式存储。一些数据甚至可能存储在关系型数据库中。为了训练你的机器学习模型,你首先需要将这些不同格式的数据源合并成一个标准格式。

    • 数据格式:也可能存在不同数据源之间数据格式不同的情况。例如,一些数据可能将地址字段拆分为街道地址、城市、州和邮政编码,而另一些则可能没有。一些数据可能使用美国日期格式(mm/dd/yyyy)表示日期字段,而另一些则可能使用英国格式(dd/mm/yyyy)。这些数据源之间的数据格式差异在解析值时可能会引起问题。为了训练你的机器学习模型,你需要为每个字段提供一个统一的数据格式。

    • 重复记录:通常你会在数据集中看到相同的记录重复出现。这个问题可能出现在数据收集过程中,你记录了一个数据点多次,或者在你准备数据的过程中合并不同的数据集时。重复的记录可能会对你的模型产生不利影响,因此在继续下一步之前检查数据集中的重复项是很好的做法。

    • 缺失值:在数据中看到一些记录有空值或缺失值也是常见的情况。在训练你的机器学习模型时,这也可能产生不利影响。处理数据中的缺失值有多种方法,但你必须非常小心,并且非常了解你的数据,因为这将极大地改变你的模型性能。你可以处理缺失值的方法包括删除包含缺失值的记录,用平均值或中位数替换缺失值,用常数替换缺失值,或者用虚拟变量和缺失指示变量替换缺失值。在处理缺失值之前研究你的数据将是有益的。

  • 数据分析:现在数据已经准备好了,是时候真正查看数据,看看你是否能识别出任何模式,并从数据中得出一些见解。总结统计量和图表是描述和理解数据的最有效方法之一。对于连续变量,查看最小值、最大值、平均值、中位数和四分位数是一个好的开始。对于分类变量,你可以查看各个类别的计数和百分比。当你查看这些总结统计量时,你还可以开始绘制图表来可视化数据的结构。以下图示展示了数据分析中常用的一些图表。直方图常用于显示和检查变量的潜在分布、异常值和偏度。箱线图常用于可视化五数摘要、异常值和偏度。成对散点图常用于检测变量之间明显的成对相关性:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00008.jpeg

数据分析和可视化。左上角:名义房屋销售价格的直方图,右上角:使用对数刻度的房屋销售价格直方图,左下角:地下室、一楼和二楼面积分布的箱线图,右下角:一楼和二楼面积之间的散点图

    • 特征工程:特征工程是应用机器学习模型构建过程中最重要的部分。然而,这是许多教科书和机器学习课程中讨论最少的话题之一。特征工程是将原始输入数据转换为算法可以从中学习的更信息化的数据的过程。例如,对于我们在第三章中将要构建的 Twitter 情感预测模型,Twitter 情感分析,你的原始输入数据可能只包含一个列中的文本列表和另一个列中的情感目标列表。你的机器学习模型可能无法从这些原始数据中很好地学习如何预测每条推文的情感。然而,如果你将数据转换成这样,每个列代表每条推文中每个单词的出现次数,那么你的学习算法就可以更容易地学习到某些单词的存在与情感之间的关系。你还可以将每个单词与其相邻的单词(二元组)分组,并将每条推文中每个二元组的出现次数作为另一组特征。正如这个例子所示,特征工程是一种使你的原始数据更具代表性和对潜在问题更信息化的方式。特征工程不仅是一门科学,也是一种艺术。特征工程需要良好的领域知识、从原始输入数据中构建新特征的创造力,以及多次迭代以获得更好的结果。随着我们学习这本书,我们将介绍如何使用一些自然语言处理NLP)技术构建文本特征,如何构建时间序列特征,如何子选择特征以避免过拟合问题,以及如何使用降维技术将高维数据转换为更少的维度。

提出特征是困难的,耗时,需要专业知识。应用机器学习基本上是特征工程。

-安德鲁·吴

  • 训练/测试算法:一旦你创建了你的特征,就是时候训练和测试一些机器学习算法了。在你开始训练你的模型之前,考虑一下性能指标是很好的。根据你要解决的问题,你的性能度量选择会有所不同。例如,如果你正在构建一个股票价格预测模型,你可能希望最小化你的预测与实际价格之间的差异,并选择均方根误差RMSE)作为你的性能指标。如果你正在构建一个信用模型来预测一个人是否可以获得贷款,你可能会想使用精确率作为你的性能指标,因为错误的贷款批准(假阳性)比错误的贷款拒绝(假阴性)有更大的负面影响。随着我们学习这些章节,我们将讨论每个项目的更具体的性能指标。

一旦您为您的模型确定了具体的性能指标,现在您可以训练和测试各种学习算法及其性能。根据您的预测目标,您选择的学习算法也会有所不同。以下图显示了某些常见机器学习问题的说明。如果您正在解决分类问题,您将想要训练分类器,例如逻辑回归模型、朴素贝叶斯分类器或随机森林分类器。另一方面,如果您有一个连续的目标变量,那么您将想要训练回归器,例如线性回归模型、k 近邻或支持向量机SVM)。如果您想通过无监督学习从数据中得出一些见解,您将想要使用 k 均值聚类或均值漂移算法:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00009.jpeg

机器学习问题的说明。左:分类,中:回归,右:聚类

最后,我们必须考虑如何测试和评估我们尝试的学习算法的性能。将数据集分为训练集和测试集以及运行交叉验证是测试和比较您的机器学习模型最常用的两种方法。将数据集分为两个子集,一个用于训练,另一个用于测试的目的,是在训练集上训练模型而不暴露给测试集,这样测试集上的预测结果就可以指示模型在不可预见数据上的总体性能。K 折交叉验证是评估模型性能的另一种方法。它首先将数据集分为大小相等的 K 个子集,并留出一个子集用于测试,其余的用于训练。例如,在 3 折交叉验证中,数据集首先分为三个大小相等的子集。在第一次迭代中,我们将使用第 1 和第 2 折来训练我们的模型并在第 3 折上测试它。在第二次迭代中,我们将使用第 1 和第 3 折来训练并在第 2 折上测试我们的模型。在第三次迭代中,我们将使用第 2 和第 3 折来训练并在第 1 折上测试我们的模型。然后,我们将平均性能指标来估计模型性能:

  • 改进结果:到目前为止,您将有一个或两个表现合理的候选模型,但可能仍有改进的空间。也许您注意到您的候选模型在一定程度上过度拟合,也许它们没有达到您的目标性能,或者也许您有更多的时间来迭代您的模型——无论您的意图如何,都有多种方法可以提高您模型的表现,它们如下:

    • 超参数调整:您可以调整模型的配置以潜在地提高性能结果。例如,对于随机森林模型,您可以调整树的最大高度或森林中的树的数量。对于支持向量机(SVMs),您可以调整核或成本值。

    • 集成方法:集成是将多个模型的输出结果结合起来以获得更好的结果。Bagging 是在数据集的不同子集上训练相同的算法,Boosting 是将训练在同一训练集上的不同模型结合起来,而 Stacking 是将模型的输出作为元模型的输入,元模型学习如何组合子模型的输出。

    • 更多特征工程:在特征工程上进行迭代是提高模型性能的另一种方法。

  • 部署:是时候将您的模型投入实际应用了!一旦您的模型准备就绪,就是让它们在生产环境中运行的时候了。在您的模型全面接管之前,请确保进行彻底的测试。在模型性能随着时间的推移和输入数据的变化而降低的情况下,计划开发模型监控工具也将是有益的。

设置 C# 环境以进行机器学习

既然我们已经讨论了本书中我们将遵循的构建机器学习模型的步骤和方法,让我们开始设置我们的 C# 机器学习环境。我们首先将安装和设置 Visual Studio,然后安装两个我们将频繁在后续章节的项目中使用的包(Accord.NET 和 Deedle)。

设置 Visual Studio 以进行 C#

假设您对 C# 有一些先前的知识,我们将简要介绍这部分内容。如果您需要安装 Visual Studio for C#,请访问 www.visualstudio.com/downloads/ 并下载 Visual Studio 的一个版本。在本书中,我们使用 Visual Studio 2017 的社区版。如果您在安装 Visual Studio 之前被提示下载 .NET Framework,请访问 www.microsoft.com/en-us/download/details.aspx?id=53344 并先安装它。

安装 Accord.NET

Accord.NET 是一个 .NET 机器学习框架。在机器学习包之上,Accord.NET 框架还包括数学、统计学、计算机视觉、计算机听觉和其他科学计算模块。我们将主要使用 Accord.NET 框架的机器学习包。

一旦您安装并设置了 Visual Studio,让我们开始安装 C# 的机器学习框架 Accord.NET。通过 NuGet 安装它是最简单的。要安装它,打开包管理器(工具 | NuGet 包管理器 | 包管理器控制台),并输入以下命令安装 Accord.MachineLearningAccord.Controls

PM> Install-Package Accord.MachineLearning
PM> Install-Package Accord.Controls

现在,让我们使用这些 Accord.NET 包构建一个示例机器学习应用程序。打开您的 Visual Studio,在 Visual C# 类别下创建一个新的 控制台应用程序。使用前面的命令通过 NuGet 安装这些 Accord.NET 包,并将它们添加到我们的项目中。您应该在 解决方案资源管理器 中看到一些 Accord.NET 包被添加到您的引用中,结果应该类似于以下截图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00010.jpeg

我们现在要构建的模型是一个非常简单的逻辑回归模型。给定二维数组和预期输出,我们将开发一个程序来训练一个逻辑回归分类器,然后绘制结果,显示该模型的预期输出和实际预测。该模型的输入和输出如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00011.jpeg

这个示例逻辑回归分类器的代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Accord.Controls;
using Accord.Statistics;
using Accord.Statistics.Models.Regression;
using Accord.Statistics.Models.Regression.Fitting;

namespace SampleAccordNETApp
{
    class Program
    {
        static void Main(string[] args)
        {
            double[][] inputs =
            {
                new double[] { 0, 0 },
                new double[] { 0.25, 0.25 }, 
                new double[] { 0.5, 0.5 }, 
                new double[] { 1, 1 },
            };

            int[] outputs =
            { 
                0,
                0,
                1,
                1,
            };

            // Train a Logistic Regression model
            var learner = new IterativeReweightedLeastSquares<LogisticRegression>()
            {
                MaxIterations = 100
            };
            var logit = learner.Learn(inputs, outputs);

            // Predict output
            bool[] predictions = logit.Decide(inputs);

            // Plot the results
            ScatterplotBox.Show("Expected Results", inputs, outputs);
            ScatterplotBox.Show("Actual Logistic Regression Output", inputs, predictions.ToZeroOne());

            Console.ReadKey();
        }
    }
}

一旦你写完这段代码,你可以通过按F5键或点击顶部的开始按钮来运行它。如果一切顺利,它应该会生成以下图中显示的两个图表。如果失败了,请检查引用或错误。你始终可以右键单击类名或灯泡图标,让 Visual Studio 帮助你找到命名空间引用中缺少的哪些包:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00012.jpeg

样本程序生成的图表。左:实际预测结果,右:预期输出

这个示例代码可以在以下链接找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.1/SampleAccordNETApp.cs

安装 Deedle

Deedle 是一个开源的.NET 库,用于数据框编程。Deedle 允许你以类似于 R 数据框和 Python 中的 pandas 数据框的方式处理数据。我们将在以下章节中使用这个包来加载和操作我们的机器学习项目数据。

与我们安装 Accord.NET 的方式类似,我们可以从 NuGet 安装 Deedle 包。打开包管理器(工具 | NuGet 包管理器 | 包管理器控制台),使用以下命令安装Deedle

PM> Install-Package Deedle

让我们简要看看我们如何使用这个包从 CSV 文件加载数据并进行简单的数据操作。更多详细信息,您可以访问bluemountaincapital.github.io/Deedle/以获取 API 文档和示例代码。我们将使用 2010 年到 2013 年的 AAPL 每日股价数据来完成这个练习。您可以从以下链接下载这些数据:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.1/table_aapl.csv

打开你的 Visual Studio,在 Visual C#类别下创建一个新的控制台应用程序。使用前面的命令通过NuGet安装Deedle库,并将引用添加到你的项目中。你应该在你的解决方案资源管理器中看到添加了Deedle包的引用。

现在,我们将把 CSV 数据加载到Deedle数据框中,然后进行一些数据处理。首先,我们将使用Date字段更新数据框的索引。然后,我们将对OpenClose列应用一些算术运算来计算从开盘价到收盘价的百分比变化。最后,我们将通过计算收盘价与前一收盘价之间的差异,将它们除以前一收盘价,然后乘以100来计算每日回报率。以下是这个示例Deedle程序的代码:

using Deedle;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DeedleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            // Read AAPL stock prices from a CSV file
            var root = Directory.GetParent(Directory.GetCurrentDirectory()).Parent.FullName;
            var aaplData = Frame.ReadCsv(Path.Combine(root, "table_aapl.csv"));
            // Print the data
            Console.WriteLine("-- Raw Data --");
            aaplData.Print();

            // Set Date field as index
            var aapl = aaplData.IndexRows<String>("Date").SortRowsByKey();
            Console.WriteLine("-- After Indexing --");
            aapl.Print();

            // Calculate percent change from open to close
            var openCloseChange = 
                ((
                    aapl.GetColumn<double>("Close") - aapl.GetColumn<double>("Open")
                ) / aapl.GetColumn<double>("Open")) * 100.0;
            aapl.AddColumn("openCloseChange", openCloseChange);
            Console.WriteLine("-- Simple Arithmetic Operations --");
            aapl.Print();

            // Shift close prices by one row and calculate daily returns
            var dailyReturn = aapl.Diff(1).GetColumn<double>("Close") / aapl.GetColumn<double>("Close") * 100.0;
            aapl.AddColumn("dailyReturn", dailyReturn);
            Console.WriteLine("-- Shift --");
            aapl.Print();

            Console.ReadKey();
        }
    }
}

当你运行这段代码时,你会看到以下输出。

原始数据集看起来如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00013.jpeg

在使用日期字段对数据集进行索引后,你会看到以下内容:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00014.jpeg

在应用简单的算术运算来计算开盘价到收盘价的变化率后,你会看到以下内容:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00015.jpeg

最后,在将收盘价移动一行并计算每日回报率之后,你会看到以下内容:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00016.jpeg

从这个示例Deedle项目中可以看出,我们可以用一行或两行代码运行各种数据处理操作,而使用原生 C#进行相同的操作则需要更多的代码。在这本书中,我们将频繁使用Deedle库进行数据处理和特征工程。

这个示例Deedle代码可以在以下链接找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.1/DeedleApp.cs

摘要

在本章中,我们简要讨论了一些关键的机器学习任务和机器学习的实际应用案例。我们还学习了开发机器学习模型的步骤以及每个步骤中常见的挑战和任务。在接下来的章节中,我们将遵循这些步骤进行我们的项目,并将更详细地探讨某些步骤,特别是特征工程、模型选择和模型性能评估。我们将根据我们解决问题的类型讨论在每个步骤中可以应用的各项技术。最后,在本章中,我们向您介绍了如何为我们的未来机器学习项目设置 C#环境。我们使用 Accord.NET 框架构建了一个简单的逻辑回归分类器,并使用Deedle库来加载数据和处理数据。

在下一章中,我们将直接应用本章所涵盖的机器学习(ML)基础知识,来构建一个用于垃圾邮件过滤的 ML 模型。我们将遵循本章讨论的构建 ML 模型的步骤,将原始电子邮件数据转换为结构化数据集,分析电子邮件文本数据以获取一些见解,并最终构建预测电子邮件是否为垃圾邮件的分类模型。我们还将讨论下一章中一些常用的分类模型评估指标。

第二章:垃圾邮件过滤

在本章中,我们将开始使用我们在第一章“机器学习建模基础”中安装的两个包,即 Accord.NET for ML 和 Deedle for data manipulation,在 C#中构建真实的机器学习ML)模型。在本章中,我们将构建一个用于垃圾邮件过滤的分类模型。我们将使用包含垃圾邮件和正常邮件(非垃圾邮件)的原始电子邮件数据集来训练我们的 ML 模型。我们将开始遵循上一章中讨论的 ML 模型开发步骤。这将帮助我们更好地理解 ML 建模的工作流程和方法,并使它们变得自然而然。在我们努力构建垃圾邮件分类模型的同时,我们还将讨论文本数据集的特征工程技术和分类模型的初步验证方法,并比较逻辑回归分类器和朴素贝叶斯分类器在垃圾邮件过滤中的应用。熟悉这些模型构建步骤、基本的文本特征工程技术和基本的分类模型验证方法将为使用自然语言处理NLP)进行更高级的特征工程以及在第三章“Twitter 情感分析”中构建多类分类模型奠定基础。

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

  • 垃圾邮件过滤项目的定义问题

  • 数据准备

  • 邮件数据分析

  • 邮件数据特征工程

  • 逻辑回归与朴素贝叶斯在垃圾邮件过滤中的应用

  • 分类模型验证

垃圾邮件过滤项目的定义问题

让我们先定义一下本章将要解决的问题。你可能已经熟悉了垃圾邮件是什么;垃圾邮件过滤是像 Gmail、Yahoo Mail 和 Outlook 这样的电子邮件服务的基本功能。垃圾邮件可能会让用户感到烦恼,但它们带来了更多的问题和风险。例如,垃圾邮件可能被设计成索要信用卡号码或银行账户信息,这些信息可能被用于信用卡欺诈或洗钱。垃圾邮件也可能被用来获取个人信息,如社会保障号码或用户 ID 和密码,然后可以用来进行身份盗窃和其他各种犯罪。拥有垃圾邮件过滤技术是电子邮件服务保护用户免受此类犯罪侵害的关键步骤。然而,拥有正确的垃圾邮件过滤解决方案是困难的。你希望过滤掉可疑邮件,但同时,你又不希望过滤太多,以至于非垃圾邮件被放入垃圾邮件文件夹,用户永远不会查看。为了解决这个问题,我们将让我们的机器学习模型从原始电子邮件数据集中学习,并使用主题行将可疑邮件分类为垃圾邮件。我们将查看两个性能指标来衡量我们的成功:精确率和召回率。我们将在以下章节中详细讨论这些指标。

总结我们的问题定义:

  • 问题是啥?我们需要一个垃圾邮件过滤解决方案,以防止我们的用户成为欺诈活动的受害者,同时提高用户体验。

  • 为什么这是一个问题?在过滤可疑邮件和不过度过滤之间取得平衡,使得非垃圾邮件仍然进入收件箱,是困难的。我们将依赖机器学习模型来学习如何从统计上分类这类可疑邮件。

  • 解决这个问题的方法有哪些?我们将构建一个分类模型,根据邮件的主题行标记潜在的垃圾邮件。我们将使用精确率和召回率作为平衡过滤邮件数量的方式。

  • 成功的标准是什么?我们希望有高的召回率(实际垃圾邮件被检索的百分比与垃圾邮件总数的比例),同时不牺牲太多的精确率(被预测为垃圾邮件的正确分类垃圾邮件的百分比)。

数据准备

既然我们已经清楚地陈述并定义了我们打算使用机器学习解决的问题,我们就需要数据。没有数据,就没有机器学习。通常,在数据准备步骤之前,您需要额外的一步来收集和整理所需的数据,但在这本书中,我们将使用一个预先编译并标记的公开可用的数据集。在本章中,我们将使用 CSDMC2010 SPAM 语料库数据集(csmining.org/index.php/spam-email-datasets-.html)来训练和测试我们的模型。您可以点击链接并下载网页底部的压缩数据。当您下载并解压缩数据后,您将看到两个名为TESTINGTRAINING的文件夹,以及一个名为SPAMTrain.label的文本文件。SPAMTrain.label文件包含了TRAINING文件夹中每封电子邮件的编码标签——0代表垃圾邮件,1代表非垃圾邮件(非垃圾邮件)。我们将使用这个文本文件以及TRAINING文件夹中的电子邮件数据来构建垃圾邮件分类模型。

一旦您下载了数据并将其放置在可以从中加载的位置,您需要为未来的特征工程和模型构建步骤准备它。我们现在有一个包含多个包含有关单个电子邮件信息的 EML 文件和包含标记信息的文本文件的原始数据集。为了使这个原始数据集可用于使用电子邮件主题行构建垃圾邮件分类模型,我们需要执行以下任务:

  1. 从 EML 文件中提取主题行:为准备我们的数据以供未来任务使用,第一步是从单个 EML 文件中提取主题和正文。我们将使用一个名为EAGetMail的包来加载和提取 EML 文件中的信息。您可以使用 Visual Studio 中的包管理器来安装此包。请查看代码的第 4 到 6 行以了解如何安装此包。使用EAGetMail包,您可以轻松地加载和提取 EML 文件的主题和正文内容(第 24-30 行)。一旦您从电子邮件中提取了主题和正文,您需要将每行数据作为一行追加到一个 Deedle 数据框中。请查看以下代码中的ParseEmails函数(从第 18 行开始),以了解如何创建一个 Deedle 数据框,其中每行包含每封电子邮件的索引号、主题行和正文内容。

  2. 将提取的数据与标签合并:在从单个 EML 文件中提取主题和正文内容之后,我们还需要做一件事。我们需要将编码后的标签(0 表示垃圾邮件,1 表示正常邮件)映射到我们在上一步创建的 DataFrame 的每一行。如果您用任何文本编辑器打开SPAMTrain.label文件,您会看到编码的标签位于第一列,相应的电子邮件文件名位于第二列,由空格分隔。使用 Deedle 框架的ReadCsv函数,您可以通过指定空格作为分隔符轻松地将这些标签数据加载到 DataFrame 中(请参阅代码中的第 50 行)。一旦您将标记数据加载到 DataFrame 中,您只需使用 Deedle 框架的AddColumn函数将此 DataFrame 的第一列添加到我们在上一步创建的另一个 DataFrame 中。查看以下代码的第 49-52 行,了解我们如何将标签信息与提取的电子邮件数据合并。

  3. 将合并后的数据导出为 CSV 文件:现在我们有一个包含邮件和标签数据的 DataFrame,是时候将这个 DataFrame 导出为 CSV 文件以供将来使用。如以下代码的第 54 行所示,导出 DataFrame 到 CSV 文件只需要一行代码。使用 Deedle 框架的SaveCsv函数,您可以轻松地将 DataFrame 保存为 CSV 文件。

此数据准备步骤的代码如下:

// Install-Package Deedle
// Install-Package FSharp.Core
using Deedle;
// if you don't have EAGetMail package already, install it 
// via the Package Manager Console by typing in "Install-Package EAGetMail"
using EAGetMail;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EmailParser
{
    class Program
    {
        private static Frame<int, string> ParseEmails(string[] files)
        {
            // we will parse the subject and body from each email
            // and store each record into key-value pairs
            var rows = files.AsEnumerable().Select((x, i) =>
            {
                // load each email file into a Mail object
                Mail email = new Mail("TryIt");
                email.Load(x, false);

                // extract the subject and body
                string emailSubject = email.Subject;
                string textBody = email.TextBody;

                // create key-value pairs with email id (emailNum), subject, and body
                return new { emailNum = i, subject = emailSubject, body = textBody };
            });

            // make a data frame from the rows that we just created above
            return Frame.FromRecords(rows);
        }

        static void Main(string[] args)
        {
            // Get all raw EML-format files
            // TODO: change the path to point to your data directory
            string rawDataDirPath = "<path-to-data-directory>";
            string[] emailFiles = Directory.GetFiles(rawDataDirPath, "*.eml");

            // Parse out the subject and body from the email files
            var emailDF = ParseEmails(emailFiles);
            // Get the labels (spam vs. ham) for each email
            var labelDF = Frame.ReadCsv(rawDataDirPath + "\\SPAMTrain.label", hasHeaders: false, separators: " ", schema: "int,string");
            // Add these labels to the email data frame
            emailDF.AddColumn("is_ham", labelDF.GetColumnAt<String>(0));
            // Save the parsed emails and labels as a CSV file
            emailDF.SaveCsv("transformed.csv");

            Console.WriteLine("Data Preparation Step Done!");
            Console.ReadKey();
        }
    }
}

在运行此代码之前,您需要将第 44 行中的<path-to-data-directory>替换为您存储数据的实际路径。运行此代码后,应创建一个名为transformed.csv的文件,它将包含四列(emailNumsubjectbodyis_ham)。我们将使用此输出数据作为以下步骤构建用于垃圾邮件过滤项目的机器学习模型的输入。不过,您可以自由发挥创意,尝试使用 Deedle 框架和EAGetMail包以不同的方式调整和准备这些数据。我们在这里展示的代码是准备原始邮件数据以供将来使用的一种方法,以及您可以从原始邮件数据中提取的一些信息。使用EAGetMail包,您可以提取其他特征,例如发件人的电子邮件地址和邮件中的附件,这些额外特征可能有助于提高您的垃圾邮件分类模型。

此数据准备步骤的代码也可以在以下仓库中找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.2/EmailParser.cs.

邮件数据分析

在数据准备步骤中,我们将原始数据集转换成了一个更易于阅读和使用的数据集。现在我们有一个文件可以查看,以确定哪些电子邮件是垃圾邮件,哪些不是。此外,我们还可以轻松地找到垃圾邮件和非垃圾邮件的主题行。使用这个转换后的数据,让我们开始查看数据的实际样子,看看我们能否在数据中找到任何模式或问题。

由于我们处理的是文本数据,我们首先想查看的是垃圾邮件和非垃圾邮件之间单词分布的差异。为了做到这一点,我们需要将之前步骤输出的数据转换成单词出现的矩阵表示。让我们一步一步地来做,以我们数据中的前三个主题行为例。我们拥有的前三个主题行如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00017.jpeg

如果我们将这些数据转换成这样,即每一列对应每个主题行中的每个单词,并将每个单元格的值编码为1,如果给定的主题行包含该单词,否则为0,那么得到的矩阵看起来可能如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00018.jpeg

这种特定的编码方式被称为独热编码,我们只关心特定单词是否出现在主题行中,而不关心每个单词在主题行中实际出现的次数。在上述情况下,我们还移除了所有的标点符号,例如冒号、问号和感叹号。为了程序化地完成这项工作,我们可以使用正则表达式将每个主题行分割成只包含字母数字字符的单词,然后使用独热编码构建一个数据框。执行此编码步骤的代码如下所示:

private static Frame<int, string> CreateWordVec(Series<int, string> rows)
{
    var wordsByRows = rows.GetAllValues().Select((x, i) =>
    {
        var sb = new SeriesBuilder<string, int>();

        ISet<string> words = new HashSet<string>(
            Regex.Matches(
                // Alphanumeric characters only
                x.Value, "\\w+('(s|d|t|ve|m))?"
            ).Cast<Match>().Select(
                // Then, convert each word to lowercase
                y => y.Value.ToLower()
            ).ToArray()
        );

        // Encode words appeared in each row with 1
        foreach (string w in words)
        {
            sb.Add(w, 1);
        }

        return KeyValue.Create(i, sb.Series);
    });

    // Create a data frame from the rows we just created
    // And encode missing values with 0
    var wordVecDF = Frame.FromRows(wordsByRows).FillMissing(0);

    return wordVecDF;
}

拥有这种独热编码的单词矩阵表示形式使我们的数据分析过程变得更加容易。例如,如果我们想查看垃圾邮件中前十位频繁出现的单词,我们只需简单地对垃圾邮件独热编码单词矩阵的每一列求和,然后取求和值最高的十个单词。这正是我们在以下代码中所做的:

var hamTermFrequencies = subjectWordVecDF.Where(
    x => x.Value.GetAs<int>("is_ham") == 1
).Sum().Sort().Reversed.Where(x => x.Key != "is_ham");

var spamTermFrequencies = subjectWordVecDF.Where(
    x => x.Value.GetAs<int>("is_ham") == 0
).Sum().Sort().Reversed;

// Look at Top 10 terms that appear in Ham vs. Spam emails
var topN = 10;

var hamTermProportions = hamTermFrequencies / hamEmailCount;
var topHamTerms = hamTermProportions.Keys.Take(topN);
var topHamTermsProportions = hamTermProportions.Values.Take(topN);

System.IO.File.WriteAllLines(
    dataDirPath + "\\ham-frequencies.csv",
    hamTermFrequencies.Keys.Zip(
        hamTermFrequencies.Values, (a, b) => string.Format("{0},{1}", a, b)
    )
);

var spamTermProportions = spamTermFrequencies / spamEmailCount;
var topSpamTerms = spamTermProportions.Keys.Take(topN);
var topSpamTermsProportions = spamTermProportions.Values.Take(topN);

System.IO.File.WriteAllLines(
    dataDirPath + "\\spam-frequencies.csv",
    spamTermFrequencies.Keys.Zip(
        spamTermFrequencies.Values, (a, b) => string.Format("{0},{1}", a, b)
    )
);

如您从这段代码中可以看到,我们使用了 Deedle 数据框的Sum方法对每一列的值进行求和,并按降序排序。我们为垃圾邮件和 ham 邮件各做一次。然后,我们使用Take方法获取在垃圾邮件和 ham 邮件中出现频率最高的前十个单词。运行此代码将生成两个 CSV 文件:ham-frequencies.csvspam-frequencies.csv。这两个文件包含有关垃圾邮件和 ham 邮件中单词出现次数的信息,我们将在后续的特征工程和模型构建步骤中使用这些信息。

现在,让我们可视化一些数据以进行进一步分析。首先,看一下以下关于数据集中 ham 电子邮件中前十位频繁出现的术语的图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00019.jpeg

一个条形图,用于展示正常邮件中频率最高的前十项术语

如从该条形图中可以看出,在数据集中,正常邮件的数量多于垃圾邮件,这与现实世界的情况相符。我们通常在我们的收件箱中收到比垃圾邮件更多的正常邮件。我们使用了以下代码来生成此条形图,以可视化数据集中正常邮件和垃圾邮件的分布:

var barChart = DataBarBox.Show(
    new string[] { "Ham", "Spam" },
    new double[] {
        hamEmailCount,
        spamEmailCount
    }
);
barChart.SetTitle("Ham vs. Spam in Sample Set");

使用 Accord.NET 框架中的DataBarBox类,我们可以轻松地将数据可视化在条形图中。现在让我们可视化正常邮件和垃圾邮件中频率最高的前十项术语。你可以使用以下代码生成正常邮件和垃圾邮件中前十项术语的条形图:

var hamBarChart = DataBarBox.Show(
    topHamTerms.ToArray(),
    new double[][] {
        topHamTermsProportions.ToArray(),
        spamTermProportions.GetItems(topHamTerms).Values.ToArray()
    }
);
hamBarChart.SetTitle("Top 10 Terms in Ham Emails (blue: HAM, red: SPAM)");

var spamBarChart = DataBarBox.Show(
    topSpamTerms.ToArray(),
    new double[][] {
        hamTermProportions.GetItems(topSpamTerms).Values.ToArray(),
        topSpamTermsProportions.ToArray()
    }
);
spamBarChart.SetTitle("Top 10 Terms in Spam Emails (blue: HAM, red: SPAM)");

类似地,我们使用了DataBarBox类来显示条形图。当你运行此代码时,你会看到以下条形图,用于展示正常邮件中频率最高的前十项术语:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00020.jpeg

一个展示正常邮件中频率最高的前十项术语的图表

垃圾邮件中频率最高的前十项术语的条形图如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00021.jpeg

一个条形图,用于展示垃圾邮件中频率最高的前十项术语

如预期的那样,垃圾邮件中的单词分布与非垃圾邮件有很大的不同。例如,如果你看右边的图表,单词垃圾邮件hibody在垃圾邮件中频繁出现,但在非垃圾邮件中并不常见。然而,有些事情并不合理。如果你仔细观察,单词试用版本出现在所有的垃圾邮件和正常邮件中,这很不可能是真的。如果你在文本编辑器中打开一些原始的 EML 文件,你可以很容易地发现并非所有的邮件都包含这两个单词在它们的主题行中。那么,发生了什么?我们的数据是否在之前的数据准备或数据分析步骤中受到了污染?

进一步的研究表明,我们使用的其中一个软件包导致了这个问题。我们使用的EAGetMail软件包,用于加载和提取电子邮件内容,当我们使用他们的试用版时,会自动将(Trial Version)附加到主题行的末尾。既然我们已经知道了这个数据问题的根本原因,我们需要回去修复它。一个解决方案是回到数据准备步骤,并更新我们的ParseEmails函数,使用以下代码,该代码简单地从主题行中删除附加的(Trial Version)标志:

private static Frame<int, string> ParseEmails(string[] files)
{
    // we will parse the subject and body from each email
    // and store each record into key-value pairs
    var rows = files.AsEnumerable().Select((x, i) =>
    {
        // load each email file into a Mail object
        Mail email = new Mail("TryIt");
        email.Load(x, false);

        // REMOVE "(Trial Version)" flags
        string EATrialVersionRemark = "(Trial Version)"; // EAGetMail appends subjects with "(Trial Version)" for trial version
        string emailSubject = email.Subject.EndsWith(EATrialVersionRemark) ? 
            email.Subject.Substring(0, email.Subject.Length - EATrialVersionRemark.Length) : email.Subject;
        string textBody = email.TextBody;

        // create key-value pairs with email id (emailNum), subject, and body
        return new { emailNum = i, subject = emailSubject, body = textBody };
    });

    // make a data frame from the rows that we just created above
    return Frame.FromRecords(rows);
}

在更新此代码并再次运行之前的数据准备和分析代码后,单词分布的条形图变得更加有意义。

以下条形图显示了修复并移除(Trial Version)标志后的正常邮件中频率最高的前十项术语:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00022.jpeg

以下条形图显示了修复并移除(Trial Version)标志后的垃圾邮件中频率最高的前十项术语:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00023.jpeg

这是在构建机器学习模型时数据分析步骤重要性的一个很好的例子。在数据准备和数据分析步骤之间迭代是非常常见的,因为我们通常在分析步骤中发现数据问题,并且我们通常可以通过更新数据准备步骤中使用的部分代码来提高数据质量。现在我们已经有了以矩阵形式表示主题行中使用的单词的干净数据,是时候开始着手构建机器学习模型所使用的实际特征了。

邮件数据的特征工程

在上一步中,我们简要地查看了一下垃圾邮件和正常邮件的单词分布,并注意到了一些事情。首先,大多数最频繁出现的单词是常用词,意义不大。例如,像 tothefora 这样的单词是常用词,我们的机器学习算法从这些单词中不会学到很多东西。这类单词被称为停用词,通常会被忽略或从特征集中删除。我们将使用 NLTK 的停用词列表来过滤掉特征集中的常用词。您可以从这里下载 NLTK 的停用词列表:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.2/stopwords.txt。过滤掉这些停用词的一种方法如下所示:

// Read in stopwords list
ISet<string> stopWords = new HashSet<string>(
    File.ReadLines("<path-to-your-stopwords.txt>")
);
// Filter out stopwords from the term frequency series
var spamTermFrequenciesAfterStopWords = spamTermFrequencies.Where(
    x => !stopWords.Contains(x.Key)
);

在过滤掉这些停用词后,非垃圾邮件的新十大高频词如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00024.jpeg

在过滤掉停用词后,垃圾邮件的前十大高频词如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00025.jpeg

如您从这些条形图中可以看到,从特征集中过滤掉这些停用词使得更有意义的单词出现在高频出现的单词列表中。然而,我们还可以注意到另一件事。数字似乎出现在一些高频出现的单词中。例如,数字 32 成为了垃圾邮件中前十位高频出现的单词。数字 8070 成为了垃圾邮件中前十位高频出现的单词。然而,很难确定这些数字是否会在训练机器学习模型以将电子邮件分类为垃圾邮件或正常邮件时做出很大贡献。有多种方法可以从特征集中过滤掉这些数字,但在这里我们将向您展示一种方法。我们更新了之前步骤中使用的 regex,以匹配仅包含字母字符的单词,而不是字母数字字符。以下代码显示了如何更新 CreateWordVec 函数以从特征集中过滤掉数字:

private static Frame<int, string> CreateWordVec(Series<int, string> rows)
{
    var wordsByRows = rows.GetAllValues().Select((x, i) =>
    {
        var sb = new SeriesBuilder<string, int>();

        ISet<string> words = new HashSet<string>(
            Regex.Matches(
                // Alphabetical characters only
                x.Value, "[a-zA-Z]+('(s|d|t|ve|m))?"
            ).Cast<Match>().Select(
                // Then, convert each word to lowercase
                y => y.Value.ToLower()
            ).ToArray()
        );

        // Encode words appeared in each row with 1
        foreach (string w in words)
        {
            sb.Add(w, 1);
        }

        return KeyValue.Create(i, sb.Series);
    });

    // Create a data frame from the rows we just created
    // And encode missing values with 0
    var wordVecDF = Frame.FromRows(wordsByRows).FillMissing(0);

    return wordVecDF;
}

一旦我们从特征集中过滤掉这些数字,垃圾邮件的单词分布看起来如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00026.jpeg

并且在过滤掉特征集中的数字后,垃圾邮件的单词分布如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00027.jpeg

如从这些条形图中可以看出,我们列出了更有意义的词语,并且垃圾邮件和正常邮件的词语分布似乎有更大的区别。那些在垃圾邮件中频繁出现的词语似乎在正常邮件中很少出现,反之亦然。

数据分析和特征工程步骤的完整代码可以在以下仓库中找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.2/DataAnalyzer.cs。运行此代码后,将生成条形图,显示垃圾邮件和正常邮件中的词语分布,以及两个 CSV 文件——一个用于包含出现次数的词语列表(正常邮件),另一个用于包含出现次数的词语列表(垃圾邮件)。在下一节构建分类模型进行垃圾邮件过滤时,我们将使用这个词频输出进行特征选择过程。

逻辑回归与朴素贝叶斯在电子邮件垃圾邮件过滤中的应用

我们已经走了很长的路,终于用 C# 构建了我们第一个机器学习模型。在本节中,我们将训练逻辑回归和朴素贝叶斯分类器,将电子邮件分类为垃圾邮件和正常邮件。我们将运行这两个学习算法的交叉验证,以估计并更好地理解我们的分类模型在实际应用中的表现。如前一章简要讨论的,在 k 折交叉验证中,训练集被分成 k 个大小相等的子集,其中一个 k 个子集被保留作为验证集,其余的 k-1 个子集用于训练模型。然后重复这个过程 k 次,其中每个迭代使用不同的子集或折作为验证集进行测试,然后将相应的 k 个验证结果平均报告为一个估计值。

首先,让我们看看如何使用 Accord.NET 框架在 C# 中通过逻辑回归实现交叉验证算法的实例化。代码如下:

var cvLogisticRegressionClassifier = CrossValidation.Create<LogisticRegression, IterativeReweightedLeastSquares<LogisticRegression>, double[], int>(
    // number of folds
    k: numFolds,
    // Learning Algorithm
    learner: (p) => new IterativeReweightedLeastSquares<LogisticRegression>()
    {
        MaxIterations = 100,
        Regularization = 1e-6
    },
    // Using Zero-One Loss Function as a Cost Function
    loss: (actual, expected, p) => new ZeroOneLoss(expected).Loss(actual),
    // Fitting a classifier
    fit: (teacher, x, y, w) => teacher.Learn(x, y, w),
    // Input with Features
    x: input,
    // Output
    y: output
);

// Run Cross-Validation
var result = cvLogisticRegressionClassifier.Learn(input, output);

让我们更深入地看看这段代码。我们可以通过提供要训练的模型类型、拟合模型的算法类型、输入数据类型和输出数据类型,使用静态 Create 函数创建一个新的 CrossValidation 算法。在这个例子中,我们创建了一个新的 CrossValidation 算法,其中 LogisticRegression 作为模型,IterativeReweightedLeastSquares 作为学习算法,双精度数组作为输入类型,整数作为输出类型(每个标签)。你可以尝试不同的学习算法来训练逻辑回归模型。在 Accord.NET 中,你可以选择随机梯度下降算法 (LogisticGradientDescent) 作为拟合逻辑回归模型的学习算法。

对于参数,你可以指定 k 折交叉验证的折数(k),具有自定义参数的学习方法(learner),你选择的损失/成本函数(loss),以及一个知道如何使用学习算法(fit)、输入(x)和输出(y)来拟合模型的功能。为了本节说明的目的,我们为 k 折交叉验证设置了一个相对较小的数字,3。此外,我们为最大迭代次数选择了相对较小的数字,100,以及相对较大的数字,1e-6 或 1/1,000,000,用于IterativeReweightedLeastSquares学习算法的正则化。对于损失函数,我们使用了一个简单的零一损失函数,其中对于正确预测分配 0,对于错误预测分配 1。这是我们学习算法试图最小化的成本函数。所有这些参数都可以进行不同的调整。你可以选择不同的损失/成本函数,k 折交叉验证中使用的折数,以及学习算法的最大迭代次数和正则化数字。你甚至可以使用不同的学习算法来拟合逻辑回归模型,例如LogisticGradientDescent,它迭代地尝试找到一个损失函数的局部最小值。

我们可以将这种方法应用于使用 k 折交叉验证训练朴素贝叶斯分类器。使用朴素贝叶斯学习算法运行 k 折交叉验证的代码如下:

var cvNaiveBayesClassifier = CrossValidation.Create<NaiveBayes<BernoulliDistribution>, NaiveBayesLearning<BernoulliDistribution>, double[], int>(
    // number of folds
    k: numFolds,
    // Naive Bayes Classifier with Binomial Distribution
    learner: (p) => new NaiveBayesLearning<BernoulliDistribution>(),
    // Using Zero-One Loss Function as a Cost Function
    loss: (actual, expected, p) => new ZeroOneLoss(expected).Loss(actual),
    // Fitting a classifier
    fit: (teacher, x, y, w) => teacher.Learn(x, y, w),
    // Input with Features
    x: input,
    // Output
    y: output
);

// Run Cross-Validation
var result = cvNaiveBayesClassifier.Learn(input, output);

之前用于逻辑回归模型的代码与这段代码之间的唯一区别是我们选择的不同模型和学习算法。我们不是使用LogisticRegressionIterativeReweightedLeastSquares,而是使用了NaiveBayes作为模型,并使用NaiveBayesLearning作为学习算法来训练我们的朴素贝叶斯分类器。由于我们的所有输入值都是二元的(要么是 0,要么是 1),我们为我们的朴素贝叶斯分类器模型使用了BernoulliDistribution

训练和验证具有 k 折交叉验证的分类模型的完整代码可以在以下仓库中找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.2/Modeling.cs。当你运行此代码时,你应该会看到一个类似以下输出的结果:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00028.jpeg

在下一节中,我们将更详细地探讨这些数字代表什么,其中我们将讨论模型验证方法。为了尝试不同的机器学习模型,只需修改代码中的第 68-88 行。你可以用我们之前讨论过的逻辑回归模型代码替换这些行,或者你也可以尝试拟合你选择的不同学习算法。

分类模型验证

我们在上一节中使用了 C#和 Accord.NET 框架构建了我们非常第一个机器学习模型。然而,我们还没有完成。如果我们更仔细地查看之前的控制台输出,有一件事相当令人担忧。训练错误率大约是 0.03,但验证错误率大约是 0.26。这意味着我们的分类模型在训练集中正确预测了 100 次中的 87 次,但在验证或测试集中的模型预测只有 100 次中的 74 次是正确的。这是一个典型的过拟合例子,其中模型与训练集拟合得太紧密,以至于其对未预见数据集的预测是不可靠的和不可预测的。如果我们将这个模型用于生产中的垃圾邮件过滤系统,实际过滤垃圾邮件的性能将是不可靠的,并且与我们在训练集中看到的不同。

过拟合通常是因为模型对于给定的数据集来说过于复杂,或者使用了过多的参数来拟合模型。我们在上一节中构建的朴素贝叶斯分类器模型中存在的过拟合问题很可能是由于模型的复杂性和我们用来训练模型的特征数量。如果你再次查看上一节末尾的控制台输出,你可以看到我们用来训练朴素贝叶斯模型的特征数量是 2,212。考虑到我们的样本集中只有大约 4,200 封电子邮件记录,而且其中只有大约三分之二(或者说大约 3,000 条记录)被用来训练我们的模型(这是因为我们使用了三折交叉验证,并且每次迭代中只有其中的两个折被用作训练集),这实在太多了。为了修复这个过拟合问题,我们将不得不减少我们用来训练模型的特征数量。为了做到这一点,我们可以过滤掉那些出现频率不高的术语。执行此操作的代码位于上一节完整代码的第 48-53 行,如下所示:

// Change number of features to reduce overfitting
int minNumOccurrences = 1;
string[] wordFeatures = indexedSpamTermFrequencyDF.Where(
    x => x.Value.GetAs<int>("num_occurences") >= minNumOccurrences
).RowKeys.ToArray();
Console.WriteLine("Num Features Selected: {0}", wordFeatures.Count());

如你所见,我们在上一节中构建的朴素贝叶斯分类器模型使用了在垃圾邮件中至少出现一次的所有单词。如果你查看垃圾邮件中的单词频率,大约有 1,400 个单词只出现了一次(查看在数据分析步骤中创建的spam-frequencies.csv文件)。直观上看,这些出现次数低的单词只会产生噪声,而不是为我们的模型提供很多学习信息。这立即告诉我们,当我们最初在上一节中构建我们的分类模型时,我们的模型会暴露于多少噪声。

既然我们已经知道了这个过拟合问题的原因,让我们来修复它。让我们尝试使用不同的阈值来选择特征。我们已经尝试了 5、10、15、20 和 25 作为垃圾邮件中最低出现次数(即我们将minNumOccurrences设置为 5、10、15 等等)并使用这些阈值训练了朴素贝叶斯分类器。

首先,具有至少五次出现的朴素贝叶斯分类器结果如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00029.jpeg

具有至少 10 次出现的朴素贝叶斯分类器结果如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00030.jpeg

具有至少 15 次出现的朴素贝叶斯分类器结果如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00031.jpeg

最后,具有至少 20 次出现的朴素贝叶斯分类器结果如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00032.jpeg

从这些实验结果中可以看出,随着我们增加最小单词出现次数并相应地减少用于训练模型的特征数量,训练错误验证错误之间的差距减小,训练错误开始看起来更接近验证错误。当我们解决了过拟合问题,我们可以对模型在不可预见的数据和在生产系统中的表现更有信心。我们使用逻辑回归分类模型进行了相同的实验,结果与朴素贝叶斯分类器发现的结果相似。逻辑回归模型的实验结果如下所示。

首先,具有至少五次出现的逻辑回归分类器结果如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00029.jpeg

具有至少十次出现的逻辑回归分类器结果如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00030.jpeg

具有至少 15 次出现的逻辑回归分类器结果如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00033.jpeg

具有至少 20 次出现的逻辑回归分类器结果如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00034.jpeg

现在我们已经讨论了如何处理过拟合问题,还有一些模型性能指标我们想要查看:

  • 混淆矩阵:混淆矩阵是一个表格,它告诉我们预测模型的总体性能。每一列代表每个实际类别,每一行代表每个预测类别。在二元分类问题的案例中,混淆矩阵将是一个 2 x 2 的矩阵,其中第一行代表负预测,第二行代表正预测。第一列代表实际负值,第二列代表实际正值。以下表格说明了二元分类问题的混淆矩阵中每个单元格代表的内容:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00035.jpeg

  • 真负TN)是指模型正确预测了类别 0;假负FN)是指模型预测为0,但实际类别是1假正FP)是指模型预测为类别1,但实际类别是0;而真正TP)是指模型正确预测了类别1。从表中可以看出,混淆矩阵描述了整体模型性能。在我们的例子中,如果我们查看之前截图中的最后一个控制台输出,其中显示了我们的逻辑回归分类模型的控制台输出,我们可以看到 TNs 的数量为2847,FNs 的数量为606,FPs 的数量为102,TPs 的数量为772。有了这些信息,我们可以进一步计算真正正率TPR)、真正负率TNR)、假正率FPR)和假负率FNR)如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00036.jpeg

使用前面的例子,我们例子中的真正正率(TPR)为 0.56,真正负率(TNR)为 0.97,假正率(FPR)为 0.03,假负率(FNR)为 0.44。

  • 准确率: 准确率是指正确预测的比例。使用之前例子混淆矩阵中的相同符号,准确率可以计算如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00037.jpeg

准确率是一个常用的模型性能指标,但有时它并不能很好地代表整体模型性能。例如,如果样本集大部分不平衡,比如说在我们的样本集中有五个垃圾邮件和 95 封正常邮件,那么一个简单地将所有邮件分类为正常邮件的分类器将不得不达到 95%的准确率。然而,它永远不会捕获垃圾邮件。这就是为什么我们需要查看混淆矩阵和其他性能指标,如精确率和召回率:

  • 精确率: 精确率是指正确预测的正例数量与总预测正例数量的比例。使用与之前相同的符号,我们可以计算精确率如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00038.jpeg

如果您查看之前截图中的最后一个控制台输出,我们的逻辑回归分类模型结果中的精确率是通过将混淆矩阵中的 TPs 数量,即 772,除以 TPs 和 FPs 的总和,即 772 和 102,得到的,结果为 0.88。

  • 召回率: 召回率是指正确预测的正例数量与实际正例总数量的比例。这是告诉我们模型检索了多少实际正例的一种方式。使用与之前相同的符号,我们可以计算召回率如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00039.jpeg

如果你查看上一张截图中的最后一个控制台输出,即我们的逻辑回归分类模型结果,召回率是通过将混淆矩阵中 TP(真阳性)的数量,772,除以 TP(真阳性)和 FN(假阴性)的总和,772 和 606,得到的,结果是 0.56。

使用这些性能指标,数据科学家的责任是选择最优模型。精确率和召回率之间总会存在权衡。一个精确率高于其他模型的模型将具有较低的召回率。在我们的垃圾邮件过滤问题中,如果你认为正确过滤掉垃圾邮件更重要,并且你可以牺牲一些通过用户收件箱的垃圾邮件,那么你可能希望优化精确率。另一方面,如果你认为过滤掉尽可能多的垃圾邮件更重要,即使你可能会过滤掉一些非垃圾邮件,那么你可能希望优化召回率。选择正确的模型不是一个容易的决定,思考需求和成功标准对于做出正确的选择至关重要。

总结来说,以下是我们可以从交叉验证结果和混淆矩阵中计算性能指标的代码:

  • 训练与验证(测试)错误:用于识别过拟合问题(第 48-52 行):
// Run Cross-Validation
var result = cvNaiveBayesClassifier.Learn(input, output);

// Training Error vs. Test Error
double trainingError = result.Training.Mean;
double validationError = result.Validation.Mean;
  • 混淆矩阵:真阳性与假阳性,以及真阴性与假阴性(第 95-108 行):
// Confusion Matrix
GeneralConfusionMatrix gcm = result.ToConfusionMatrix(input, output);

float truePositive = (float)gcm.Matrix[1, 1];
float trueNegative = (float)gcm.Matrix[0, 0];
float falsePositive = (float)gcm.Matrix[1, 0];
float falseNegative = (float)gcm.Matrix[0, 1];
  • 准确率与精确率与召回率:用于衡量机器学习模型的正确性(第 122-130 行):
// Accuracy vs. Precision vs. Recall
float accuracy = (truePositive + trueNegative) / numberOfSamples;
float precision = truePositive / (truePositive + falsePositive);
float recall = truePositive / (truePositive + falseNegative);

摘要

在本章中,我们使用 C#构建了我们第一个机器学习模型,它可以用于垃圾邮件过滤。我们首先定义并清楚地说明了我们试图解决的问题以及成功标准。然后,我们从原始电子邮件数据中提取相关信息,并将其转换成我们可以用于数据分析、特征工程和机器学习模型构建步骤的格式。在数据分析步骤中,我们学习了如何应用独热编码,并构建了用于主题行中使用的单词的矩阵表示。我们还从我们的数据分析过程中识别出一个数据问题,并学习了我们通常如何在数据准备和分析步骤之间来回迭代。然后,我们通过过滤掉停用词和使用正则表达式来分割非字母数字或非字母词来进一步改进我们的特征集。有了这个特征集,我们使用逻辑回归和朴素贝叶斯分类器算法构建了我们第一个分类模型,简要介绍了过拟合的危险,并学习了如何通过查看准确率、精确率和召回率来评估和比较模型性能。最后,我们还学习了精确率和召回率之间的权衡,以及如何根据这些指标和业务需求来选择模型。

在下一章中,我们将进一步扩展我们在使用文本数据集构建分类模型方面的知识和技能。我们将从分析一个包含超过两个类别的数据集开始,使用 Twitter 情感数据。我们将学习二分类模型和多分类模型之间的区别。我们还将讨论一些用于特征工程的 NLP 技术,以及如何使用随机森林算法构建多分类分类模型。

第三章:Twitter 情感分析

在本章中,我们将扩展我们在 C#中构建分类模型的知识。除了我们在上一章中使用过的 Accord.NET 和 Deedle 这两个包,我们还将开始使用 Stanford CoreNLP 包来应用更高级的自然语言处理(NLP)技术,例如分词、词性标注和词元化。使用这些包,本章的目标是构建一个多类分类模型,用于预测推文的情感。我们将使用一个包含不仅只有单词,还有表情符号的原始 Twitter 数据集,并使用它来训练一个用于情感预测的机器学习(ML)模型。我们将遵循构建 ML 模型时遵循的相同步骤。我们将从问题定义开始,然后进行数据准备和分析,特征工程,以及模型开发和验证。在我们的特征工程步骤中,我们将扩展我们对 NLP 技术的知识,并探讨如何将分词、词性标注和词元化应用于构建更高级的文本特征。在模型构建步骤中,我们将探索一个新的分类算法,即随机森林分类器,并将其性能与朴素贝叶斯分类器进行比较。最后,在我们的模型验证步骤中,我们将扩展我们对混淆矩阵、精确率和召回率的了解,这些我们在上一章中已经介绍过,并讨论接收者操作特征(ROC)曲线和曲线下面积(AUC)是什么,以及这些概念如何用于评估我们的 ML 模型。

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

  • 使用 Stanford CoreNLP 包设置环境

  • Twitter 情感分析项目的定义问题

  • 使用 Stanford CoreNLP 进行数据准备

  • 使用词元作为标记的数据分析

  • 使用词元化和表情符号进行特征工程

  • 朴素贝叶斯与随机森林的比较

  • 使用 ROC 曲线和 AUC 指标进行模型验证

设置环境

在我们深入 Twitter 情感分析项目之前,让我们设置我们的开发环境,我们将使用 Stanford CoreNLP 包来完成本章的所有工作。要准备好包含 Stanford CoreNLP 包的环境,需要多个步骤,所以最好按以下步骤进行:

  1. 第一步是在 Visual Studio 中创建一个新的控制台应用程序(.NET Framework)项目。确保你使用的是 4.6.1 或更高版本的.NET Framework。如果你安装了较旧版本,请访问docs.microsoft.com/en-us/dotnet/framework/install/guide-for-developers并遵循安装指南。以下是一个项目设置页面的截图(注意:你可以在顶部栏中选择你的.NET Framework 版本):

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00040.jpeg

  1. 现在,让我们安装 Stanford CoreNLP 包。你可以在你的包管理控制台中输入以下命令:
Install-Package Stanford.NLP.CoreNLP

我们在本章中将使用的是 Stanford.NLP.CoreNLP 3.9.1 版本。随着时间的推移,版本可能会发生变化,你可能需要更新你的安装。

  1. 我们只需再做一些事情,我们的环境就准备好开始使用这个包了。我们需要安装 CoreNLP 模型 JAR 文件,它包含用于解析、POS 标记、命名实体识别NER)和其他一些工具的各种模型。点击此链接下载并解压 Stanford CoreNLP:stanfordnlp.github.io/CoreNLP/. 下载并解压后,你将看到那里有多个文件。我们感兴趣的特定文件是 stanford-corenlp-<版本号>-models.jar。我们需要从该 jar 文件中提取内容到一个目录中,以便我们可以在我们的 C# 项目中加载所有模型文件。你可以使用以下命令从 stanford-corenlp-<版本号>-models.jar 中提取内容:
jar xf stanford-corenlp-<version-number>-models.jar 

当你从模型 jar 文件中提取完所有模型文件后,你现在就可以开始在 C# 项目中使用 Stanford CoreNLP 包了。

现在,让我们检查我们的安装是否成功。以下代码是对本例的轻微修改 (sergey-tihon.github.io/Stanford.NLP.NET/StanfordCoreNLP.html) :

using System;
using System.IO;
using java.util;
using java.io;
using edu.stanford.nlp.pipeline;
using Console = System.Console;

namespace Tokenizer
{
    class Program
    {
        static void Main()
        {
            // Path to the folder with models extracted from Step #3
            var jarRoot = @"<path-to-your-model-files-dir>";

            // Text for processing
            var text = "We're going to test our CoreNLP installation!!";

            // Annotation pipeline configuration
            var props = new Properties();
            props.setProperty("annotators", "tokenize, ssplit, pos, lemma");
            props.setProperty("ner.useSUTime", "0");

            // We should change current directory, so StanfordCoreNLP could find all the model files automatically
            var curDir = Environment.CurrentDirectory;
            Directory.SetCurrentDirectory(jarRoot);
            var pipeline = new StanfordCoreNLP(props);
            Directory.SetCurrentDirectory(curDir);

            // Annotation
            var annotation = new Annotation(text);
            pipeline.annotate(annotation);

            // Result - Pretty Print
            using (var stream = new ByteArrayOutputStream())
            {
                pipeline.prettyPrint(annotation, new PrintWriter(stream));
                Console.WriteLine(stream.toString());
                stream.close();
            }

            Console.ReadKey();
        }
    }
}

如果你的安装成功,你应该会看到以下类似的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00041.gif

让我们更仔细地看看这个输出。标记是作为单个语义单元组合的字符序列。通常,标记是单词术语。在每一行标记输出中,我们可以看到原始文本,例如 We'regoingPartOfSpeech 标签指的是每个单词的类别,例如名词、动词和形容词。例如,我们例子中第一个标记 WePartOfSpeech 标签是 PRP,它代表人称代词。我们例子中第二个标记 'rePartOfSpeech 标签是 VBP,它代表动词,非第三人称单数现在时。完整的 POS 标签列表可以在以下位置找到 (www.ling.upenn.edu/courses/Fall_2003/ling001/penn_treebank_pos.html) 或在以下屏幕截图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00042.jpeg

POS 标签列表

最后,在我们标记化示例中的 Lemma 标签指的是给定单词的标准形式。例如,amare 的词元是 be。在我们的例子中,第三个标记中的单词 going 的词元是 go。我们将在以下章节中讨论如何使用词元化进行特征工程。

Twitter 情感分析问题定义

让我们通过明确定义我们将构建的模型及其预测内容来开始我们的 Twitter 情感分析项目。你可能已经听说过“情感分析”这个术语。情感分析本质上是一个计算过程,用于确定给定的文本表达的是积极、中性还是消极情感。社交媒体内容的情感分析可以用于多种方式。例如,营销人员可以使用它来识别营销活动有多有效,以及它如何影响消费者对某个产品或公司的看法和态度。情感分析还可以用于预测股市变化。对某个公司的正面新闻和整体正面情感往往推动其股价上涨,而对于某个公司的新闻和社交媒体中的情感分析可以用来预测股价在不久的将来会如何变动。为了实验如何构建情感分析模型,我们将使用来自 CrowdFlower 的 Data for Everyone 库的预编译和标记的航空情感 Twitter 数据集(www.figure-eight.com/data-for-everyone/)。然后,我们将应用一些 NLP 技术,特别是词素化、词性标注和词形还原,从原始推文数据中构建有意义的文本和表情符号特征。由于我们想要预测每条推文的三个不同情感(积极、中性和消极),我们将构建一个多类分类模型,并尝试不同的学习算法——朴素贝叶斯和随机森林。一旦我们构建了情感分析模型,我们将主要通过以下三个指标来评估其性能:精确度、召回率和 AUC。

让我们总结一下 Twitter 情感分析项目的需求定义:

  • 问题是什么?我们需要一个 Twitter 情感分析模型来计算识别推文中的情感。

  • 为什么这是一个问题?识别和衡量用户或消费者对某个主题(如产品、公司、广告等)的情感,通常是衡量某些任务影响力和成功的重要工具。

  • 解决这个问题的方法有哪些?我们将使用斯坦福 CoreNLP 包来应用各种 NLP 技术,如分词、词性标注和词形还原,从原始 Twitter 数据集中构建有意义的特征。有了这些特征,我们将尝试不同的学习算法来构建情感分析模型。我们将使用精确度、召回率和 AUC 指标来评估模型的性能。

  • 成功的标准是什么?我们希望有高精确率,同时不牺牲太多的召回率,因为正确地将一条推文分类到三个情感类别(正面、中立和负面)比更高的检索率更重要。此外,我们希望有高 AUC 值,我们将在本章后面的部分详细讨论。

使用斯坦福 CoreNLP 进行数据准备

既然我们已经知道了本章的目标,现在是时候深入数据了。与上一章类似,我们将使用预编译和预标记的 Twitter 情感数据。我们将使用来自 CrowdFlower 的 Data for Everyone 库的数据集(www.figure-eight.com/data-for-everyone/),你可以从这个链接下载数据:www.kaggle.com/crowdflower/twitter-airline-sentiment。这里的数据是关于大约 15,000 条关于美国航空公司的推文。这些 Twitter 数据是从 2015 年 2 月抓取的,然后被标记为三个类别——正面、负面和中立。链接提供了两种类型的数据:CSV 文件和 SQLite 数据库。我们将在这个项目中使用 CSV 文件。

一旦你下载了这些数据,我们需要为未来的分析和模型构建做准备。数据集中我们感兴趣的两大列是airline_sentimenttextairline_sentiment列包含关于情感的信息——一条推文是否有积极、消极或中性的情感——而text列包含原始的 Twitter 文本。为了使这些原始数据便于我们未来的数据分析和管理模型构建步骤,我们需要完成以下任务:

  • 清理不必要的文本:很难证明文本的某些部分提供了许多见解和信息,供我们的模型学习,例如 URL、用户 ID 和原始数字。因此,准备我们原始数据的第一个步骤是清理不包含太多信息的无用文本。在这个例子中,我们移除了 URL、Twitter 用户 ID、数字和标签符号。我们使用Regex将此类文本替换为空字符串。以下代码展示了我们用来过滤这些文本的Regex表达式:
// 1\. Remove URL's
string urlPattern = @"https?:\/\/\S+\b|www\.(\w+\.)+\S*";
Regex rgx = new Regex(urlPattern);
tweet = rgx.Replace(tweet, "");

// 2\. Remove Twitter ID's
string userIDPattern = @"@\w+";
rgx = new Regex(userIDPattern);
tweet = rgx.Replace(tweet, "");

// 3\. Remove Numbers
string numberPattern = @"[-+]?[.\d]*[\d]+[:,.\d]*";
tweet = Regex.Replace(tweet, numberPattern, "");

// 4\. Replace Hashtag
string hashtagPattern = @"#";
tweet = Regex.Replace(tweet, hashtagPattern, "");

如你所见,有两种方式可以替换匹配Regex模式的字符串。你可以实例化一个Regex对象,然后用另一个字符串替换匹配的字符串,如前两个案例所示。你也可以直接调用静态的Regex.Replace方法来达到同样的目的,如最后两个案例所示。静态方法会在每次调用Regex.Replace方法时创建一个Regex对象,所以如果你在多个地方使用相同的模式,第一种方法会更好:

  • 将相似的表情符号分组并编码:表情符号,如笑脸和悲伤脸,在推文中经常被使用,并提供了关于每条推文情感的见解。直观地,一个用户会用笑脸表情符号来推文关于积极事件,而另一个用户会用悲伤脸表情符号来推文关于负面事件。然而,不同的笑脸表现出相似的正向情感,可以分组在一起。例如,带括号的笑脸:)与带大写字母D的笑脸:D具有相同的意义。因此,我们希望将这些相似的表情符号分组在一起,并将它们编码为一个组,而不是让它们分别在不同的组中。我们将使用 Romain Paulus 和 Jeffrey Pennington 分享的 R 代码(nlp.stanford.edu/projects/glove/preprocess-twitter.rb),将其翻译成 C#,然后将其应用于我们的原始 Twitter 数据集。以下是如何将 R 中编写的表情符号Regex代码翻译成 C#,以便我们可以将相似的表情符号分组并编码:
// 1\. Replace Smiley Faces
string smileyFacePattern = String.Format(@"{0}{1}[)dD]+|[)dD]+{1}{0}", eyesPattern, nosePattern);
tweet = Regex.Replace(tweet, smileyFacePattern, " emo_smiley ");

// 2\. Replace LOL Faces
string lolFacePattern = String.Format(@"{0}{1}[pP]+", eyesPattern, nosePattern);
tweet = Regex.Replace(tweet, lolFacePattern, " emo_lol ");

// 3\. Replace Sad Faces
string sadFacePattern = String.Format(@"{0}{1}\(+|\)+{1}{0}", eyesPattern, nosePattern);
tweet = Regex.Replace(tweet, sadFacePattern, " emo_sad ");

// 4\. Replace Neutral Faces
string neutralFacePattern = String.Format(@"{0}{1}[\/|l*]", eyesPattern, nosePattern);
tweet = Regex.Replace(tweet, neutralFacePattern, " emo_neutral ");

// 5\. Replace Heart
string heartPattern = "<3";
tweet = Regex.Replace(tweet, heartPattern, " emo_heart ");
  • 将其他有用的表达式分组并编码:最后,还有一些可以帮助我们的模型检测推文情感的表达式。重复的标点符号,如!!!???,以及长单词,如wayyyysoooo,可以提供一些关于推文情感的额外信息。我们将分别将它们分组并编码,以便我们的模型可以从这些表达式中学习。以下代码展示了如何编码这样的表达式:
// 1\. Replace Punctuation Repeat
string repeatedPunctuationPattern = @"([!?.]){2,}";
tweet = Regex.Replace(tweet, repeatedPunctuationPattern, " $1_repeat ");

// 2\. Replace Elongated Words (i.e. wayyyy -> way_emphasized)
string elongatedWordsPattern = @"\b(\S*?)(.)\2{2,}\b";
tweet = Regex.Replace(tweet, elongatedWordsPattern, " $1$2_emphasized ");

如代码所示,对于重复的标点符号,我们在字符串后附加一个后缀_repeat。例如,!!!将变成!_repeat,而???将变成?_repeat。对于长单词,我们在字符串后附加一个后缀_emphasized。例如,wayyyy将变成way_emphasized,而soooo将变成so_emphasized

将原始数据集处理成单个 Twitter 文本,并导出处理后的 Twitter 文本到另一个数据文件的全代码可以在本存储库中找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.3/DataProcessor.cs。让我们简要地浏览一下代码。它首先将原始Tweets.csv数据集读入一个 Deedle 数据框(第 76-82 行)。然后,它调用一个名为FormatTweets的方法,该方法包含一个包含所有原始 Twitter 文本的列序列。第 56-65 行的FormatTweets方法代码如下所示:

private static string[] FormatTweets(Series<int, string> rows)
{
    var cleanTweets = rows.GetAllValues().Select((x, i) =>
    {
        string tweet = x.Value;
        return CleanTweet(tweet);
    });

    return cleanTweets.ToArray();
}

FormatTweets方法遍历序列中的每个元素,即原始推文,并调用CleanTweet方法。在CleanTweet方法中,每条原始推文都会与之前定义的所有Regex模式进行匹配,然后按照之前讨论的方式进行处理。第 11-54 行的CleanTweet方法如下所示:

private static string CleanTweet(string rawTweet)
{
      string eyesPattern = @"[8:=;]";
      string nosePattern = @"['`\-]?";

      string tweet = rawTweet;
      // 1\. Remove URL's
      string urlPattern = @"https?:\/\/\S+\b|www\.(\w+\.)+\S*";
      Regex rgx = new Regex(urlPattern);
      tweet = rgx.Replace(tweet, "");
      // 2\. Remove Twitter ID's
      string userIDPattern = @"@\w+";
      rgx = new Regex(userIDPattern);
      tweet = rgx.Replace(tweet, "");
      // 3\. Replace Smiley Faces
      string smileyFacePattern = String.Format(@"{0}{1}[)dD]+|[)dD]+{1}{0}", eyesPattern, nosePattern);
      tweet = Regex.Replace(tweet, smileyFacePattern, " emo_smiley ");
      // 4\. Replace LOL Faces
      string lolFacePattern = String.Format(@"{0}{1}[pP]+", eyesPattern, nosePattern);
      tweet = Regex.Replace(tweet, lolFacePattern, " emo_lol ");
      // 5\. Replace Sad Faces
      string sadFacePattern = String.Format(@"{0}{1}\(+|\)+{1}{0}", eyesPattern, nosePattern);
      tweet = Regex.Replace(tweet, sadFacePattern, " emo_sad ");
      // 6\. Replace Neutral Faces
      string neutralFacePattern = String.Format(@"{0}{1}[\/|l*]", eyesPattern, nosePattern);
      tweet = Regex.Replace(tweet, neutralFacePattern, " emo_neutral ");
      // 7\. Replace Heart
      string heartPattern = "<3";
      tweet = Regex.Replace(tweet, heartPattern, " emo_heart ");
      // 8\. Replace Punctuation Repeat
      string repeatedPunctuationPattern = @"([!?.]){2,}";
      tweet = Regex.Replace(tweet, repeatedPunctuationPattern, " $1_repeat ");
      // 9\. Replace Elongated Words (i.e. wayyyy -> way_emphasized)
      string elongatedWordsPattern = @"\b(\S*?)(.)\2{2,}\b";
      tweet = Regex.Replace(tweet, elongatedWordsPattern, " $1$2_emphasized ");
      // 10\. Replace Numbers
      string numberPattern = @"[-+]?[.\d]*[\d]+[:,.\d]*";
      tweet = Regex.Replace(tweet, numberPattern, "");
      // 11\. Replace Hashtag
      string hashtagPattern = @"#";
      tweet = Regex.Replace(tweet, hashtagPattern, "");

      return tweet;
}

一旦所有原始的 Twitter 推文都被清理和加工处理,结果就会被添加到原始的 Deedle 数据框中作为一个单独的列,其列名为tweet。以下代码(第 89 行)展示了如何将字符串数组添加到数据框中:

rawDF.AddColumn("tweet", processedTweets);

当您已经走到这一步时,我们唯一需要做的额外步骤就是导出处理后的数据。使用 Deedle 数据框的SaveCsv方法,您可以轻松地将数据框导出为 CSV 文件。以下代码展示了我们如何将处理后的数据导出为 CSV 文件:

rawDF.SaveCsv(Path.Combine(dataDirPath, "processed-training.csv"));

现在我们有了干净的 Twitter 文本,让我们对其进行分词并创建推文的矩阵表示。类似于我们在第二章中做的,垃圾邮件过滤,我们将字符串分解成单词。然而,我们将使用我们在本章前一部分安装的 Stanford CoreNLP 包,并利用我们在前一部分编写的示例代码。分词推文并构建其矩阵表示的代码如下:

private static Frame<int, string> CreateWordVec(Series<int, string> rows, ISet<string> stopWords, bool useLemma=false)
        {
            // Path to the folder with models extracted from `stanford-corenlp-<version>-models.jar`
            var jarRoot = @"<path-to-model-files-dir>";

            // Annotation pipeline configuration
            var props = new Properties();
            props.setProperty("annotators", "tokenize, ssplit, pos, lemma");
            props.setProperty("ner.useSUTime", "0");

            // We should change current directory, so StanfordCoreNLP could find all the model files automatically
            var curDir = Environment.CurrentDirectory;
            Directory.SetCurrentDirectory(jarRoot);
            var pipeline = new StanfordCoreNLP(props);
            Directory.SetCurrentDirectory(curDir);

            var wordsByRows = rows.GetAllValues().Select((x, i) =>
            {
                var sb = new SeriesBuilder<string, int>();

                // Annotation
                var annotation = new Annotation(x.Value);
                pipeline.annotate(annotation);

                var tokens = annotation.get(typeof(CoreAnnotations.TokensAnnotation));
                ISet<string> terms = new HashSet<string>();

                foreach (CoreLabel token in tokens as ArrayList)
                {
                    string lemma = token.lemma().ToLower();
                    string word = token.word().ToLower();
                    string tag = token.tag();
                    //Console.WriteLine("lemma: {0}, word: {1}, tag: {2}", lemma, word, tag);

                    // Filter out stop words and single-character words
                    if (!stopWords.Contains(lemma) && word.Length > 1)
                    {
                        if (!useLemma)
                        {
                            terms.Add(word);
                        }
                        else
                        {
                            terms.Add(lemma);
                        }
                    }
                }

                foreach (string term in terms)
                {
                    sb.Add(term, 1);
                }

                return KeyValue.Create(i, sb.Series);
            });

            // Create a data frame from the rows we just created
            // And encode missing values with 0
            var wordVecDF = Frame.FromRows(wordsByRows).FillMissing(0);

            return wordVecDF;
        }

如您从代码中可以看到,这段代码与上一节中的示例代码的主要区别在于,这段代码会遍历每条推文并将标记存储到 Deedle 的数据框中。正如在第二章中,垃圾邮件过滤,我们使用独热编码来分配矩阵中每个术语的值(0 或 1)。在这里需要注意的一点是我们有创建包含词元或单词的矩阵的选项。单词是从每条推文中分解出来的原始未修改的术语。例如,字符串I am a data scientist,如果您使用单词作为标记,将会分解成Iamadatascientist。词元是每个标记中单词的标准形式。例如,相同的字符串I am a data scientist,如果您使用词元作为标记,将会分解成Ibeadatascientist。请注意beam的词元。我们将在使用词元化和表情符号进行特征工程部分讨论词元是什么以及词元化是什么。

分词和创建推文矩阵表示的完整代码可以在以下链接找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.3/TwitterTokenizer.cs。在这段代码中有几点需要注意。首先,让我们看看它是如何计算每种情感样本数量的。以下代码片段(第 122-127 行)展示了我们如何计算每种情感的样本数量:

// Look at the sentiment distributions in our sample set
var sampleSetDistribution = rawDF.GetColumn<string>(
    "airline_sentiment"
).GroupBy<string>(x => x.Value).Select(x => x.Value.KeyCount);
sampleSetDistribution.Print();

如您从这段代码中可以看到,我们首先获取情感列,airline_sentiment,并按值对其进行分组,其中值可以是中立负面正面。然后,它计算出现的次数并返回计数。

TwitterTokenizer代码中需要注意的第二件事是我们如何用整数值编码情感。以下是在完整代码的第 149-154 行中看到的内容:

tweetLemmaVecDF.AddColumn(
    "tweet_polarity", 
    rawDF.GetColumn<string>("airline_sentiment").Select(
        x => x.Value == "neutral" ? 0 : x.Value == "positive" ? 1 : 2
    )
);
tweet_polarity, to the term matrix data frame. We are taking the values of the airline_sentiment column and encoding 0 for neutral, 1 for positive, and 2 for negative. We are going to use this newly added column in our future model building steps.

最后,请注意我们是如何两次调用CreateWordVec方法的——一次没有词形还原(第 135-144 行),一次有词形还原(第 147-156 行)。如果我们创建一个没有词形还原的单热编码的术语矩阵,我们实际上是将所有单词作为术语矩阵中的单个标记。正如您所想象的,这将比有词形还原的矩阵大得多,稀疏性也更高。我们留下了这两段代码供您探索两种选项。您可以尝试使用以单词为列的矩阵构建 ML 模型,并与以词元为列的模型进行比较。在本章中,我们将使用词元矩阵而不是单词矩阵。

当您运行此代码时,它将输出一个条形图,显示样本集中的情感分布。如您在以下图表中看到的,在我们的样本集中大约有 3,000 条中性推文,2,000 条积极推文和 9,000 条消极推文。图表如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00043.jpeg

使用词元作为标记的数据分析

现在是时候查看实际数据,并寻找术语频率分布与推文不同情感之间的任何模式或差异了。我们将使用上一步的输出,并获取每个情感中最常出现的七个标记的分布。在这个例子中,我们使用了一个包含词元的术语矩阵。您可以自由地运行相同的分析,使用以单词为列的术语矩阵。分析推文中每个情感中最常使用的 N 个标记的代码可以在以下位置找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.3/DataAnalyzer.cs

在这段代码中有一点需要注意。与上一章不同,我们需要为三个情感类别——中性、消极和积极——计算术语频率。以下是从完整代码中摘录的代码片段(第 54-73 行):

var neutralTermFrequencies = ColumnWiseSum(
    tweetLemmaDF.Where(
        x => x.Value.GetAs<int>("tweet_polarity") == 0
    ),
    "tweet_polarity"
).Sort().Reversed;

var positiveTermFrequencies = ColumnWiseSum(
    tweetLemmaDF.Where(
        x => x.Value.GetAs<int>("tweet_polarity") == 1
    ),
    "tweet_polarity"
).Sort().Reversed;

var negativeTermFrequencies = ColumnWiseSum(
    tweetLemmaDF.Where(
        x => x.Value.GetAs<int>("tweet_polarity") == 2
    ),
    "tweet_polarity"
).Sort().Reversed;

如您从代码中可以看到,我们为每个情感类别调用了ColumnWiseSum方法,这个方法的代码如下:

private static Series<string, double> ColumnWiseSum(Frame<int, string> frame, string exclude)
{
    var sb = new SeriesBuilder<string, double>();
    foreach(string colname in frame.ColumnKeys)
    {
        double frequency = frame[colname].Sum();
        if (!colname.Equals(exclude))
        {
            sb.Add(colname, frequency);
        }
    }

    return sb.ToSeries();
}

如您从这段代码中看到的,它遍历每一列或术语,并计算该列内的所有值。由于我们使用了单热编码,简单的列求和将给我们 Twitter 数据集中每个术语的出现次数。一旦我们计算了所有列求和,我们就将它们作为 Deedle 系列对象返回。有了这些结果,我们按频率对术语进行排名,并将这些信息存储在三个单独的文件中,分别是neutral-frequencies.csvnegative-frequencies.csvpositive-frequencies.csv。我们将在后面的章节中使用术语频率输出进行特征工程和模型构建。

当您运行代码时,它将生成以下图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00044.jpeg

如您从图表中可以看到,不同情感之间的分布存在一些明显的差异。例如,谢谢很好是积极推文中出现频率最高的七个词中的两个,而延误取消则是消极推文中出现频率最高的七个词中的两个。直观上看,这些是有道理的。您通常会在表达对某人或某事的积极感受时使用谢谢很好。另一方面,延误取消与飞行或航空领域的负面事件相关。也许有些用户的航班延误或取消,他们就在推特上表达了自己的挫败感。另一个值得注意的有趣现象是,emo_smiley这个术语在积极推文中被列为出现频率最高的七个词中的第七位。如果您还记得,在上一个步骤中,我们将所有笑脸表情符号(如:):D等)分组并编码为emo_smiley。这告诉我们,表情符号可能在我们的模型学习如何分类每条推文的情感方面发挥重要作用。现在我们已经对数据的外观以及每种情感出现的术语有了大致的了解,让我们来谈谈在本章中我们将采用的特征工程技术。

使用词元化和表情符号进行特征工程

在上一节中,我们简要地讨论了词元。让我们更深入地了解一下什么是词元以及什么是词元化。根据一个词在句子中的使用方式和位置,这个词会以不同的形式出现。例如,单词like可以以likesliked的形式出现,这取决于前面的内容。如果我们只是简单地将句子分词成单词,那么我们的程序将会把likelikesliked看作是三个不同的标记。然而,这可能不是我们想要的。这三个词具有相同的意义,当我们构建模型时,将它们作为特征集中的同一个标记分组会很有用。这就是词元化的作用。词元是一个词的基本形式,词元化是根据每个词在句子中的使用部分将每个词转换成词元。在上面的例子中,likelikesliked的词元,将likesliked系统地转换成like就是词元化。

下面是一个使用 Stanford CoreNLP 进行词元化的例子:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00045.jpeg

在这里,您可以看到likeslike都被词元化为like。这是因为这两个词在句子中都被用作动词,而动词形式的词元是like。让我们再看另一个例子:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00046.jpeg

在这里,第一个 likes 和第二个 likes 有不同的词干。第一个有一个 like 作为其词干,而第二个有一个 likes 作为其词干。这是因为第一个被用作动词,而第二个被用作名词。正如您可以从这些例子中看到的那样,根据句子的不同部分,相同单词的词干可能会有所不同。对您的文本数据集进行词形还原可以大大减少特征空间的稀疏性和维度,并有助于模型在没有过多噪声的情况下更好地学习。

类似于词形还原,我们也把相似的表情符号分到了同一个组。这是基于这样的假设:相似的表情符号具有相似的含义。例如,:):D 几乎具有相同的含义,如果不是完全相同。在另一种情况下,根据用户的不同,冒号和括号的顺序可能不同。一些用户可能会输入 :),但另一些用户可能会输入 (:。然而,这两者之间唯一的区别是冒号和括号的顺序,而含义是相同的。在所有这些情况下,我们都希望我们的模型能够学习到相同的情感,并且不会产生任何噪声。将相似的表情符号分组到同一个组,就像我们在上一步所做的那样,有助于减少模型的不必要噪声,并帮助它们从这些表情符号中学习到最多。

高斯贝叶斯与随机森林

现在终于到了训练我们的机器学习模型来预测推文的情感的时候了。在本节中,我们将尝试使用朴素贝叶斯和随机森林分类器。我们将要做两件与上一章不同的事情。首先,我们将把我们的样本集分成训练集和验证集,而不是运行 k 折交叉验证。这也是一种常用的技术,其中模型只从样本集的一个子集中学习,然后它们用未在训练时观察到的其余部分进行测试和验证。这样,我们可以测试模型在不可预见的数据集上的表现,并模拟它们在实际世界中的行为。我们将使用 Accord.NET 包中的 SplitSetValidation 类,将我们的样本集分成训练集和验证集,并为每个集合预先定义比例,并将学习算法拟合到训练集。

其次,我们的目标变量不再是二进制(0 或 1),与之前的第二章垃圾邮件过滤不同。相反,它可以取 0、1 或 2 的任何值,其中 0 代表中性情感推文,1 代表积极情感推文,2 代表消极情感推文。因此,我们现在处理的是一个多类分类问题,而不是二类分类问题。在评估我们的模型时,我们必须采取不同的方法。我们必须修改上一章中的准确率、精确率和召回率的计算代码,以计算本项目三个目标情感类别中的每个类别的这些数字。此外,当我们查看某些指标时,例如 ROC 曲线和 AUC,我们将在下一节讨论这些指标,我们必须使用一对一的方法。

首先,让我们看看如何在 Accord.NET 框架中使用 SplitSetValidation 类实例化我们的学习算法。以下是如何使用朴素贝叶斯分类器算法实例化一个 SplitSetValidation 对象的方法:

var nbSplitSet = new SplitSetValidation<NaiveBayes<BernoulliDistribution>, double[]>()
{
    Learner = (s) => new NaiveBayesLearning<BernoulliDistribution>(),

    Loss = (expected, actual, p) => new ZeroOneLoss(expected).Loss(actual),

    Stratify = false,

    TrainingSetProportion = 0.8,

    ValidationSetProportion = 0.2
};
var nbResult = nbSplitSet.Learn(input, output);
SplitSetValidation object—TrainingSetProportionand ValidationSetProportion. As the name suggests, you can define what percentage of your sample set is should be used for training with the TrainingSetProportionparameter and what percentage of your sample set to be used for validation with the ValidationSetProportion parameter. Here in our code snippet, we are telling our program to use 80% of our sample for training and 20% for validation. In the last line of the code snippet, we fit a Naive Bayes classification model to the train set that was split from the sample set. Also, note here that we used BernoulliDistribution for our Naive Bayes classifier, as we used one-hot encoding to encode our features and all of our features have binary values, similar to what we did in the previous chapter.

与我们使用朴素贝叶斯分类器实例化 SplitSetValidation 对象的方式类似,你还可以按照以下方式实例化另一个对象:

var rfSplitSet = new SplitSetValidation<RandomForest, double[]>()
{
    Learner = (s) => new RandomForestLearning()
    {
        NumberOfTrees = 100, // Change this hyperparameter for further tuning

        CoverageRatio = 0.5, // the proportion of variables that can be used at maximum by each tree

        SampleRatio = 0.7 // the proportion of samples used to train each of the trees

    },

    Loss = (expected, actual, p) => new ZeroOneLoss(expected).Loss(actual),

    Stratify = false,

    TrainingSetProportion = 0.7,

    ValidationSetProportion = 0.3
};
var rfResult = rfSplitSet.Learn(input, output);

我们将之前的代码替换为随机森林作为模型,以及 RandomForestLearning 作为学习算法。如果你仔细观察,会发现一些我们可以调整的 RandomForestLearning 的超参数。第一个是 NumberOfTrees。这个超参数允许你选择要进入你的随机森林中的决策树的数量。一般来说,随机森林中的树越多,性能越好,因为你实际上在森林中构建了更多的决策树。然而,性能的提升是以训练和预测时间为代价的。随着你在随机森林中增加树的数量,训练和预测将需要更多的时间。这里需要注意的其他两个参数是 CoverageRatioSampleRatioCoverageRatio 设置了每个树中使用的特征集的比例,而 SampleRatio 设置了每个树中使用的训练集的比例。较高的 CoverageRatioSampleRatio 会提高森林中单个树的表现,但也会增加树之间的相关性。树之间的低相关性有助于减少泛化误差;因此,在单个树预测能力和树之间的相关性之间找到一个良好的平衡对于构建一个好的随机森林模型至关重要。调整和实验这些超参数的各种组合可以帮助你避免过拟合问题,并在训练随机森林模型时提高你的模型性能。我们建议你构建多个具有不同超参数组合的随机森林分类器,并实验它们对模型性能的影响。

我们用来训练朴素贝叶斯和随机森林分类模型并输出验证结果的完整代码可以在以下链接找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.3/TwitterSentimentModeling.cs。让我们更仔细地看看这段代码。在第 36-41 行,它首先读取我们数据准备步骤中构建的标记矩阵文件tweet-lemma.csv。然后在第 43-51 行,我们读取我们数据分析步骤中构建的词频文件positive-frequencies.csvnegative-frequencies.csv。类似于我们在上一章中做的,我们在第 64 行基于词的出现次数进行特征选择。在这个例子中,我们尝试了 5、10、50、100 和 150 作为样本推文中词出现次数的最小阈值。从第 65 行开始,我们迭代这些阈值,并开始训练和评估朴素贝叶斯和随机森林分类器。每次在训练集上训练一个模型后,它就会对在训练时间内未观察到的验证集进行运行。

以下是在训练集和验证集上运行训练好的朴素贝叶斯模型以测量样本内和样本外性能的完整代码(第 113-135 行)的一部分:

// Get in-sample & out-sample prediction results for NaiveBayes Classifier
var nbTrainedModel = nbResult.Model;

int[] nbTrainSetIDX = nbSplitSet.IndicesTrainingSet;
int[] nbTestSetIDX = nbSplitSet.IndicesValidationSet;

Console.WriteLine("* Train Set Size: {0}, Test Set Size: {1}", nbTrainSetIDX.Length, nbTestSetIDX.Length);

int[] nbTrainPreds = new int[nbTrainSetIDX.Length];
int[] nbTrainActual = new int[nbTrainSetIDX.Length];
for (int i = 0; i < nbTrainPreds.Length; i++)
{
   nbTrainActual[i] = output[nbTrainSetIDX[i]];
   nbTrainPreds[i] = nbTrainedModel.Decide(input[nbTrainSetIDX[i]]);
}

int[] nbTestPreds = new int[nbTestSetIDX.Length];
int[] nbTestActual = new int[nbTestSetIDX.Length];
for (int i = 0; i < nbTestPreds.Length; i++)
{
   nbTestActual[i] = output[nbTestSetIDX[i]];
   nbTestPreds[i] = nbTrainedModel.Decide(input[nbTestSetIDX[i]]);
}

以下是在训练集和验证集上运行训练好的随机森林模型以测量样本内和样本外性能的完整代码(第 167-189 行)的一部分:

// Get in-sample & out-sample prediction results for RandomForest Classifier
var rfTrainedModel = rfResult.Model;

int[] rfTrainSetIDX = rfSplitSet.IndicesTrainingSet;
int[] rfTestSetIDX = rfSplitSet.IndicesValidationSet;

Console.WriteLine("* Train Set Size: {0}, Test Set Size: {1}", rfTrainSetIDX.Length, rfTestSetIDX.Length);

int[] rfTrainPreds = new int[rfTrainSetIDX.Length];
int[] rfTrainActual = new int[rfTrainSetIDX.Length];
for (int i = 0; i < rfTrainPreds.Length; i++)
{
    rfTrainActual[i] = output[rfTrainSetIDX[i]];
    rfTrainPreds[i] = rfTrainedModel.Decide(input[rfTrainSetIDX[i]]);
}

int[] rfTestPreds = new int[rfTestSetIDX.Length];
int[] rfTestActual = new int[rfTestSetIDX.Length];
for (int i = 0; i < rfTestPreds.Length; i++)
{
    rfTestActual[i] = output[rfTestSetIDX[i]];
    rfTestPreds[i] = rfTrainedModel.Decide(input[rfTestSetIDX[i]]);
}

让我们更仔细地看看这些。为了简洁起见,我们只看随机森林模型的情况,因为朴素贝叶斯分类器的情况将相同。在第 168 行,我们首先从学习结果中获取训练好的模型。然后,在第 170-171 行,我们从SplitSetValidation对象中获取样本内(训练集)和样本外(测试/验证集)的索引,以便我们可以迭代每一行或记录并做出预测。我们迭代这个过程两次——一次在第 175-181 行的样本内训练集上,再次在第 183-189 行的样本外验证集上。

一旦我们在训练集和测试集上获得了预测结果,我们就将这些结果通过一些验证方法进行验证(第 138-141 行用于朴素贝叶斯分类器,第 192-196 行用于随机森林分类器)。我们为这个项目专门编写了两种方法来验证模型——PrintConfusionMatrixDrawROCCurvePrintConfusionMatrix是我们在第二章,“垃圾邮件过滤”中使用的更新版本,现在它打印的是一个 3 x 3 的混淆矩阵,而不是 2 x 2 的混淆矩阵。另一方面,DrawROCCurve方法为这个项目引入了一些新的概念和新的模型验证方法。让我们在下一节更详细地讨论这些新的评估指标,这是我们在这个项目中使用的。

模型验证——ROC 曲线和 AUC

如前所述,我们在本章中使用不同的模型验证指标:ROC 曲线和 AUC。ROC 曲线是在各种阈值下,真实正率与假正率的关系图。曲线上的每个点代表在某个概率阈值下对应的真实正率和假正率对。它通常用于从不同的模型候选者中选择最佳和最优化模型。

ROC 曲线下的面积(AUC)衡量模型区分两个类别的好坏。在二元分类的情况下,AUC 衡量模型区分正结果和负结果的好坏。由于我们在这个项目中处理的是一个多类分类问题,我们使用一对一的方法来构建 ROC 曲线并计算 AUC。例如,一条 ROC 曲线可以将正面推文作为正面结果,将中立和负面推文作为负面结果,而另一条 ROC 曲线可以将中立推文作为正面结果,将正面和负面推文作为负面结果。如图表所示,我们为每个构建的模型绘制了三个 ROC 图表——一个用于中立与剩余(正面和负面)的对比,一个用于正面与剩余(中立和负面)的对比,以及一个用于负面与剩余(中立和正面)的对比。AUC 数值越高,模型越好,因为它表明模型有更大的可能性区分正类别和负类别。

以下图表显示了具有10个最小词频的朴素贝叶斯分类器的 ROC 曲线:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00047.jpeg

以下图表显示了具有50个最小词频的朴素贝叶斯分类器的 ROC 曲线:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00048.jpeg

以下图表显示了具有150个最小词频的朴素贝叶斯分类器的 ROC 曲线:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00049.jpeg

如您从图表中可以看到,我们也可以通过观察训练和测试结果曲线之间的差距来从 ROC 图表中检测过拟合问题。差距越大,模型过拟合的程度就越高。如果您看第一个案例,我们只过滤掉那些在推文中出现次数少于十次的术语,两个曲线之间的差距就很大。随着我们提高阈值,我们可以看到差距减小。当我们选择最终模型时,我们希望训练 ROC 曲线和测试/验证 ROC 曲线尽可能小。由于这种分辨率是以模型性能为代价的,我们需要找到这个权衡的正确截止线。

让我们现在看看我们的随机森林分类器中的一个样本。以下是从拟合随机森林分类器中得到的一个样本结果:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00050.jpeg

集成方法,例如随机森林,通常在分类问题中表现良好,通过集成更多树可以提高准确率。然而,它们也有一些局限性,其中之一在前面的随机森林分类器示例结果中已经展示。对于所有基于决策树模型而言,随机森林模型倾向于过拟合,尤其是在它试图从许多分类变量中学习时。正如从随机森林分类器的 ROC 曲线中可以看到的,训练集和测试集 ROC 曲线之间的差距很大,尤其是与朴素贝叶斯分类器的 ROC 曲线相比。具有最小词频出现阈值 150 的朴素贝叶斯分类器在训练集和测试集 ROC 曲线之间几乎没有差距,而相同阈值下的随机森林分类器在两个 ROC 曲线之间显示出较大的差距。在处理存在大量分类变量的数据集时,我们需要小心选择模型,并特别注意调整超参数,例如NumberOfTreesCoverageRatioSampleRatio*,*以优化随机森林模型。

摘要

在本章中,我们为 Twitter 情感分析构建和训练了更高级的分类模型。我们将前一章学到的知识应用于一个具有更复杂文本数据的多元分类问题。我们首先通过设置我们的环境开始,使用斯坦福 CoreNLP 包进行分词、词性标注和词形还原,在数据准备和分析步骤中。然后,我们将原始 Twitter 数据集通过分词和词形还原转换为一个独热编码矩阵。在数据准备步骤中,我们还讨论了如何使用正则表达式将相似的表情符号分组,并从推文中移除不必要的文本,例如 URL、Twitter ID 和原始数字。在数据分析步骤中,我们进一步分析了常用术语和表情符号的分布,并看到词形还原和将相似的表情符号分组如何有助于减少数据集中的噪声。在之前的步骤中获取数据和洞察后,我们尝试使用朴素贝叶斯和随机森林分类器构建多元分类模型。在构建这些模型的过程中,我们介绍了一种常用的模型验证技术,即将样本集分为两个子集,训练集和验证集,使用训练集来拟合模型,使用验证集来评估模型性能。我们还介绍了新的模型验证指标,ROC 曲线和 AUC,我们可以使用这些指标在模型候选者中选择最佳和最优化模型。

在下一章中,我们将转换方向,开始构建回归模型,其中目标变量是连续变量。我们将使用外汇汇率数据集来构建时间序列特征,并探索一些其他用于回归问题的机器学习模型。我们还将讨论评估回归模型性能与分类模型性能的不同之处。

第四章:外汇汇率预测

在本章中,我们将开始使用 C#构建回归模型。到目前为止,我们已经构建了机器学习ML)模型,目的是使用逻辑回归、朴素贝叶斯和随机森林学习算法将数据分类到二元或多个类别中。然而,我们现在将转换方向,开始构建预测连续结果的模型。在本章中,我们将探索一个金融数据集,更具体地说是一个外汇汇率市场数据集。我们将使用欧元(EUR)和美元(USD)之间的每日汇率的历史数据来构建一个预测未来汇率的回归模型。我们将从问题定义开始,然后转向数据准备和数据分析。在数据准备和分析步骤中,我们将探讨如何管理时间序列数据和分析每日回报率的分布。然后,在特征工程步骤中,我们将开始构建可以预测货币汇率的特征。我们将讨论金融市场中常用的几个技术指标,例如移动平均线、布林带和滞后变量。使用这些技术指标,我们将使用线性回归和支持向量机(SVM)学习算法构建回归 ML 模型。在构建这些模型的同时,我们还将探讨一些微调 SVM 模型超参数的方法。最后,我们将讨论几个验证指标和评估回归模型的方法。我们将讨论如何使用均方根误差RMSE)、R²以及观察值与拟合值图来评估我们模型的性能。到本章结束时,你将拥有用于预测每日 EUR/USD 汇率的工作回归模型。

在本章中,我们将涵盖以下步骤:

  • 外汇汇率(欧元对美元)预测项目的问题定义

  • 使用 Deedle 框架中的时间序列功能进行数据准备

  • 时间序列数据分析

  • 使用外汇中的各种技术指标进行特征工程

  • 线性回归与支持向量机(SVM)的比较

  • 使用 RMSE、R²以及实际与预测图进行模型验证

问题定义

让我们从定义本项目试图解决的问题开始这一章。你可能听说过术语算法交易量化金融/交易。这是金融行业中数据科学和机器学习与金融相结合的知名领域之一。算法交易或量化金融指的是一种策略,即你使用从大量历史数据构建的统计学习模型来预测未来的金融市场走势。这些策略和技术被各种交易者和投资者广泛使用,以预测各种金融资产的未来价格。外汇市场是最大的、流动性最强的金融市场之一,大量的交易者和投资者参与其中。这是一个独特的市场,每天 24 小时、每周 5 天开放,来自世界各地的交易者进入市场买卖特定的货币对。由于这一优势和独特性,外汇市场也是算法交易和量化交易者构建机器学习模型来预测未来汇率并自动化他们的交易以利用计算机做出的快速决策和执行的吸引人的金融市场。

为了了解我们如何将我们的机器学习知识应用于金融市场和回归模型,我们将使用从 1999 年 1 月 1 日到 2017 年 12 月 31 日的每日 EUR/USD 汇率的历史数据。我们将使用一个公开可用的数据集,可以从以下链接下载:www.global-view.com/forex-trading-tools/forex-history/index.html。使用这些数据,我们将通过使用常用的技术指标,如移动平均线、布林带和滞后变量来构建特征。然后,我们将使用线性回归和 SVM 学习算法构建回归模型,以预测 EUR/USD 货币对的未来每日汇率。一旦我们构建了这些模型,我们将使用 RMSE、R^([2])以及观察值与预测值对比图来评估我们的模型。

为了总结我们对外汇汇率预测项目的问题定义:

  • 问题是怎样的?我们需要一个回归模型来预测欧元和美元之间的未来汇率;更具体地说,我们希望构建一个机器学习模型来预测 EUR/USD 汇率每日的变化。

  • 为什么这是一个问题?由于外汇市场的快节奏和波动性环境,拥有一个能够预测并自主决定何时买入和何时卖出特定货币对的机器学习模型是有利的。

  • 解决这个问题的有哪些方法?我们将使用欧元与美元之间的每日汇率的历史数据。使用这个数据集,我们将使用常用的技术指标,如移动平均线、布林带和滞后变量来构建金融特征。我们将探索线性回归和 SVM 学习算法作为我们的回归模型候选。然后,我们将查看 RMSE、R²,并使用观察值与预测值图来评估我们构建的模型的表现。

  • 成功标准是什么?我们希望 RMSE 低,因为我们希望我们的预测尽可能接近实际值。我们希望 R²高,因为它表示我们模型的拟合优度。最后,我们希望看到数据点在观察值与预测值图中紧密地排列在对角线上。

数据准备

既然我们已经知道了本章试图解决的问题类型,让我们开始查看数据。与前面两章不同,那里我们预先编译并预先标记了数据,我们将从原始的 EUR/USD 汇率数据开始。点击此链接:www.global-view.com/forex-trading-tools/forex-history/index.html并选择EUR/USD 收盘价EUR/USD 最高价EUR/USD 最低价。如果您想探索不同的数据集,您也可以选择不同的货币对。一旦您选择了想要的数据点,您可以选择开始和结束日期,也可以选择您想要下载的每日、每周或每月数据。对于本章,我们选择1999 年 1 月 1 日作为开始日期2017 年 12 月 31 日作为结束日期,并下载包含 EUR/USD 货币对收盘价、最高价和最低价的每日数据集。

下载完数据后,我们需要做一些任务来为未来的数据分析、特征工程和机器学习建模做好准备。首先,我们需要定义目标变量。正如我们在问题定义步骤中讨论的,我们的目标变量将是 EUR/USD 汇率每日变化。为了计算每日回报率,我们需要从今天的收盘价中减去昨天的收盘价,然后除以昨天的收盘价。计算每日回报率的公式如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00051.jpeg

我们可以使用 Deedle 数据框中的Diff方法来计算前一个价格和当前价格之间的差异。实际上,您可以使用Diff方法来计算任何任意时间点的数据点与当前数据点之间的差异。例如,以下代码显示了如何计算当前数据点与一步之遥、三步之遥和五步之遥的数据点之间的差异:

rawDF["DailyReturn"].Diff(1)
rawDF["DailyReturn"].Diff(3)
rawDF["DailyReturn"].Diff(5)

上述代码的输出如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00052.gif

使用这个 Diff 方法,以下是如何计算 EUR/USD 汇率的每日回报的代码:

// Compute Daily Returns
rawDF.AddColumn(
    "DailyReturn", 
    rawDF["Close"].Diff(1) / rawDF["Close"] * 100.0
);

在此代码中,我们计算了前一天和当天收盘价之间的差异,然后除以前一天的收盘价。通过乘以 100,我们可以得到百分比形式的每日回报。最后,我们使用 Deedle 数据框中的 AddColumn 方法,将这个每日回报序列添加到原始数据框中,列名为 DailyReturn

然而,我们在构建目标变量方面还没有完成。由于我们正在构建一个预测模型,我们需要将下一天的回报作为目标变量。我们可以使用 Deedle 数据框中的 Shift 方法将每个记录与下一天的回报关联起来。类似于 Diff 方法,你可以使用 Shift 方法将序列向前或向后移动到任何任意的时间点。以下是如何将 DailyReturn 列通过 135 步移动的代码:

rawDF["DailyReturn"].Shift(1)
rawDF["DailyReturn"].Shift(3)
rawDF["DailyReturn"].Shift(5)

上述代码的输出如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00053.gif

如此示例所示,DailyReturn 列或序列已经根据你输入到 Shift 方法的参数向前移动了 135 步。使用这个 Shift 方法,我们将每日回报向前移动一步,以便每个记录都有下一天的回报作为目标变量。以下是如何创建目标变量列 Target 的代码:

// Encode Target Variable - Predict Next Daily Return
rawDF.AddColumn(
    "Target",
    rawDF["DailyReturn"].Shift(-1)
);

现在我们已经编码了目标变量,我们还需要进行一个额外的步骤来为未来的任务准备数据。当你处理金融数据时,你经常会听到术语 OHLC 图表OHLC 价格。OHLC 代表开盘价、最高价、最低价和收盘价,通常用于显示价格随时间的变化。如果你查看我们下载的数据,你会注意到数据集中缺少开盘价。然而,为了我们未来的特征工程步骤,我们需要开盘价。鉴于外汇市场每天 24 小时运行,并且交易量很大,非常流动,我们将假设给定一天的开盘价是前一天收盘价。为了将前一天的收盘价作为开盘价,我们将使用 Shift 方法。以下是如何创建并添加开盘价到我们的数据框中的代码:

// Assume Open prices are previous Close prices
rawDF.AddColumn(
    "Open",
    rawDF["Close"].Shift(1)
);

以下是我们用于数据准备步骤的完整代码:

using Deedle;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DataPrep
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.SetWindowSize(100, 50);

            // Read in the raw dataset
            // TODO: change the path to point to your data directory
            string dataDirPath = @"\\Mac\Home\Documents\c-sharp-machine-
learning\ch.4\input-data";

            // Load the data into a data frame
            string rawDataPath = Path.Combine(dataDirPath, "eurusd-daily.csv");
            Console.WriteLine("Loading {0}\n", rawDataPath);
            var rawDF = Frame.ReadCsv(
                rawDataPath,
                hasHeaders: true,
                schema: "Date,float,float,float",
                inferTypes: false
            );

            // Rename & Simplify Column Names
            rawDF.RenameColumns(c => c.Contains("EUR/USD ") ? c.Replace("EUR/USD ", "") : c);

            // Assume Open prices are previous Close prices
            rawDF.AddColumn(
                "Open",
                rawDF["Close"].Shift(1)
            );

            // Compute Daily Returns
            rawDF.AddColumn(
                "DailyReturn", 
                rawDF["Close"].Diff(1) / rawDF["Close"] * 100.0
            );

            // Encode Target Variable - Predict Next Daily Return
            rawDF.AddColumn(
                "Target",
                rawDF["DailyReturn"].Shift(-1)
            );

            rawDF.Print();

            // Save OHLC data
            string ohlcDataPath = Path.Combine(dataDirPath, "eurusd-daily-ohlc.csv");
            Console.WriteLine("\nSaving OHLC data to {0}\n", rawDataPath);
            rawDF.SaveCsv(ohlcDataPath);

            Console.WriteLine("DONE!!");
            Console.ReadKey();
        }
    }
}

当你运行这段代码时,它将输出结果到一个名为 eurusd-daily-ohlc.csv 的文件中,该文件包含开盘价、最高价、最低价和收盘价,以及每日回报和目标变量。我们将使用这个文件进行未来的数据分析特征工程步骤。

此代码也可以在以下仓库中找到: github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.4/DataPrep.cs

时间序列数据分析

让我们开始查看数据。我们将从之前的数据准备步骤的输出开始,查看每日收益的分布情况。与之前的章节不同,我们主要处理的是分类变量,而现在我们处理的是连续和时间序列变量。我们将以几种不同的方式查看这些数据。首先,让我们查看时间序列收盘价图表。以下代码展示了如何使用 Accord.NET 框架构建折线图:

// Time-series line chart of close prices
DataSeriesBox.Show(
    ohlcDF.RowKeys.Select(x => (double)x),
    ohlcDF.GetColumn<double>("Close").ValuesAll
);

请参考 Accord.NET 文档中的DataSeriesBox.Show方法,了解显示折线图的多种其他方式。在这个例子中,我们使用数据框的整数索引作为x轴值,收盘价作为y轴值来构建折线图。以下是在运行代码时您将看到的时序线图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00054.jpeg

此图表显示了从 1999 年到 2017 年 EUR/USD 汇率随时间的变化情况。它从大约 1.18 开始,在 2000 年和 2001 年降至 1.0 以下。然后,它在 2008 年达到了 1.6 的高点,并在 2017 年结束时的约为 1.20。现在,让我们查看历史每日收益。以下代码展示了如何构建历史每日收益的折线图:

// Time-series line chart of daily returns
DataSeriesBox.Show(
    ohlcDF.RowKeys.Select(x => (double)x),
    ohlcDF.FillMissing(0.0)["DailyReturn"].ValuesAll
);

这里有一点需要注意,即FillMissing方法的用法。如果您还记得之前的数据准备步骤,DailyReturn序列是通过计算前一时期与当前时期的差值来构建的。因此,对于第一个数据点,我们没有前一时期的数据点,所以存在一个缺失值。FillMissing方法可以帮助您使用自定义值来编码缺失值。根据您的数据集和假设,您可以使用不同的值来编码缺失值,Deedle 数据框中的FillMissing方法将非常有用。

当您运行前面的代码时,它将显示如下图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00055.jpeg

如您从这张图表中可以看到,每日收益在0附近波动,主要介于-2.0%和+2.0%之间。让我们更仔细地查看每日收益的分布情况。我们将查看最小值、最大值、平均值和标准差。然后,我们将查看每日收益的四分位数,我们将在查看代码后更详细地讨论这一点。计算这些数字的代码如下:

// Check the distribution of daily returns
double returnMax = ohlcDF["DailyReturn"].Max();
double returnMean = ohlcDF["DailyReturn"].Mean();
double returnMedian = ohlcDF["DailyReturn"].Median();
double returnMin = ohlcDF["DailyReturn"].Min();
double returnStdDev = ohlcDF["DailyReturn"].StdDev();

double[] quantiles = Accord.Statistics.Measures.Quantiles(
    ohlcDF.FillMissing(0.0)["DailyReturn"].ValuesAll.ToArray(),
    new double[] {0.25, 0.5, 0.75}
);

Console.WriteLine("-- DailyReturn Distribution-- ");

Console.WriteLine("Mean: \t\t\t{0:0.00}\nStdDev: \t\t{1:0.00}\n", returnMean, returnStdDev);

Console.WriteLine(
    "Min: \t\t\t{0:0.00}\nQ1 (25% Percentile): \t{1:0.00}\nQ2 (Median): \t\t{2:0.00}\nQ3 (75% Percentile): \t{3:0.00}\nMax: \t\t\t{4:0.00}", 
    returnMin, quantiles[0], quantiles[1], quantiles[2], returnMax
);

如您从这段代码中可以看到,Deedle 框架提供了许多内置方法用于计算基本统计数据。正如代码的前六行所示,您可以使用 Deedle 框架中的MaxMeanMedianMinStdDev方法来获取每日收益的相应统计数据。

为了获取四分位数,我们需要在 Accord.NET 框架的 Accord.Statistics.Measures 模块中使用 Quantiles 方法。四分位数是将有序分布划分为等长度区间的点。例如,十个四分位数将有序分布划分为十个大小相等的子集,因此第一个子集代表分布的底部 10%,最后一个子集代表分布的顶部 10%。同样,四个四分位数将有序分布划分为四个大小相等的子集,其中第一个子集代表分布的底部 25%,最后一个子集代表分布的顶部 25%。四个四分位数通常被称为四分位数,十个四分位数被称为十分位数,而一百个四分位数被称为百分位数。从这些定义中,你可以推断出第一个四分位数与 0.25 分位数和 25 分位数相同。同样,第二个和第三个四分位数与 0.50 分位数和 0.75 分位数以及 50 分位数和 75 分位数相同。由于我们对四分位数感兴趣,我们在 Quantiles 方法中使用了 25%、50% 和 75% 作为 percentiles 参数的输入。以下是在运行此代码时的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00056.gif

与我们从日回报率时间序列线图、平均值和中位数中观察到的类似,平均值和中位数大约为 0,这表明日回报率围绕 0% 振荡。从 1999 年到 2017 年,历史上最大的负日回报率是 -2.86%,最大的正日回报率是 3.61%。第一个四分位数,即最小值和平均值之间的中间数,为 -0.36%,第三个四分位数,即平均值和最大值之间的中间数,为 0.35%。从这些汇总统计数据中,我们可以看到日回报率几乎对称地分布在 0% 附近。为了更直观地展示这一点,现在让我们看一下日回报率的直方图。绘制日回报率直方图的代码如下:

var dailyReturnHistogram = HistogramBox
.Show(
    ohlcDF.FillMissing(0.0)["DailyReturn"].ValuesAll.ToArray()
)
.SetNumberOfBins(20);

我们在 Accord.NET 框架中使用了 HistogramBox 来构建日回报率的直方图图表。在这里,我们将桶的数量设置为 20。你可以增加或减少桶的数量以显示更多或更少的粒度桶。当你运行此代码时,你将看到以下图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00057.jpeg

与我们在摘要统计中观察到的类似,日回报率几乎对称地分布在 0% 附近。这个日回报率的直方图显示了一个清晰的钟形曲线,这表明日回报率遵循正态分布。

我们运行此数据分析步骤的完整代码可以在以下链接中找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.4/DataAnalyzer.cs

特征工程

现在我们对每日收益率的分布有了更好的理解,让我们开始为我们的机器学习建模构建特征。在这一步,我们将讨论一些在外汇市场中被交易者频繁使用的技术指标,以及我们如何使用这些技术指标为我们的机器学习模型构建特征。

移动平均线

我们将要构建的第一个特征集是移动平均线。移动平均线是预定义数量的周期内的滚动平均,是一种常用的技术指标。移动平均线有助于平滑价格波动,并显示价格行为的整体趋势。关于移动平均线在交易金融资产中的应用的深入讨论超出了本书的范围,但简而言之,查看不同时间框架的多个移动平均线有助于交易者识别趋势和交易中的支撑和阻力水平。在本章中,我们将使用四个移动平均线,其回望周期分别为 10 天、20 天、50 天和 200 天。以下代码展示了我们如何使用Window方法计算移动平均线:

// 1\. Moving Averages
ohlcDF.AddColumn("10_MA", ohlcDF.Window(10).Select(x => x.Value["Close"].Mean()));
ohlcDF.AddColumn("20_MA", ohlcDF.Window(20).Select(x => x.Value["Close"].Mean()));
ohlcDF.AddColumn("50_MA", ohlcDF.Window(50).Select(x => x.Value["Close"].Mean()));
ohlcDF.AddColumn("200_MA", ohlcDF.Window(200).Select(x => x.Value["Close"].Mean()));

Deedle 框架中的Window方法帮助我们轻松计算移动平均线。Window方法接受一个数据框,并构建一系列数据框,其中每个数据框包含一个预定义数量的记录。例如,如果你的Window方法的输入是10,那么它将构建一系列数据框,其中第一个数据框包含从 0 索引到 9 索引的记录,第二个数据框包含从 1 索引到 11 索引的记录,依此类推。使用这种方法,我们可以轻松计算不同时间窗口的移动平均线,如代码所示。现在,让我们绘制一个包含这些移动平均线的时序收盘价图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00058.jpeg

如您从这张图表中可以看到,移动平均线平滑了价格波动。红色线表示 10 天的移动平均线,绿色线表示 20 天的移动平均线,黑色线表示 50 天,粉色线表示 200 天。从这张图表中可以看出,时间窗口越短,它就越接近价格变动,图表就越不平滑。我们用来生成这张图表的代码如下:

// Time-series line chart of close prices & moving averages
var maLineChart = DataSeriesBox.Show(
    ohlcDF.Where(x => x.Key > 4400 && x.Key < 4900).RowKeys.Select(x => (double)x),
    ohlcDF.Where(x => x.Key > 4400 && x.Key < 4900).GetColumn<double>("Close").ValuesAll,
    ohlcDF.Where(x => x.Key > 4400 && x.Key < 4900).GetColumn<double>("10_MA").ValuesAll,
    ohlcDF.Where(x => x.Key > 4400 && x.Key < 4900).GetColumn<double>("20_MA").ValuesAll,
    ohlcDF.Where(x => x.Key > 4400 && x.Key < 4900).GetColumn<double>("50_MA").ValuesAll,
    ohlcDF.Where(x => x.Key > 4400 && x.Key < 4900).GetColumn<double>("200_MA").ValuesAll
);

通过我们刚刚计算出的这些移动平均线,我们将用于我们模型的实际特征是收盘价与移动平均线之间的距离。正如简要提到的,移动平均线通常充当支撑和阻力水平,通过观察每个价格点与每个移动平均线的距离,我们可以判断我们是否正在接近支撑和阻力线。计算收盘价与移动平均线之间距离的代码如下:

// Distance from moving averages
ohlcDF.AddColumn("Close_minus_10_MA", ohlcDF["Close"] - ohlcDF["10_MA"]);
ohlcDF.AddColumn("Close_minus_20_MA", ohlcDF["Close"] - ohlcDF["20_MA"]);
ohlcDF.AddColumn("Close_minus_50_MA", ohlcDF["Close"] - ohlcDF["50_MA"]);
ohlcDF.AddColumn("Close_minus_200_MA", ohlcDF["Close"] - ohlcDF["200_MA"]);

布林带

我们将要查看的第二项技术指标是布林带。布林带由移动平均和与移动平均相同时间窗口的移动标准差组成。然后,布林带在价格时间序列图上绘制在移动平均上方和下方两个标准差的位置。我们将使用 20 日时间窗口来计算布林带。计算布林带的代码如下:

// 2\. Bollinger Band
ohlcDF.AddColumn("20_day_std", ohlcDF.Window(20).Select(x => x.Value["Close"].StdDev()));
ohlcDF.AddColumn("BollingerUpperBound", ohlcDF["20_MA"] + ohlcDF["20_day_std"] * 2);
ohlcDF.AddColumn("BollingerLowerBound", ohlcDF["20_MA"] - ohlcDF["20_day_std"] * 2);

如您从这段代码中可以看到,我们正在使用WindowStdDev方法来计算移动标准差。然后,我们通过从 20 日移动平均中加减两个标准差来计算布林带的上限和下限。当您在价格时间序列图上绘制布林带时,结果如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00059.jpeg

蓝线表示价格变动,绿线表示 20 日移动平均,红线表示布林带的上限,即比移动平均高出两个标准差,黑线表示布林带的下限,即比移动平均低出两个标准差。如您从这张图表中看到,布林带在价格变动周围形成带状。显示此图表的代码如下:

// Time-series line chart of close prices & bollinger bands
var bbLineChart = DataSeriesBox.Show(
    ohlcDF.Where(x => x.Key > 4400 && x.Key < 4900).RowKeys.Select(x => (double)x),
    ohlcDF.Where(x => x.Key > 4400 && x.Key < 4900).GetColumn<double>("Close").ValuesAll,
    ohlcDF.Where(x => x.Key > 4400 && x.Key < 4900).GetColumn<double>("BollingerUpperBound").ValuesAll,
    ohlcDF.Where(x => x.Key > 4400 && x.Key < 4900).GetColumn<double>("20_MA").ValuesAll,
    ohlcDF.Where(x => x.Key > 4400 && x.Key < 4900).GetColumn<double>("BollingerLowerBound").ValuesAll
);

与之前的移动平均案例类似,我们将使用收盘价与布林带之间的距离。由于大多数交易都是在上下带之间进行的,因此价格与带之间的距离可以成为我们机器学习模型的特征。计算距离的代码如下:

// Distance from Bollinger Bands
ohlcDF.AddColumn("Close_minus_BollingerUpperBound", ohlcDF["Close"] - ohlcDF["BollingerUpperBound"]);
ohlcDF.AddColumn("Close_minus_BollingerLowerBound", ohlcDF["Close"] - ohlcDF["BollingerLowerBound"]);

滞后变量

最后,我们将使用的一组最后特征是滞后变量。滞后变量包含关于先前时期的信息。例如,如果我们使用前一天的日回报值作为我们模型的特征,那么它就是一个滞后了一个周期的滞后变量。我们还可以使用当前日期前两天的日回报作为我们模型的特征。这类变量被称为滞后变量,通常用于时间序列建模。我们将使用日回报和先前构建的特征作为滞后变量。在这个项目中,我们回顾了五个周期,但您可以尝试更长的或更短的回顾周期。创建日回报滞后变量的代码如下:

// 3\. Lagging Variables
ohlcDF.AddColumn("DailyReturn_T-1", ohlcDF["DailyReturn"].Shift(1));
ohlcDF.AddColumn("DailyReturn_T-2", ohlcDF["DailyReturn"].Shift(2));
ohlcDF.AddColumn("DailyReturn_T-3", ohlcDF["DailyReturn"].Shift(3));
ohlcDF.AddColumn("DailyReturn_T-4", ohlcDF["DailyReturn"].Shift(4));
ohlcDF.AddColumn("DailyReturn_T-5", ohlcDF["DailyReturn"].Shift(5));

类似地,我们可以使用以下代码为移动平均与收盘价之间的差异创建滞后变量:

ohlcDF.AddColumn("Close_minus_10_MA_T-1", ohlcDF["Close_minus_10_MA"].Shift(1));
ohlcDF.AddColumn("Close_minus_10_MA_T-2", ohlcDF["Close_minus_10_MA"].Shift(2));
ohlcDF.AddColumn("Close_minus_10_MA_T-3", ohlcDF["Close_minus_10_MA"].Shift(3));
ohlcDF.AddColumn("Close_minus_10_MA_T-4", ohlcDF["Close_minus_10_MA"].Shift(4));
ohlcDF.AddColumn("Close_minus_10_MA_T-5", ohlcDF["Close_minus_10_MA"].Shift(5));

ohlcDF.AddColumn("Close_minus_20_MA_T-1", ohlcDF["Close_minus_20_MA"].Shift(1));
ohlcDF.AddColumn("Close_minus_20_MA_T-2", ohlcDF["Close_minus_20_MA"].Shift(2));
ohlcDF.AddColumn("Close_minus_20_MA_T-3", ohlcDF["Close_minus_20_MA"].Shift(3));
ohlcDF.AddColumn("Close_minus_20_MA_T-4", ohlcDF["Close_minus_20_MA"].Shift(4));
ohlcDF.AddColumn("Close_minus_20_MA_T-5", ohlcDF["Close_minus_20_MA"].Shift(5));

ohlcDF.AddColumn("Close_minus_50_MA_T-1", ohlcDF["Close_minus_50_MA"].Shift(1));
ohlcDF.AddColumn("Close_minus_50_MA_T-2", ohlcDF["Close_minus_50_MA"].Shift(2));
ohlcDF.AddColumn("Close_minus_50_MA_T-3", ohlcDF["Close_minus_50_MA"].Shift(3));
ohlcDF.AddColumn("Close_minus_50_MA_T-4", ohlcDF["Close_minus_50_MA"].Shift(4));
ohlcDF.AddColumn("Close_minus_50_MA_T-5", ohlcDF["Close_minus_50_MA"].Shift(5));

ohlcDF.AddColumn("Close_minus_200_MA_T-1", ohlcDF["Close_minus_200_MA"].Shift(1));
ohlcDF.AddColumn("Close_minus_200_MA_T-2", ohlcDF["Close_minus_200_MA"].Shift(2));
ohlcDF.AddColumn("Close_minus_200_MA_T-3", ohlcDF["Close_minus_200_MA"].Shift(3));
ohlcDF.AddColumn("Close_minus_200_MA_T-4", ohlcDF["Close_minus_200_MA"].Shift(4));
ohlcDF.AddColumn("Close_minus_200_MA_T-5", ohlcDF["Close_minus_200_MA"].Shift(5));

最后,我们可以使用以下代码为布林带指标创建滞后变量:

ohlcDF.AddColumn("Close_minus_BollingerUpperBound_T-1", ohlcDF["Close_minus_BollingerUpperBound"].Shift(1));
ohlcDF.AddColumn("Close_minus_BollingerUpperBound_T-2", ohlcDF["Close_minus_BollingerUpperBound"].Shift(2));
ohlcDF.AddColumn("Close_minus_BollingerUpperBound_T-3", ohlcDF["Close_minus_BollingerUpperBound"].Shift(3));
ohlcDF.AddColumn("Close_minus_BollingerUpperBound_T-4", ohlcDF["Close_minus_BollingerUpperBound"].Shift(4));
ohlcDF.AddColumn("Close_minus_BollingerUpperBound_T-5", ohlcDF["Close_minus_BollingerUpperBound"].Shift(5));

如您从这些代码片段中可以看到,创建这样的滞后变量非常简单直接。我们只需在 Deedle 框架中使用Shift方法,并根据回顾周期更改方法输入。

在本节中,我们还将做的一件事是删除缺失值。因为我们构建了许多时间序列特征,所以我们创建了很多缺失值。例如,当我们计算 200 天的移动平均时,前 199 条记录将没有移动平均,因此将会有缺失值。当您在数据集中遇到缺失值时,有两种方法可以处理它们——您可以用某些值编码它们,或者从数据集中删除缺失值。由于我们有足够的数据,我们将删除所有包含缺失值的记录。从我们的数据框中删除缺失值的代码如下:

Console.WriteLine("\n\nDF Shape BEFORE Dropping Missing Values: ({0}, {1})", ohlcDF.RowCount, ohlcDF.ColumnCount);
ohlcDF = ohlcDF.DropSparseRows();
Console.WriteLine("\nDF Shape AFTER Dropping Missing Values: ({0}, {1})\n\n", ohlcDF.RowCount, ohlcDF.ColumnCount);

如您从这段代码中可以看到,Deedle 框架有一个方便的函数,我们可以用它来删除缺失值。我们可以使用DropSparseRows方法来删除所有缺失值。当您运行这段代码时,您的输出将如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00060.gif

如您从输出中可以看到,它删除了 250 条因缺失值而导致的记录。运行数据分析步骤的完整代码可以在以下链接中找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.4/FeatureEngineer.cs

线性回归与 SVM 对比

在本节中,我们将构建与之前章节完全不同的模型。我们将构建预测连续变量的模型,并提供 EUR/USD 汇率每日回报率,我们将使用两种新的学习算法,即线性回归和 SVM。线性回归模型试图在目标变量和特征之间找到线性关系,而 SVM 模型试图构建最大化不同类别之间距离的超平面。对于这个外汇汇率预测项目,我们将讨论如何使用 Accord.NET 框架在 C#中构建线性回归和 SVM 模型来解决回归问题。

在我们构建模型之前,我们必须将我们的样本集分成两个子集——一个用于训练,另一个用于测试。在上一章中,我们使用了 Accord.NET 框架中的SplitSetValidation来随机将样本集分成训练集和测试集,按照预定义的比例。然而,我们无法在本章中采用相同的方法。因为我们处理的是时间序列数据,我们不能随机选择并分割记录为训练集和测试集。如果我们随机分割样本集,那么我们可能会遇到用未来的事件训练我们的机器学习模型,而在过去的事件上测试模型的情况。因此,我们希望在某个时间点分割我们的样本集,并将那个时间点之前的记录放入训练集,之后的记录放入测试集。下面的代码展示了我们如何将样本集分割成训练集和测试集:

// Read in the file we created in the previous step
// TODO: change the path to point to your data directory
string dataDirPath = @"<path-to-data-dir>";

// Load the data into a data frame
Console.WriteLine("Loading data...");
var featuresDF = Frame.ReadCsv(
    Path.Combine(dataDirPath, "eurusd-features.csv"),
    hasHeaders: true,
    inferTypes: true
);

// Split the sample set into train and test sets
double trainProportion = 0.9;

int trainSetIndexMax = (int)(featuresDF.RowCount * trainProportion);

var trainSet = featuresDF.Where(x => x.Key < trainSetIndexMax);
var testSet = featuresDF.Where(x => x.Key >= trainSetIndexMax);

Console.WriteLine("\nTrain Set Shape: ({0}, {1})", trainSet.RowCount, trainSet.ColumnCount);
Console.WriteLine("Test Set Shape: ({0}, {1})", testSet.RowCount, testSet.ColumnCount);
Where method to filter records in the sample set by index. The next thing we need to do before training our ML models is select the features that we want to train our models with. Since we are only interested in using lagged variables and the distances between the prices and moving averages or Bollinger Bands, we do not want to include raw moving average or Bollinger Band numbers into our feature space. The following code snippet shows how we define the feature set for our models:
string[] features = new string[] {
    "DailyReturn", 
    "Close_minus_10_MA", "Close_minus_20_MA", "Close_minus_50_MA",
    "Close_minus_200_MA", "20_day_std", 
    "Close_minus_BollingerUpperBound", "Close_minus_BollingerLowerBound",
    "DailyReturn_T-1", "DailyReturn_T-2",
    "DailyReturn_T-3", "DailyReturn_T-4", "DailyReturn_T-5",
    "Close_minus_10_MA_T-1", "Close_minus_10_MA_T-2", 
    "Close_minus_10_MA_T-3", "Close_minus_10_MA_T-4",
    "Close_minus_10_MA_T-5", 
    "Close_minus_20_MA_T-1", "Close_minus_20_MA_T-2",
    "Close_minus_20_MA_T-3", "Close_minus_20_MA_T-4", "Close_minus_20_MA_T-5",
    "Close_minus_50_MA_T-1", "Close_minus_50_MA_T-2", "Close_minus_50_MA_T-3",
    "Close_minus_50_MA_T-4", "Close_minus_50_MA_T-5", 
    "Close_minus_200_MA_T-1", "Close_minus_200_MA_T-2", 
    "Close_minus_200_MA_T-3", "Close_minus_200_MA_T-4",
    "Close_minus_200_MA_T-5",
    "Close_minus_BollingerUpperBound_T-1",
    "Close_minus_BollingerUpperBound_T-2", "Close_minus_BollingerUpperBound_T-3",
    "Close_minus_BollingerUpperBound_T-4", "Close_minus_BollingerUpperBound_T-5"
};

现在我们已经准备好开始构建模型对象并训练我们的机器学习模型了。让我们首先看看如何实例化一个线性回归模型。我们用来训练线性回归模型的代码如下:

Console.WriteLine("\n**** Linear Regression Model ****");

// OLS learning algorithm
var ols = new OrdinaryLeastSquares()
{
    UseIntercept = true
};

// Fit a linear regression model
MultipleLinearRegression regFit = ols.Learn(trainX, trainY);

// in-sample predictions
double[] regInSamplePreds = regFit.Transform(trainX);

// out-of-sample predictions
double[] regOutSamplePreds = regFit.Transform(testX);
OrdinaryLeastSquares as a learning algorithm and MultipleLinearRegression as a model. Ordinary Least Squares (OLS) is a way of training a linear regression model by minimizing and optimizing on the sum of squares of errors. A multiple linear regression model is a model where the number of input features is larger than 1\. Lastly, in order to make predictions on data, we are using the Transform method of the MultipleLinearRegression object. We will be making predictions on both the train and test sets for our model validations in the following section.

现在我们来看一下本章将要使用的另一个学习算法和模型。以下代码展示了如何为回归问题构建和训练一个 SVM 模型:

Console.WriteLine("\n**** Linear Support Vector Machine ****");
// Linear SVM Learning Algorithm
var teacher = new LinearRegressionNewtonMethod()
{
    Epsilon = 2.1,
    Tolerance = 1e-5,
    UseComplexityHeuristic = true
};

// Train SVM
var svm = teacher.Learn(trainX, trainY);

// in-sample predictions
double[] linSVMInSamplePreds = svm.Score(trainX);

// out-of-sample predictions
double[] linSVMOutSamplePreds = svm.Score(testX);

如你所见,我们正在使用LinearRegressionNewtonMethod作为学习算法来训练一个 SVM 模型。LinearRegressionNewtonMethod是使用线性核的 SVM 学习算法。简单来说,核是一种将数据点投影到另一个空间的方法,在这个空间中,数据点比在原始空间中更容易分离。在训练 SVM 模型时,也经常使用其他核,如多项式核和高斯核。我们将在下一章中实验和进一步讨论这些其他核,但你当然可以在这个项目中尝试其他核对模型性能的影响。在使用训练好的 SVM 模型进行预测时,你可以使用代码片段中所示的Score方法。

我们用来训练和验证线性回归和 SVM 模型的完整代码可以在以下链接找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.4/Modeling.cs

模型验证

现在你已经为本章的外汇汇率预测项目构建并训练了回归模型,让我们开始探讨我们的模型表现如何。在本节中,我们将讨论两个常用的基本指标,RMSE 和 R²,以及一个诊断图,实际或观察值与预测值对比。在我们深入探讨这些指标和诊断图之前,让我们首先简要讨论如何从线性回归模型中提取系数和截距值。

MultipleLinearRegressionobject:
Console.WriteLine("\n* Linear Regression Coefficients:");

for (int i = 0; i < features.Length; i++)
{
    Console.WriteLine("\t{0}: {1:0.0000}", features[i], regFit.Weights[i]);
}

Console.WriteLine("\tIntercept: {0:0.0000}", regFit.Intercept);

当你运行此代码时,你将看到以下类似的输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00061.gif

观察拟合的线性回归模型的系数和截距有助于我们理解模型,并深入了解每个特征如何影响预测结果。我们能够理解和可视化特征与目标变量之间的关系是如何形成的,以及它们是如何相互作用的,这使得线性回归模型即使在其他黑盒模型(如随机森林模型或支持向量机)通常优于线性回归模型的情况下,仍然具有吸引力。正如你可以从输出中看到的那样,你可以轻松地判断哪些特征对每日回报预测有负面影响或正面影响,以及它们的影响程度。

现在我们来看看本章中用于回归模型验证的第一个指标。你可能已经熟悉 RMSE,它衡量的是预测值与实际值之间误差的平方根。RMSE 值越低,模型拟合度越好。以下代码展示了如何计算模型拟合的 RMSE:

// RMSE for in-sample 
double regInSampleRMSE = Math.Sqrt(new SquareLoss(trainX).Loss(regInSamplePreds));

// RMSE for out-sample 
double regOutSampleRMSE = Math.Sqrt(new SquareLoss(testX).Loss(regOutSamplePreds));

Console.WriteLine("RMSE: {0:0.0000} (Train) vs. {1:0.0000} (Test)", regInSampleRMSE, regOutSampleRMSE);

如此代码所示,我们正在使用 Accord.NET 框架中的SquareLoss类,该类计算预测值与实际值之间差异的平方值。为了得到 RMSE,我们需要取这个值的平方根。

我们接下来要看的下一个指标是 R²。R²经常被用作拟合优度的一个指标。值越接近 1,模型拟合度越好。以下代码展示了如何计算 R²值:

//for in-sample 
double regInSampleR2 = new RSquaredLoss(trainX[0].Length, trainX).Loss(regInSamplePreds);

//for out-sample 
double regOutSampleR2 = new RSquaredLoss(testX[0].Length, testX).Loss(regOutSamplePreds);

Console.WriteLine("R²: {0:0.0000} (Train) vs. {1:0.0000} (Test)", regInSampleR2, regOutSampleR2);

如此代码所示,我们正在使用 Accord.NET 框架中的RSquaredLoss类。我们分别对样本内预测(在训练集上的预测)和样本外预测(在测试集上的预测)进行计算。这两个值越接近,模型的过拟合程度就越低。

当你为线性回归模型运行 RMSE 和 R²的代码时,你将得到以下输出:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00062.gif

对于 SVM 模型,你将看到的输出如下:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00063.gif

从这些输出中,我们可以看到 SVM 模型在性能上远超线性回归模型。与线性回归模型相比,SVM 模型的 RMSE 要低得多。此外,SVM 模型的 R²值也远高于线性回归模型。注意线性回归模型的 R²值。当模型的拟合度不如一条简单的水平线时,就会出现这种情况,这表明我们的线性回归模型拟合度不佳。另一方面,SVM 模型的 R²值约为 0.26,这意味着 26%的目标变量方差可以通过此模型解释。

最后,我们将查看一个诊断图;实际值与预测值之间的比较。这个诊断图是观察模型拟合优度的一个很好的视觉方式。理想情况下,我们希望所有点都位于对角线上。例如,如果实际值是 1.0,那么我们希望预测值接近 1.0。点越接近对角线,模型拟合度越好。你可以使用以下代码来绘制实际值与预测值:

// Scatter Plot of expected and actual
ScatterplotBox.Show(
    String.Format("Actual vs. Prediction ({0})", modelName), testY, regOutSamplePreds
);

我们正在使用 Accord.NET 框架中的ScatterplotBox类来构建实际值与预测值之间的散点图。当你为线性回归模型运行此代码时,你会看到以下诊断图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00064.jpeg

当你为 SVM 模型运行相同的代码时,诊断图如下所示:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00065.jpeg

如您从这些图中可以看到,线性回归模型的预测值在 0 附近更为集中,而 SVM 模型的预测值则在一个更宽的范围内分布更广。尽管线性回归和 SVM 模型结果的两个图现在都没有显示完美的对角线,但 SVM 模型的图显示了更好的结果,并且与 RMSE 和 R² 指标我们看到的结果一致。

我们编写并使用的方法来运行模型验证如下:

private static void ValidateModelResults(string modelName, double[] regInSamplePreds, double[] regOutSamplePreds, double[][] trainX, double[] trainY, double[][] testX, double[] testY)
{
    // RMSE for in-sample 
    double regInSampleRMSE = Math.Sqrt(new SquareLoss(trainX).Loss(regInSamplePreds));

    // RMSE for in-sample 
    double regOutSampleRMSE = Math.Sqrt(new SquareLoss(testX).Loss(regOutSamplePreds));

    Console.WriteLine("RMSE: {0:0.0000} (Train) vs. {1:0.0000} (Test)", regInSampleRMSE, regOutSampleRMSE);

    //for in-sample 
    double regInSampleR2 = new RSquaredLoss(trainX[0].Length, trainX).Loss(regInSamplePreds);

    //for in-sample 
    double regOutSampleR2 = new RSquaredLoss(testX[0].Length, testX).Loss(regOutSamplePreds);

    Console.WriteLine("R²: {0:0.0000} (Train) vs. {1:0.0000} (Test)", regInSampleR2, regOutSampleR2);

    // Scatter Plot of expected and actual
    ScatterplotBox.Show(
        String.Format("Actual vs. Prediction ({0})", modelName), testY, regOutSamplePreds
    );
}

摘要

在本章中,我们构建并训练了我们的第一个回归模型。我们使用了一个包含 1999 年至 2017 年间欧元和美元历史每日汇率的时间序列数据集。我们首先讨论了如何从一个未标记的原始数据集中创建目标变量,以及如何在 Deedle 框架中应用 ShiftDiff 方法来计算每日回报并创建目标变量,即一个周期前的一日回报。我们还从几个不同的角度研究了每日回报的分布,例如时间序列折线图、使用均值、标准差和分位数进行的总结统计。我们还研究了每日回报的直方图,并看到了一个绘制得很好的钟形曲线,它遵循正态分布。然后,我们介绍了外汇市场中一些常用的技术指标以及如何将它们应用于我们的特征构建过程。使用移动平均线、布林带和滞后变量等技术指标,我们构建了各种特征,帮助我们的学习算法学习如何预测未来的每日回报。在特征工程步骤中构建的这些特征,我们构建了线性回归和 SVM 模型来预测 EUR/USD 汇率。我们学习了如何从 MultipleLinearRegression 对象中提取系数和截距,以深入了解每个特征如何影响预测结果。我们还简要讨论了在构建 SVM 模型时核函数的使用。最后,我们回顾了两个常用的回归模型指标,RMSE 和 R²,以及实际值与预测值之间的诊断图。从这个模型验证步骤中,我们观察到 SVM 模型在性能上大幅优于线性回归模型。我们还讨论了与其他黑盒模型(如随机森林和 SVM 模型)相比,使用线性回归模型可以获得的可解释性比较优势。

在下一章中,我们将通过使用 Accord.NET 框架在 C# 中构建回归模型来扩展我们的知识和经验。我们将使用一个包含连续和分类变量的房价数据集,并学习如何为如此复杂的数据集构建回归模型。我们还将讨论我们可以用于 SVM 的各种核函数以及它们如何影响我们的 SVM 模型的性能。

第五章:房屋和财产的公允价值

在本章中,我们将扩展我们在 C#中构建回归机器学习(ML)模型的知识和技能。在上一章中,我们在外汇汇率数据集上构建了线性回归和线性支持向量机模型,其中所有特征都是连续变量。然而,我们将处理一个更复杂的数据集,其中一些特征是分类变量,而其他一些是连续变量。

在本章中,我们将使用一个包含房屋众多属性且变量类型混合的房价数据集。使用这些数据,我们将开始研究两种常见的分类变量类型(有序与无序)以及住房数据集中一些分类变量的分布。我们还将研究数据集中一些连续变量的分布以及使用对数变换对显示偏态分布的变量的好处。然后,我们将学习如何编码和工程化这些分类特征,以便我们可以拟合机器学习模型。与上一章我们探索支持向量机(SVM)基础知识不同,我们将为我们的 SVM 模型应用不同的核方法,并观察它如何影响模型性能。

与上一章类似,我们将使用均方根误差RMSE)、R²以及实际值与预测值的对比图来评估我们的机器学习模型的性能。在本章结束时,你将更好地理解如何处理分类变量,如何为回归模型编码和工程化这些特征,如何应用各种核方法来构建支持向量机(SVM)模型,以及如何构建预测房屋公允价值的模型。

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

  • 房屋/财产公允价值项目的问题定义

  • 分类变量与连续变量的数据分析

  • 特征工程和编码

  • 线性回归与带核的支持向量机

  • 使用 RMSE、R²和实际值与预测值对比图进行模型验证

问题定义

让我们从了解我们将要构建的机器学习模型的具体内容开始这一章。当你寻找要购买的房屋或财产时,你会考虑你看到的这些房屋或财产的众多属性。你可能正在查看卧室和浴室的数量,你能在你的车库中停放多少辆车,社区,房屋的材料或装饰,等等。所有这些房屋或财产的属性都进入了你决定为特定财产支付的价格,或者你如何与卖家协商价格的决定。然而,理解并估计财产的公允价值是非常困难的。通过拥有一个预测每个财产公允价值或最终价格的模型,你可以在与卖家协商时做出更明智的决定。

为了构建预测房屋公平价值的模型,我们将使用一个包含 79 个解释变量的数据集,这些变量涵盖了美国爱荷华州艾姆斯市几乎所有住宅的属性及其 2006 年至 2010 年的最终销售价格。这个数据集由杜鲁门州立大学的迪安·德·科克(ww2.amstat.org/publications/jse/v19n3/decock.pdf)编制,可以通过此链接下载:www.kaggle.com/c/house-prices-advanced-regression-techniques/data。利用这些数据,我们将构建包含关于房屋面积或不同部分尺寸、房屋使用的风格和材料、房屋不同部分的状况和表面处理以及描述每栋房屋信息的其他各种属性的特征。使用这些特征,我们将探索不同的回归机器学习模型,如线性回归、线性支持向量机以及具有多项式和高斯核的支持向量机(SVMs)。然后,我们将通过查看 RMSE、R²以及实际值与预测值之间的图表来评估这些模型。

为了总结我们对房屋和财产公平价值项目的定义问题:

  • 问题是什么? 我们需要一个回归模型来预测美国爱荷华州艾姆斯市的住宅公平价值,这样我们就可以在购买房屋时更好地理解和做出更明智的决策。

  • 为什么这是一个问题? 由于决定房屋或财产公平价值具有复杂性和众多变量,拥有一个能够预测并告知购房者他们所看房屋预期价值的机器学习模型是有利的。

  • 解决这个问题的方法有哪些? 我们将使用一个包含 79 个解释变量的预编译数据集,这些变量包含了美国爱荷华州艾姆斯市住宅的信息,并构建和编码混合类型(既是分类变量也是连续变量)的特征。然后,我们将探索使用不同核函数的线性回归和支持向量机来预测房屋的公平价值。我们将通过查看 RMSE、R²以及实际值与预测值之间的图表来评估模型候选者。

  • 成功的标准是什么? 我们希望我们的房价预测尽可能接近实际的房屋销售价格,因此我们希望获得尽可能低的 RMSE(均方根误差),同时不损害我们的拟合优度指标 R²,以及实际值与预测值之间的图表。

分类变量与连续变量

现在,让我们开始查看实际的数据集。您可以点击以下链接:www.kaggle.com/c/house-prices-advanced-regression-techniques/data 下载train.csvdata_description.txt文件。我们将使用train.csv文件来构建模型,而data_description.txt文件将帮助我们更好地理解数据集的结构,特别是关于我们已有的分类变量。

如果你查看训练数据文件和描述文件,你可以很容易地找到一些具有特定名称或代码的变量,它们代表每栋房屋属性的特定类型。例如,Foundation变量可以取BrkTilCBlockPConcSlabStoneWood中的任何一个值,其中这些值或代码分别代表房屋建造时所用的地基类型——砖和瓦、混凝土块、浇筑混凝土、板、石头和木材。另一方面,如果你查看数据中的TotalBsmtSF变量,你可以看到它可以取任何数值,并且这些值是连续的。如前所述,这个数据集包含混合类型的变量,我们在处理既有分类变量又有连续变量的数据集时需要谨慎处理。

非序数分类变量

让我们首先看看一些分类变量及其分布。我们将首先查看的建筑属性是建筑类型。显示建筑类型分布的柱状图的代码如下:

// Categorical Variable #1: Building Type
Console.WriteLine("\nCategorical Variable #1: Building Type");
var buildingTypeDistribution = houseDF.GetColumn&lt;string&gt;(
    "BldgType"
).GroupBy&lt;string&gt;(x =&gt; x.Value).Select(x =&gt; (double)x.Value.KeyCount);
buildingTypeDistribution.Print();

var buildingTypeBarChart = DataBarBox.Show(
    buildingTypeDistribution.Keys.ToArray(),
    buildingTypeDistribution.Values.ToArray()
);
buildingTypeBarChart.SetTitle("Building Type Distribution (Categorical)");

当你运行这段代码时,它将显示一个类似于以下的柱状图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00066.jpeg

如您从这张柱状图中可以看出,我们数据集中大多数的建筑类型是 1Fam,这代表着独立单户住宅建筑类型。第二常见的建筑类型是 TwnhsE,代表着联排别墅端单元建筑类型。

让我们再看看一个分类变量,地块配置(数据集中的LotConfig字段)。绘制地块配置分布柱状图的代码如下:

// Categorical Variable #2: Lot Configuration
Console.WriteLine("\nCategorical Variable #1: Building Type");
var lotConfigDistribution = houseDF.GetColumn&lt;string&gt;(
    "LotConfig"
).GroupBy&lt;string&gt;(x =&gt; x.Value).Select(x =&gt; (double)x.Value.KeyCount);
lotConfigDistribution.Print();

var lotConfigBarChart = DataBarBox.Show(
    lotConfigDistribution.Keys.ToArray(),
    lotConfigDistribution.Values.ToArray()
);
lotConfigBarChart.SetTitle("Lot Configuration Distribution (Categorical)");

当你运行这段代码时,它将显示以下柱状图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00067.jpeg

如您从这张柱状图中可以看出,内部地块是我们数据集中最常见的地块配置,其次是角地块。

序数分类变量

我们刚才查看的两个分类变量没有自然的顺序。一种类型并不在另一种类型之前,或者一种类型并不比另一种类型更重要。然而,有些分类变量具有自然顺序,我们称这样的分类变量为序数分类变量。例如,当你将材料的品质从 1 到 10 进行排名时,其中 10 代表最佳,1 代表最差,这就存在一个自然顺序。让我们看看这个数据集中的一些序数分类变量。

我们将要查看的第一个有序分类变量是OverallQual属性,它代表房屋的整体材料和装修。查看该变量分布的代码如下:

// Ordinal Categorical Variable #1: Overall material and finish of the house
Console.WriteLine("\nOrdinal Categorical #1: Overall material and finish of the house");
var overallQualDistribution = houseDF.GetColumn&lt;string&gt;(
    "OverallQual"
).GroupBy&lt;int&gt;(
    x =&gt; Convert.ToInt32(x.Value)
).Select(
    x =&gt; (double)x.Value.KeyCount
).SortByKey().Reversed;
overallQualDistribution.Print();

var overallQualBarChart = DataBarBox.Show(
    overallQualDistribution.Keys.Select(x =&gt; x.ToString()),
    overallQualDistribution.Values.ToArray()
);
overallQualBarChart.SetTitle("Overall House Quality Distribution (Ordinal)");

当你运行这段代码时,它将按从 10 到 1 的顺序显示以下条形图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00068.jpeg

如预期的那样,在非常优秀(编码为 10)或优秀(编码为 9)类别中的房屋数量比在高于平均水平(编码为 6)或平均水平(编码为 5)类别中的房屋数量要少。

我们将要查看的另一个有序分类变量是ExterQual变量,它代表外部质量。查看该变量分布的代码如下:

// Ordinal Categorical Variable #2: Exterior Quality
Console.WriteLine("\nOrdinal Categorical #2: Exterior Quality");
var exteriorQualDistribution = houseDF.GetColumn&lt;string&gt;(
    "ExterQual"
).GroupBy&lt;string&gt;(x =&gt; x.Value).Select(
    x =&gt; (double)x.Value.KeyCount
)[new string[] { "Ex", "Gd", "TA", "Fa" }];
exteriorQualDistribution.Print();

var exteriorQualBarChart = DataBarBox.Show(
    exteriorQualDistribution.Keys.Select(x =&gt; x.ToString()),
    exteriorQualDistribution.Values.ToArray()
);
exteriorQualBarChart.SetTitle("Exterior Quality Distribution (Ordinal)");

当你运行这段代码时,它将显示以下条形图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00069.jpeg

OverallQual变量不同,ExterQual变量没有用于排序的数值。在我们的数据集中,它有以下几种值:ExGdTAFA,分别代表优秀、良好、平均/典型和公平。尽管这个变量没有数值,但它显然有一个自然排序,其中优秀类别(Ex)代表外部材料质量的最佳,良好类别(Gd)代表外部材料质量的第二佳。在特征工程步骤中,我们将讨论如何为我们的未来模型构建步骤编码此类变量。

连续变量

我们已经查看了我们数据集中的两种类型的分类变量。然而,数据集中还有另一种类型的变量;连续变量。与分类变量不同,连续变量可以取无限多个值。例如,房屋地下室面积的平方英尺可以是任何正数。一栋房屋可以有 0 平方英尺的地下室面积(或没有地下室),或者一栋房屋可以有 1,000 平方英尺的地下室面积。我们将要查看的第一个连续变量是1stFlrSF,它代表一楼平方英尺。以下代码展示了我们如何为1stFlrSF构建直方图:

// Continuous Variable #1-1: First Floor Square Feet
var firstFloorHistogram = HistogramBox
.Show(
    houseDF.DropSparseRows()["1stFlrSF"].ValuesAll.ToArray(),
    title: "First Floor Square Feet (Continuous)"
)
.SetNumberOfBins(20);

当你运行这段代码时,将显示以下直方图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00070.gif

从这张图表中明显可以看出,它在正方向上有一个长尾,换句话说,分布是右偏的。数据中的偏斜性可能会在我们构建机器学习模型时对我们产生不利影响。处理数据集中这种偏斜性的一种方法是对数据进行一些转换。一种常用的转换方法是对数转换,即取给定变量的对数值。在这个例子中,以下代码展示了我们如何对1stFlrSF变量应用对数转换并显示转换变量的直方图:

// Continuous Variable #1-2: Log of First Floor Square Feet
var logFirstFloorHistogram = HistogramBox
.Show(
    houseDF.DropSparseRows()["1stFlrSF"].Log().ValuesAll.ToArray(),
    title: "First Floor Square Feet - Log Transformed (Continuous)"
)
.SetNumberOfBins(20);

当你运行这段代码时,你将看到以下直方图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00071.jpeg

如您从这张图表中可以看到,与之前查看的同一变量的直方图相比,分布看起来更加对称,更接近我们熟悉的钟形。对数变换通常用于处理数据集中的偏斜,并使分布更接近正态分布。让我们看看我们数据集中的另一个连续变量。以下代码用于展示 GarageArea 变量的分布,它代表车库的面积(平方英尺):

// Continuous Variable #2-1: Size of garage in square feet
var garageHistogram = HistogramBox
.Show(
    houseDF.DropSparseRows()["GarageArea"].ValuesAll.ToArray(),
    title: "Size of garage in square feet (Continuous)"
)
.SetNumberOfBins(20);

当你运行此代码时,你会看到以下直方图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00072.jpeg

1stFlrSF 的前一个案例类似,它也是右偏斜的,尽管看起来偏斜的程度小于 1stFlrSF。我们使用了以下代码对 GarageArea 变量应用对数变换:

// Continuous Variable #2-2: Log of Value of miscellaneous feature
var logGarageHistogram = HistogramBox
.Show(
    houseDF.DropSparseRows()["GarageArea"].Log().ValuesAll.ToArray(),
    title: "Size of garage in square feet - Log Transformed (Continuous)"
)
.SetNumberOfBins(20);

当你运行此代码时,将显示以下直方图图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00073.jpeg

如预期的那样,当对变量应用对数变换时,分布看起来更接近正态分布。

目标变量 - 销售价格

在我们进行特征工程步骤之前,还有一个变量需要查看;即目标变量。在这个房屋公允价值项目中,我们的预测目标变量是 SalePrice,它代表美国爱荷华州艾姆斯市从 2006 年到 2010 年销售的每套住宅的最终销售价格(以美元计)。由于销售价格可以取任何正数值,因此它是一个连续变量。让我们首先看看我们是如何为销售价格变量构建直方图的:

// Target Variable: Sale Price
var salePriceHistogram = HistogramBox
.Show(
    houseDF.DropSparseRows()["SalePrice"].ValuesAll.ToArray(),
    title: "Sale Price (Continuous)"
)
.SetNumberOfBins(20);

当你运行此代码时,将显示以下直方图图表:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00074.gif

与之前连续变量的案例类似,SalePrice 的分布具有较长的右尾,并且严重向右偏斜。这种偏斜通常会对回归模型产生不利影响,因为一些模型,例如线性回归模型,假设变量是正态分布的。正如之前所讨论的,我们可以通过应用对数变换来解决这个问题。以下代码展示了我们如何对销售价格变量进行对数变换并构建直方图:

// Target Variable: Sale Price - Log Transformed
var logSalePriceHistogram = HistogramBox
.Show(
    houseDF.DropSparseRows()["SalePrice"].Log().ValuesAll.ToArray(),
    title: "Sale Price - Log Transformed (Continuous)"
)
.SetNumberOfBins(20);

当你运行此代码时,你会看到以下针对对数变换后的销售价格变量的直方图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00075.gif

如预期的那样,SalePrice 变量的分布看起来与正态分布非常接近。我们将使用这个对数变换后的 SalePrice 变量作为我们未来模型构建步骤的目标变量。

此数据分析步骤的完整代码可以在以下链接中找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.5/DataAnalyzer.cs

特征工程和编码

现在我们已经查看过我们的数据集以及分类、连续和目标变量的分布,让我们开始为我们的机器学习模型构建特征。正如我们之前讨论的,我们数据集中的分类变量有特定的字符串值来表示每种变量类型。然而,正如你可能已经清楚的那样,我们不能使用字符串类型来训练我们的机器学习模型。所有变量的值都需要是数值型的,以便能够用于拟合模型。处理具有多种类型或类别的分类变量的一种方法是通过创建虚拟变量。

虚拟变量

虚拟变量是一个变量,它取 0 或 1 的值来指示给定的类别或类型是否存在。例如,在BldgType变量的情况下,它有五个不同的类别1Fam2FmConDuplxTwnhsETwnhs,我们将创建五个虚拟变量,其中每个虚拟变量代表在给定记录中这些五个类别中的每一个的存在或不存在。以下是如何进行虚拟变量编码的一个示例:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00076.gif

如您从本例中可以看到,建筑类型中每个类别的存在和不存在都被编码为单独的虚拟变量,值为01。例如,对于 ID 为1的记录,建筑类型是1Fam,这在新变量BldgType_1Fam中编码为值 1,而在其他四个新变量BldgType_2fmConBldgType_DuplexBldgType_TwnhsEBldgType_Twnhs中编码为 0。另一方面,对于 ID 为10的记录,建筑类型是2fmCon,这在新变量BldgType_2fmCon中编码为值 1,而在其他四个新变量BldgType_1FamBldgType_DuplexBldgType_TwnhsEBldgType_Twnhs中编码为 0。

对于本章,我们为以下列表中的分类变量创建了虚拟变量:

string[] categoricalVars = new string[]
{
    "Alley", "BldgType", "BsmtCond", "BsmtExposure", "BsmtFinType1", "BsmtFinType2",
    "BsmtQual", "CentralAir", "Condition1", "Condition2", "Electrical", "ExterCond",
    "Exterior1st", "Exterior2nd", "ExterQual", "Fence", "FireplaceQu", "Foundation",
    "Functional", "GarageCond", "GarageFinish", "GarageQual", "GarageType",
    "Heating", "HeatingQC", "HouseStyle", "KitchenQual", "LandContour", "LandSlope", 
    "LotConfig", "LotShape", "MasVnrType", "MiscFeature", "MSSubClass", "MSZoning", 
    "Neighborhood", "PavedDrive", "PoolQC", "RoofMatl", "RoofStyle", 
    "SaleCondition", "SaleType", "Street", "Utilities"
};

以下代码显示了我们所编写的一种创建和编码虚拟变量的方法:

private static Frame&lt;int, string&gt; CreateCategories(Series&lt;int, string&gt; rows, string originalColName)
{

    var categoriesByRows = rows.GetAllValues().Select((x, i) =&gt;
    {
        // Encode the categories appeared in each row with 1
        var sb = new SeriesBuilder&lt;string, int&gt;();
        sb.Add(String.Format("{0}_{1}", originalColName, x.Value), 1);

        return KeyValue.Create(i, sb.Series);
    });

    // Create a data frame from the rows we just created
    // And encode missing values with 0
    var categoriesDF = Frame.FromRows(categoriesByRows).FillMissing(0);

    return categoriesDF;
}

如您从该方法的第 8 行中可以看到,我们在新创建的虚拟变量前加上原始分类变量的名称,并在其后加上每个类别。例如,属于1Fam类别的BldgType变量将被编码为BldgType_1Fam。然后,在第 15 行的CreateCategories方法中,我们将所有其他值编码为 0,以表示在给定的分类变量中不存在此类类别。

特征编码

现在我们知道了哪些分类变量需要编码,并为这些分类变量创建了一个虚拟变量编码方法,是时候构建一个包含特征及其值的 DataFrame 了。让我们首先看看以下代码片段中我们是如何创建特征 DataFrame 的:

var featuresDF = Frame.CreateEmpty&lt;int, string&gt;();

foreach(string col in houseDF.ColumnKeys)
{
    if (categoricalVars.Contains(col))
    {
        var categoryDF = CreateCategories(houseDF.GetColumn&lt;string&gt;(col), col);

        foreach (string newCol in categoryDF.ColumnKeys)
        {
            featuresDF.AddColumn(newCol, categoryDF.GetColumn&lt;int&gt;(newCol));
        }
    }
    else if (col.Equals("SalePrice"))
    {
        featuresDF.AddColumn(col, houseDF[col]);
        featuresDF.AddColumn("Log"+col, houseDF[col].Log());
    }
    else
    {
        featuresDF.AddColumn(col, houseDF[col].Select((x, i) =&gt; x.Value.Equals("NA")? 0.0: (double) x.Value));
    }
}
featuresDF(in line 1), and start adding in features one by one. For those categorical variables for which we are going to create dummy variables, we are calling the encoding method, CreateCategories, that we wrote previously and then adding the newly created dummy variable columns to the featuresDF data frame (in lines 5-12).  For the SalePrice variable, which is the target variable for this project, we are applying log transformation and adding it to the featuresDF data frame (in lines 13-17). Lastly, we append all the other continuous variables, after replacing the NA values with 0s, to the featuresDF data frame (in lines 18-20).

一旦我们为模型训练创建了并编码了所有特征,我们就将这个featuresDF DataFrame 导出为.csv文件。以下代码显示了如何将 DataFrame 导出为.csv文件:

string outputPath = Path.Combine(dataDirPath, "features.csv");
Console.WriteLine("Writing features DF to {0}", outputPath);
featuresDF.SaveCsv(outputPath);

现在我们有了所有必要的特征,我们可以开始构建机器学习模型来预测房屋的公允价值。特征编码和工程的全代码可以在以下链接中找到:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.5/FeatureEngineering.cs

线性回归与核支持向量机

在我们开始训练机器学习模型之前,我们需要将我们的数据集分成训练集和测试集。在本节中,我们将通过随机子选择和按预定义比例划分索引来将样本集分成训练集和测试集。我们将用于将数据集分成训练集和测试集的代码如下:

// Split the sample set into train and test sets
double trainProportion = 0.8;

int[] shuffledIndexes = featuresDF.RowKeys.ToArray();
shuffledIndexes.Shuffle();

int trainSetIndexMax = (int)(featuresDF.RowCount * trainProportion);
int[] trainIndexes = shuffledIndexes.Where(i =&gt; i &lt; trainSetIndexMax).ToArray();
int[] testIndexes = shuffledIndexes.Where(i =&gt; i &gt;= trainSetIndexMax).ToArray();

var trainSet = featuresDF.Where(x =&gt; trainIndexes.Contains(x.Key));
var testSet = featuresDF.Where(x =&gt; testIndexes.Contains(x.Key));

Console.WriteLine("\nTrain Set Shape: ({0}, {1})", trainSet.RowCount, trainSet.ColumnCount);
Console.WriteLine("Test Set Shape: ({0}, {1})", testSet.RowCount, testSet.ColumnCount);
featuresDF data frame that we created in the previous feature engineering and encoding step into train and test sets.

一旦我们准备好了这些训练和测试数据框,我们需要从数据框中过滤掉不必要的列,因为训练和测试数据框目前有诸如SalePriceId等列的值。然后,我们将不得不将这两个数据框转换为双精度数组数组,这些数组将被输入到我们的学习算法中。过滤掉训练和测试数据框中不需要的列以及将两个数据框转换为数组数组的代码如下:

string targetVar = "LogSalePrice";
string[] features = featuresDF.ColumnKeys.Where(
    x =&gt; !x.Equals("Id") && !x.Equals(targetVar) && !x.Equals("SalePrice")
).ToArray();

double[][] trainX = BuildJaggedArray(
    trainSet.Columns[features].ToArray2D&lt;double&gt;(),
    trainSet.RowCount,
    features.Length
);
double[][] testX = BuildJaggedArray(
    testSet.Columns[features].ToArray2D&lt;double&gt;(),
    testSet.RowCount,
    features.Length
);

double[] trainY = trainSet[targetVar].ValuesAll.ToArray();
double[] testY = testSet[targetVar].ValuesAll.ToArray();

线性回归

本章将要探索的第一个机器学习模型用于房屋价格预测项目是线性回归模型。你应该已经熟悉使用 Accord.NET 框架在 C#中构建线性回归模型。我们使用以下代码构建线性回归模型:

Console.WriteLine("\n**** Linear Regression Model ****");
// OLS learning algorithm
var ols = new OrdinaryLeastSquares()
{
    UseIntercept = true,
    IsRobust = true
};

// Fit a linear regression model
MultipleLinearRegression regFit = ols.Learn(
    trainX,
    trainY
);

// in-sample predictions
double[] regInSamplePreds = regFit.Transform(trainX);
// out-of-sample predictions
double[] regOutSamplePreds = regFit.Transform(testX);

本章的线性回归模型代码与上一章代码的唯一区别是传递给OrdinaryLeastSquares学习算法的IsRobust参数。正如其名所示,它使学习算法拟合一个更稳健的线性回归模型,这意味着它对异常值不太敏感。当我们有非正态分布的变量时,就像本项目的情况一样,在拟合线性回归模型时,这通常会导致问题,因为传统的线性回归模型对非正态分布的异常值很敏感。将此参数设置为true有助于解决这个问题。

线性支持向量机

在本章中,我们将要实验的第二种学习算法是线性支持向量机。以下代码展示了我们如何构建线性支持向量机模型:

Console.WriteLine("\n**** Linear Support Vector Machine ****");
// Linear SVM Learning Algorithm
var teacher = new LinearRegressionNewtonMethod()
{
    Epsilon = 0.5,
    Tolerance = 1e-5,
    UseComplexityHeuristic = true
};

// Train SVM
var svm = teacher.Learn(trainX, trainY);

// in-sample predictions
double[] linSVMInSamplePreds = svm.Score(trainX);
// out-of-sample predictions
double[] linSVMOutSamplePreds = svm.Score(testX);

如你可能已经注意到的,并且与上一章类似,我们使用LinearRegressionNewtonMethod作为学习算法来拟合线性支持向量机。

多项式核支持向量机

我们接下来要实验的下一个模型是具有多项式核的 SVM。我们不会过多地介绍核方法,简单来说,核是输入特征变量的函数,可以将原始变量转换并投影到一个新的特征空间,这个空间更易于线性分离。多项式核考虑了原始输入特征的组合,这些输入特征变量的组合通常在回归分析中被称为交互变量。使用不同的核方法会使 SVM 模型在相同的数据集上学习并表现出不同的行为。

以下代码展示了如何构建具有多项式核的 SVM 模型:

Console.WriteLine("\n**** Support Vector Machine with a Polynomial Kernel ****");
// SVM with Polynomial Kernel
var polySVMLearner = new FanChenLinSupportVectorRegression&lt;Polynomial&gt;()
{
    Epsilon = 0.1,
    Tolerance = 1e-5,
    UseKernelEstimation = true,
    UseComplexityHeuristic = true,
    Kernel = new Polynomial(3)
};

// Train SVM with Polynomial Kernel
var polySvm = polySVMLearner.Learn(trainX, trainY);

// in-sample predictions
double[] polySVMInSamplePreds = polySvm.Score(trainX);
// out-of-sample predictions
double[] polySVMOutSamplePreds = polySvm.Score(testX);

我们使用FanChenLinSupportVectorRegression学习算法来构建具有多项式核的支持向量机。在这个例子中,我们使用了 3 次多项式,但您可以尝试不同的次数。然而,次数越高,越有可能过拟合训练数据。因此,当您使用高次多项式核时,必须谨慎行事。

具有高斯核的 SVM

另一种常用的核方法是高斯核。简单来说,高斯核考虑了输入特征变量之间的距离,对于接近或相似的特征给出较高的值,而对于距离较远的特征给出较低的值。高斯核可以帮助将线性不可分的数据集转换为一个更易于线性分离的特征空间,并可以提高模型性能。

以下代码展示了如何构建具有高斯核的 SVM 模型:

Console.WriteLine("\n**** Support Vector Machine with a Gaussian Kernel ****");
// SVM with Gaussian Kernel
var gaussianSVMLearner = new FanChenLinSupportVectorRegression&lt;Gaussian&gt;()
{
    Epsilon = 0.1,
    Tolerance = 1e-5,
    Complexity = 1e-4,
    UseKernelEstimation = true,
    Kernel = new Gaussian()
};

// Train SVM with Gaussian Kernel
var gaussianSvm = gaussianSVMLearner.Learn(trainX, trainY);

// in-sample predictions
double[] guassianSVMInSamplePreds = gaussianSvm.Score(trainX);
// out-of-sample predictions
double[] guassianSVMOutSamplePreds = gaussianSvm.Score(testX);

与多项式核的情况类似,我们使用了FanChenLinSupportVectorRegression学习算法,但将核替换为Gaussian方法。

到目前为止,我们已经讨论了如何为 SVM 使用不同的核方法。现在,我们将比较这些模型在房价数据集上的性能。您可以在以下链接找到我们构建和评估模型所使用的完整代码:github.com/yoonhwang/c-sharp-machine-learning/blob/master/ch.5/Modeling.cs

模型验证

在我们开始查看上一节中构建的线性回归和 SVM 模型的性能之前,让我们回顾一下上一章中讨论的指标和诊断图。我们将查看 RMSE、R²以及实际值与预测值对比的图表来评估我们模型的性能。本节中我们将用于模型评估的代码如下:

private static void ValidateModelResults(string modelName, double[] regInSamplePreds, double[] regOutSamplePreds, double[][] trainX, double[] trainY, double[][] testX, double[] testY)
{
    // RMSE for in-sample 
    double regInSampleRMSE = Math.Sqrt(new SquareLoss(trainX).Loss(regInSamplePreds));
    // RMSE for out-sample 
    double regOutSampleRMSE = Math.Sqrt(new SquareLoss(testX).Loss(regOutSamplePreds));

    Console.WriteLine("RMSE: {0:0.0000} (Train) vs. {1:0.0000} (Test)", regInSampleRMSE, regOutSampleRMSE);

    //for in-sample 
    double regInSampleR2 = new RSquaredLoss(trainX[0].Length, trainX).Loss(regInSamplePreds);
    //for out-sample 
    double regOutSampleR2 = new RSquaredLoss(testX[0].Length, testX).Loss(regOutSamplePreds);

    Console.WriteLine("R²: {0:0.0000} (Train) vs. {1:0.0000} (Test)", regInSampleR2, regOutSampleR2);

    // Scatter Plot of expected and actual
    var scatterplot = ScatterplotBox.Show(
        String.Format("Actual vs. Prediction ({0})", modelName), testY, regOutSamplePreds
    );

}

我们使用这种方法构建模型的方式如下:

ValidateModelResults("Linear Regression", regInSamplePreds, regOutSamplePreds, trainX, trainY, testX, testY);
ValidateModelResults("Linear SVM", linSVMInSamplePreds, linSVMOutSamplePreds, trainX, trainY, testX, testY);
ValidateModelResults("Polynomial SVM", polySVMInSamplePreds, polySVMOutSamplePreds, trainX, trainY, testX, testY);
ValidateModelResults("Guassian SVM", guassianSVMInSamplePreds, guassianSVMOutSamplePreds, trainX, trainY, testX, testY);
ValidateModelResults method. When you run this code, you will see the following output on your console:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00077.gif

当查看拟合优度、R²和 RMSE 值时,线性 SVM 模型似乎与数据集的拟合最佳,具有高斯核的 SVM 模型似乎与数据集的拟合次之。查看这个输出,多项式核的 SVM 模型似乎不适合预测房价公允价值。现在,让我们查看诊断图来评估我们的模型在预测房价方面的表现。

下面的图显示了线性回归模型的诊断图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00078.jpeg

这个线性回归模型的诊断图看起来很好。大多数点似乎都位于对角线上,这表明线性回归模型的预测值与实际值很好地对齐。

下面的图显示了线性 SVM 模型的诊断图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00079.jpeg

如前所述的 R²指标值所预期的那样,线性 SVM 模型的拟合效果看起来很好,尽管似乎有一个预测值与实际值相差甚远。大多数点似乎都位于对角线上,这表明线性 SVM 模型的预测值与实际值很好地对齐。

下面的图显示了具有多项式核的 SVM 模型的诊断图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00080.jpeg

这个具有多项式核的 SVM 模型的诊断图表明,该模型的拟合效果并不好。大多数预测值都位于大约 12 的直线上。这与其他指标很好地一致,我们在其中看到 RMSE 和 R²指标在我们尝试的四个模型中是最差的。

下面的图显示了具有高斯核的 SVM 模型的诊断图:

https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/csp-ml-proj/img/00081.jpeg

这个具有高斯核的 SVM 模型的诊断图结果相当令人惊讶。从 RMSE 和 R²指标来看,我们原本预期使用高斯核的 SVM 模型拟合效果会很好。然而,这个模型的大部分预测都位于一条直线上,没有显示出任何对角线的模式。查看这个诊断图,我们不能得出结论说具有高斯核的 SVM 模型拟合效果良好,尽管 R²指标显示了模型拟合良好的强烈正信号。

通过查看指标数值和诊断图,我们可以得出结论,线性回归模型和线性 SVM 模型似乎在预测房价公允价值方面表现最佳。这个项目向我们展示了查看诊断图的重要性。仅仅关注单个指标可能很有吸引力,但始终最好使用多个验证指标来评估模型,查看诊断图,如实际值与预测值的图,对于回归模型尤其有帮助。

摘要

在本章中,我们扩展了关于构建回归模型的知识和技能。我们使用了美国爱荷华州艾姆斯市的住宅房屋的销售价格数据来构建预测模型。与其他章节不同,我们有一个更复杂的数据库,其中的变量具有混合类型,包括分类和连续变量。我们研究了分类变量,其中没有自然顺序(非序数)和有自然顺序(序数)的类别。然后我们研究了连续变量,其分布具有长的右尾。我们还讨论了如何使用对数变换来处理数据中具有高偏度的变量,以调节偏度并使这些变量的分布更接近正态分布。

我们讨论了如何处理数据集中的分类变量。我们学习了如何为每种类型的分类变量创建和编码虚拟变量。使用这些特征,我们尝试了四种不同的机器学习模型——线性回归、线性支持向量机、具有多项式核的支持向量机和具有高斯核的支持向量机。我们简要讨论了核方法的目的和用法以及它们如何用于线性不可分的数据集。使用 RMSE、R²以及实际值与预测值的图表,我们评估了我们构建的四个模型在预测美国爱荷华州艾姆斯市房屋公平价值方面的性能。在我们的模型验证步骤中,我们看到了一个案例,其中验证指标的结果与诊断图的结果相矛盾,我们学到了查看多个指标和诊断图的重要性,以确保我们模型的表现。

在下一章中,我们将再次转换方向。到目前为止,我们一直在学习如何使用和构建监督学习算法。然而,在下一章中,我们将学习无监督学习,特别是聚类算法。我们将讨论如何使用聚类算法通过在线零售数据集来深入了解客户细分。

Logo

惟楚有才,于斯为盛。欢迎来到长沙!!! 茶颜悦色、臭豆腐、CSDN和你一个都不能少~

更多推荐