Java-机器学习-二-

78 阅读1小时+

Java 机器学习(二)

原文:annas-archive.org/md5/618b6772e26c666b9787d77f8f28d78b

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:亲和度分析

亲和度分析是市场篮分析MBA)的核心。它可以发现特定用户或群体执行的活动之间的共现关系。在零售领域,亲和度分析可以帮助你了解顾客的购买行为。这些见解可以通过智能交叉销售和升级销售策略来推动收入,并帮助你开发忠诚度计划、销售促销和折扣计划。

在本章中,我们将探讨以下主题:

  • MBA

  • 关联规则学习

  • 在各个领域的其他应用

首先,我们将复习核心的关联规则学习概念和算法,例如支持度和提升度的 Apriori 算法和 FP-Growth 算法。接下来,我们将使用 Weka 对超市数据集进行首次亲和度分析,并研究如何解释生成的规则。我们将通过分析关联规则学习在其他领域中的应用来结束本章,例如 IT 运营分析和医学。

购物篮分析

自电子销售点引入以来,零售商一直在收集大量数据。为了利用这些数据产生商业价值,他们首先开发了一种方法来整合和汇总数据,以了解业务的基本情况。

最近,焦点转向了粒度最低的市场篮交易。在这个细节级别,零售商可以直接看到在其商店购物的每个顾客的市场篮子,不仅了解该特定篮子中购买物品的数量,还了解这些物品是如何一起购买的。这可以用来做出如何区分商店商品组合和商品以及如何有效组合多个产品(在类别内和跨类别)的决策,以推动更高的销售额和利润。这些决策可以在整个零售链、通过渠道、在本地商店层面甚至针对特定顾客(通过所谓的个性化营销)实施,为每个顾客提供独特的产品:

图片

MBA 涵盖了广泛的分析:

  • 物品亲和度:这定义了两个(或更多)物品一起购买的可能性。

  • 驱动项识别:这使能够识别推动人们到商店并始终需要保持库存的物品。

  • 行程分类:这分析篮子内容,并将购物行程分类到某一类别:每周杂货之旅、特殊场合等。

  • 店铺间比较:了解篮子数量可以使任何指标除以总篮子数,从而有效地创建一种方便且易于比较店铺不同特征(每客户的销售单位数、每笔交易的收入、每篮物品数量等)的方法。

  • 收入优化:这有助于确定该商店的神奇价格点,增加购物篮的大小和价值。

  • 营销:这有助于识别更有利可图的广告和促销活动,更精确地定位优惠以提高投资回报率,通过纵向分析生成更好的忠诚度卡促销活动,并吸引更多客流到商店。

  • 运营优化:这有助于通过定制商店和商品组合以适应贸易区域人口统计,并优化商店布局,来匹配库存与需求。

预测模型帮助零售商将正确的优惠提供给正确的客户群体或个人资料,以及了解哪些优惠适用于哪些客户,预测客户对这一优惠的响应概率得分,并理解客户从接受优惠中获得的增值。

关联分析

关联分析用于确定一组商品同时被购买的可能性。在零售业中,存在自然的产品关联;例如,对于购买汉堡饼的人来说,购买汉堡包、番茄酱、芥末、番茄和其他构成汉堡体验的物品是非常典型的。

虽然有些产品关联可能看似微不足道,但也有一些关联并不十分明显。一个经典的例子是牙膏和金枪鱼。似乎吃金枪鱼的人更有可能在用餐后立即刷牙。那么,为什么零售商需要很好地掌握产品关联呢?这些信息对于适当规划促销活动至关重要,因为降低某些商品的价格可能会在不进一步推广这些相关商品的情况下,导致相关高关联商品的需求激增。

在下一节中,我们将探讨关联规则学习的算法:Apriori 和 FP-Growth。

关联规则学习

关联规则学习是发现大型数据库中项目之间有趣关系的一种流行方法。它最常应用于零售业,以揭示产品之间的规律性。

关联规则学习方法通过使用不同的有趣性度量标准,在数据库中寻找有趣且强大的规则模式。例如,以下规则表明,如果一个顾客同时购买洋葱和土豆,他们很可能也会购买汉堡肉:{洋葱,土豆} -> {汉堡}。

另一个可能在每个机器学习课程中都会讲述的经典故事可能是啤酒和尿布的故事。对超市购物者行为的分析显示,购买尿布的客户,可能是年轻男性,也倾向于购买啤酒。这立即成为了一个流行的例子,说明了如何从日常数据中发现意外的关联规则;然而,关于这个故事的真实性存在不同的观点。在《DSS 新闻 2002》中,Daniel Power 说:

"在 1992 年,托马斯·布利斯克,特达数据零售咨询集团的管理员及其团队,对大约 25 家奥斯科药店的 120 万份购物篮进行了分析。开发了数据库查询以识别亲和力。分析确实发现,在下午 5:00 到 7:00 之间,消费者购买啤酒和尿布。奥斯科的管理者没有通过将产品在货架上放得更近来利用啤酒和尿布之间的关系。"

除了前面 MBA 的例子之外,关联规则今天被应用于许多应用领域,包括网络使用挖掘、入侵检测、连续生产和生物信息学。我们将在本章后面更详细地探讨这些领域。

基本概念

在我们深入算法之前,让我们首先回顾基本概念。

交易数据库

在关联规则挖掘中,数据集的结构与第一章中介绍的方法略有不同。首先,没有类值,因为学习关联规则不需要类值。接下来,数据集以事务表的形式呈现,其中每个超市商品对应一个二元属性。因此,特征向量可能非常大。

考虑以下示例。假设我们拥有以下四个收据,如后文所示。每个收据对应一笔购买交易:

为了将这些收据以事务数据库的形式编写,我们首先确定收据中出现的所有可能的项目。这些项目是洋葱土豆汉堡啤酒薯条。每一笔购买,即交易,都在一行中呈现,如果交易中购买了项目,则为1,否则为0,如下表所示:

交易 ID洋葱土豆汉堡啤酒薯条
101100
211110
300011
410110

这个例子实际上很小。在实际应用中,数据集通常包含成千上万甚至数百万笔交易,这允许学习算法发现具有统计意义的模式。

项集和规则

项集简单地说是一组项目,例如,{洋葱,土豆,汉堡}。一个规则由两个项集 X 和 Y 组成,以下格式:

X -> Y

这表示一个模式,即当观察到 X 项集时,也会观察到 Y。为了选择有趣的规则,可以使用各种显著性度量。

支持度

对于项集的支持度定义为包含该项集的交易比例。在先前的表中,{土豆,汉堡}项集具有以下支持度,因为它出现在 50%的交易中(四笔交易中的两笔):supp({土豆,汉堡}) = 2/4 = 0.5。

直观上看,它表示支持该模式的交易份额。

提升度

升值是一个衡量目标模型(关联规则)在预测或分类案例为具有增强响应(相对于整体人口)时的性能的指标,与随机选择的目标模型相比。它使用以下公式定义:

图片

置信度

规则的置信度表示其准确性。它使用以下公式定义:

图片

例如,{onions, burger} -> {beer}规则在先前的表中具有置信度0.5/0.5 = 1.0,这意味着当洋葱和汉堡一起购买时,100%的时间也会购买啤酒。

Apriori 算法

Apriori 算法是一种经典算法,用于在事务上进行频繁模式挖掘和关联规则学习。通过识别数据库中的频繁单个项目并将它们扩展到更大的项目集,Apriori 可以确定关联规则,这些规则突出了数据库的一般趋势。

Apriori 算法构建一组项目集,例如,itemset1= {Item A, Item B},并计算支持,即数据库中出现的次数。然后 Apriori 使用自下而上的方法,其中频繁项目集逐个扩展,它通过首先查看较小的集合并认识到一个大的集合不能是频繁的,除非它的所有子集都是频繁的,来消除最大的集合作为候选者。当找不到进一步的扩展时,算法终止。

尽管 Apriori 算法是机器学习中的一个重要里程碑,但它存在许多低效性和权衡。在下一节中,我们将探讨更近期的 FP-Growth 技术。

FP-Growth 算法

FP-Growth(其中 FP 是频繁模式)将事务数据库表示为后缀树。首先,算法统计数据集中项目的出现次数。在第二次遍历中,它构建一个后缀树,这是一种有序的树形数据结构,通常用于存储字符串。以下是根据前一个示例的后缀树示例图:

图片

如果许多事务共享最频繁的项目,后缀树在树根附近提供接近高压缩。直接生长大项目集,而不是生成候选项目并对其与整个数据库进行测试。生长从树的底部开始,通过找到所有匹配最小支持和置信度的项目集。一旦递归过程完成,所有具有最小覆盖范围的大项目集都已找到,并开始创建关联规则。

FP-Growth 算法具有几个优点。首先,它构建了一个 FP 树,以大量紧凑的表示编码原始数据集。其次,它利用 FP 树结构和分而治之策略有效地构建频繁项目集。

超市数据集

位于data/supermarket.arff的超市数据集描述了超市顾客的购物习惯。大多数属性代表特定的商品组,例如,乳制品、牛肉和土豆;或者代表一个部门,例如,部门 79、部门 81 等。以下表格显示了数据库的摘录,其中值为t表示顾客购买了该商品,否则为空。每个顾客有一个实例。该数据集不包含类属性,因为学习关联规则不需要它。以下表格显示了数据的一个样本:

图片

发现模式

为了发现购物模式,我们将使用之前已经研究过的两种算法:Apriori 和 FP-Growth。

Apriori

我们将使用 Weka 中实现的Apriori算法。该算法迭代地减少最小支持度,直到找到给定最小置信度的所需规则数量。我们将使用以下步骤实现该算法:

  1. 我们将使用以下代码行导入所需的库:
import java.io.BufferedReader; 
import java.io.FileReader; 
import weka.core.Instances; 
import weka.associations.Apriori; 
  1. 首先,我们将加载supermarket.arff数据集:
Instances data = new Instances(new BufferedReader(new FileReader("data/supermarket.arff"))); 
  1. 我们将初始化一个Apriori实例,并调用buildAssociations(Instances)函数来开始频繁模式挖掘,如下所示:
Apriori model = new Apriori(); 
model.buildAssociations(data); 
  1. 我们可以输出发现的项集和规则,如下所示:
System.out.println(model); 

输出如下:

   Apriori
    =======

    Minimum support: 0.15 (694 instances)
    Minimum metric <confidence>: 0.9
    Number of cycles performed: 17

    Generated sets of large itemsets:
    Size of set of large itemsets L(1): 44
    Size of set of large itemsets L(2): 380
    Size of set of large itemsets L(3): 910
    Size of set of large itemsets L(4): 633
    Size of set of large itemsets L(5): 105
    Size of set of large itemsets L(6): 1

    Best rules found:

     1\. biscuits=t frozen foods=t fruit=t total=high 788 ==> bread and cake=t 723    <conf:(0.92)> lift:(1.27) lev:(0.03) [155] conv:(3.35)
     2\. baking needs=t biscuits=t fruit=t total=high 760 ==> bread and cake=t 696    <conf:(0.92)> lift:(1.27) lev:(0.03) [149] conv:(3.28)
     3\. baking needs=t frozen foods=t fruit=t total=high 770 ==> bread and cake=t 705    <conf:(0.92)> lift:(1.27) lev:(0.03) [150] conv:(3.27)
    ...

该算法根据置信度输出了十个最佳规则。让我们看看第一条规则并解释输出,如下所示:

biscuits=t frozen foods=t fruit=t total=high 788 ==> bread and cake=t 723    <conf:(0.92)> lift:(1.27) lev:(0.03) [155] conv:(3.35)

它表示,当饼干冷冻食品水果一起购买且总购买价格较高时,也很可能还会购买面包蛋糕{饼干, 冷冻食品, 水果, 总价高}项集出现在788笔交易中,而{面包, 蛋糕}项集出现在723笔交易中。该规则的置信度为0.92,意味着在存在{饼干, 冷冻食品, 水果, 总价高}项集的 92%的交易中,该规则是成立的。

输出还报告了额外的度量,如提升度、杠杆和确信度,这些度量估计了与我们的初始假设的准确性;例如,3.35确信度值表示,如果关联纯粹是随机机会,该规则将错误3.35次。提升度衡量 X 和 Y 在一起出现的次数与它们如果统计独立时预期的次数之比(提升度=1)。X -> Y 规则中的2.16提升度表示 X 的概率是 Y 概率的2.16倍。

FP-Growth

现在,让我们尝试使用更高效的 FP-Growth 算法来获得相同的结果。

FP-Growth 也实现于weka.associations包中:

import weka.associations.FPGrowth; 

FP-Growth 算法的初始化方式与之前我们所做的一样:

FPGrowth fpgModel = new FPGrowth(); 
fpgModel.buildAssociations(data); 
System.out.println(fpgModel); 

输出揭示 FP-Growth 发现了16 条规则

    FPGrowth found 16 rules (displaying top 10)

    1\. [fruit=t, frozen foods=t, biscuits=t, total=high]: 788 ==> [bread and cake=t]: 723   <conf:(0.92)> lift:(1.27) lev:(0.03) conv:(3.35) 
    2\. [fruit=t, baking needs=t, biscuits=t, total=high]: 760 ==> [bread and cake=t]: 696   <conf:(0.92)> lift:(1.27) lev:(0.03) conv:(3.28) 
    ...

我们可以观察到,FP-Growth 找到了与 Apriori 相同的规则集;然而,处理大型数据集所需的时间可以显著缩短。

其他领域的应用

我们研究了亲和力分析,以揭示超市中的购物行为模式。尽管关联规则学习的根源在于分析销售点交易,但它们可以应用于零售业以外的领域,以找到其他类型篮子之间的关系。篮子的概念可以很容易地扩展到服务和产品,例如,分析使用信用卡购买的项目,如租车和酒店房间,以及分析电信客户购买的价值增加服务的信息(呼叫等待、呼叫转接、DSL、快速呼叫等),这可以帮助运营商确定改进服务套餐捆绑的方式。

此外,我们还将探讨以下潜在的跨行业应用示例:

  • 医疗诊断

  • 蛋白质序列

  • 人口普查数据

  • 客户关系管理

  • IT 运营分析

医疗诊断

在医疗诊断中应用关联规则可以帮助医生在治疗患者时。可靠诊断规则归纳的一般问题很困难,因为理论上,没有任何归纳过程可以保证自身归纳出的假设的正确性。实际上,诊断不是一个容易的过程,因为它涉及到不可靠的诊断测试和训练示例中的噪声。

尽管如此,关联规则可以用来识别可能同时出现的症状。在这种情况下,事务对应于一个医疗案例,而症状对应于项目。当患者接受治疗时,记录的症状列表作为一个事务。

蛋白质序列

在理解蛋白质的组成和性质方面已经进行了大量的研究;然而,还有很多事情需要充分理解。现在普遍认为,蛋白质的氨基酸序列不是随机的。

通过关联规则,可以识别蛋白质中存在的不同氨基酸之间的关联。蛋白质是由 20 种氨基酸组成的序列。每种蛋白质都有其独特的三维结构,这取决于氨基酸序列;序列的微小变化可能会改变蛋白质的功能。要应用关联规则,蛋白质对应于一个事务,而氨基酸及其结构对应于项目。

这类关联规则对于增强我们对蛋白质组成的理解是有益的,并且有可能为蛋白质中某些特定氨基酸集合的全球相互作用提供线索。了解这些关联规则或约束对于人工蛋白质的合成是非常有价值的。

人口普查数据

人口普查为研究人员和公众提供了关于社会的各种统计信息。与人口和经济普查相关的信息可以用于规划公共服务(教育、卫生、交通和资金)以及商业(建立新工厂、购物中心或银行,甚至营销特定产品)的预测。

为了发现频繁模式,每个统计区域(例如,市镇、城市和社区)对应一个事务,收集的指标对应于项目。

客户关系管理

客户关系管理(CRM),正如我们在前几章中简要讨论的,是数据丰富的来源,通过这些数据,公司希望识别不同客户群体、产品和服务的偏好,以便增强其产品和服务与客户之间的凝聚力。

关联规则可以加强知识管理过程,并使市场营销人员更好地了解客户,以提供更优质的服务。例如,关联规则可以应用于检测从客户档案和销售数据中不同时间快照的客户行为变化。基本思想是从两个数据集中发现变化,并从每个数据集中生成规则以执行规则匹配。

IT 运营分析

基于大量交易记录,关联规则学习非常适合应用于日常 IT 运营中常规收集的数据,使 IT 运营分析工具能够检测频繁模式并识别关键变化。IT 专家需要看到大局,例如,了解数据库上的问题如何影响应用服务器。

对于特定的一天,IT 运营可能会接收各种警报,并在事务数据库中展示它们。使用关联规则学习算法,IT 运营分析工具可以关联并检测同时出现的警报的频繁模式。这可以更好地理解一个组件如何影响另一个组件。

通过识别出的警报模式,可以应用预测分析。例如,一个特定的数据库服务器托管一个网络应用程序,突然触发了关于数据库的警报。通过分析关联规则学习算法识别出的频繁模式,这意味着 IT 人员需要在网络应用程序受到影响之前采取行动。

关联规则学习还可以发现源自同一 IT 事件的警报事件。例如,每次添加新用户时,都会检测到 Windows 操作系统的六个变化。接下来,在应用组合管理APM)中,IT 可能会面临多个警报,显示数据库中的事务时间过高。如果所有这些问题都源自同一来源(例如,收到数百个关于更改的警报,所有这些更改都归因于 Windows 更新),这种频繁模式挖掘可以帮助快速筛选出多个警报,使 IT 操作员能够专注于真正关键的变化。

摘要

在本章中,你学习了如何利用关联规则学习在事务数据集中获取关于频繁模式的洞察。我们在 Weka 中进行了亲和力分析,并了解到努力的工作在于结果分析——在解释规则时需要仔细注意,因为关联(即相关性)并不等同于因果关系。

在下一章中,我们将探讨如何使用可扩展的机器学习库 Apache Mahout 将商品推荐问题提升到下一个层次,它能够处理大数据。

第六章:使用 Apache Mahout 的推荐引擎

推荐引擎是目前初创公司中最常用的数据科学方法之一。构建推荐系统有两种主要技术:基于内容的过滤和协同过滤。基于内容的算法使用项目的属性来找到具有相似属性的项目。协同过滤算法使用用户评分或其他用户行为,并根据具有相似行为的用户喜欢或购买的项目来做出推荐。

在本章中,我们首先将解释理解推荐引擎原理所需的基本概念,然后我们将展示如何利用 Apache Mahout 实现的各种算法来快速构建可扩展的推荐引擎。

本章将涵盖以下主题:

  • 如何构建推荐引擎

  • 准备 Apache Mahout

  • 基于内容的推荐方法

  • 协同过滤方法

到本章结束时,你将了解适合我们问题的推荐引擎类型以及如何快速实现该引擎。

基本概念

推荐引擎的目标是向用户展示感兴趣的项目。它们与搜索引擎的不同之处在于,相关内容通常在网站上出现,而无需用户请求,用户也不需要构建查询,因为推荐引擎会观察用户的行为并在用户不知情的情况下为用户构建查询。

毫无疑问,最著名的推荐引擎例子是www.amazon.com,它以多种方式提供个性化推荐。以下截图显示了“购买此商品的用户也购买了”的示例。正如你稍后将会看到的,这是一个基于项目的协同过滤推荐的例子,其中推荐了与特定项目相似的项目:

图片

在本节中,我们将介绍与理解构建推荐引擎相关的关键概念。

关键概念

推荐引擎需要以下输入来做出推荐:

  • 项目信息,用属性描述

  • 用户资料,例如年龄范围、性别、位置、朋友等

  • 用户交互,包括评分、浏览、标记、比较、保存和电子邮件

  • 项目将显示的上下文;例如,项目的类别和项目的地理位置

这个输入随后会被推荐引擎结合使用,以帮助获取以下信息:

  • 购买、观看、查看或收藏此项目的用户也购买了、观看、查看或收藏

  • 与此项目相似的项目

  • 你可能认识的其他用户

  • 与你相似的其他用户

现在,让我们更详细地看看这种组合是如何工作的。

基于用户和基于项目的分析

构建推荐引擎取决于引擎在尝试推荐特定物品时是搜索相关物品还是用户。

在基于物品的分析中,引擎专注于识别与特定物品相似的物品,而在基于用户的分析中,首先确定与特定用户相似的用户。例如,确定具有相同配置文件信息(年龄、性别等)或行为历史(购买、观看、查看等)的用户,然后向其他类似用户推荐相同的物品。

这两种方法都需要我们计算一个相似度矩阵,这取决于我们是分析物品属性还是用户行为。让我们深入了解这是如何完成的。

计算相似度

计算相似度有三种基本方法,如下所示:

  • 协同过滤算法通过用户评分或其他用户行为,根据具有相似行为的用户喜欢或购买的内容进行推荐

  • 基于内容的算法使用物品的特性来寻找具有相似特性的物品

  • 混合方法结合了协同过滤和基于内容的过滤

在接下来的几节中,我们将详细探讨每种方法。

协同过滤

协同过滤仅基于用户评分或其他用户行为,根据具有相似行为的用户喜欢或购买的内容进行推荐。

协同过滤的一个关键优势是它不依赖于物品内容,因此能够准确地推荐复杂物品,如电影,而不需要了解物品本身。其基本假设是,过去达成共识的人将来也会达成共识,并且他们将会喜欢与过去相似类型的物品。

这种方法的一个主要缺点是所谓的冷启动问题,这意味着如果我们想构建一个准确的协同过滤系统,算法通常需要大量的用户评分。这通常会将协同过滤排除在产品的第一版之外,并在收集到一定量的数据后引入。

基于内容的过滤

另一方面,基于内容的过滤是基于项目描述和用户偏好配置文件的,它们如下组合。首先,项目用属性进行描述,为了找到相似的项目,我们使用距离度量(如余弦距离或皮尔逊系数)来测量项目之间的距离(关于距离度量,请参阅第一章,《应用机器学习快速入门》)。现在,用户配置文件进入方程。给定用户喜欢的项目类型的反馈,我们可以引入权重,指定特定项目属性的重要性。例如,Pandora 流媒体服务应用基于内容的过滤来创建电台,使用超过 400 个属性。用户最初选择具有特定属性的曲目,并通过提供反馈,强调重要的歌曲属性。

初始时,这种方法对用户反馈信息的需求非常少;因此,它有效地避免了冷启动问题。

混合方法

现在,在协作式和基于内容的推荐之间,你应该选择哪一个?协作式过滤能够从用户针对一个内容源的行为中学习用户偏好,并将这些偏好应用于其他内容类型。基于内容的过滤仅限于推荐用户已经使用过的同一类型的内容。这在某些用例中提供了价值;例如,基于新闻浏览推荐新闻文章是有用的,但如果可以根据新闻浏览推荐不同来源的内容,如书籍和电影,那就更有用了。

协作式过滤和基于内容的过滤不是相互排斥的;在某些情况下,它们可以结合使用,以更有效。例如,Netflix 使用协作式过滤来分析类似用户的搜索和观看模式,以及基于内容的过滤来提供与用户高度评价的电影具有相似特征的电影。

存在着各种各样的混合技术:加权、切换、混合、特征组合、特征增强、级联、元级等。推荐系统是机器学习和数据挖掘社区中的一个活跃领域,在数据科学会议上设有专门的轨道。Adomavicius 和 Tuzhilin 在 2005 年的论文《迈向下一代推荐系统:对现有技术和可能扩展的调查》中对技术进行了很好的概述,其中作者讨论了不同的方法和底层算法,并提供了进一步论文的参考文献。为了更深入地了解特定方法何时合理,并理解所有细微的细节,你应该查看 Ricci 等人编辑的书籍:《推荐系统手册》(第一版,2010 年,Springer-Verlag,纽约)。

探索与利用

在推荐系统中,总是存在一种权衡,即在基于我们已知用户信息推荐符合用户喜好的项目(利用)和推荐不符合用户喜好的项目,旨在让用户接触一些新奇之处(探索)之间。缺乏探索的推荐系统只会推荐与之前用户评分一致的项目,从而防止展示用户当前泡泡之外的项目。在实践中,从用户喜好的泡泡之外获得新项目的惊喜往往是非常受欢迎的,这可能导致令人愉快的惊喜,甚至可能发现新的喜好的区域。

在本节中,我们讨论了开始构建推荐引擎所需的基本概念。现在,让我们看看如何使用 Apache Mahout 实际构建一个推荐引擎。

获取 Apache Mahout

Mahout 在 第二章,“Java 机器学习库和平台”中被介绍为一个可扩展的机器学习库。它提供了一套丰富的组件,您可以使用这些组件从算法选择中构建定制的推荐系统。Mahout 的创造者表示,它被设计为适用于企业;它旨在性能、可扩展性和灵活性。

Mahout 可以配置为以两种方式运行:带有或没有 Hadoop,以及对于单机和分布式处理。我们将专注于配置不带 Hadoop 的 Mahout。对于更高级的配置和 Mahout 的进一步使用,我推荐两本近期出版的书籍:由 Chandramani Tiwary 编著的 Learning Apache Mahout,Packt 出版,以及 Ashish Gupta 编著的 Learning Apache Mahout Classification,Packt 出版。

由于 Apache Mahout 的构建和发布系统基于 Maven,您需要学习如何安装它。我们将查看最方便的方法;使用带有 Maven 插件的 Eclipse。

在 Eclipse 中使用 Maven 插件配置 Mahout

您需要一个较新的 Eclipse 版本,可以从其主页下载(www.eclipse.org/downloads/)。在本书中,我们将使用 Eclipse Luna。打开 Eclipse 并使用默认设置启动一个新的 Maven 项目,如图所示:

图片

将会显示新的 Maven 项目屏幕,如图所示:

图片

现在,我们需要告诉项目添加 Mahout JAR 文件及其依赖项。定位到 pom.xml 文件,并使用文本编辑器打开它(左键单击“打开方式”|“文本编辑器”),如图所示:

图片

定位到以 <dependencies> 开头的行,并在下一行添加以下代码:

<dependency> 
 <groupId>org.apache.mahout</groupId> 
  <artifactId>mahout-mr</artifactId> 
  <version>0.10.0</version> 
</dependency> 

就这样;Mahout 已经添加,我们准备开始。

构建推荐引擎

为了演示基于内容的过滤和协同过滤方法,我们将构建一个书籍推荐引擎。

书籍评分数据集

在本章中,我们将使用一个书籍评分数据集(Ziegler et al., 2005),该数据集是在四周的爬取中收集的。它包含了 Book-Crossing 网站 278,858 名成员的数据和 1,157,112 个评分,包括隐式和显式评分,涉及 271,379 个不同的 ISBN。用户数据被匿名化,但包含人口统计信息。数据集来自Cai-Nicolas Ziegler, Sean M. McNee, Joseph A. Konstan, Georg Lausen: Proceedings of the 14th International World Wide Web Conference (WWW '05)Cai-Nicolas Ziegler,Sean M. McNee,Joseph A. Konstan,Georg Lausen:第十四届国际万维网会议论文集(WWW '05)*,2005 年 5 月 10 日至 14 日,日本千叶(www2.informatik.uni-freiburg.de/~cziegler/BX/)。

Book-Crossing 数据集由三个文件组成,如下:

  • BX-Users:这个文件包含用户信息。请注意,用户 ID(User-ID)已经被匿名化并映射到整数。如果可用,提供人口统计信息(位置和年龄)。否则,这些字段包含空值。

  • BX-Books:书籍通过其相应的 ISBN 进行标识。无效的 ISBN 已经从数据集中删除。此外,还提供了一些基于内容的信息(Book-Title,Book-Author,Year-Of-Publication,和 Publisher),这些信息是从亚马逊网络服务获得的。请注意,在多个作者的情况下,只提供第一个作者。还提供了链接到封面图片的 URL,有三种不同的格式(Image-URL-S,Image-URL-M,和 Image-URL-L),分别指小、中、大 URL。这些 URL 指向亚马逊网站。

  • BX-Book-Ratings:这个文件包含书籍评分信息。评分(Book-Rating)要么是显式的,表示在 1-10 的刻度上(数值越高表示越受欣赏),要么是隐式的,用 0 表示。

加载数据

根据数据存储的位置(文件或数据库),有两种加载数据的方法:首先,我们将详细探讨如何从文件加载数据,包括如何处理自定义格式。最后,我们将快速查看如何从数据库加载数据。

从文件加载数据

可以使用FileDataModel类从文件加载数据。我们期望的是一个逗号分隔的文件,其中每行包含一个userID,一个itemID,一个可选的preference值和一个可选的timestamp,顺序如下:

userID,itemID[,preference[,timestamp]] 

一个可选的偏好设置可以适应具有二元偏好值的程序,也就是说,用户要么表达对一个项目的偏好,要么不表达,没有偏好程度;例如,用喜欢或不喜欢表示。

以井号(#)或空行开始的行将被忽略。这些行也可以包含额外的字段,这些字段将被忽略。

DataModel类假定以下类型:

  • userIDitemID可以被解析为long

  • preference值可以被解析为double

  • timestamp可以被解析为long

如果您能够以先前格式提供数据集,您可以使用以下行加载数据:

DataModel model = new FileDataModel(new File(path)); 

这个类不适用于处理大量数据;例如,数千万行。对于这种情况,一个基于 JDBC 的DataModel和数据库更为合适。

然而,在现实世界中,我们并不能总是确保提供给我们的输入数据只包含userIDitemID的整数值。例如,在我们的案例中,itemID对应于 ISBN 书号,这些书号唯一标识项目,但它们不是整数,FileDataModel的默认设置将不适合处理我们的数据。

现在,让我们考虑如何处理我们的itemID是一个字符串的情况。我们将通过扩展FileDataModel并重写长readItemIDFromString(String)方法来定义我们的自定义数据模型,以便将itemID作为字符串读取并将其转换为long,并返回一个唯一的long值。为了将String转换为唯一的long,我们将扩展另一个 Mahout 的AbstractIDMigrator辅助类,该类正是为此任务设计的。

现在,让我们看看如何扩展FileDataModel

class StringItemIdFileDataModel extends FileDataModel { 

  //initialize migrator to covert String to unique long 
  public ItemMemIDMigrator memIdMigtr; 

  public StringItemIdFileDataModel(File dataFile, String regex) 
     throws IOException { 
    super(dataFile, regex); 
  } 

  @Override 
  protected long readItemIDFromString(String value) { 

    if (memIdMigtr == null) { 
      memIdMigtr = new ItemMemIDMigrator(); 
    } 

    // convert to long 
    long retValue = memIdMigtr.toLongID(value); 
    //store it to cache  
    if (null == memIdMigtr.toStringID(retValue)) { 
      try { 
        memIdMigtr.singleInit(value); 
      } catch (TasteException e) { 
        e.printStackTrace(); 
      } 
    } 
    return retValue; 
  } 

  // convert long back to String 
  String getItemIDAsString(long itemId) { 
    return memIdMigtr.toStringID(itemId); 
  } 
} 

可以覆盖的其他有用方法如下:

  • readUserIDFromString(String value),如果用户 ID 不是数字

  • readTimestampFromString(String value),以改变timestamp的解析方式

现在,让我们看看如何扩展AbstractIDMIgrator

class ItemMemIDMigrator extends AbstractIDMigrator { 

  private FastByIDMap<String> longToString; 

  public ItemMemIDMigrator() { 
    this.longToString = new FastByIDMap<String>(10000); 
  } 

  public void storeMapping(long longID, String stringID) { 
    longToString.put(longID, stringID); 
  } 

  public void singleInit(String stringID) throws TasteException { 
    storeMapping(toLongID(stringID), stringID); 
  } 

  public String toStringID(long longID) { 
    return longToString.get(longID); 
  } 
} 

现在,我们已经准备好了所有东西,我们可以使用以下代码加载数据集:

StringItemIdFileDataModel model = new StringItemIdFileDataModel( 
  new File("datasets/chap6/BX-Book-Ratings.csv"), ";"); 
System.out.println( 
"Total items: " + model.getNumItems() +  
"\nTotal users: " +model.getNumUsers()); 

这提供了用户和项目的总数作为输出:

    Total items: 340556
    Total users: 105283

我们已经准备好继续前进并开始制作推荐。

从数据库加载数据

或者,我们可以使用 JDBC 数据模型之一从数据库加载数据。在本章中,我们不会深入介绍如何设置数据库、连接等详细说明,但我们将给出如何做到这一点的概要。

数据库连接器已被移动到单独的包mahout-integration中;因此,我们必须将此包添加到我们的dependency列表中。打开pom.xml文件并添加以下dependency

<dependency> 
  <groupId>org.apache.mahout</groupId> 
  <artifactId>mahout-integration</artifactId> 
  <version>0.7</version> 
</dependency> 

考虑到我们想要连接到 MySQL 数据库。在这种情况下,我们还需要一个处理数据库连接的包。将以下内容添加到pom.xml文件中:

<dependency> 
  <groupId>mysql</groupId> 
  <artifactId>mysql-connector-java</artifactId> 
  <version>5.1.35</version> 
</dependency> 

现在,我们已经有了所有的包,因此我们可以创建一个连接。首先,让我们使用以下方式初始化一个带有连接详情的DataSource类:

MysqlDataSource dbsource = new MysqlDataSource(); 
  dbsource.setUser("user"); 
  dbsource.setPassword("pass"); 
  dbsource.setServerName("hostname.com"); 
  dbsource.setDatabaseName("db"); 

Mahout 集成实现了JDBCDataModel,以便通过 JDBC 访问各种数据库。默认情况下,此类假定在 JNDI 名称jdbc/taste下有一个可用的DataSource,它提供了访问数据库的权限,

taste_preferences表,具有以下模式:

CREATE TABLE taste_preferences ( 
  user_id BIGINT NOT NULL, 
  item_id BIGINT NOT NULL, 
  preference REAL NOT NULL, 
  PRIMARY KEY (user_id, item_id) 
) 
CREATE INDEX taste_preferences_user_id_index ON taste_preferences 
   (user_id); 
CREATE INDEX taste_preferences_item_id_index ON taste_preferences 
   (item_id); 

基于数据库的数据模型初始化如下。除了 DB 连接对象外,我们还可以指定自定义表名和表列名,如下所示:

DataModel dataModel = new MySQLJDBCDataModel(dbsource, 
   "taste_preferences",  
  "user_id", "item_id", "preference", "timestamp"); 

内存数据库

最后,但同样重要的是,数据模型可以即时创建并保存在内存中。可以从偏好数组创建一个数据库,它将保存一组物品的用户评分。

我们可以按照以下步骤进行。首先,我们创建一个偏好数组PreferenceArrayFastByIdMap哈希表,它存储了一个偏好数组:

FastByIDMap <PreferenceArray> preferences = new FastByIDMap 
   <PreferenceArray> ();  

接下来,我们可以为用户创建一个新的偏好数组,该数组将保存他们的评分。数组必须使用大小参数初始化,以在内存中预留这么多槽位:

PreferenceArray prefsForUser1 =  
  new GenericUserPreferenceArray (10);   

接着,我们将当前偏好的用户 ID 设置在位置0。这实际上会将所有偏好的用户 ID 都设置好:

prefsForUser1.setUserID (0, 1L);  

itemID设置为0位置的当前偏好,如下所示:

prefsForUser1.setItemID (0, 101L);  

将偏好值设置为0位置的偏好,如下所示:

prefsForUser1.setValue (0, 3.0f);   

按照以下方式继续对其他物品评分:

prefsForUser1.setItemID (1, 102L);  
prefsForUser1.setValue (1, 4.5F);  

最后,将用户的preferences添加到哈希表中:

preferences.put (1L, prefsForUser1); // use userID as the key  

现在可以使用偏好哈希表来初始化GenericDataModel

DataModel dataModel = new GenericDataModel(preferences); 

这段代码演示了如何为单个用户添加两个偏好;在实际应用中,你可能需要为多个用户添加多个偏好:

协同过滤

Mahout 中的推荐引擎可以使用org.apache.mahout.cf.taste包构建,该包之前是一个名为Taste的独立项目,并且一直在 Mahout 中继续开发。

基于 Mahout 的协同过滤引擎通过用户的物品偏好(口味)并返回其他物品的估计偏好。例如,一个销售书籍或 CD 的网站可以很容易地使用 Mahout 来找出顾客可能感兴趣的 CD,借助之前的购买数据。

最高级别的包定义了 Mahout 接口到以下关键抽象:

  • DataModel:这代表了一个关于用户及其对物品偏好的信息库

  • UserSimilarity:这定义了两个用户之间相似性的概念

  • ItemSimilarity:这定义了两个物品之间相似性的概念

  • UserNeighborhood:这为给定用户计算邻近用户

    • Recommender:这为用户推荐物品

上述概念的一般结构如下所示:

基于用户的过滤

最基本的基于用户的协同过滤可以通过初始化之前描述的组件来实现,如下所示:

首先,加载数据模型:

StringItemIdFileDataModel model = new StringItemIdFileDataModel( 
    new File("/datasets/chap6/BX-Book-Ratings.csv", ";"); 

接下来,定义如何计算用户之间的相关性;例如,使用皮尔逊相关系数:

UserSimilarity similarity =  
  new PearsonCorrelationSimilarity(model); 

接下来,定义如何根据用户的评分来判断哪些用户是相似的,即彼此靠近的用户:

UserNeighborhood neighborhood =  
  new ThresholdUserNeighborhood(0.1, similarity, model); 

现在,我们可以使用modelneighborhood和类似对象的数据初始化一个GenericUserBasedRecommender默认引擎,如下所示:

UserBasedRecommender recommender =  
new GenericUserBasedRecommender(model, neighborhood, similarity); 

就这样。我们的第一个基本推荐引擎已经准备好了。让我们讨论如何调用推荐。首先,让我们打印出用户已经评价的项目,以及为该用户提供的十个推荐项目:

long userID = 80683; 
int noItems = 10; 

List<RecommendedItem> recommendations = recommender.recommend( 
  userID, noItems); 

System.out.println("Rated items by user:"); 
for(Preference preference : model.getPreferencesFromUser(userID)) { 
  // convert long itemID back to ISBN 
  String itemISBN = model.getItemIDAsString( 
  preference.getItemID()); 
  System.out.println("Item: " + books.get(itemISBN) +  
    " | Item id: " + itemISBN +  
    " | Value: " + preference.getValue()); 
} 

System.out.println("\nRecommended items:"); 
for (RecommendedItem item : recommendations) { 
  String itemISBN = model.getItemIDAsString(item.getItemID()); 
  System.out.println("Item: " + books.get(itemISBN) +  
    " | Item id: " + itemISBN +  
    " | Value: " + item.getValue()); 
} 

这将输出以下推荐,包括它们的分数:

    Rated items:
    Item: The Handmaid's Tale | Item id: 0395404258 | Value: 0.0
    Item: Get Clark Smart : The Ultimate Guide for the Savvy Consumer | Item id: 1563526298 | Value: 9.0
    Item: Plum Island | Item id: 0446605409 | Value: 0.0
    Item: Blessings | Item id: 0440206529 | Value: 0.0
    Item: Edgar Cayce on the Akashic Records: The Book of Life | Item id: 0876044011 | Value: 0.0
    Item: Winter Moon | Item id: 0345386108 | Value: 6.0
    Item: Sarah Bishop | Item id: 059032120X | Value: 0.0
    Item: Case of Lucy Bending | Item id: 0425060772 | Value: 0.0
    Item: A Desert of Pure Feeling (Vintage Contemporaries) | Item id: 0679752714 | Value: 0.0
    Item: White Abacus | Item id: 0380796155 | Value: 5.0
    Item: The Land of Laughs : A Novel | Item id: 0312873115 | Value: 0.0
    Item: Nobody's Son | Item id: 0152022597 | Value: 0.0
    Item: Mirror Image | Item id: 0446353957 | Value: 0.0
    Item: All I Really Need to Know | Item id: 080410526X | Value: 0.0
    Item: Dreamcatcher | Item id: 0743211383 | Value: 7.0
    Item: Perplexing Lateral Thinking Puzzles: Scholastic Edition | Item id: 0806917695 | Value: 5.0
    Item: Obsidian Butterfly | Item id: 0441007813 | Value: 0.0

    Recommended items:
    Item: Keeper of the Heart | Item id: 0380774933 | Value: 10.0
    Item: Bleachers | Item id: 0385511612 | Value: 10.0
    Item: Salem's Lot | Item id: 0451125452 | Value: 10.0
    Item: The Girl Who Loved Tom Gordon | Item id: 0671042858 | Value: 10.0
    Item: Mind Prey | Item id: 0425152898 | Value: 10.0
    Item: It Came From The Far Side | Item id: 0836220730 | Value: 10.0
    Item: Faith of the Fallen (Sword of Truth, Book 6) | Item id: 081257639X | Value: 10.0
    Item: The Talisman | Item id: 0345444884 | Value: 9.86375
    Item: Hamlet | Item id: 067172262X | Value: 9.708363
    Item: Untamed | Item id: 0380769530 | Value: 9.708363

基于项目的过滤

在这里需要讨论的最重要的一点是ItemSimilarity属性。基于项目的推荐器很有用,因为它们可以利用一些非常快速的东西;它们基于项目相似性进行计算,而不是用户相似性,并且项目相似性相对静态。它可以预先计算,而不是实时重新计算。

因此,强烈建议您在使用此类时使用预计算的相似性的GenericItemSimilarity。您也可以使用PearsonCorrelationSimilarity,它实时计算相似性,但您可能会发现对于大量数据来说这非常慢:

StringItemIdFileDataModel model = new StringItemIdFileDataModel( 
  new File("datasets/chap6/BX-Book-Ratings.csv"), ";"); 

ItemSimilarity itemSimilarity = new 
   PearsonCorrelationSimilarity(model); 

ItemBasedRecommender recommender = new 
   GenericItemBasedRecommender(model, itemSimilarity); 

String itemISBN = "0395272238"; 
long itemID = model.readItemIDFromString(itemISBN); 
int noItems = 10; 
List<RecommendedItem> recommendations = 
   recommender.mostSimilarItems(itemID, noItems); 

System.out.println("Recommendations for item: 
   "+books.get(itemISBN)); 

System.out.println("\nMost similar items:"); 
for (RecommendedItem item : recommendations) { 
  itemISBN = model.getItemIDAsString(item.getItemID()); 
  System.out.println("Item: " + books.get(itemISBN) + " | Item id: 
     " + itemISBN + " | Value: " + item.getValue()); 
} 
Recommendations for item: Close to the BoneMost similar items:Item: Private Screening | Item id: 0345311396 | Value: 1.0Item: Heartstone | Item id: 0553569783 | Value: 1.0Item: Clockers / Movie Tie In | Item id: 0380720817 | Value: 1.0Item: Rules of Prey | Item id: 0425121631 | Value: 1.0Item: The Next President | Item id: 0553576666 | Value: 1.0Item: Orchid Beach (Holly Barker Novels (Paperback)) | Item id: 0061013412 | Value: 1.0Item: Winter Prey | Item id: 0425141233 | Value: 1.0Item: Night Prey | Item id: 0425146413 | Value: 1.0Item: Presumed Innocent | Item id: 0446359866 | Value: 1.0Item: Dirty Work (Stone Barrington Novels (Paperback)) | Item id: 
   0451210158 | Value: 1.0

返回的列表将返回一组与我们选定的特定项目相似的项目。

向推荐中添加自定义规则

经常会有一些业务规则要求我们提高所选项目的分数。例如,在书籍数据集中,如果一本书是新的,我们希望给它一个更高的分数。这可以通过使用IDRescorer接口来实现,如下所示:

  • rescore(long, double)接受itemId和原始分数作为参数,并返回一个修改后的分数。

  • isFiltered(long)返回true以排除推荐中的特定项目,或者返回false

我们的示例可以如下实现:

class MyRescorer implements IDRescorer { 

  public double rescore(long itemId, double originalScore) { 
    double newScore = originalScore; 
    if(bookIsNew(itemId)){ 
      originalScore *= 1.3; 
    } 
    return newScore; 
  } 

  public boolean isFiltered(long arg0) { 
    return false; 
  } 

} 

在调用recommender.recommend时提供了一个IDRescorer实例:

IDRescorer rescorer = new MyRescorer(); 
List<RecommendedItem> recommendations =  
recommender.recommend(userID, noItems, rescorer); 

评估

您可能想知道如何确保返回的推荐是有意义的。真正确保推荐有效性的唯一方法是在实际系统中使用 A/B 测试,并使用真实用户。例如,A 组收到一个随机项目作为推荐,而 B 组收到我们引擎推荐的项。

由于这并不总是可能(也不实用),我们可以通过离线统计分析来获得一个估计值。一种进行的方法是使用第一章中介绍的 k 折交叉验证,《应用机器学习快速入门》。我们将数据集划分为多个集合;一些用于训练我们的推荐引擎,其余的用于测试推荐引擎对未知用户推荐项目的效果。

Mahout 实现了RecommenderEvaluator类,它将数据集分成两部分。第一部分(默认为 90%)用于生成推荐,而其余数据则与估计的偏好值进行比较,以测试匹配度。该类不接受recommender对象直接;你需要构建一个实现RecommenderBuilder接口的类,该类为给定的DataModel对象构建一个recommender对象,然后用于测试。让我们看看这是如何实现的。

首先,我们创建一个实现RecommenderBuilder接口的类。我们需要实现buildRecommender方法,它将返回一个recommender,如下所示:

public class BookRecommender implements RecommenderBuilder  { 
  public Recommender buildRecommender(DataModel dataModel) { 
    UserSimilarity similarity =  
      new PearsonCorrelationSimilarity(model); 
    UserNeighborhood neighborhood =  
      new ThresholdUserNeighborhood(0.1, similarity, model); 
    UserBasedRecommender recommender =  
      new GenericUserBasedRecommender( 
        model, neighborhood, similarity); 
    return recommender; 
  } 
} 

现在我们有一个返回推荐对象类的类,我们可以初始化一个RecommenderEvaluator实例。这个类的默认实现是AverageAbsoluteDifferenceRecommenderEvaluator类,它计算预测评分和实际评分之间的平均绝对差异。以下代码展示了如何组合这些部分并运行一个保留测试:

首先,加载一个数据模型,如下所示:

DataModel dataModel = new FileDataModel( 
  new File("/path/to/dataset.csv")); 

接下来,初始化一个evaluator实例,如下所示:

RecommenderEvaluator evaluator =  
  new AverageAbsoluteDifferenceRecommenderEvaluator(); 

初始化实现RecommenderBuilder接口的BookRecommender对象,如下所示:

RecommenderBuilder builder = new MyRecommenderBuilder(); 

最后,调用evaluate()方法,它接受以下参数:

  • RecommenderBuilder:这是实现RecommenderBuilder的对象,可以构建用于测试的recommender

  • DataModelBuilder:这表示要使用的DataModelBuilder;如果为 null,将使用默认的DataModel实现

  • DataModel:这是用于测试的数据集

  • trainingPercentage:这表示用于生成推荐的每个用户的偏好百分比;其余的与估计的偏好值进行比较,以评估recommender的性能

  • evaluationPercentage:这是用于评估的用户百分比

该方法调用如下:

double result = evaluator.evaluate(builder, null, model, 0.9, 
   1.0); 
System.out.println(result); 

该方法返回一个double类型的值,其中0代表最佳可能的评估,意味着推荐器完美匹配用户偏好。一般来说,值越低,匹配度越好。

在线学习引擎

在任何在线平台上,新用户将持续增加。之前讨论的方法对现有用户效果良好。为每个新增用户创建推荐实例的成本很高。我们不能忽视在推荐引擎制作后添加到系统中的用户。为了应对类似这种情况,Apache Mahout 具有将临时用户添加到数据模型的能力。

一般设置如下:

  • 定期使用当前数据重新创建整个推荐(例如,每天或每小时,具体取决于所需时间)

  • 在进行推荐之前,始终检查用户是否存在于系统中

  • 如果用户存在,则完成推荐

  • 如果用户不存在,创建一个临时用户,填写偏好,然后进行推荐

第一步似乎有些棘手,因为它涉及到使用当前数据生成整个推荐的频率。如果系统很大,内存限制将会存在,因为当新的推荐器正在生成时,旧的、正在工作的推荐器应该保留在内存中,以便在新的推荐器准备好之前,请求由旧副本提供服务。

至于临时用户,我们可以用PlusAnonymousConcurrentUserDataModel实例包装我们的数据模型。这个类允许我们获取一个临时用户 ID;该 ID 必须稍后释放以便可以重用(这类 ID 的数量有限)。在获取 ID 后,我们必须填写偏好,然后我们可以像往常一样进行推荐:

class OnlineRecommendation{ 

  Recommender recommender; 
  int concurrentUsers = 100; 
  int noItems = 10; 

  public OnlineRecommendation() throws IOException { 

    DataModel model = new StringItemIdFileDataModel( 
      new File /chap6/BX-Book-Ratings.csv"), ";"); 
    PlusAnonymousConcurrentUserDataModel plusModel = new 
       PlusAnonymousConcurrentUserDataModel
         (model, concurrentUsers); 
    recommender = ...; 

  } 

  public List<RecommendedItem> recommend(long userId, 
     PreferenceArray preferences){ 

    if(userExistsInDataModel(userId)){ 
      return recommender.recommend(userId, noItems); 
    } 

    else{ 

      PlusAnonymousConcurrentUserDataModel plusModel = 
        (PlusAnonymousConcurrentUserDataModel) 
           recommender.getDataModel(); 

      // Take an available anonymous user form the poll 
      Long anonymousUserID = plusModel.takeAvailableUser(); 

      // Set temporary preferences 
      PreferenceArray tempPrefs = preferences; 
      tempPrefs.setUserID(0, anonymousUserID); 
      tempPrefs.setItemID(0, itemID); 
       plusModel.setTempPrefs(tempPrefs, anonymousUserID); 

      List<RecommendedItem> results = 
         recommender.recommend(anonymousUserID, noItems); 

      // Release the user back to the poll 
      plusModel.releaseUser(anonymousUserID); 

      return results; 

    } 

  } 
} 

基于内容的过滤

基于内容的过滤不在 Mahout 框架的范围内,主要是因为如何定义相似项目取决于你。如果我们想进行基于内容的项目相似度,我们需要实现自己的ItemSimilarity。例如,在我们的书籍数据集中,我们可能想要为书籍相似度制定以下规则:

  • 如果流派相同,将0.15加到相似度

  • 如果作者相同,将0.50加到相似度

我们现在可以实施自己的相似度度量,如下所示:

class MyItemSimilarity implements ItemSimilarity { 
 ... 
 public double itemSimilarity(long itemID1, long itemID2) { 
  MyBook book1 = lookupMyBook (itemID1); 
  MyBook book2 = lookupMyBook (itemID2); 
  double similarity = 0.0; 
  if (book1.getGenre().equals(book2.getGenre())  
   similarity += 0.15; 
  } 
  if (book1.getAuthor().equals(book2\. getAuthor ())) { 
   similarity += 0.50; 
  } 
  return similarity; 
 } 
 ... 
} 

我们可以使用这个ItemSimilarity,而不是像LogLikelihoodSimilarity或其他带有GenericItemBasedRecommender的实现。这就是全部。这就是我们在 Mahout 框架中执行基于内容推荐所必须做的。

我们在这里看到的是基于内容推荐的最简单形式之一。另一种方法是根据项目特征的加权向量创建用户的内容配置文件。权重表示每个特征对用户的重要性,并且可以从单独评分的内容向量中计算得出。

摘要

在本章中,你了解了推荐引擎的基本概念,协作过滤和基于内容过滤之间的区别,以及如何使用 Apache Mahout,这是一个创建推荐器的绝佳基础,因为它非常可配置,并提供了许多扩展点。我们探讨了如何选择正确的配置参数值,设置重新评分,并评估推荐结果。

通过本章,我们完成了对用于分析客户行为的数据科学技术的概述,这始于第四章中关于客户关系预测的讨论,即使用集成进行客户关系预测,并继续到第五章的亲和力分析,即亲和力分析。在下一章中,我们将转向其他主题,例如欺诈和异常检测。

第七章:欺诈和异常检测

异常检测用于识别异常、罕见事件和其他异常情况。这些异常可能像针尖上的麦芒,但它们的后果可能非常严重;例如,信用卡欺诈检测、识别网络入侵、制造过程中的故障、临床试验、投票活动以及电子商务中的犯罪活动。因此,当发现异常时,它们具有很高的价值;如果没有发现,则成本高昂。将机器学习应用于异常检测问题可以带来新的见解和更好的异常事件检测。机器学习可以考虑到许多不同的数据来源,并可以发现人类分析难以识别的相关性。

以电子商务欺诈检测为例。在机器学习算法到位的情况下,购买者的在线行为,即网站浏览历史,成为欺诈检测算法的一部分,而不仅仅是持卡人购买历史的记录。这涉及到分析各种数据来源,但这也是一种更稳健的电子商务欺诈检测方法。

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

  • 问题与挑战

  • 可疑模式检测

  • 异常模式检测

  • 与不平衡数据集合作

  • 时间序列中的异常检测

可疑和异常行为检测

从传感器数据中学习模式的问题出现在许多应用中,包括电子商务、智能环境、视频监控、网络分析、人机交互、环境辅助生活等等。我们专注于检测偏离常规行为且可能代表安全风险、健康问题或任何其他异常行为的情况。

换句话说,偏差行为是一种数据模式,它要么不符合预期的行为(异常行为),要么与先前定义的不希望的行为相匹配(可疑行为)。偏差行为模式也被称为异常值、例外、特殊性、惊喜、滥用等等。这种模式相对较少发生;然而,当它们发生时,其后果可能非常严重,并且往往是负面的。典型的例子包括信用卡欺诈、网络入侵和工业损害。在电子商务中,欺诈估计每年使商家损失超过 2000 亿美元;在医疗保健中,欺诈估计每年使纳税人损失 600 亿美元;对于银行来说,成本超过 120 亿美元。

未知之未知

当美国国防部长唐纳德·拉姆斯菲尔德于 2002 年 2 月 12 日举行新闻发布会,关于缺乏证据将伊拉克政府与向恐怖组织供应大规模杀伤性武器联系起来时,这立即成为许多评论的焦点。拉姆斯菲尔德陈述了以下内容(DoD News, 2012):

“关于某些事情尚未发生的报告总是让我感到很有趣,因为我们知道,有已知已知;有我们已知我们知道的事情。我们也知道有已知未知;也就是说,我们知道有一些我们不知道的事情。但还有未知未知——那些我们不知道我们不知道的事情。如果我们回顾我国和其他自由国家的历史,往往是后者更难处理。”

这句话一开始可能听起来有些令人困惑,但未知未知的概念在处理风险、国家安全局和其他情报机构的学者中得到了很好的研究。这个声明基本上意味着以下内容:

  • 已知已知:这些是众所周知的问题或问题;我们知道如何识别它们以及如何处理它们

  • 已知未知:这些是预期或可预见的问题,可以合理预测,但之前尚未发生

  • 未知未知:这些是意外和不可预见的问题,它们具有重大风险,因为基于以往的经验,它们无法被预测

在以下章节中,我们将探讨两种处理前两种已知和未知类型的基本方法:可疑模式检测处理已知已知,以及针对已知未知的异常模式检测。

可疑模式检测

第一种方法涉及一个行为库,该库编码负模式,在以下图中用红色减号表示,并识别观察到的行为是否与库中的匹配。如果一种新模式可以与负模式相匹配,那么它被认为是可疑的:

图片

例如,当你去看医生时,他会检查各种健康症状(体温、疼痛程度、受影响区域等)并将症状与已知疾病相匹配。在机器学习的术语中,医生收集属性并执行分类。

这种方法的一个优点是我们立即知道出了什么问题;例如,如果我们知道疾病,我们可以选择合适的治疗方案。

这种方法的重大缺点是它只能检测事先已知的可疑模式。如果一种模式没有被插入到负模式库中,那么我们就无法识别它。因此,这种方法适用于建模已知已知。

异常模式检测

第二种方法以相反的方式使用模式库,这意味着库只编码正模式,在以下图中用绿色加号标记。当一个观察到的行为(蓝色圆圈)无法与库相匹配时,它被认为是异常的:

图片

这种方法要求我们仅对过去所见到的内容进行建模,即正常模式。如果我们回到医生这个例子,我们最初去看医生的主要原因是因为我们感觉不舒服。我们感知到的感觉状态(例如,头痛和皮肤疼痛)与我们通常的感觉不符,因此我们决定寻求医生的帮助。我们不知道是什么疾病导致了这种状态,也不知道治疗方法,但我们能够观察到它不符合通常的状态。

这种方法的一个主要优点是它不需要我们说任何关于异常模式的内容;因此,它适合于建模已知未知和未知未知。另一方面,它并没有告诉我们具体是什么出了问题。

分析类型

已经提出了几种方法来解决这个问题。我们将异常和可疑行为检测大致分为以下三个类别:模式分析、事务分析和计划识别。在接下来的几节中,我们将快速查看一些实际应用。

模式分析

基于视觉模式(如摄像头)的异常和可疑行为检测是一个活跃的研究领域。Zhang 等人(2007 年)提出了一种从视频序列中分析人类运动的方法,它根据行走轨迹识别异常行为;Lin 等人(2009 年)描述了一个基于颜色特征、距离特征和计数特征的视频监控系统,其中使用了进化技术来测量观察相似性。该系统跟踪每个人,并通过分析他们的轨迹模式来对他们的行为进行分类。该系统从图像的不同部分提取一组视觉低级特征,并使用 SVM 进行分类,以检测攻击性、愉快、醉酒、紧张、中立和疲劳行为。

事务分析

与连续观察不同,事务分析假设离散状态/事务。一个主要的研究领域是入侵检测ID),其目的是检测针对信息系统的一般攻击。有两种类型的 ID 系统,基于签名和基于异常,它们广泛遵循前几节中描述的可疑和异常模式检测。Gyanchandani 等人(2012 年)发表了对 ID 方法的综合评论。

此外,基于可穿戴传感器的环境辅助生活应用也适合于交易分析,因为传感通常是事件驱动的。Lymberopoulos 等人(2008)提出了一种自动提取用户时空模式的方法,这些模式编码为传感器网络中的传感器激活,该传感器网络部署在他们家中。所提出的方法基于位置、时间和持续时间,能够使用 Apriori 算法提取频繁模式,并以马尔可夫链的形式编码最频繁的模式。相关工作的另一个领域包括广泛用于传统活动识别的隐马尔可夫模型(HMMs),但这些问题已经超出了本书的范围。

计划识别

计划识别关注于识别一个代理不可观察状态的机制,给定其与环境交互的观察结果(Avrahami-Zilberbrand,2009)。大多数现有研究假设以活动形式存在的离散观察。为了执行异常和可疑行为检测,计划识别算法可能使用混合方法。一个符号计划识别器用于过滤一致假设,并将它们传递给评估引擎,该引擎专注于排名。

这些是应用于各种现实场景的高级方法,旨在发现异常。在接下来的章节中,我们将深入了解用于可疑和异常模式检测的基本方法。

使用 ELKI 进行异常检测

ELKI代表用于 KDD 应用索引的环境结构,其中KDD代表数据库中的知识发现。它是一个主要用于数据挖掘的开源软件,侧重于无监督学习。它支持各种聚类分析和异常检测算法。以下是一些异常检测算法:

  • 基于距离的异常检测:这用于指定两个参数。如果一个对象与 c 的距离大于 d,且其所有数据对象的分数 p 被标记为异常。有许多算法,如DBOutlierDetectionDBOutlierScoreKNNOutlierKNNWeightOutlierParallelKNNOutlierParallelKNNWeightOutlierReferenceBasedOutlierDetection等。

  • LOF 系列方法:这种方法在特定参数上计算基于密度的局部异常因子。它包括LOFParallelLOFALOCICOFLDFLDOF等算法。

  • 基于角度的异常检测:这使用角度的方差分析,主要使用高维数据集。常见的算法包括ABODFastABODLBABOD

  • 基于聚类的异常检测:这使用 EM 聚类;如果一个对象不属于任何聚类,则被视为异常。这包括EMOutlierKMeansOutlierDetection等算法。

  • 子空间异常检测:这使用的是轴平行子空间的异常检测方法。它包括SODOutRankS1OUTRESAggrawalYuNaiveAggrawalYuEvolutionary等算法。

  • 空间异常检测:它基于从不同来源收集的基于位置的大量数据集,以及相对于邻居的极端数据点。它包括CTLuGLSBackwardSearchAlgorithmCTLuMeanMultipleAttributesCTLuMedianAlgorithmCTLuScatterplotOutlier等算法。

使用 ELKI 的一个示例

在第三章,“基本算法 – 分类、回归和聚类”中,你已经看到了如何为 ELKI 获取所需的.jar文件。我们将遵循类似的过程,如下所示:

打开命令提示符或终端,并执行以下命令:

java -jar elki-bundle-0.7.1.jar

这将提供 GUI 界面,如下面的截图所示:

图片

在 GUI 中,dbc.in和算法参数被突出显示并需要设置。我们将使用pov.csv文件作为dbc.in。此 CSV 文件可以从github.com/elki-project/elki/blob/master/data/synthetic/ABC-publication/pov.csv下载。

对于算法,选择outlier.clustering.EMOutlier,并在em.k中传递3作为值。以下截图显示了所有已填写的选项:

图片

点击“运行任务”按钮,它将处理并生成以下输出:

图片

这显示了聚类和可能的异常。

保险索赔中的欺诈检测

首先,我们将查看可疑行为检测,其目标是了解欺诈模式,这对应于已知已知建模。

数据集

我们将使用一个描述保险交易的数据库集进行工作,该数据库集在 Oracle 数据库在线文档中公开可用,网址为docs.oracle.com/cd/B28359_01/datamine.111/b28129/anomalies.htm

该数据集描述了一个未公开保险公司的车辆事故索赔。它包含 15,430 个索赔;每个索赔由 33 个属性组成,描述以下组件:

  • 客户人口统计详细信息(年龄、性别、婚姻状况等)

  • 购买的政策(政策类型、车辆类别、补充数量、代理商类型等)

  • 索赔情况(索赔的日/月/周、政策报告提交、目击者在场、事故与政策报告之间的过去天数、事故索赔等)

  • 其他客户数据(汽车数量、以前的索赔、驾驶员评分等)

  • 发现欺诈(是或否)

下面的截图显示了已加载到 Weka 中的数据库样本:

图片

现在,任务是创建一个能够识别未来可疑声明的模型。这个任务具有挑战性的地方在于只有 6% 的声明是可疑的。如果我们创建一个虚拟分类器,声称没有任何声明是可疑的,那么它在 94% 的情况下将是准确的。因此,在这个任务中,我们将使用不同的准确度指标:精确度和召回率。

让我们回顾一下第一章《应用机器学习快速入门》中的结果表,其中包含四种可能的结果,分别表示真阳性、假阳性、假阴性和真阴性:

分类为
实际欺诈无欺诈
欺诈TP - 真阳性FN - 假阴性
无欺诈FP - 假阳性TN - 真阴性

精确度和召回率定义如下:

  • 精确度等于正确发出警报的比例,如下所示:

  • 召回率等于正确识别的异常签名比例,如下所示:

  • 使用这些指标——我们的虚拟分类器得分——我们发现 Pr = 0Re = 0,因为它从未将任何实例标记为欺诈 (TP = 0)。在实践中,我们希望通过数字比较分类器;因此,我们使用 F - measure。这是一个事实上的指标,它计算精确度和召回率之间的调和平均值,如下所示:

现在,让我们继续设计一个真正的分类器。

建立可疑模式模型

为了设计一个分类器,我们可以遵循标准的有监督学习步骤,如第一章《应用机器学习快速入门》中所述。在这个菜谱中,我们将包括一些额外的步骤来处理不平衡数据集并根据精确度和召回率评估分类器。计划如下:

  1. .csv 格式加载数据。

  2. 分配类属性。

  3. 将所有属性从数值转换为名义值,以确保没有错误加载的数值。

  4. 实验 1:使用 k 折交叉验证评估模型。

  5. 实验 2:将数据集重新平衡到更平衡的类别分布,并手动执行交叉验证。

  6. 通过召回率、精确度和 f-measure 比较分类器。

首先,让我们使用 CSVLoader 类加载数据,如下所示:

String filePath = "/Users/bostjan/Dropbox/ML Java Book/book/datasets/chap07/claims.csv"; 

CSVLoader loader = new CSVLoader(); 
loader.setFieldSeparator(","); 
loader.setSource(new File(filePath)); 
Instances data = loader.getDataSet(); 

接下来,我们需要确保所有属性都是名义的。在数据导入过程中,Weka 应用一些启发式方法来猜测最可能的属性类型,即数值、名义、字符串或日期。由于启发式方法不能总是猜对类型,我们可以手动设置类型,如下所示:

NumericToNominal toNominal = new NumericToNominal(); 
toNominal.setInputFormat(data); 
data = Filter.useFilter(data, toNominal); 

在我们继续之前,我们需要指定我们将尝试预测的属性。我们可以通过调用 setClassIndex(int) 函数来实现这一点:

int CLASS_INDEX = 15; 
data.setClassIndex(CLASS_INDEX); 

接下来,我们需要移除一个描述政策编号的属性,因为它没有预测价值。我们只需应用Remove过滤器,如下所示:

Remove remove = new Remove(); 
remove.setInputFormat(data); 
remove.setOptions(new String[]{"-R", ""+POLICY_INDEX}); 
data = Filter.useFilter(data, remove); 

现在,我们准备开始建模。

基本方法

基本方法是将课程直接应用,就像在第三章,“基本算法 - 分类、回归、聚类”中所展示的那样,没有任何预处理,也不考虑数据集的具体情况。为了展示基本方法的缺点,我们将简单地使用默认参数构建一个模型,并应用 k 折交叉验证。

首先,让我们定义一些我们想要测试的分类器,如下所示:

ArrayList<Classifier>models = new ArrayList<Classifier>(); 
models.add(new J48()); 
models.add(new RandomForest()); 
models.add(new NaiveBayes()); 
models.add(new AdaBoostM1()); 
models.add(new Logistic()); 

接下来,我们需要创建一个Evaluation对象,并通过调用crossValidate(Classifier, Instances, int, Random, String[])方法执行 k 折交叉验证,提供precisionrecallfMeasure作为输出:

int FOLDS = 3; 
Evaluation eval = new Evaluation(data); 

for(Classifier model : models){ 
  eval.crossValidateModel(model, data, FOLDS,  
  new Random(1), new String[] {}); 
  System.out.println(model.getClass().getName() + "\n"+ 
    "\tRecall:    "+eval.recall(FRAUD) + "\n"+ 
    "\tPrecision: "+eval.precision(FRAUD) + "\n"+ 
    "\tF-measure: "+eval.fMeasure(FRAUD)); 
} 

评估提供了以下分数作为输出:

    weka.classifiers.trees.J48
      Recall:    0.03358613217768147
      Precision: 0.9117647058823529
      F-measure: 0.06478578892371996
    ...
    weka.classifiers.functions.Logistic
      Recall:    0.037486457204767065
      Precision: 0.2521865889212828
      F-measure: 0.06527070364082249

我们可以看到,结果并不十分令人鼓舞。召回率,即发现的欺诈在所有欺诈中的比例,仅为 1-3%,这意味着只有 1-3/100 的欺诈被检测到。另一方面,精确度,即警报的准确性,为 91%,这意味着在 10 个案例中有 9 个,当一个索赔被标记为欺诈时,模型是正确的。

数据集重平衡

由于负面示例(即欺诈实例)的数量与正面示例相比非常小,学习算法在归纳方面遇到了困难。我们可以通过提供一个正负示例比例相当的数据集来帮助他们。这可以通过数据集重平衡来实现。

Weka 有一个内置的过滤器,Resample,它使用有放回或无放回的抽样方法生成数据集的随机子样本。该过滤器还可以使分布偏向均匀的类别分布。

我们将手动实现 k 折交叉验证。首先,我们将数据集分成k个相等的部分。第k部分将用于测试,而其他部分将用于学习。为了将数据集分成部分,我们将使用StratifiedRemoveFolds过滤器,该过滤器在部分内保持类别分布,如下所示:

StratifiedRemoveFolds kFold = new StratifiedRemoveFolds(); 
kFold.setInputFormat(data); 

double measures[][] = new double[models.size()][3]; 

for(int k = 1; k <= FOLDS; k++){ 

  // Split data to test and train folds 
  kFold.setOptions(new String[]{ 
    "-N", ""+FOLDS, "-F", ""+k, "-S", "1"}); 
  Instances test = Filter.useFilter(data, kFold); 

  kFold.setOptions(new String[]{ 
    "-N", ""+FOLDS, "-F", ""+k, "-S", "1", "-V"}); 
    // select inverse "-V" 
  Instances train = Filter.useFilter(data, kFold); 

接下来,我们可以重平衡训练数据集,其中-Z参数指定要重采样的数据集百分比,而-B参数使类别分布偏向均匀分布:

Resample resample = new Resample(); 
resample.setInputFormat(data); 
resample.setOptions(new String[]{"-Z", "100", "-B", "1"}); //with 
   replacement 
Instances balancedTrain = Filter.useFilter(train, resample); 

接下来,我们可以构建分类器并执行评估:

for(ListIterator<Classifier>it = models.listIterator(); 
   it.hasNext();){ 
  Classifier model = it.next(); 
  model.buildClassifier(balancedTrain); 
  eval = new Evaluation(balancedTrain); 
  eval.evaluateModel(model, test); 

// save results for average 
  measures[it.previousIndex()][0] += eval.recall(FRAUD); 
  measures[it.previousIndex()][1] += eval.precision(FRAUD); 
 measures[it.previousIndex()][2] += eval.fMeasure(FRAUD); 
} 

最后,我们使用以下代码行计算平均值并提供最佳模型作为输出:

// calculate average 
for(int i = 0; i < models.size(); i++){ 
  measures[i][0] /= 1.0 * FOLDS; 
  measures[i][1] /= 1.0 * FOLDS; 
  measures[i][2] /= 1.0 * FOLDS; 
} 

// output results and select best model 
Classifier bestModel = null; double bestScore = -1; 
for(ListIterator<Classifier> it = models.listIterator(); 
   it.hasNext();){ 
  Classifier model = it.next(); 
  double fMeasure = measures[it.previousIndex()][2]; 
  System.out.println( 
    model.getClass().getName() + "\n"+ 
    "\tRecall:    "+measures[it.previousIndex()][0] + "\n"+ 
    "\tPrecision: "+measures[it.previousIndex()][1] + "\n"+ 
    "\tF-measure: "+fMeasure); 
  if(fMeasure > bestScore){ 
    bestScore = fMeasure; 
    bestModel = model; 

  } 
} 
System.out.println("Best model:"+bestModel.getClass().getName()); 

现在,模型的性能显著提高,如下所示:

    weka.classifiers.trees.J48
      Recall:    0.44204845100610574
      Precision: 0.14570766048577555
      F-measure: 0.21912423640160392
    ...
    weka.classifiers.functions.Logistic
      Recall:    0.7670657247204478
      Precision: 0.13507459756495374
      F-measure: 0.22969038530557626
    Best model: weka.classifiers.functions.Logistic

我们可以看到,所有模型的表现都显著提高;例如,表现最好的模型——逻辑回归,正确地发现了 76%的欺诈行为,同时产生合理数量的误报——只有 13%被标记为欺诈的索赔实际上是欺诈的。如果未检测到的欺诈比误报的调查成本高得多,那么处理更多的误报是有意义的。

总体性能很可能还有提升空间;我们可以进行属性选择和特征生成,并应用更复杂的模型学习,这些内容我们在第三章,基本算法 – 分类、回归、聚类中讨论过。

网站流量中的异常检测

在第二个示例中,我们将专注于建模前一个示例的相反情况。不是讨论典型的无欺诈案例,而是讨论系统的正常预期行为。如果某些东西无法与我们的预期模型匹配,它将被认为是异常的。

数据集

我们将使用一个由 Yahoo! Labs 发布的公开数据集,这个数据集对于讨论如何检测时间序列数据中的异常非常有用。对于 Yahoo 来说,主要用例是在检测 Yahoo 服务器上的异常流量。

尽管 Yahoo 已经宣布他们的数据是公开的,但你必须申请使用,并且批准通常需要大约 24 小时。数据集可在webscope.sandbox.yahoo.com/catalog.php?datatype=s&did=70获取。

该数据集由 Yahoo 服务的真实流量和一些合成数据组成。总共有 367 个时间序列,每个时间序列包含 741 到 1,680 个观测值,这些观测值以固定间隔记录。每个序列都写在它自己的文件中,每行一个观测值。每个序列还伴随一个第二列的指示符,如果观测值是异常,则使用 1,否则使用 0。真实数据中的异常是通过人工判断确定的,而合成数据中的异常是通过算法生成的。以下表格显示了合成时间序列数据的一个片段:

图片

在下一节中,你将学习如何将时间序列数据转换成属性表示,这样我们就可以应用机器学习算法。

时间序列数据中的异常检测

在原始的、流式的时间序列数据中检测异常需要一些数据转换。最明显的方法是选择一个时间窗口,并采样一个固定长度的时间序列。在下一步,我们希望将新的时间序列与之前收集的集合进行比较,以检测是否有异常情况发生。

比较可以使用各种技术进行,如下所示:

  • 预测最可能的后继值以及置信区间(例如,Holt-Winters 指数平滑)。如果一个新值超出了预测的置信区间,它被认为是异常的。

  • 互相关比较一个新样本与一组正样本库,并寻找精确匹配。如果没有找到匹配,它将被标记为异常。

  • 动态时间卷绕与互相关类似,但允许信号在比较中发生扭曲。

  • 将信号离散化为频带,其中每个频带对应一个字母。例如,A=[min, mean/3]B=[mean/3, mean*2/3],和C=[mean*2/3, max]将信号转换为一系列字母,例如aAABAACAABBA....这种方法减少了存储空间,并允许我们应用第十章中将要讨论的文本挖掘算法,即Mallet 文本挖掘 – 主题建模和垃圾邮件检测

  • 基于分布的方法估计特定时间窗口中值的分布。当我们观察到一个新的样本时,我们可以比较分布是否与之前观察到的分布相匹配。

这个列表绝对不是详尽的。不同的方法专注于检测不同的异常(例如,在值、频率和分布上)。在本章中,我们将关注基于分布的方法的一个版本。

使用 Encog 进行时间序列分析

我们必须从solarscience.msfc.nasa.gov/greenwch/spot_num.txt下载时间序列数据,并将文件保存在data文件夹中。在.java文件中,我们将指定文件路径,然后我们将使用以下代码块指示文件的格式:

File filename = new File("data/spot_num.txt");
CSVFormat format = new CSVFormat('.', ' ');
VersatileDataSource source = new CSVDataSource(filename, true, format);
VersatileMLDataSet data = new VersatileMLDataSet(source);
data.getNormHelper().setFormat(format);
ColumnDefinition columnSSN = data.defineSourceColumn("SSN", ColumnType.continuous);
ColumnDefinition columnDEV = data.defineSourceColumn("DEV", ColumnType.continuous);
data.analyze();
data.defineInput(columnSSN);
data.defineInput(columnDEV);
data.defineOutput(columnSSN);

现在,我们将创建窗口大小为1的前馈网络。在处理时间序列时,你应该记住它永远不应该被打乱。我们将保留一些数据用于验证。我们将使用以下代码行来完成:

EncogModel model = new EncogModel(data);
model.selectMethod(data, MLMethodFactory.TYPE_FEEDFORWARD);

model.setReport(new ConsoleStatusReportable());
data.normalize();

// Set time series.
data.setLeadWindowSize(1);
data.setLagWindowSize(WINDOW_SIZE);
model.holdBackValidation(0.3, false, 1001);
model.selectTrainingType(data);

下一步是使用以下行运行带有五折交叉验证的训练:

MLRegression bestMethod = (MLRegression) model.crossvalidate(5, false);

现在,是时候展示错误和最终模型了。我们将通过以下代码行来完成:

System.out.println("Training error: " + model.calculateError(bestMethod, model.getTrainingDataset()));
System.out.println("Validation error: " + model.calculateError(bestMethod, model.getValidationDataset()));

NormalizationHelper helper = data.getNormHelper();
System.out.println(helper.toString());

// Display the final model.
System.out.println("Final model: " + bestMethod);

输出将类似于以下截图:

图片

现在,我们将使用以下代码块测试模型:

while (csv.next() && stopAfter > 0) {
                StringBuilder result = new StringBuilder();

                line[0] = csv.get(2);// ssn
                line[1] = csv.get(3);// dev
                helper.normalizeInputVector(line, slice, false);

                if (window.isReady()) {
                    window.copyWindow(input.getData(), 0);
                    String correct = csv.get(2); // trying to predict SSN.
                    MLData output = bestMethod.compute(input);
                    String predicted = helper
                            .denormalizeOutputVectorToString(output)[0];

                    result.append(Arrays.toString(line));
                    result.append(" -> predicted: ");
                    result.append(predicted);
                    result.append("(correct: ");
                    result.append(correct);
                    result.append(")");

                    System.out.println(result.toString());
                }

                window.add(slice);

                stopAfter--;
            }

输出将类似于以下截图:

图片

基于直方图的异常检测

在基于直方图的异常检测中,我们根据选定的时间窗口分割信号,如图所示。

对于每个窗口,我们计算直方图;也就是说,对于选定的桶数,我们计算每个桶中有多少值。直方图捕捉了选定时间窗口中值的分布情况,如图中所示。

直方图可以随后直接作为实例展示,其中每个桶对应一个属性。此外,我们可以通过应用降维技术,如主成分分析PCA),来减少属性的数量,这允许我们在图中可视化降维后的直方图,如图表右下角所示,其中每个点对应一个直方图。

在我们的例子中,想法是观察几天内的网站流量,然后创建直方图;例如,四小时的时间窗口,以建立一个积极行为的库。如果一个新时间窗口的直方图无法与积极库匹配,我们可以将其标记为异常:

图片

为了比较一个新直方图与一组现有直方图,我们将使用基于密度的 k 近邻算法,局部异常因子LOF)(Breunig 等,2000)。该算法能够处理具有不同密度的簇,如下面的图所示。例如,右上角的簇较大且分布广泛,与左下角的簇相比,后者较小且密度更高:

图片

让我们开始吧!

加载数据

在第一步,我们需要将数据从文本文件加载到 Java 对象中。这些文件存储在一个文件夹中,每个文件包含一个时间序列,每行一个值。我们将它们加载到一个Double列表中,如下所示:

String filePath = "chap07/ydata/A1Benchmark/real"; 
List<List<Double>> rawData = new ArrayList<List<Double>>(); 

我们需要minmax值来进行直方图归一化;因此,让我们在这个数据传递中收集它们:

double max = Double.MIN_VALUE; 
double min = Double.MAX_VALUE; 

for(int i = 1; i<= 67; i++){ 
  List<Double> sample = new ArrayList<Double>(); 
  BufferedReader reader = new BufferedReader(new 
     FileReader(filePath+i+".csv")); 

  boolean isAnomaly = false; 
  reader.readLine(); 
  while(reader.ready()){ 
    String line[] = reader.readLine().split(","); 
    double value = Double.parseDouble(line[1]); 
    sample.add(value); 

    max = Math.max(max, value); 
    min = Double.min(min, value); 

    if(line[2] == "1") 
      isAnomaly = true; 

  } 
  System.out.println(isAnomaly); 
  reader.close(); 

  rawData.add(sample); 
} 

数据已经加载。接下来,让我们继续到直方图部分。

创建直方图

我们将使用WIN_SIZE宽度创建一个选定时间窗口的直方图。

直方图将包含HIST_BINS值桶。由双列表组成的直方图将存储在数组列表中:

int WIN_SIZE = 500; 
int HIST_BINS = 20; 
int current = 0; 

List<double[]> dataHist = new ArrayList<double[]>(); 
for(List<Double> sample : rawData){ 
  double[] histogram = new double[HIST_BINS]; 
  for(double value : sample){ 
    int bin = toBin(normalize(value, min, max), HIST_BINS); 
    histogram[bin]++; 
    current++; 
    if(current == WIN_SIZE){ 
      current = 0; 
      dataHist.add(histogram); 
      histogram = new double[HIST_BINS]; 
    } 
  } 
  dataHist.add(histogram); 
} 

直方图现在已经完成。最后一步是将它们转换为 Weka 的Instance对象。每个直方图值将对应一个 Weka 属性,如下所示:

ArrayList<Attribute> attributes = new ArrayList<Attribute>(); 
for(int i = 0; i<HIST_BINS; i++){ 
  attributes.add(new Attribute("Hist-"+i)); 
} 
Instances dataset = new Instances("My dataset", attributes, 
   dataHist.size()); 
for(double[] histogram: dataHist){ 
  dataset.add(new Instance(1.0, histogram)); 
} 

数据集现在已经加载,并准备好插入到异常检测算法中。

基于密度的 k 近邻

为了演示 LOF 如何计算分数,我们将首先使用testCV(int, int)函数将数据集分为训练集和测试集。第一个参数指定了折数,而第二个参数指定了要返回的折:

// split data to train and test 
Instances trainData = dataset.testCV(2, 0); 
Instances testData = dataset.testCV(2, 1); 

LOF 算法不是 Weka 默认分布的一部分,但可以通过 Weka 的包管理器在weka.sourceforge.net/packageMetaData/localOutlierFactor/index.html下载。

LOF 算法有两个实现接口:作为一个无监督过滤器,计算 LOF 值(已知未知),以及作为一个有监督的 k 最近邻分类器(已知已知)。在我们的情况下,我们想要计算异常因子,因此我们将使用无监督过滤器接口:

import weka.filters.unsupervised.attribute.LOF; 

过滤器初始化的方式与常规过滤器相同。我们可以使用-min-max参数指定邻居的数量(例如,k=3)。LOF允许我们指定两个不同的k参数,这些参数在内部用作上限和下限,以找到最小或最大的lof值:

LOF lof = new LOF(); 
lof.setInputFormat(trainData); 
lof.setOptions(new String[]{"-min", "3", "-max", "3"}); 

接下来,我们将训练实例加载到作为正例库的过滤器中。加载完成后,我们将调用batchFinished()方法来初始化内部计算:

for(Instance inst : trainData){ 
  lof.input(inst); 
} 
lof.batchFinished(); 

最后,我们可以将过滤器应用于测试数据。Filter() 函数将处理实例并在末尾附加一个额外的属性,包含 LOF 分数。我们可以在控制台中简单地提供分数作为输出:

Instances testDataLofScore = Filter.useFilter(testData, lof); 

for(Instance inst : testDataLofScore){ 
  System.out.println(inst.value(inst.numAttributes()-1)); 
} 

前几个测试实例的 LOF 分数如下:

    1.306740014927325
    1.318239332210458
    1.0294812291949587
    1.1715039094530768

要理解 LOF 值,我们需要了解 LOF 算法的一些背景知识。它比较实例的密度与其最近邻的密度。这两个分数相除,产生 LOF 分数。大约为 1 的 LOF 分数表示密度大致相等,而更高的 LOF 值表示实例的密度显著低于其邻居的密度。在这种情况下,实例可以被标记为异常。

摘要

在本章中,我们探讨了检测异常和可疑模式。我们讨论了两种基本方法,重点关注库编码,即正模式或负模式。接下来,我们处理了两个真实数据集,并讨论了如何处理不平衡的类别分布以及如何在时间序列数据上执行异常检测。

在下一章中,我们将更深入地探讨模式以及更高级的基于模式构建分类器的方法,并讨论如何使用深度学习自动为图像分配标签。

第八章:使用 Deeplearning4j 进行图像识别

图像在 Web 服务、社交网络和在线商店中无处不在。与人类相比,计算机在理解图像及其所代表的内容方面有很大困难。在本章中,我们将首先探讨教会计算机如何理解图像的挑战,然后重点介绍基于深度学习的方法。我们将探讨配置深度学习模型所需的高级理论,并讨论如何使用 Java 库 Deeplearning4j 实现一个能够通过分类图像的模型。

本章将涵盖以下主题:

  • 介绍图像识别

  • 讨论深度学习基础

  • 构建图像识别模型

介绍图像识别

图像识别的一个典型目标是检测和识别数字图像中的对象。图像识别应用于工厂自动化以监控产品质量;监控系统以识别潜在风险活动,如移动的人或车辆;安全应用通过指纹、虹膜或面部特征提供生物识别;自动驾驶汽车以重建道路和环境条件;等等。

数字图像不是以基于属性的描述方式呈现的;相反,它们被编码为不同通道中的颜色量,例如,黑白和红绿蓝通道。学习目标是识别与特定对象相关的模式。图像识别的传统方法包括将图像转换为不同的形式,例如,识别物体角落、边缘、同色块和基本形状。然后使用这些模式来训练一个学习者区分对象。以下是一些传统算法的显著例子:

  • 边缘检测找到图像中对象的边界

  • 角点检测识别两条边缘或其他有趣点(如线端、曲率极大值或极小值等)的交点

  • 块检测识别与周围区域在属性(如亮度或颜色)上不同的区域

  • 岭谷检测使用平滑函数在图像中识别额外的有趣点

  • 尺度不变特征变换SIFT)是一种鲁棒的算法,即使对象的尺度或方向与数据库中的代表性样本不同,也能匹配对象

  • Hough 变换识别图像中的特定模式

一种更近期的方法是基于深度学习。深度学习是一种神经网络形式,它模仿大脑处理信息的方式。深度学习的主要优势在于可以设计出能够自动提取相关模式的神经网络,这些模式反过来又可以用来训练学习器。随着神经网络技术的最新进展,图像识别的准确性得到了显著提升。例如,ImageNet 挑战赛,其中参赛者获得了来自 1,000 个不同物体类别的超过 1,200 万张图片,报告称,最佳算法的错误率从 2010 年的 28%(使用支持向量机SVM))降低到了 2014 年的仅 7%(使用深度神经网络)。

在本章中,我们将快速浏览神经网络,从基本构建块感知器开始,逐渐引入更复杂的结构。

神经网络

最早期的神经网络,在六十年代被引入,其灵感来源于生物神经网络。神经网络的想法是映射生物神经系统,即大脑如何处理信息。它由相互连接的神经元层组成,共同工作。在计算机术语中,它们也被称为人工神经网络ANN)。使用计算机,需要训练来使这个模型学习,就像人类大脑一样。大脑中的神经元在接收到来自附近相互连接的神经元的信号时会被激活,对人工神经网络也是如此。神经网络技术的最新进展已经证明,深度神经网络非常适合模式识别任务,因为它们能够自动提取有趣的特征并学习其背后的表示。在本节中,我们将回顾从单个感知器到深度网络的基本结构和组件。

感知器

感知器是基本神经网络构建块之一,也是最早的监督算法之一。它被定义为特征的总和,这些特征乘以相应的权重和一个偏差。当接收到输入信号时,它将这些信号与分配的权重相乘。这些权重为每个传入的信号或输入定义,并在学习阶段持续调整。权重的调整取决于最后结果的误差。将所有输入与一些称为偏差的偏移值相乘后,所有输入都加在一起。偏差的值也由权重调整。因此,它从随机的权重和偏差开始,并在每次迭代中调整权重和偏差,以便下一个结果向期望的输出移动。最后,最终结果被转换成输出信号。将所有这些加在一起的功能称为求和传递函数,并将其输入到激活函数中。如果二元步激活函数达到阈值,则输出为 1,否则为 0,这为我们提供了一个二元分类器。以下图显示了示意图:

图片

训练感知器涉及一个相当简单的学习算法,该算法计算计算输出值与正确训练输出值之间的误差,并使用此误差来创建对权重的调整,从而实现一种梯度下降的形式。此算法通常称为delta 规则

单层感知器并不非常先进,无法使用它来模拟非线性可分函数,如 XOR。为了解决这个问题,引入了一种具有多个感知器的结构,称为多层感知器,也称为前馈神经网络

前馈神经网络

前馈神经网络是一种由多个感知器组成的 ANN,这些感知器被组织成层,如下面的图所示:输入层、输出层和一层或多层隐藏层。隐藏层与外界没有任何联系,因此得名。每个层感知器,也称为神经元,与下一层的感知器有直接连接,而两个神经元之间的连接则携带一个与感知器权重相似的权重。因此,同一层的所有感知器都与下一层的感知器相连,信息被正向传递到下一层。此图显示了一个具有四个单元的输入层的网络,对应于长度为4的特征向量大小,一个四个单元的隐藏层和一个两个单元的输出层,其中每个单元对应一个类别值:

图片

前馈神经网络通过寻找输入和输出值之间的关系来学习,这些值被多次输入到网络中。训练多层网络最流行的方法是反向传播。在反向传播中,计算出的输出值与相同方式下的正确值进行比较,就像在 delta 规则中一样。然后,通过各种技术将错误反馈到网络中,调整每个连接的权重,以减少错误的值。错误是通过网络输出值与原始输出值之间的平方差来计算的。错误表示我们离原始输出值的距离。这个过程在足够多的训练周期中重复进行,直到错误低于某个阈值。

前馈神经网络可以有多于一个的隐藏层,其中每个额外的隐藏层在先前的层之上构建一个新的抽象。这通常会导致更精确的模型;然而,增加隐藏层的数量会导致两个已知问题:

  • 梯度消失问题:随着隐藏层的增加,使用反向传播的训练越来越不适用于将信息传递到前层,导致这些层训练速度非常慢

  • 过拟合:模型对训练数据拟合得太好,在真实示例上的表现不佳

让我们看看一些其他网络结构,它们解决了这些问题。

自动编码器

自动编码器是一种前馈神经网络,旨在学习如何压缩原始数据集。其目标是复制输入到输出。因此,我们不会将特征映射到输入层,将标签映射到输出层,而是将特征映射到输入和输出层。隐藏层中的单元数量通常不同于输入层中的单元数量,这迫使网络增加或减少原始特征的数量。这样,网络将学习重要的特征,同时有效地应用降维。

下面的图中展示了网络的一个示例。三个单元的输入层首先扩展为四个单元的层,然后压缩为单个单元的层。网络的另一侧将单个层单元恢复到四个单元层,然后回到原始的三个输入层:

图片

一旦网络被训练,我们可以从左侧提取图像特征,就像使用传统的图像处理一样。它由编码器和解码器组成,其中编码器的工作是创建或隐藏一个或多个层,以捕捉输入的本质,而解码器则从这些层中重建输入。

自动编码器也可以组合成堆叠自动编码器,如下面的图所示。首先,我们将讨论基本自动编码器中的隐藏层,如之前所述。然后,我们将重复使用学习到的隐藏层(绿色圆圈)并重复该过程,这实际上学习了一个更抽象的表示。我们可以重复此过程多次,将原始特征转换为越来越低的维度。最后,我们将所有隐藏层堆叠成一个常规的前馈网络,如图表右上角所示:

限制性玻尔兹曼机

限制性玻尔兹曼机RBM)是一种无向神经网络,也称为生成随机网络GSNs),可以学习其输入集上的概率分布。正如其名所示,它们起源于 20 世纪 80 年代引入的玻尔兹曼机,这是一种循环神经网络。在玻尔兹曼机中,每个节点或神经元都与所有其他节点相连,这使得当节点数量增加时难以处理。限制性意味着神经元必须形成两个完全连接的层,一个输入层和一个隐藏层,如下面的图所示:

与前馈网络不同,可见层和隐藏层之间的连接是无向的,因此值可以在可见到隐藏和隐藏到可见的方向上传播。

RBMs 的训练基于对比散度算法,该算法使用梯度下降过程,类似于反向传播,来更新权重,并在马尔可夫链上应用吉布斯采样来估计梯度,即如何改变权重的方向。

RBMs 也可以堆叠起来,形成一个称为深度信念网络DBNs)的类别。在这种情况下,RBM 的隐藏层作为 RBM 层的可见层,如下面的图所示:

在这种情况下,训练是增量式的:层与层地训练。

深度卷积网络

最近在图像识别基准测试中取得非常好的结果的网络结构是卷积神经网络CNN)或 ConvNet。CNN 是一种结构化的前馈神经网络,其结构模仿了视觉皮层的功能,利用输入图像的 2D 结构,即表现出空间局部相关性的模式。它基于大脑如何回忆或记住图像的基本原理。作为人类,我们根据特征来记住图像。给定特征后,我们的大脑将开始形成图像本身。在计算机中,考虑以下图表,它显示了如何检测特征:

同样地,可以从图像中检测到许多特征,如下图所示:

CNN 由多个卷积和子采样层组成,可选地后面跟着全连接层。以下图示展示了这一示例。输入层读取图像中的所有像素,然后我们应用多个滤波器。在以下图中,应用了四个不同的滤波器。每个滤波器都应用于原始图像;例如,一个 6 x 6 滤波器的一个像素是通过计算 6 x 6 输入像素的加权总和以及相应的 6 x 6 权重来计算的。这有效地引入了类似于标准图像处理的滤波器,如平滑、相关性、边缘检测等。生成的图像称为特征图。在以下图的示例中,我们有四个特征图,每个滤波器一个。

下一个层是子采样层,它减小了输入的大小。每个特征图通常通过在连续区域(对于大图像可达 2 x 2 到 5 x 5)上的平均或最大池化进行子采样。例如,如果特征图大小为 16 x 16,子采样区域为 2 x 2,则减小后的特征图大小为 8 x 8,其中 4 个像素(一个 2 x 2 的正方形)通过计算最大值、最小值、平均值或其他函数合并成一个单独的像素:

网络可能包含几个连续的卷积和子采样层,如前图所示。特定的特征图连接到下一个减小/卷积的特征图,而同一层的特征图之间不相互连接。

在最后一个子采样或卷积层之后,通常有一个全连接层,与标准多层神经网络中的层相同,它表示目标数据。

CNN 的训练使用了一种修改后的反向传播算法,该算法考虑了子采样层,并根据该滤波器应用的所有值更新卷积滤波器权重。

一些好的 CNN 设计可以在 ImageNet 竞赛结果页面上找到:www.image-net.org/。一个例子是A. Krizhevsky 等人ImageNet 分类与深度卷积神经网络论文中描述的A. Krizhevsky

这就结束了我们对主要神经网络结构的回顾。在下一节中,我们将继续实际实现。

图像分类

在本节中,我们将讨论如何使用 Deeplearning4j 库实现一些神经网络结构。让我们开始吧。

Deeplearning4j

如我们在第二章“Java 机器学习库和平台”中讨论的那样,Deeplearning4j 是一个开源的、基于 Java 和 Scala 的分布式深度学习项目。Deeplearning4j 依赖于 Spark 和 Hadoop 进行 MapReduce,并行训练模型,并在中心模型中迭代平均它们产生的参数。详细的库总结在第二章“Java 机器学习库和平台”中给出。

获取 DL4J

获取 Deeplearning4j 最方便的方式是通过 Maven 仓库:

  1. 启动一个新的 Eclipse 项目,选择 Maven 项目,如下面的截图所示:

图片

  1. 打开pom.xml文件,并在<dependencies>部分添加以下依赖项:
<dependency> 
    <groupId>org.deeplearning4j</groupId> 
    <artifactId>deeplearning4j-nlp</artifactId> 
   <version>${dl4j.version}</version> 
</dependency> 

<dependency> 
    <groupId>org.deeplearning4j</groupId> 
    <artifactId>deeplearning4j-core</artifactId> 
    <version>${dl4j.version}</version> 
</dependency> 
  1. 最后,右键单击项目,选择 Maven,然后选择更新项目。

MNIST 数据集

最著名的数据库集之一是 MNIST 数据集,它由手写数字组成,如下面的图像所示。该数据集包括 60,000 个训练图像和 10,000 个测试图像:

图片

该数据集通常用于图像识别问题中的算法基准测试。记录的最坏错误率是 12%,没有预处理,在一个层神经网络中使用 SVM。目前,截至 2016 年,最低错误率仅为 0.21%,使用DropConnect神经网络,其次是深度卷积网络,错误率为 0.23%,以及深度前馈网络,错误率为 0.35%。

现在,让我们看看如何加载数据集。

加载数据

Deeplearning4j 自带 MNIST 数据集加载器。加载器初始化为DataSetIterator。首先,让我们导入DataSetIterator类以及impl包中所有支持的数据库集,例如 iris、MNIST 等:

import org.deeplearning4j.datasets.iterator.DataSetIterator; 
import org.deeplearning4j.datasets.iterator.impl.*; 

接下来,我们将定义一些常量,例如图像由 28 x 28 像素组成,有 10 个目标类别和 60,000 个样本。我们将初始化一个新的MnistDataSetIterator类,该类将下载数据集及其标签。参数是迭代批处理大小、示例总数以及数据集是否应该二值化:

int numRows = 28; 
int numColumns = 28; 
int outputNum = 10;
int numSamples = 60000;
int batchSize = 100;
int iterations = 10;
int seed = 123;
DataSetIterator iter = new MnistDataSetIterator(batchSize, 
numSamples,true);  

拥有一个已经实现的数据导入器非常方便,但它不会在你的数据上工作。让我们快速看一下它的实现方式以及需要修改什么以支持你的数据集。如果你急于开始实现神经网络,你可以安全地跳过本节的其余部分,并在需要导入自己的数据时返回。

要加载自定义数据,你需要实现两个类:DataSetIterator,它包含有关数据集的所有信息,以及BaseDataFetcher,它实际上从文件、数据库或网络中提取数据。示例实现可在 GitHub 上找到,地址为github.com/deeplearning4j/deeplearning4j/tree/master/deeplearning4j-core/src/main/java/org/deeplearning4j/datasets/iterator/impl

另一个选项是使用由同一作者开发的Canova库,该库的文档位于deeplearning4j.org/canovadoc/

构建模型

在本节中,我们将讨论如何构建实际的神经网络模型。我们将从一个基本的单层神经网络开始,以建立基准并讨论基本操作。稍后,我们将使用 DBN 和多层卷积网络来改进这个初始结果。

构建单层回归模型

让我们从基于 softmax 激活函数的单层回归模型开始构建,如下面的图所示。由于我们只有一个层,神经网络的输入将是所有图像像素,即 28 x 28 = 748个神经元。输出神经元的数量是10,每个数字一个。网络层是完全连接的,如下面的图所示:

图片

神经网络通过NeuralNetConfiguration.Builder()对象定义,如下所示:

MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder() 

我们将定义梯度搜索的参数,以便使用共轭梯度优化算法进行迭代。momentum参数决定了优化算法收敛到局部最优的速度。momentum值越高,训练速度越快;但过高的速度可能会降低模型的准确性:

.seed(seed) 
.gradientNormalization(GradientNormalization.ClipElementWiseAbsolu
   teValue) 
   .gradientNormalizationThreshold(1.0) 
   .iterations(iterations) 
   .momentum(0.5) 
   .momentumAfter(Collections.singletonMap(3, 0.9)) 
   .optimizationAlgo(OptimizationAlgorithm.CONJUGATE_GRADIENT) 

接下来,我们将指定网络有一层,并定义错误函数NEGATIVELOGLIKELIHOOD,内部感知器激活函数softmax以及与总输入和输出层相对应的数量。

图像像素和目标变量的数量,如下面的代码块所示:

.list(1) 
.layer(0, new  
OutputLayer.Builder(LossFunction.NEGATIVELOGLIKELIHOOD) 
.activation("softmax") 
.nIn(numRows*numColumns).nOut(outputNum).build()) 

最后,我们将网络设置为pretrain,禁用反向传播,并实际构建未训练的网络结构:

   .pretrain(true).backprop(false) 
   .build(); 

一旦定义了网络结构,我们就可以使用它来初始化一个新的MultiLayerNetwork,如下所示:

MultiLayerNetwork model = new MultiLayerNetwork(conf); 
model.init(); 

接下来,我们将通过调用setListeners方法将模型指向训练数据,如下所示:

model.setListeners(Collections.singletonList((IterationListener) 
   new ScoreIterationListener(listenerFreq))); 

我们还将调用fit(int)方法来触发端到端网络训练:

model.fit(iter);  

为了评估模型,我们将初始化一个新的Evaluation对象,该对象将存储批处理结果:

Evaluation eval = new Evaluation(outputNum); 

然后,我们可以按批处理迭代数据集,以保持合理的内存消耗并存储在eval对象中的结果:

DataSetIterator testIter = new MnistDataSetIterator(100,10000); 
while(testIter.hasNext()) { 
    DataSet testMnist = testIter.next(); 
    INDArray predict2 =  
    model.output(testMnist.getFeatureMatrix()); 
    eval.eval(testMnist.getLabels(), predict2); 
} 

最后,我们可以通过调用stats()函数来获取结果:

log.info(eval.stats()); 

一个基本的单层模型达到了以下准确率:

    Accuracy:  0.8945 
    Precision: 0.8985
    Recall:    0.8922
    F1 Score:  0.8953

在 MNIST 数据集上获得 89.22%的准确率,即 10.88%的错误率,相当糟糕。我们将通过从简单的一层网络到使用受限玻尔兹曼机和多层卷积网络的复杂深度信念网络来改进这一点。

构建深度信念网络

在本节中,我们将基于 RBM 构建一个深度信念网络(DBN),如下面的图所示。该网络由四层组成。第一层将748个输入减少到500个神经元,然后到250个,接着到200个,最后到最后的10个目标值:

图片

由于代码与上一个示例相同,让我们看看如何配置这样一个网络:

MultiLayerConfiguration conf = new 
   NeuralNetConfiguration.Builder() 

我们将定义梯度优化算法,如下面的代码所示:

    .seed(seed) 
    .gradientNormalization( 
    GradientNormalization.ClipElementWiseAbsoluteValue) 
    .gradientNormalizationThreshold(1.0) 
    .iterations(iterations) 
    .momentum(0.5) 
    .momentumAfter(Collections.singletonMap(3, 0.9)) 
    .optimizationAlgo(OptimizationAlgorithm.CONJUGATE_GRADIENT) 

我们还将指定我们的网络将有四层:

   .list(4) 

第一层的输入将是748个神经元,输出将是500个神经元。我们将使用均方根误差交叉熵,并使用 Xavier 算法初始化权重,该算法会自动根据输入和输出神经元的数量确定初始化的规模,如下所示:

.layer(0, new RBM.Builder() 
.nIn(numRows*numColumns) 
.nOut(500)          
.weightInit(WeightInit.XAVIER) 
.lossFunction(LossFunction.RMSE_XENT) 
.visibleUnit(RBM.VisibleUnit.BINARY) 
.hiddenUnit(RBM.HiddenUnit.BINARY) 
.build()) 

接下来的两层将具有相同的参数,除了输入和输出神经元的数量:

.layer(1, new RBM.Builder() 
.nIn(500) 
.nOut(250) 
.weightInit(WeightInit.XAVIER) 
.lossFunction(LossFunction.RMSE_XENT) 
.visibleUnit(RBM.VisibleUnit.BINARY) 
.hiddenUnit(RBM.HiddenUnit.BINARY) 
.build()) 
.layer(2, new RBM.Builder() 
.nIn(250) 
.nOut(200) 
.weightInit(WeightInit.XAVIER) 
.lossFunction(LossFunction.RMSE_XENT) 
.visibleUnit(RBM.VisibleUnit.BINARY) 
.hiddenUnit(RBM.HiddenUnit.BINARY) 
.build()) 

现在,最后一层将映射神经元到输出,我们将使用softmax激活函数,如下所示:

.layer(3, new OutputLayer.Builder() 
.nIn(200) 
.nOut(outputNum) 
.lossFunction(LossFunction.NEGATIVELOGLIKELIHOOD) 
.activation("softmax") 
.build()) 
.pretrain(true).backprop(false) 
.build(); 

剩余的训练和评估与单层网络示例相同。请注意,训练深度网络可能比单层网络花费的时间显著更多。准确率应约为 93%。

现在,让我们看看另一个深度网络。

构建多层卷积网络

在这个最后的例子中,我们将讨论如何构建一个卷积网络,如下面的图所示。该网络将包含七层。首先,我们将重复两对卷积和子采样层与最大池化。然后,最后一个子采样层连接到一个由 120 个神经元、84 个神经元和最后三层分别有 10 个神经元的密集连接前馈神经元网络。这样一个网络有效地形成了完整的图像识别流程,其中前四层对应于特征提取,最后三层对应于学习模型:

图片

网络配置初始化与我们之前所做的一样:

MultiLayerConfiguration.Builder conf = new 
   NeuralNetConfiguration.Builder() 

我们将指定梯度下降算法及其参数,如下所示:

.seed(seed) 
.iterations(iterations) 
.activation("sigmoid") 
.weightInit(WeightInit.DISTRIBUTION) 
.dist(new NormalDistribution(0.0, 0.01)) 
.learningRate(1e-3) 
.learningRateScoreBasedDecayRate(1e-1) 
.optimizationAlgo( 
OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT) 

我们还将指定七个网络层,如下所示:

.list(7) 

第一卷积层的输入是完整的图像,输出是六个特征图。卷积层将应用一个 5 x 5 的滤波器,结果将存储在一个 1 x 1 的单元中:

.layer(0, new ConvolutionLayer.Builder( 
    new int[]{5, 5}, new int[]{1, 1}) 
    .name("cnn1") 
    .nIn(numRows*numColumns) 
    .nOut(6) 
    .build()) 

第二层是一个子采样层,它将取一个 2 x 2 的区域,并将最大结果存储在一个 2 x 2 的元素中:

.layer(1, new SubsamplingLayer.Builder( 
SubsamplingLayer.PoolingType.MAX,  
new int[]{2, 2}, new int[]{2, 2}) 
.name("maxpool1") 
.build()) 

接下来的两层将重复前两层:

.layer(2, new ConvolutionLayer.Builder(new int[]{5, 5}, new 
   int[]{1, 1}) 
    .name("cnn2") 
    .nOut(16) 
    .biasInit(1) 
    .build()) 
.layer(3, new SubsamplingLayer.Builder
   (SubsamplingLayer.PoolingType.MAX, new 
   int[]{2, 2}, new int[]{2, 2}) 
    .name("maxpool2") 
    .build()) 

现在,我们将子采样层的输出连接到一个由120个神经元组成的密集前馈网络,然后通过另一个层连接到84个神经元,如下所示:

.layer(4, new DenseLayer.Builder() 
    .name("ffn1") 
    .nOut(120) 
    .build()) 
.layer(5, new DenseLayer.Builder() 
    .name("ffn2") 
    .nOut(84) 
    .build()) 

最后一层将84个神经元与10个输出神经元连接:

.layer(6, new OutputLayer.Builder
   (LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD) 
    .name("output") 
    .nOut(outputNum) 
    .activation("softmax") // radial basis function required 
    .build()) 
.backprop(true) 
.pretrain(false) 
.cnnInputSize(numRows,numColumns,1); 

为了训练这个结构,我们可以重用我们在前两个示例中开发的代码。再次强调,训练可能需要一些时间。网络的准确率应该在大约 98%左右。

由于模型训练在很大程度上依赖于线性代数,因此可以通过使用图形处理单元GPU)来显著加快训练速度。由于撰写本书时,GPU 后端正在进行重写,请查阅最新的文档deeplearning4j.org/documentation

正如我们在不同示例中看到的那样,越来越复杂的神经网络使我们能够自动提取相关特征,从而完全避免传统的图像处理。然而,我们为此付出的代价是处理时间的增加以及大量的学习示例来使这种方法有效。

摘要

在本章中,我们讨论了如何通过覆盖深度学习的基本原理并讨论如何使用 Deeplearning4j 库来实现它们,来识别图像中的模式以区分不同的类别。我们首先刷新了基本神经网络结构,并讨论了如何实现它们来解决手写数字识别问题。

在下一章中,我们将进一步探讨模式;然而,我们将处理具有时间依赖性的模式,这些模式可以在传感器数据中找到。

第九章:使用移动电话传感器进行活动识别

虽然上一章侧重于图像中的模式识别,但本章全部关于在传感器数据中识别模式,这与图像不同,具有时间依赖性。我们将讨论如何使用移动电话惯性传感器识别细粒度的日常活动,如行走、坐着和跑步。本章还提供了相关研究的参考文献,并强调了活动识别社区中的最佳实践。

本章将涵盖以下主题:

  • 介绍活动识别,涵盖移动电话传感器和活动识别流程

  • 从移动设备收集传感器数据

  • 讨论活动分类和模型评估

  • 部署活动识别模型

介绍活动识别

活动识别是行为分析的基础步骤,涉及健康生活方式、健身追踪、远程协助、安全应用、老年护理等。活动识别将来自加速度计、陀螺仪、压力传感器和 GPS 位置等传感器的低级传感器数据转换为对行为原语的高级描述。

在大多数情况下,这些是基本活动,例如以下图中所示的行走、坐着、躺着、跳跃等,或者它们可能是更复杂的行为,如去上班、准备早餐和购物等:

图片

在本章中,我们将讨论如何将活动识别功能添加到移动应用程序中。我们首先将探讨活动识别问题是什么样的,我们需要收集哪些类型的数据,主要挑战是什么,以及如何解决这些问题。

之后,我们将通过一个示例来了解如何在 Android 应用程序中实际实现活动识别,包括数据收集、数据转换和构建分类器。

让我们开始吧!

移动电话传感器

让我们先回顾一下有哪些类型的移动电话传感器以及它们报告的内容。现在大多数智能设备都配备了几个内置传感器,这些传感器可以测量运动、位置、朝向和环境条件。由于传感器提供高精度、频率和准确性的测量,因此可以重建复杂用户运动、手势和动作。传感器常被集成到各种应用中;例如,陀螺仪读数用于在游戏中控制物体,GPS 数据用于定位用户,加速度计数据用于推断用户正在执行的活动,例如骑自行车、跑步或行走。

下图展示了传感器能够检测到的几种交互类型示例:

图片

移动电话传感器可以分为以下三个广泛的类别:

  • 运动传感器:此传感器测量沿三个垂直轴的加速度和旋转力。此类传感器包括加速度计、重力传感器和陀螺仪。

  • 环境传感器:此传感器测量各种环境参数,如光照、空气温度、压力和湿度。此类包括气压计、光度计和温度计。

  • 位置传感器:此传感器测量设备的物理位置。此类包括方向传感器和磁力计。

不同移动平台的更详细描述可在以下链接中找到:

在本章中,我们将仅使用 Android 的传感器框架。

活动识别流程

与我们在前几章中看到的不同,对多维时间序列传感器数据进行分类本质上比分类传统名义数据更复杂。首先,每个观测值在时间上都与前一个和后一个观测值相关联,这使得仅对一组观测值进行直接分类变得非常困难。其次,传感器在不同时间点获得的数据是随机的,即由于传感器噪声、环境干扰等因素的影响,是不可预测的。此外,一项活动可以由以不同方式执行的各种子活动组成,每个人执行活动的方式也略有不同,这导致类内差异很大。最后,所有这些原因使得活动识别模型不够精确,导致新数据经常被错误分类。活动识别分类器的一个高度期望的特性是确保识别的活动序列的连续性和一致性。

为了应对这些挑战,活动识别被应用于以下流程,如图所示:

图片

在第一步,我们尽可能地减少噪声,例如,通过降低传感器采样率、移除异常值、应用高通或低通滤波器等。在下一阶段,我们构建一个特征向量。例如,我们通过应用离散傅里叶变换DFT)将传感器数据从时域转换为频域。DFT 是一种将一系列样本作为输入并返回按频率顺序排列的正弦系数列表的方法。它们代表了原始样本列表中存在的频率组合。

Pete Bevelacqua 在 www.thefouriertransform.com/ 写了一篇关于傅里叶变换的温和介绍。如果你想要获取关于傅里叶变换的更技术性和理论性的背景知识,可以查看罗伯特·加勒格尔和郑丽中在麻省理工学院开放课程中的第八和第九讲:theopenacademy.com/content/principles-digital-communication

接下来,基于特征向量和训练数据集,我们可以构建一个活动识别模型,将原子动作分配给每个观察结果。因此,对于每个新的传感器读数,模型将输出最可能的动作标签。然而,模型会犯错误。因此,最后一个阶段通过移除在现实中不可能发生的转换来平滑活动之间的过渡;例如,活动从躺到站再到躺的转换在不到半秒内发生在物理上是不可能的,因此这种活动之间的转换被平滑为躺-躺-躺。

活动识别模型是通过监督学习方法构建的,包括训练和分类步骤。在训练步骤中,提供一组标记数据来训练模型。第二步是使用训练好的模型为新未见数据分配标签。两个阶段中的数据都必须使用相同的工具进行预处理,例如过滤和特征向量计算。

后处理阶段,即虚假活动的移除,也可以是一个模型本身,因此也需要一个学习步骤。在这种情况下,预处理步骤也包括活动识别,这使得这样的分类器排列成为一个元学习问题。为了避免过拟合,重要的是用于训练后处理阶段的训练数据集不能与用于训练活动识别模型的训练数据集相同。

计划

计划包括一个训练阶段和一个部署阶段。训练阶段可以归结为以下步骤:

  1. 安装 Android Studio 并导入 MyRunsDataCollector.zip

  2. 在你的 Android 手机上加载应用程序。

  3. 收集您的数据,例如站立、行走和跑步,并将数据转换为包含 FFT 的特征向量。不要慌张;我们不会从头开始编写低级信号处理函数,如 FFT,我们将使用现有的代码来完成这项工作。数据将被保存在您的手机上,文件名为 features.arff

  4. 使用导出的数据创建并评估一个活动识别分类器,并实现一个用于去除虚假活动转换的过滤器。

  5. 将分类器重新连接到移动应用程序。

如果您没有 Android 手机,或者想跳过所有与移动应用程序相关的步骤,只需获取位于 data/features.arff 中的收集数据集,然后直接跳转到“构建分类器”部分。

从手机收集数据

本节描述了计划中的前三个步骤。如果您想直接处理数据,可以跳过本节,继续到“构建分类器”部分。该应用程序实现了收集不同活动类(例如站立、行走、跑步等)传感器数据的必要功能。

让我们从准备 Android 开发环境开始。如果您已经安装了它,请跳转到“加载数据收集器”部分。

安装 Android Studio

Android Studio 是 Android 平台的开发环境。我们将快速回顾启动手机上应用程序所需的安装步骤和基本配置。如果您想了解更多关于 Android 开发的信息,我推荐阅读 Kyle Mew 编著的 Packt Publishing 出版的入门书籍《Android 5 编程实例》。

developer.android.com/studio/ 为开发者获取最新的 Android Studio,并按照 developer.android.com/sdk/installing/index.html?pkg=studio 中的安装说明进行操作。安装大约需要 10 分钟,大约占用 0.5 GB 的空间。

按照说明操作,选择您想要的安装选项,最后点击完成以开始安装,如下面的截图所示:

图片

加载数据收集器

首先,从 GitHub 获取 MyRunsDataCollector 的源代码。一旦安装了 Android Studio,选择“打开现有的 Android Studio 项目”选项,如下面的截图所示,并选择 MyRunsDataCollector 文件夹。这将把项目导入到 Android Studio 中:

图片

项目导入完成后,您应该能够看到项目文件结构,如下面的截图所示。收集器包括 CollectorActivity.javaGlobals.javaSensorsService.java。项目还显示了实现低级信号处理的 FFT.java

图片

myrunscollector主包包含以下类:

  • Globals.java:这个类定义了全局常量,例如活动标签和 ID,以及数据文件名。

  • CollectorActivity.java:这个类实现了用户界面动作,即当按下特定按钮时会发生什么。

  • SensorsService.java:这个类实现了一个收集数据、计算特征向量(我们将在以下章节中讨论)并将数据存储到手机上的文件中的服务。

我们接下来要解决的问题是如何设计特征。

特征提取

找到一个人活动适当的表现形式可能是活动识别中最具挑战性的部分。行为需要用简单和通用的特征来表示,这样使用这些特征的模型也将是通用的,并且在不同行为上也能很好地工作。

实际上,设计针对训练集中捕获的观察特定特征并不困难;这些特征在它们身上会工作得很好。然而,由于训练集仅捕获人类行为范围的一部分,过于特定的特征可能在一般行为上失败:

图片

让我们看看这在MyRunsDataCollector中的实现方式。当应用程序启动时,一个名为onSensorChanged()的方法会获取一个包含加速度计传感器读数的三元组(xyz)以及特定的时戳,并从传感器读数中计算振幅。方法会缓冲最多 64 个连续的振幅,在计算 FFT 系数之前标记它们。

现在,让我们继续实际数据收集。

收集训练数据

我们现在可以使用收集器来收集活动识别的训练数据。收集器默认支持三种活动:站立、行走和跑步,如下面的截图所示。

您可以选择一个活动,即目标类值,然后通过点击“开始收集”按钮开始记录数据。确保每个活动至少记录三分钟;例如,如果选择了行走活动,请按“开始收集”并至少行走三分钟。活动结束后,请按“停止收集”。对每个活动重复此操作。

你还可以收集涉及这些活动的不同场景,例如,在厨房里走动、在外面走动、成列行走等。通过这样做,你将为每个活动类别拥有更多数据,并且分类器会更好。有道理,对吧?数据越多,分类器就越不会困惑。如果你只有少量数据,就会发生过拟合,分类器会混淆类别——站立与行走、行走与跑步等。然而,数据越多,它们就越不容易混淆。在调试时,你可能每个类别收集不到三分钟的数据,但对你最终的产品来说,数据越多越好。多个录制实例将简单地累积在同一个文件中。

注意,删除数据按钮会删除存储在手机文件上的数据。如果你想重新开始,请在开始之前点击删除数据;否则,新收集的数据将被附加到文件末尾:

图片

收集器实现了前几节中讨论的图:它收集加速度计样本,计算幅度,使用FFT.java类计算系数,并生成特征向量。然后,数据存储在 Weka 格式的features.arff文件中。特征向量的数量将根据你收集的数据量而变化。你收集数据的时间越长,累积的特征向量就越多。

一旦你停止使用收集工具收集训练数据,我们需要抓取数据以继续工作流程。我们可以使用 Android 设备监控器中的文件浏览器上传手机上的features.arff文件并将其存储在计算机上。你可以通过点击以下截图中的 Android 机器人图标来访问你的 Android 设备监控器:

图片

通过在左侧选择你的设备,你可以在右侧看到你的手机存储内容。导航到mnt/shell/emulated/Android/data/edu.dartmouth.cs.myrunscollector/files/features.arff,如下截图所示:

图片

要将此文件上传到你的计算机,你需要选择文件(它被突出显示)并点击上传。

现在,我们准备构建一个分类器。

构建分类器

一旦将传感器样本表示为特征向量并分配了类别,就可以应用标准的有监督分类技术,包括特征选择、特征离散化、模型学习、k-折交叉验证等。本章不会深入探讨机器学习算法的细节。任何支持数值特征的算法都可以应用,包括 SVMs、随机森林、AdaBoost、决策树、神经网络、多层感知器等。

因此,让我们从一个基本的开始:决策树。在这里,我们将加载数据集,构建类属性集,构建决策树模型,并输出模型:

String databasePath = "/Users/bostjan/Dropbox/ML Java Book/book/datasets/chap9/features.arff"; 

// Load the data in arff format 
Instances data = new Instances(new BufferedReader(new 
   FileReader(databasePath))); 

// Set class the last attribute as class 
data.setClassIndex(data.numAttributes() - 1); 

// Build a basic decision tree model 
String[] options = new String[]{}; 
J48 model = new J48(); 
model.setOptions(options); 
model.buildClassifier(data); 

// Output decision tree 
System.out.println("Decision tree model:\n"+model); 

算法首先输出模型,如下所示:

    Decision tree model:
    J48 pruned tree
    ------------------

    max <= 10.353474
    |   fft_coef_0000 <= 38.193106: standing (46.0)
    |   fft_coef_0000 > 38.193106
    |   |   fft_coef_0012 <= 1.817792: walking (77.0/1.0)
    |   |   fft_coef_0012 > 1.817792
    |   |   |   max <= 4.573082: running (4.0/1.0)
    |   |   |   max > 4.573082: walking (24.0/2.0)
    max > 10.353474: running (93.0)

    Number of Leaves  : 5

    Size of the tree : 9

树的结构相当简单且看似准确,因为终端节点中的多数类分布相当高。让我们运行一个基本的分类器评估来验证结果,如下所示:

// Check accuracy of model using 10-fold cross-validation 
Evaluation eval = new Evaluation(data); 
eval.crossValidateModel(model, data, 10, new Random(1), new 
   String[] {}); 
System.out.println("Model performance:\n"+ 
   eval.toSummaryString()); 

这将输出以下模型性能:

    Correctly Classified Instances         226               92.623  %
    Incorrectly Classified Instances        18                7.377  %
    Kappa statistic                          0.8839
    Mean absolute error                      0.0421
    Root mean squared error                  0.1897
    Relative absolute error                 13.1828 %
    Root relative squared error             47.519  %
    Coverage of cases (0.95 level)          93.0328 %
    Mean rel. region size (0.95 level)      27.8689 %
    Total Number of Instances              244     

分类准确率得分非常高,92.62%,这是一个惊人的结果。结果之所以如此之好,一个重要原因在于我们的评估设计。我的意思是以下内容:序列实例彼此非常相似,因此如果我们在一个 10 折交叉验证过程中随机分割它们,那么我们使用几乎相同的实例进行训练和测试的可能性很高;因此,直接的 k 折交叉验证会产生对模型性能的乐观估计。

一个更好的方法是使用与不同测量集或甚至不同人员相对应的折数。例如,我们可以使用该应用程序从五个人那里收集学习数据。然后,进行 k 个人交叉验证是有意义的,其中模型在四个人身上训练,在第五个人身上测试。对于每个人重复此过程,并将结果平均。这将给我们提供一个更现实的模型性能估计。

不考虑评估注释,让我们看看如何处理分类错误。

减少虚假转换

在活动识别管道的末尾,我们想要确保分类不是太波动,也就是说,我们不希望活动每毫秒都改变。一个基本的方法是设计一个过滤器,它忽略活动序列中的快速变化。

我们构建一个过滤器,它记住最后一个窗口活动并返回最频繁的一个。如果有多个活动具有相同的分数,它返回最近的一个。

首先,我们创建一个新的SpuriousActivityRemoval类,它将包含活动列表和window参数:

class SpuriousActivityRemoval{ 

  List<Object> last; 
  int window; 

  public SpuriousActivityRemoval(int window){ 
    this.last = new ArrayList<Object>(); 
    this.window = window; 
  } 

接下来,我们创建Object filter(Object)方法,该方法将接受一个活动并返回一个过滤后的活动。该方法首先检查我们是否有足够的观察结果。如果没有,它简单地存储观察结果并返回相同的值,如下面的代码所示:

  public Object filter(Object obj){ 
    if(last.size() < window){ 
      last.add(obj); 
      return obj; 
  } 

如果我们已收集了window观察结果,我们只需返回最频繁的观察结果,删除最老的观察结果,并插入新的观察结果:

    Object o = getMostFrequentElement(last); 
    last.add(obj); 
    last.remove(0); 
    return o; 
  } 

这里缺少的是一个从对象列表中返回最频繁元素的函数。我们使用哈希映射来实现这一点,如下所示:

  private Object getMostFrequentElement(List<Object> list){ 

    HashMap<String, Integer> objectCounts = new HashMap<String, 
       Integer>(); 
    Integer frequntCount = 0; 
    Object frequentObject = null; 

现在,我们遍历列表中的所有元素,将每个唯一元素插入到哈希映射中,或者如果它已经在哈希映射中,则更新其计数器。循环结束时,我们存储迄今为止找到的最频繁元素,如下所示:

    for(Object obj : list){ 
      String key = obj.toString(); 
      Integer count = objectCounts.get(key); 
      if(count == null){ 
        count = 0; 
      } 
      objectCounts.put(key, ++count); 

      if(count >= frequntCount){ 
        frequntCount = count; 
        frequentObject = obj; 
      } 
    } 

    return frequentObject; 
  } 

} 

让我们运行一个简单的例子:

String[] activities = new String[]{"Walk", "Walk", "Walk", "Run", 
   "Walk", "Run", "Run", "Sit", "Sit", "Sit"}; 
SpuriousActivityRemoval dlpFilter = new 
   SpuriousActivityRemoval(3); 
for(String str : activities){ 
  System.out.println(str +" -> "+ dlpFilter.filter(str)); 
} 

示例输出了以下活动:

    Walk -> Walk
    Walk -> Walk
    Walk -> Walk
    Run -> Walk
    Walk -> Walk
    Run -> Walk
    Run -> Run
    Sit -> Run
    Sit -> Run
    Sit -> Sit

结果是一个连续的活动序列,也就是说,我们没有快速的变化。这增加了一些延迟,但除非这对应用程序至关重要,否则是可以接受的。

通过将分类器识别的前n个活动附加到特征向量中,可以增强活动识别。将先前活动附加的危险是,机器学习算法可能会学习到当前活动总是与先前活动相同,因为这种情况通常会发生。可以通过拥有两个分类器 A 和 B 来解决这个问题:分类器 B 的属性向量包含由分类器 A 识别的n个先前活动。分类器 A 的属性向量不包含任何先前活动。这样,即使 B 对先前活动给予了很大的权重,由 A 识别的先前活动也会随着 A 不受 B 惯性的影响而改变。

剩下的工作是将分类器和过滤器嵌入到我们的移动应用程序中。

将分类器插入到移动应用中

有两种方法可以将分类器集成到移动应用程序中。第一种方法涉及使用 Weka 库将模型导出为 Weka 格式,将 Weka 库作为我们的移动应用程序的依赖项,加载模型等。该过程与我们在第三章中看到的示例相同,即基本算法-分类、回归和聚类。第二种方法更轻量级:我们将模型导出为源代码,例如,我们创建一个实现决策树分类器的类。然后,我们可以简单地复制并粘贴源代码到我们的移动应用中,甚至不需要导入任何 Weka 依赖项。

幸运的是,一些 Weka 模型可以通过toSource(String)函数轻松导出为源代码:

// Output source code implementing the decision tree 
System.out.println("Source code:\n" +  
  model.toSource("ActivityRecognitionEngine")); 

这将输出一个与我们的模型相对应的ActivityRecognitionEngine类。现在,让我们更仔细地看看输出代码:

class ActivityRecognitionEngine { 

  public static double classify(Object[] i) 
    throws Exception { 

    double p = Double.NaN; 
    p = ActivityRecognitionEngine.N17a7cec20(i); 
    return p; 
  } 
  static double N17a7cec20(Object []i) { 
    double p = Double.NaN; 
    if (i[64] == null) { 
      p = 1; 
    } else if (((Double) i[64]).doubleValue() <= 10.353474) { 
    p = ActivityRecognitionEngine.N65b3120a1(i); 
    } else if (((Double) i[64]).doubleValue() > 10.353474) { 
      p = 2; 
    }  
    return p; 
  } 
... 

输出的ActivityRecognitionEngine类实现了我们之前讨论的决策树。机器生成的函数名,如N17a7cec20(Object []),对应于决策树节点。可以通过classify(Object[])方法调用分类器,其中我们应该传递通过与之前章节中讨论的相同程序获得的特征向量。像往常一样,它返回一个double值,表示类标签索引。

摘要

在本章中,我们讨论了如何为移动应用程序实现活动识别模型。我们探讨了整个流程,包括数据收集、特征提取、模型构建、评估和模型部署。

在下一章中,我们将继续介绍另一个针对文本分析的 Java 库:Mallet。