Java 机器学习(二)
四、集成的客户关系预测
提供服务、产品或体验的任何类型的公司都需要对他们与客户的关系有充分的了解;因此,客户关系管理 ( CRM )是现代营销策略的关键要素。企业面临的最大挑战之一是需要准确理解是什么导致客户购买新产品。
在本章中,我们将使用由法国电信公司 Orange 提供的真实世界营销数据库。该任务将评估以下客户行为的可能性:
- 交换机提供商(流失)
- 购买新产品或服务(欲望)
- 购买向他们建议的升级或附加产品,以提高销售利润(追加销售)
我们将参加 2009 年知识发现和数据挖掘 ( KDD )杯挑战赛,并展示使用 Weka 处理数据的步骤。首先,我们将解析和加载数据,并实现基本的基线模型。稍后,我们将讨论高级建模技术,包括数据预处理、属性选择、模型选择和评估。
The KDD Cup is the leading data mining competition in the world. It is organized annually by the ACM Special Interest Group on Knowledge Discovery and Data Mining. The winners are announced at the Conference on Knowledge Discovery and Data Mining, which is usually held in August. Yearly archives, including all of the corresponding datasets, are available at www.kdd.org/kdd-cup.
客户关系数据库
建立客户行为知识的最实用的方法是产生解释目标变量的分数,如客户流失、亲和力或追加销售。分数是由模型使用描述客户的输入变量计算的;例如,他们当前的订阅、购买的设备、消耗的分钟数等等。这些分数随后被信息系统用于提供相关的个性化营销活动。
在大多数基于客户的关系数据库中,客户是主要实体;了解顾客的行为很重要。客户的行为会产生一个与客户流失、欲望或追加销售相关的分数。基本思想是使用计算模型产生分数,该计算模型可以使用不同的参数,例如客户的当前订购、购买的设备、消耗的分钟数等等。分数一旦形成,信息系统就根据他或她的行为决定下一步的策略,这是专门为顾客设计的。
2009 年,KDD 会议组织了一次关于客户关系预测的机器学习挑战。
挑战
给定一大组客户属性,挑战中的任务是估计以下目标变量:
- 流失概率:这是客户更换提供商的可能性。流失率也称为损耗率或参与者周转率,是一种用于计算在给定时间段内进出给定集合的个人、对象、术语或项目数量的方法。该术语在由客户驱动并使用基于订户的模型的行业中大量使用;例如,手机行业和有线电视运营商。
- 欲望概率:这是购买服务或产品的倾向。
- 追加销售概率:这是客户购买附加产品或升级产品的可能性。追加销售意味着销售客户已经使用的产品之外的产品。就像大多数手机运营商提供的增值服务一样。使用销售技巧,销售人员试图让客户选择增值服务,这将带来更多的收入。很多时候,客户不知道还有其他选择,销售人员说服他们使用或考虑这些选择。
挑战在于击败 Orange Labs 开发的内部系统。对于参与者来说,这是一个证明他们能够处理大型数据库的机会,包括异构的、有噪声的数据和不平衡的类分布。
资料组
为了应对这一挑战,Orange 发布了一个大型客户数据数据集,包含大约 100 万个客户,在十个包含数百个字段的表中进行描述。第一步,他们对数据进行了重新采样,以选择一个不太不平衡的子集,其中包含 100,000 名客户。在第二步中,他们使用了一个自动特征构造工具,该工具生成了 20,000 个描述客户的特征,然后这些特征被缩小到 15,000 个。第三步,通过随机化要素顺序、丢弃属性名称、用随机生成的字符串替换名义变量以及将连续属性乘以随机因子,对数据集进行匿名化。最后,所有实例被随机分成训练和测试数据集。
KDD 杯提供了两组数据,一组大数据和一组小数据,分别对应于快挑战和慢挑战。训练集和测试集都包含 50,000 个示例,数据的划分类似,但是每个集的样本排序不同。
在本章中,我们将使用由 50,000 个实例组成的小型数据集,每个实例都用 230 个变量来描述。50,000 行数据中的每一行都对应于一个客户,它们与三个二元结果相关联,三个挑战(向上销售、流失和欲望)中的每一个都有一个。
为了使这一点更清楚,下表说明了数据集:
该表描述了前 25 个实例,即客户,每个用 250 个属性描述。对于本例,只显示了 10 个属性的选定子集。数据集包含许多缺失值,甚至空属性或常量属性。表格的最后三列对应于涉及基本事实的三个不同的类别标签,即客户是否确实更换了提供商(流失)、购买了服务(欲望)或购买了升级(追加销售)。但是,请注意,标签是与三个不同文件中的数据分开提供的,因此保留实例和相应类标签的顺序以确保正确对应是非常重要的。
估价
根据三项任务(流失、亲和力和追加销售)的 ROC 曲线下面积的算术平均值对提交的内容进行评估。ROC 曲线将模型的性能显示为一条曲线,该曲线通过绘制用于确定分类结果的各种阈值的灵敏度对特异性而获得(参见第一章、应用机器学习快速入门、章节 ROC 曲线)。现在,ROC 曲线 ( AUC )下的面积与该曲线下的面积相关——面积越大,分类器越好)。包括 Weka 在内的大多数工具箱都提供了计算 AUC 分数的 API。
基本朴素贝叶斯分类器基线
根据挑战赛的规则,参与者必须超越基本的朴素贝叶斯分类器才能有资格获得奖励,它假设特征是独立的(参见第一章、应用机器学习快速入门)。
KDD 杯组织者运行了香草朴素贝叶斯分类器,没有任何特征选择或超参数调整。对于大型数据集,朴素贝叶斯在测试集上的总体得分如下:
- 流失问题 : AUC = 0.6468
- 亲和力问题 : AUC = 0.6453
- 追加销售问题 : AUC=0.7211
请注意,基线结果仅针对大型数据集进行报告。此外,虽然训练和测试数据集都在 KDD 杯网站上提供,但测试集的实际真实标签并未提供。因此,当我们用模型处理数据时,没有办法知道模型在测试集上的表现如何。我们要做的只是使用训练数据,并通过交叉验证来评估我们的模型。结果不会是直接可比的,但是尽管如此,我们将对 AUC 评分的合理幅度有所了解。
获取数据
在 KDD 杯的网页上(kdd.org/kdd-cup/view/kdd-cup-2009/Data,你应该会看到一个类似下面截图的页面。一、下小版(230 var。)头,下载orange_small_train.data.zip。接下来,下载与该训练数据相关联的三组真实标签。以下文件位于 Real binary targets (small)标题下:
orange_small_train_appentency.labelsorange_small_train_churn.labelsorange_small_train_upselling.labels
保存并解压缩红框中标记的所有文件,如屏幕截图所示:
在接下来的部分中,首先,我们将数据加载到 Weka 中,并用朴素贝叶斯分类器应用基本建模,以获得我们自己的基线 AUC 分数。稍后,我们将研究更高级的建模技术和技巧。
加载数据
我们将直接从.csv格式加载数据到 Weka。为此,我们将编写一个函数来接受数据文件和真实标签文件的路径。该函数将加载并合并两个数据集,并移除空属性。我们将从下面的代码块开始:
public static Instances loadData(String pathData, String
pathLabeles) throws Exception {
首先,我们使用CSVLoader()类加载数据。此外,我们将\t选项卡指定为字段分隔符,并强制将最后 40 个属性解析为名义属性:
// Load data
CSVLoader loader = new CSVLoader();
loader.setFieldSeparator("\t");
loader.setNominalAttributes("191-last");
loader.setSource(new File(pathData));
Instances data = loader.getDataSet();
CSVLoader类接受许多附加参数,指定列分隔符、字符串包围符、是否存在标题行等等。完整的文档可在WEKA . SourceForge . net/doc . dev/WEKA/core/converters/CSV loader . html获得。
有些属性不包含单个值,Weka 会自动将它们识别为String属性。我们实际上并不需要它们,所以我们可以使用RemoveType过滤器安全地删除它们。此外,我们指定了-T参数,它删除了一个特定类型的属性,并指定了我们想要删除的属性类型:
// remove empty attributes identified as String attribute
RemoveType removeString = new RemoveType();
removeString.setOptions(new String[]{"-T", "string"});
removeString.setInputFormat(data);
Instances filteredData = Filter.useFilter(data, removeString);
或者,我们可以使用在Instances类中实现的void deleteStringAttributes()方法,它具有相同的效果;例如,data.removeStringAttributes()。
现在,我们将加载数据并为其分配类别标签。我们将再次使用CVSLoader,这里我们指定文件没有任何标题行,即setNoHeaderRowPresent(true):
// Load labeles
loader = new CSVLoader();
loader.setFieldSeparator("\t");
loader.setNoHeaderRowPresent(true);
loader.setNominalAttributes("first-last");
loader.setSource(new File(pathLabeles));
Instances labels = loader.getDataSet();
一旦我们加载了两个文件,我们就可以通过调用Instances.mergeInstances (Instances, Instances)静态方法将它们合并在一起。该方法返回一个新的数据集,该数据集包含第一个数据集中的所有属性,以及第二个数据集中的属性。请注意,两个数据集中的实例数量必须相同:
// Append label as class value
Instances labeledData = Instances.mergeInstances(filteredData,
labeles);
最后,我们设置最后一个属性,也就是我们刚刚添加的标签属性,作为目标变量,并返回结果数据集:
// set the label attribute as class
labeledData.setClassIndex(labeledData.numAttributes() - 1);
System.out.println(labeledData.toSummaryString());
return labeledData;
}
该函数提供一个摘要作为输出,如下面的代码块所示,并返回带标签的数据集:
Relation Name: orange_small_train.data-weka.filters.unsupervised.attribute.RemoveType-Tstring_orange_small_train_churn.labels.txt
Num Instances: 50000
Num Attributes: 215
Name Type Nom Int Real Missing Unique Dist
1 Var1 Num 0% 1% 0% 49298 / 99% 8 / 0% 18
2 Var2 Num 0% 2% 0% 48759 / 98% 1 / 0% 2
3 Var3 Num 0% 2% 0% 48760 / 98% 104 / 0% 146
4 Var4 Num 0% 3% 0% 48421 / 97% 1 / 0% 4
...
基本建模
在这一部分,我们将按照 KDD 杯组织者采用的方法来实现我们自己的基线模型。然而,在我们进入模型之前,让我们首先实现评估引擎,它将返回所有三个问题的 AUC。
评估模型
现在,让我们仔细看看评估函数。评估函数接受一个初始化的模型,在所有三个问题上交叉验证该模型,并以 ROC 曲线下面积(AUC)的形式报告结果,如下所示:
public static double[] evaluate(Classifier model)
throws Exception {
double results[] = new double[4];
String[] labelFiles = new String[]{
"churn", "appetency", "upselling"};
double overallScore = 0.0;
for (int i = 0; i < labelFiles.length; i++) {
首先,我们调用前面实现的Instance loadData(String, String)函数来加载训练数据,并将其与选定的标签合并:
// Load data
Instances train_data = loadData(
path + "orange_small_train.data",
path+"orange_small_train_"+labelFiles[i]+".labels.txt");
接下来,我们初始化weka.classifiers.Evaluation类并传递我们的数据集。(数据集仅用于提取数据属性;不考虑实际数据。)我们调用void crossValidateModel(Classifier, Instances, int, Random)方法开始交叉验证,我们创建了五个折叠。因为验证是在数据的随机子集上进行的,所以我们也需要传递一个随机种子:
// cross-validate the data
Evaluation eval = new Evaluation(train_data);
eval.crossValidateModel(model, train_data, 5,
new Random(1));
评估完成后,我们通过调用double areUnderROC(int)方法读取结果。因为度量取决于我们感兴趣的目标值,所以该方法需要一个类值索引,可以通过搜索 class 属性中的"1"值的索引来提取该索引,如下所示:
// Save results
results[i] = eval.areaUnderROC(
train_data.classAttribute().indexOfValue("1"));
overallScore += results[i];
}
最后,计算结果的平均值并返回:
// Get average results over all three problems
results[3] = overallScore / 3;
return results;
}
实现朴素贝叶斯基线
现在,当我们有了所有的成分,我们可以复制朴素贝叶斯方法,我们有望超越它。这种方法不包括任何额外的数据预处理、属性选择或模型选择。由于我们没有测试数据的真实标签,我们将应用五重交叉验证来评估小数据集上的模型。
首先,我们初始化一个朴素贝叶斯分类器,如下:
Classifier baselineNB = new NaiveBayes();
接下来,我们将分类器传递给我们的评估函数,该函数加载数据并应用交叉验证。该函数返回所有三个问题的 ROC 曲线得分下的面积,以及总体结果:
double resNB[] = evaluate(baselineNB);
System.out.println("Naive Bayes\n" +
"\tchurn: " + resNB[0] + "\n" +
"\tappetency: " + resNB[1] + "\n" +
"\tup-sell: " + resNB[2] + "\n" +
"\toverall: " + resNB[3] + "\n");
在我们的例子中,模型返回以下结果:
Naive Bayes
churn: 0.5897891153549814
appetency: 0.630778394752436
up-sell: 0.6686116692438094
overall: 0.6297263931170756
当我们用更高级的建模来应对挑战时,这些结果将作为基线。如果我们用更加精密、耗时和复杂的技术来处理数据,我们希望结果会好得多。否则,我们只是在浪费资源。一般来说,在解决机器学习问题时,创建一个简单的基线分类器作为方向点总是一个好主意。
集成高级建模
在前面的部分中,我们实现了一个方向基线;现在,我们来关注一下重型机械。我们将遵循 IBM 研究团队(Niculescu-Mizil 等人)开发的 2009 年 KDD 杯获奖解决方案所采用的方法。
为了应对这一挑战,他们使用了集合选择算法(Caruana 和 Niculescu-Mizil,2004)。这是一种集成方法,这意味着它构建了一系列模型,并以特定的方式组合它们的输出,以便提供最终的分类。它有几个令人满意的特性,非常适合这一挑战,如下所示:
- 它被证明是健壮的,产生出色的性能。
- 它可以针对特定的性能指标进行优化,包括 AUC。
- 它允许将不同的分类器添加到库中。
- 这是一种随时可用的方法,意味着如果我们没有时间了,我们有一个可用的解决方案。
在本节中,我们将大致按照他们报告中描述的步骤进行操作。请注意,这并不是他们方法的精确实现,而是一个解决方案概述,其中包括深入研究的必要步骤。
这些步骤的概述如下:
- 首先,我们将通过删除显然不会带来任何值的属性来预处理数据——例如,所有缺失值或常量值;修复缺失值,以帮助无法处理它们的机器学习算法;并将分类属性转换成数字属性。
- 接下来,我们将运行属性选择算法,仅选择有助于任务预测的属性子集。
- 在第三步中,我们将用各种各样的模型实例化集成选择算法,最后,我们将评估性能。
开始之前
对于这个任务,我们将需要一个额外的 Weka 包,ensembleLibrary。Weka 3.7.2 及更高版本支持外部包,主要由学术界开发。WEKA 软件包列表可在weka.sourceforge.net/packageMetaData获得,如下图所示:
在pr downloads . SourceForge . net/WEKA/ensemble library 1 . 0 . 5 . zip 找到并下载最新版本的ensembleLibrary包?下载。
解压软件包后,找到ensembleLibrary.jar并将其导入到代码中,如下所示:
import weka.classifiers.meta.EnsembleSelection;
数据预处理
首先,我们将利用 Weka 的内置weka.filters.unsupervised.attribute.RemoveUseless过滤器,正如其名称所暗示的那样。它会删除变化不大的属性,例如,所有常量属性都会被删除。仅适用于名义属性的最大方差由-M参数指定。默认参数为 99%,这意味着如果 99%以上的实例都具有唯一的属性值,则该属性将被删除,如下所示:
RemoveUseless removeUseless = new RemoveUseless();
removeUseless.setOptions(new String[] { "-M", "99" });// threshold
removeUseless.setInputFormat(data);
data = Filter.useFilter(data, removeUseless);
接下来,我们将使用weka.filters.unsupervised.attribute.ReplaceMissingValues过滤器,用训练数据中的模式(名义属性)和均值(数值属性)替换数据集中所有缺失的值。一般而言,在考虑属性的含义和上下文时,应谨慎进行缺失值替换:
ReplaceMissingValues fixMissing = new ReplaceMissingValues();
fixMissing.setInputFormat(data);
data = Filter.useFilter(data, fixMissing);
最后,我们将离散化数值属性,也就是说,我们将使用weka.filters.unsupervised.attribute.Discretize过滤器将数值属性转换为区间。使用-B选项,我们将数值属性分割成四个区间,-R选项指定属性的范围(只有数值属性会被离散化):
Discretize discretizeNumeric = new Discretize();
discretizeNumeric.setOptions(new String[] {
"-B", "4", // no of bins
"-R", "first-last"}); //range of attributes
fixMissing.setInputFormat(data);
data = Filter.useFilter(data, fixMissing);
属性选择
在下一步中,我们将只选择信息属性,即更有可能帮助预测的属性。解决这个问题的标准方法是检查每个属性携带的信息增益。我们将使用weka.attributeSelection.AttributeSelection过滤器,它需要两个额外的方法:赋值器(如何计算属性有用性)和搜索算法(如何选择属性子集)。
在我们的例子中,首先,我们初始化weka.attributeSelection.InfoGainAttributeEval,它实现了信息增益的计算:
InfoGainAttributeEval eval = new InfoGainAttributeEval();
Ranker search = new Ranker();
为了只选择高于阈值的顶部属性,我们初始化weka.attributeSelection.Ranker,以便对信息增益高于特定阈值的属性进行排序。我们用-T参数指定它,同时保持阈值较低,以便保持属性至少包含一些信息:
search.setOptions(new String[] { "-T", "0.001" });
The general rule for setting this threshold is to sort the attributes by information gain and pick the threshold where the information gain drops to a negligible value.
接下来,我们可以初始化AttributeSelection类,设置赋值器和排序器,并将属性选择应用于数据集,如下所示:
AttributeSelection attSelect = new AttributeSelection();
attSelect.setEvaluator(eval);
attSelect.setSearch(search);
// apply attribute selection
attSelect.SelectAttributes(data);
最后,我们通过调用reduceDimensionality(Instances)方法来删除上次运行中没有选择的属性:
// remove the attributes not selected in the last run
data = attSelect.reduceDimensionality(data);
最后,我们只剩下 230 个属性中的 214 个。
型号选择
多年来,机器学习领域的从业者开发了各种各样的学习算法,并对现有算法进行了改进。有这么多独特的监督学习方法,很难跟踪所有这些方法。由于数据集的特征各不相同,没有一种方法在所有情况下都是最好的,但是不同的算法能够利用给定数据集的不同特征和关系。
首先,我们需要通过初始化weka.classifiers.EnsembleLibrary类来创建模型库,这将帮助我们定义模型:
EnsembleLibrary ensembleLib = new EnsembleLibrary();
接下来,我们将模型及其参数作为字符串值添加到库中;例如,我们可以添加三个具有不同参数的决策树学习器,如下所示:
ensembleLib.addModel("weka.classifiers.trees.J48 -S -C 0.25 -B -M
2");
ensembleLib.addModel("weka.classifiers.trees.J48 -S -C 0.25 -B -M
2 -A");
如果您熟悉 Weka 图形界面,您还可以在那里探索算法及其配置,并复制配置,如下图所示。右键单击算法名称并导航至编辑配置|复制配置字符串:
为了完成此示例,我们添加了以下算法及其参数:
- 用作默认基线的朴素贝叶斯:
ensembleLib.addModel("weka.classifiers.bayes.NaiveBayes");
- 基于懒惰模型的 k-最近邻:
ensembleLib.addModel("weka.classifiers.lazy.IBk");
- 具有默认参数的简单逻辑回归:
ensembleLib.addModel("weka.classifiers.functions.SimpleLogi
stic");
- 具有默认参数的支持向量机:
ensembleLib.addModel("weka.classifiers.functions.SMO");
AdaBoost,其本身是一种集合方法:
ensembleLib.addModel("weka.classifiers.meta.AdaBoostM1");
LogitBoost,基于逻辑回归的集成方法:
ensembleLib.addModel("weka.classifiers.meta.LogitBoost");
DecisionStump,一种基于一级决策树的集成方法:
ensembleLib.addModel("classifiers.trees.DecisionStump");
由于EnsembleLibrary实现主要关注 GUI 和控制台用户,我们必须通过调用saveLibrary(File, EnsembleLibrary, JComponent)方法将模型保存到一个文件中,如下所示:
EnsembleLibrary.saveLibrary(new
File(path+"ensembleLib.model.xml"), ensembleLib, null);
System.out.println(ensembleLib.getModels());
接下来,我们可以通过实例化weka.classifiers.meta.EnsembleSelection类来初始化集成选择算法。首先,让我们回顾一下以下方法选项:
-L </path/to/modelLibrary>:指定modelLibrary文件,继续所有型号列表。-W </path/to/working/directory>:指定工作目录,所有的模型都存储在这里。-B <numModelBags>:设置行李数量,即运行集合选择算法的迭代次数。-E <modelRatio>:设置随机选择的库模型的比例,以填充每个模型包。-V <validationRatio>:设置将被保留用于验证的训练数据集的比率。-H <hillClimbIterations>:设置每个模型包要进行的爬山迭代次数。-I <sortInitialization>:设置在初始化每个模型行李的集合时,分拣初始化算法能够选择的集合库的比率。-X <numFolds>:设置交叉验证的折叠次数。-P <hillclimbMetric>:指定在爬山算法中用于模型选择的度量。有效的指标包括准确性、rmse、roc、精确度、召回率、fscore 和所有指标。-A <algorithm>:指定用于集合选择的算法。有效的算法包括用于正向选择的正向(默认)、用于反向消除的反向、用于正向和反向消除的反向、仅打印来自集合库的最佳表现者、以及仅训练集合库中的模型的库。-R:该标志表示是否可以为一个组合多次选择模型。-G:表示当性能下降时,排序初始化是否贪婪地停止添加模型。-O:这是详细输出的标志。这将打印所有选定型号的性能。-S <num>:这是一个随机数种子(默认为1)。-D:如果设置,分类器在调试模式下运行,并且可以向控制台提供附加信息作为输出。
我们用以下初始参数初始化算法,其中我们指定优化 ROC 度量:
EnsembleSelection ensambleSel = new EnsembleSelection();
ensambleSel.setOptions(new String[]{
"-L", path+"ensembleLib.model.xml", // </path/to/modelLibrary>
"-W", path+"esTmp", // </path/to/working/directory> -
"-B", "10", // <numModelBags>
"-E", "1.0", // <modelRatio>.
"-V", "0.25", // <validationRatio>
"-H", "100", // <hillClimbIterations>
"-I", "1.0", // <sortInitialization>
"-X", "2", // <numFolds>
"-P", "roc", // <hillclimbMettric>
"-A", "forward", // <algorithm>
"-R", "true", // - Flag to be selected more than once
"-G", "true", // - stops adding models when performance degrades
"-O", "true", // - verbose output.
"-S", "1", // <num> - Random number seed.
"-D", "true" // - run in debug mode
});
性能赋值
计算和内存方面的评估都很繁重,所以确保用额外的堆空间初始化 JVM(例如,java -Xmx16g)。根据模型库中包含的算法数量,计算可能需要几个小时或几天。该示例在一个 12 核英特尔至强 E5-2420 CPU 上运行了 4 小时 22 分钟,该 CPU 配有 32 GB 内存,平均使用了 10%的 CPU 和 6 GB 的内存。
我们调用我们的评估方法并提供结果作为输出,如下所示:
double resES[] = evaluate(ensambleSel);
System.out.println("Ensemble Selection\n"
+ "\tchurn: " + resES[0] + "\n"
+ "\tappetency: " + resES[1] + "\n"
+ "\tup-sell: " + resES[2] + "\n"
+ "\toverall: " + resES[3] + "\n");
模型库中的特定分类器集实现了以下结果:
Ensamble
churn: 0.7109874158176481
appetency: 0.786325687118347
up-sell: 0.8521363243575182
overall: 0.7831498090978378
总的来说,与我们在本章开始时设计的初始基线相比,这种方法为我们带来了超过 15 个百分点的显著改进。虽然很难给出一个明确的答案,但这种改进主要归因于三个因素:数据预处理和属性选择,探索各种各样的学习方法,以及使用集成构建技术,该技术能够利用各种基分类器而不会过度拟合。然而,这种改进需要显著增加处理时间和工作记忆。
整体方法 MOA
顾名思义,就是一起或同时观看。它用于组合多个学习算法,以获得更好的结果和性能。有各种各样的技巧可以用于合奏。一些常用的集成技术或分类器包括装袋、提升、堆叠、一桶模型等等。
海量在线分析 ( MOA )支持集成分类器,比如准确度加权集成、准确度更新集成等等。在本节中,我们将向您展示如何利用 bagging 算法:
- 打开终端并执行以下命令:
java -cp moa.jar -javaagent:sizeofag-1.0.4.jar moa.gui.GUI
- 选择分类选项卡,然后单击配置按钮:
这将打开配置任务选项。
- 在“学习者”选项中,选择贝叶斯。NaiveBayes,然后在 stream 选项中,点击 Edit,如下图截图所示:
- 选择 ConceptDriftStream,并在 Stream 和 DriftStream 中选择 AgrawalGenerator 它将为流生成器使用 Agrawal 数据集:
- 关闭所有窗口,然后单击运行按钮:
这将运行任务并生成以下输出:
- 让我们使用 LeveragingBag 选项。为此,打开“配置任务”窗口并选择 baseLearner 中的“编辑”选项,这将显示以下内容:从第一个下拉框中选择 LeveragingBag。您可以在第一个下拉框中找到其他选项,如增强和平均重量组合:
将流保留为 AgrawalGenerator,如下面的屏幕截图所示:
- 关闭“配置任务”窗口,然后单击“运行”按钮;这将需要一些时间来完成:
输出显示了每 10,000 个实例后的评估、分类正确性花费的 RAM 时间以及 Kappa 统计。正如您所看到的,随着时间的推移,分类的正确性会随着实例的增加而增加。前面截图中的图表显示了实例的正确性和数量。
摘要
在这一章中,我们解决了 2009 年 KDD 杯客户关系预测挑战,实现了数据预处理步骤,并解决了丢失值和冗余属性的问题。我们跟踪了获胜的 KDD 杯解决方案,并研究了如何通过使用一篮子学习算法来利用集成方法,这可以显著提高分类性能。
在下一章,我们将解决另一个关于顾客行为的问题:购买行为。您将学习如何使用算法来检测频繁出现的模式。
五、亲和力分析
亲和力分析是购物篮分析 ( MBA )的核心。它可以发现由特定用户或组执行的活动之间的共现关系。在零售业,亲和力分析可以帮助你了解顾客的购买行为。这些见解可以通过智能交叉销售和追加销售策略增加收入,并帮助您制定忠诚度计划、促销和折扣计划。
在本章中,我们将探讨以下主题:
- 工商管理硕士
- 关联规则学习
- 各种领域中的其他应用
首先,我们将修改核心关联规则学习的概念和算法,如支持和提升 Apriori 算法和 FP-Growth 算法。接下来,我们将使用 Weka 对一个超市数据集执行我们的第一次相似性分析,并研究如何解释产生的规则。我们将通过分析关联规则学习如何应用于其他领域(如 IT 运营分析和医学)来结束本章。
市场篮子分析
自从引入电子销售点以来,零售商已经收集了数量惊人的数据。为了利用这些数据产生业务价值,他们首先开发了一种方法来整合和聚合数据,以了解业务的基础。
最近,焦点转移到最底层的粒度——市场篮交易。在这种详细程度下,零售商可以直接看到在他们商店购物的每个顾客的购物篮,不仅了解特定购物篮中购买的商品数量,还了解这些商品是如何相互搭配购买的。这可用于驱动关于如何区分商店分类和商品的决策,以及有效地组合多个类别内和跨类别的产品,以推动更高的销售额和利润。这些决策可以在整个零售链中、通过渠道、在当地商店层面、甚至针对特定客户实施,通过所谓的个性化营销,为每个客户提供独特的产品:
MBA 涵盖各种各样的分析:
- 物品相似度:定义两件(或多件)物品被一起购买的可能性。
- 驱动项的识别:可以识别驱动人们到商店并且总是需要库存的项目。
- 行程分类:分析购物篮的内容,并将购物行程分类为一个类别:每周一次的购物行程、特殊场合等等。
- 店与店比较:了解购物篮的数量可以让任何指标除以购物篮的总数,从而有效地创建一种方便简单的方法来比较不同特征的商店(每个顾客售出的数量、每笔交易的收入、每个购物篮的商品数量等等)。
- 收入优化:这有助于确定这家商店的神奇价位,增加购物篮的规模和价值。
- 营销:这有助于识别更有利可图的广告和促销活动,更精确地定位优惠以提高投资回报率,通过纵向分析产生更好的会员卡促销活动,并为商店吸引更多流量。
- 运营优化:这有助于根据贸易区人口统计数据定制商店和商品组合,并优化商店布局,从而使库存与需求相匹配。
预测模型有助于零售商将正确的报价引导到正确的客户群或客户群,并了解什么对哪个客户有效,预测客户对该报价做出响应的概率得分,以及了解客户从接受报价中获得的价值。
亲和力分析
相似性分析用于确定一组商品被一起购买的可能性。在零售业,有天然的产品亲缘关系;例如,对于买汉堡肉饼的人来说,很典型的是买汉堡卷,以及番茄酱、芥末、西红柿和其他组成汉堡体验的物品。
虽然有些产品相似性可能看起来微不足道,但有些相似性并不十分明显。一个经典的例子是牙膏和金枪鱼。似乎吃金枪鱼的人更倾向于在吃完饭后马上刷牙。那么,为什么对零售商来说,很好地掌握产品亲缘关系很重要呢?该信息对于适当地计划促销是至关重要的,因为降低某些项目的价格可能会导致相关高亲和力项目的价格飙升,而无需进一步促销这些相关项目。
在下一节中,我们将研究关联规则学习的算法:Apriori 和 FP-Growth。
关联规则学习
关联规则学习已经成为在大型数据库中发现项目间有趣关系的流行方法。它最常用于零售业,以揭示产品之间的规律性。
关联规则学习方法使用不同的兴趣度在数据库中发现作为有趣的强规则的模式。例如,下面的规则表明,如果客户一起购买洋葱和土豆,他们可能也会购买汉堡肉:{洋葱,土豆}--> {汉堡}。
另一个可能在每个机器学习课上都会讲的经典故事是啤酒和尿布的故事。对超市购物者行为的分析显示,购买尿布的顾客,大概是年轻男性,也倾向于购买啤酒。它立即成为一个流行的例子,说明如何从日常数据中发现意想不到的关联规则;然而,对于这个故事有多少是真实的,人们有不同的看法。在《DSS 新闻 2002》中,丹尼尔·鲍尔 s 这样说道:
“1992 年,Teradata 零售咨询集团经理 Thomas Blischok 和他的工作人员对大约 25 家 Osco 药店的 120 万个市场篮进行了分析。开发数据库查询是为了识别亲缘关系。分析确实发现,在下午 5 点到 7 点之间,消费者会购买啤酒和尿布。Osco 经理没有利用啤酒和尿布的关系,将货架上的产品靠得更近。
除了前面 MBA 的例子之外,关联规则现在还应用于许多应用领域,包括 web 使用挖掘、入侵检测、连续生产和生物信息学。我们将在本章的后面更仔细地研究这些领域。
基本概念
在我们深入研究算法之前,让我们先回顾一下基本概念。
交易数据库
在关联规则挖掘中,数据集的结构与第一章中介绍的方法稍有不同。首先,没有类值,因为这不是学习关联规则所必需的。接下来,数据集被表示为一个事务表,其中每个超市商品对应一个二进制属性。因此,特征向量可能非常大。
考虑下面的例子。假设我们有四张收据,如下所示。每张收据对应一项采购交易:
为了以交易数据库的形式书写这些收据,我们首先识别收据中出现的所有可能的项目。这些物品是洋葱、土豆、汉堡、啤酒和蘸料。每一次购买,即交易,都在一行中呈现,如果是在交易中购买的,则有 1 ,否则有 0 ,如下表所示:
| 交易 ID | 洋葱 | 土豆 | 汉堡 | 啤酒 | 铲斗 | | one | Zero | one | one | Zero | Zero | | Two | one | one | one | one | Zero | | three | Zero | Zero | Zero | one | one | | four | one | Zero | one | one | Zero |
这个例子真的很小。在实际应用中,数据集通常包含数千或数百万个交易,这使得学习算法能够发现统计上显著的模式。
项目集和规则
Itemset 简单来说就是一组项目,例如,{洋葱,土豆,汉堡}。规则由两个项目集 X 和 Y 组成,格式如下:
X -> Y
这表明当观察 X 项目集时,也观察 Y 的模式。为了选择感兴趣的规则,可以使用各种显著性度量。
支持
对于项集,支持度被定义为包含该项集的事务的比例。上表中的{potatoes, burger}项集具有以下支持,因为它出现在 50%的事务中(四个事务中的两个):supp({ potatos,burger}) = 2/4 = 0.5。
直观地说,它表明了支持该模式的事务的份额。
电梯
Lift 是对目标模型(关联规则)在预测或分类具有增强响应(相对于总体而言)的病例时的性能的度量,根据随机选择目标模型进行测量。使用以下公式定义:
信心
规则的可信度表明它的准确性。使用以下公式定义:
![]
比如{洋葱,汉堡}--> {啤酒}规则,上表中的置信度 0.5/0.5 = 1.0 ,表示洋葱和汉堡一起买的时候,100%的时候啤酒也买。
Apriori 算法
Apriori 算法是一种经典算法,用于事务上的频繁模式挖掘和关联规则学习。通过识别数据库中频繁出现的单个项目并将其扩展到更大的项目集,Apriori 可以确定关联规则,这些规则突出了数据库的总体趋势。
Apriori 算法构造一组项集,例如 itemset1= {Item A,Item B},并计算支持度,支持度计算数据库中出现的次数。Apriori 然后使用自底向上的方法,在这种方法中,频繁项集被一次一项地扩展,它的工作方式是通过首先查看较小的集并认识到除非其所有子集都是频繁的,否则大型集不可能是频繁的,从而排除最大的集作为候选集。当没有找到进一步的成功扩展时,算法终止。
虽然 Apriori 算法是机器学习中的一个重要里程碑,但它存在许多低效和折衷之处。在下一节中,我们将研究一种更新的 FP-Growth 技术。
FP-增长算法
FP-Growth (其中 FP 是频繁模式)将事务数据库表示为后缀树。首先,该算法计算数据集中项目的出现次数。在第二遍中,它构建一个后缀树,这是一个有序的树数据结构,通常用于存储字符串。下图显示了基于上一个示例的后缀树示例:
如果许多事务共享最频繁的项目,后缀树提供接近树根的高压缩。大项集直接增长,而不是生成候选项并对整个数据库进行测试。通过找到匹配最小支持度和置信度的所有项目集,从树的底部开始增长。一旦递归过程完成,所有具有最小覆盖的大项目集都被找到,关联规则创建开始。
FP-Growth 算法有几个优点。首先,它构建了一个 FP-tree,以一种非常紧凑的方式对原始数据集进行编码。其次,它利用 FP-tree 结构和分治策略高效地构建频繁项集。
超市数据集
位于data/supermarket.arff的超市数据集描述了超市顾客的购物习惯。大多数属性代表特定的项目组,例如,乳制品、牛肉和土豆;或者它们代表一个部门,例如,79 部门、81 部门等等。下表显示了数据库的摘录,其中如果客户购买了一件商品,则值为 t ,否则将丢失。每个客户有一个实例。数据集不包含类属性,因为这不是学习关联规则所必需的。下表显示了一个数据示例:
发现模式
为了发现购物模式,我们将使用我们之前研究过的两种算法:Apriori 和 FP-Growth。
推测的
我们将使用 Weka 中实现的Apriori算法。它迭代地减少最小支持度,直到找到所需数量的具有给定最小置信度的规则。我们将使用以下步骤实现该算法:
- 我们将使用以下代码行导入所需的库:
import java.io.BufferedReader;
import java.io.FileReader;
import weka.core.Instances;
import weka.associations.Apriori;
- 首先,我们将加载
supermarket.arff数据集:
Instances data = new Instances(new BufferedReader(new FileReader("data/supermarket.arff")));
- 我们将初始化一个
Apriori实例并调用buildAssociations(Instances)函数来开始频繁模式挖掘,如下所示:
Apriori model = new Apriori();
model.buildAssociations(data);
- 我们可以输出发现的项目集和规则,如下面的代码所示:
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)
...
该算法根据置信度输出 10 个最佳规则。让我们看看第一条规则,并解释输出,如下所示:
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)
上面说当biscuits、frozen foods、fruits一起购买,购买总价高的时候,也很有可能bread、cake也被购买。{biscuits, frozen foods, fruit, total high}项集出现在788事务中,而{bread, cake}项集出现在723事务中。该规则的置信度是0.92,这意味着该规则在存在{biscuits, frozen foods, fruit, total high}项集的 92%的事务中成立。
输出还报告了额外的度量,如提升、杠杆和信念,这些度量根据我们的初始假设来估计准确性;例如,3.35信念值表明,如果关联纯粹是随机的,则该规则将错误3.35倍。如果 X 和 Y 在统计上是独立的(lift=1),Lift 测量它们一起出现的次数。X - > Y 法则中的2.16升力意味着 X 的概率比 Y 的概率大2.16倍
FP-增长
现在,让我们试着用更有效的 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 rules:
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 种氨基酸组成的序列。每种蛋白质都有独特的三维结构,这取决于氨基酸序列;序列的微小变化可能会改变蛋白质的功能。应用关联规则,一个蛋白质对应一个事务,氨基酸及其结构对应条目。
这种关联规则对于增强我们对蛋白质组成的理解是可取的,并且有可能提供关于蛋白质中出现的一些特定氨基酸组之间的全局相互作用的线索。人工蛋白质的合成非常需要这些关联规则或约束的知识。
普查数据
人口普查为研究人员和公众提供了大量关于社会的一般统计信息。与人口和经济普查相关的信息可以在规划公共服务(教育、卫生、交通和资金)以及商业(建立新工厂、购物中心或银行,甚至营销特定产品)时进行预测。
为了发现频繁模式,每个统计区域(例如,自治市、城市和街区)对应于一个事务,收集的指标对应于项目。
客户关系管理
正如我们在前面章节中简要讨论的那样,客户关系管理是一个丰富的数据来源,公司希望通过它来识别不同客户群、产品和服务的偏好,以增强他们的产品、服务和客户之间的凝聚力。
关联规则可以强化知识管理过程,让营销人员更好地了解客户,提供更优质的服务。例如,可以应用关联规则从客户档案和销售数据中检测不同时间快照的客户行为变化。基本思想是从两个数据集发现变化,从每个数据集生成规则,进行规则匹配。
IT 运营分析
基于大量交易的记录,关联规则学习非常适合应用于日常 IT 运营中定期收集的数据,使 IT 运营分析工具能够检测频繁模式并识别关键变化。IT 专家需要看到全局并理解,例如,数据库上的问题如何影响应用服务器。
对于特定的一天,IT 运营部门可能会接收各种警报,并将它们呈现在事务性数据库中。使用关联规则学习算法,IT 运营分析工具可以关联和检测频繁出现的警报模式。这有助于更好地理解一个组件如何影响另一个组件。
通过识别警报模式,可以应用预测分析。例如,一个特定的数据库服务器托管了一个 web 应用程序,突然触发了一个关于数据库的警报。通过研究由关联规则学习算法识别的频繁模式,这意味着 IT 人员需要在 web 应用程序受到影响之前采取行动。
关联规则学习还可以发现源自同一 IT 事件的警报事件。例如,每次添加新用户时,都会检测到 Windows 操作系统中的六个变化。接下来,在应用组合管理 ( APM )中,它可能会面临多个警报,显示数据库中的事务时间很长。如果所有这些问题都源于同一个来源(例如,收到数百个关于全部由 Windows 更新引起的更改的警报),这种频繁的模式挖掘有助于快速排除大量警报,使 IT 操作员能够专注于真正关键的更改。
摘要
在本章中,您学习了如何利用事务数据集上的关联规则学习来深入了解频繁模式。我们在 Weka 中进行了关联性分析,并了解到困难在于结果分析——在解释规则时需要仔细注意,因为关联(即相关性)与因果关系不同。
在下一章中,我们将看看如何使用可扩展的机器学习库 Apache Mahout 将商品推荐问题提升到一个新的水平,Apache Mahout 能够处理大数据。
六、Apache Mahout 推荐引擎
推荐引擎是当今初创公司中应用最广泛的数据科学方法之一。构建推荐系统有两种主要技术:基于内容的过滤和协同过滤。基于内容的算法使用项目的属性来查找具有相似属性的项目。协同过滤算法采用用户评级或其他用户行为,并根据具有相似行为的用户喜欢或购买的内容进行推荐。
在本章中,我们将首先解释理解推荐引擎原理所需的基本概念,然后我们将演示如何利用 Apache Mahout 的各种算法的实现来快速获得可扩展的推荐引擎。
本章将涵盖以下主题:
- 如何构建推荐引擎
- 准备好 Apache Mahout
- 基于内容的方法
- 协作过滤方法
在本章结束时,你将会学到适合我们问题的推荐引擎的种类,以及如何快速实现这个引擎。
基本概念
推荐引擎旨在向用户展示感兴趣的项目。与搜索引擎不同的是,相关内容通常在没有被请求的情况下出现在网站上,用户不必构建查询,因为推荐引擎会观察用户的行为,并在用户不知情的情况下为用户构建查询。
可以说,推荐引擎最著名的例子是 www.amazon.com,它以多种方式提供个性化推荐。下面的屏幕截图显示了购买了该商品的客户也购买了该商品的示例。稍后您将会看到,这是一个基于项目的协作推荐示例,其中推荐与特定项目相似的项目:
在本节中,我们将介绍与理解和构建推荐引擎相关的关键概念。
关键概念
推荐引擎需要以下输入才能做出推荐:
- 项目信息,用属性描述
- 用户资料,如年龄范围、性别、位置、朋友等
- 用户交互,以评级、浏览、标记、比较、保存和发送电子邮件的形式
- 将显示项目的上下文;例如,项目的类别和项目的地理位置
该输入然后被推荐引擎组合以帮助获得以下内容:
- 购买、观看、查看或书签标记了该项目的用户也购买、观看、查看或书签标记了该项
- 与此项类似的项目
- 您可能认识的其他用户
- 与您相似的其他用户
现在,让我们仔细看看这种组合是如何工作的。
基于用户和基于项目的分析
构建推荐引擎取决于当试图推荐特定项目时,引擎是否搜索相关项目或用户。
在基于项目的分析中,引擎侧重于识别与特定项目相似的项目,而在基于用户的分析中,首先确定与特定用户相似的用户。例如,确定具有相同简档信息(年龄、性别等)或行为历史(购买、观看、观看等)的用户,然后将相同的项目推荐给其他类似的用户。
这两种方法都需要我们计算相似性矩阵,这取决于我们是在分析物品属性还是用户行为。让我们更深入地了解一下这是如何做到的。
计算相似度
有三种计算相似性的基本方法,如下所示:
- 协同过滤算法采用用户评级或其他用户行为,并根据具有相似行为的用户喜欢或购买的内容进行推荐
- 基于内容的算法使用项目的属性来查找具有相似属性的项目
- 一种混合方法结合了协作过滤和基于内容的过滤
让我们在接下来的小节中详细了解一下每种方法。
协同过滤
协同过滤完全基于用户评分或其他用户行为,根据具有相似行为的用户喜欢或购买的内容进行推荐。
协同过滤的一个关键优势是它不依赖于项目内容,因此,它能够准确地推荐复杂的项目,例如电影,而无需了解项目本身。潜在的假设是,过去同意的人将来也会同意,并且他们会喜欢与他们过去喜欢的东西相似的东西。
这种方法的一个主要缺点是所谓的冷启动,这意味着如果我们想要建立一个精确的协同过滤系统,算法往往需要大量的用户评级。这通常会在产品的第一个版本中去掉协同过滤,然后在收集了大量数据后再引入。
基于内容的过滤
另一方面,基于内容的过滤是基于项目的描述和用户偏好的简档,其组合如下。首先,用属性描述项目,为了找到相似的项目,我们使用距离度量来测量项目之间的距离,例如余弦距离或皮尔逊系数(在第一章、应用机器学习快速入门中有更多关于距离度量的内容)。现在,用户配置文件开始起作用了。给定关于用户喜欢的项目种类的反馈,我们可以引入权重,指定特定项目属性的重要性。例如,Pandora 广播流媒体服务应用基于内容的过滤来创建电台,使用 400 多个属性。用户最初挑选具有特定属性的歌曲,并且通过提供反馈,强调重要的歌曲属性。
最初,这种方法需要很少的用户反馈信息;因此,它有效地避免了冷启动问题。
混合工艺
现在,在协作和基于内容之间,你应该选择哪一个?协同过滤能够从用户关于一个内容源的动作中学习用户偏好,并在其他内容类型中使用它们。基于内容的过滤仅限于推荐用户已经在使用的相同类型的内容。这在某些用例中提供了价值;比如基于新闻浏览推荐新闻文章是有用的,但是如果基于新闻浏览可以推荐不同的来源,比如书籍、电影,那就有用得多了。
协同过滤和基于内容的过滤并不相互排斥;在某些情况下,它们可以结合起来更有效。例如,网飞使用协同过滤来分析相似用户的搜索和观看模式,以及基于内容的过滤来提供与用户高度评价的电影具有共同特征的电影。
有各种各样的杂交技术:加权、切换和混合、特征组合、特征增强、级联、元级等等。推荐系统是机器学习和数据挖掘社区中的一个活跃领域,在数据科学会议上有专门的跟踪。Adomavicius 和 Tuzhilin (2005)在论文中对技术进行了很好的概述,该论文讨论了不同的方法和底层算法,并为后续论文提供了参考。为了获得更多的技术知识并理解某个特定方法有意义时的所有微小细节,你应该看看 Ricci 等人编辑的书:推荐系统手册(2010 年第一版,出版社。
开发与探索
在推荐系统中,根据我们对用户的了解,在推荐落入用户最佳位置的项目(开发)和推荐不落入用户最佳位置的项目之间总是有一个权衡,目的是向用户展示一些新奇的东西(探索)。很少探索的推荐系统将仅推荐与先前用户评级一致的项目,从而防止显示其当前气泡之外的项目。在实践中,从用户的甜蜜点中获得新项目的意外收获通常是可取的,这导致了令人愉快的惊喜,并且潜在地发现了新的甜蜜点。
在本节中,我们讨论了开始构建推荐引擎所需的基本概念。现在,让我们看看如何用 Apache Mahout 实际构建一个。
获取 Apache Mahout
Mahout 是在第二章、 Java 库和机器学习平台中介绍的,作为一个可扩展的机器学习库。它提供了一组丰富的组件,您可以使用这些组件从选择的算法中构建一个定制的推荐系统。Mahout 的创建者说它是为企业准备的;它专为性能、可扩展性和灵活性而设计。
Mahout 可以被配置为以两种方式运行:使用或不使用 Hadoop,以及分别用于单机和分布式处理。我们将着重于在没有 Hadoop 的情况下配置 Mahout。关于 Mahout 更高级的配置和进一步的使用,我推荐两本最近的书:*学习 Apache Mahout,*作者 C handramani Tiwary,Packt 出版社,*学习 Apache Mahout 分类,*作者 Ashish Gupta,Packt 出版社。
因为 Apache Mahout 的构建和发布系统是基于 Maven 的,所以您需要学习如何安装它。我们将研究最方便的方法;使用 Eclipse 和 Maven 插件。
用 Maven 插件在 Eclipse 中配置 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>
就是这样;已经添加了看象人,我们准备开始了。
构建推荐引擎
为了演示基于内容的过滤和协作过滤方法,我们将构建一个图书推荐引擎。
图书评分数据集
在这一章中,我们将使用一个图书评级数据集(齐格勒等人,2005 年),该数据集是在为期四周的搜索中收集的。它包含了图书交叉网站的 278,858 个成员的数据和 1,157,112 个隐含和明确的评级,涉及 271,379 个不同的 ISBNs。用户数据是匿名的,但带有人口统计信息。数据集摘自通过主题多样化改进推荐列表、 C ai-Nicolas Ziegler、Sean M. McNee、Joseph A. Konstan、Georg Lausen: 第 14 届国际万维网会议论文集 (WWW '05) *、*2005 年 5 月 10-14 日,日本千叶(www2.informatik.uni-freiburg.de/~cziegler/BX/)。
跨帐簿数据集由以下三个文件组成:
BX-Users:包含用户。注意,用户 ID(User-ID)已经被匿名化并映射到整数。如果可能,提供人口统计数据(位置和年龄)。否则,这些字段包含空值。- 书籍由它们各自的 ISBNs 来识别。无效的 ISBNs 已从数据集中删除。此外,给出了一些基于内容的信息(书名、作者、出版年份和出版商),这些信息是从 Amazon Web Services 获得的。注意,在有几个作者的情况下,只提供第一作者。还给出链接到封面图片的 URL,以三种不同的风格出现(Image-URL-S、Image-URL-M 和 Image-URL-L),分别指小型、中型和大型 URL。这些网址指向亚马逊网站。
BX-Book-Ratings:包含图书评级信息。评级(书籍评级)要么是显性的,用 1-10 的等级表示(数值越高表示欣赏程度越高),要么是隐性的,用 0 表示。
加载数据
根据数据存储的位置,有两种加载数据的方法:文件或数据库。首先,我们将详细了解如何从文件中加载数据,包括如何处理自定义格式。最后,我们将快速看一下如何从数据库加载数据。
从文件加载数据
从文件中加载数据可以通过FileDataModel类来实现。我们将期待一个逗号分隔的文件,其中每行包含一个userID、一个itemID、一个可选的preference值和一个可选的timestamp,顺序相同,如下所示:
userID,itemID[,preference[,timestamp]]
可选偏好适应具有二进制偏好值的应用,也就是说,用户要么表达对项目的偏好,要么不表达,没有偏好程度;例如,喜欢或不喜欢。
以散列(#)或空行开头的行将被忽略。这些行包含附加字段也是可以接受的,这些字段将被忽略。
DataModel类采用以下类型:
userID和itemID可以被解析为longpreference值可以解析为doubletimestamp可以解析为long
如果您能够以前面的格式提供数据集,您可以简单地使用下面的行来加载数据:
DataModel model = new FileDataModel(new File(path));
此类不适用于大量数据;比如几千万行。为此,一个 JDBC 支持的DataModel和一个数据库更合适。
然而,在现实世界中,我们不能总是确保提供给我们的输入数据只包含userID和itemID的整数值。例如,在我们的例子中,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 集成实现了可以通过 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");
内存数据库
最后但同样重要的是,数据模型可以动态创建并保存在内存中。可以从一组偏好中创建一个数据库,该数据库将保存一组项目的用户评级。
我们可以如下进行。首先,我们创建一个偏好数组的FastByIdMap散列映射PreferenceArray,它存储一个偏好数组:
FastByIDMap <PreferenceArray> preferences = new FastByIDMap
<PreferenceArray> ();
接下来,我们可以为用户创建一个新的偏好数组来保存他们的评级。该数组必须用一个 size 参数初始化,该参数在内存中保留这么多的槽:
PreferenceArray prefsForUser1 =
new GenericUserPreferenceArray (10);
接下来,我们在位置0设置当前偏好的用户 ID。这将实际设置所有首选项的用户 ID:
prefsForUser1.setUserID (0, 1L);
在位置0为当前偏好设置一个itemID,如下所示:
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 接口定义为以下关键抽象:
- 数据模型(data model):这是一个关于用户和他们对项目的偏好的信息库
- 用户相似性:这定义了两个用户之间相似性的概念
- 项目相似性:这定义了两个项目之间相似性的概念
- UserNeighborhood :计算给定用户的邻居用户
- 推荐者:为用户推荐商品
下图显示了上述概念的一般结构:
基于用户的过滤
最基本的基于用户的协同过滤可以通过初始化前面描述的组件来实现,如下所示:
首先,加载数据模型:
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);
现在,我们可以用model、neighborhood和类似对象的数据初始化一个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-fold 交叉验证,这在第一章、应用机器学习快速入门中有介绍。我们将一个数据集划分成多个集合;一些用于训练我们的推荐引擎,其余的用于测试它向未知用户推荐商品的效果如何。
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();
初始化BookRecommender对象,实现RecommenderBuilder接口,如下所示:
RecommenderBuilder builder = new MyRecommenderBuilder();
最后,调用evaluate()方法,该方法接受以下参数:
RecommenderBuilder:这是实现RecommenderBuilder的对象,可以构建recommender进行测试DataModelBuilder:表示要使用的DataModelBuilder;如果为空,将使用默认的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添加到similarity - 如果作者相同,在
similarity上加上0.50
我们现在可以实现我们自己的similarity度量,如下所示:
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 日,美国国防部长唐纳德·拉姆斯菲尔德在一次新闻发布会上说,没有证据表明伊拉克政府向恐怖组织提供大规模杀伤性武器,这立即成为人们议论纷纷的话题。拉姆斯菲尔德声明如下(国防部新闻,2012 年):
“我总是对那些说某事尚未发生的报道感兴趣,因为正如我们所知,有已知的已知;有些事情我们知道我们知道。我们也知道有已知的未知;也就是说,我们知道有些事情我们不知道。但也有未知的未知——那些我们不知道我们不知道的。如果纵观我们国家和其他自由国家的历史,后一类往往是困难的。”
这种说法乍一看似乎令人困惑,但未知的未知的概念在处理风险、NSA 和其他情报机构的学者中得到了很好的研究。该声明的基本含义如下:
- 已知已知:这些都是众所周知的问题;我们知道如何识别它们,如何处理它们
- 已知未知:这些是预期或可预见的问题,可以合理地预见,但以前没有发生过
- 未知的未知:这些是意料之外和不可预见的问题,根据以往的经验,这些问题是无法预测的,因此会带来重大风险
在下面的部分中,我们将研究处理前两种类型的已知和未知的两种基本方法:处理已知的可疑模式检测,以及针对已知未知的异常模式检测。
可疑模式检测
第一种方法涉及一个行为库,它对否定模式进行编码,在下图中显示为红色减号,并识别观察到的行为对应于识别库中的匹配。如果一个新模式可以与否定模式匹配,那么它就被认为是可疑的:
例如,当你去看医生时,他/她会检查各种健康症状(体温、疼痛程度、患处等),并将症状与已知疾病进行匹配。用机器学习的术语来说,医生收集属性并进行分类。
这种方法的一个优点是,我们可以立即知道哪里出了问题;例如,假设我们知道疾病,我们可以选择一个适当的治疗程序。
这种方法的一个主要缺点是它只能检测预先已知的可疑模式。如果一个模式没有被插入到否定模式库中,那么我们将不能识别它。因此,这种方法适用于已知知识的建模。
异常模式检测
第二种方法以相反的方式使用模式库,这意味着该库只对积极的模式进行编码,这些模式在下图中用绿色加号标记。当观察到的行为(蓝色圆圈)无法与库匹配时,它被认为是异常的:
这种方法要求我们只对过去所见的建模,即正常模式。如果我们回到医生的例子,我们首先去看医生的主要原因是因为我们感觉不舒服。我们感觉到的感觉状态(例如,头痛和皮肤疼痛)与我们通常的感觉不符,因此,我们决定去看医生。我们不知道是哪种疾病导致了这种状态,也不知道治疗方法,但我们能够观察到这与通常的状态不符。
这种方法的一个主要优点是它不需要我们说任何关于异常模式的事情;因此,它适用于建模已知的未知和未知的未知。另一方面,它并没有告诉我们到底出了什么问题。
分析类型
已经提出了几种方法来解决这个问题。我们将异常和可疑行为检测大致分为以下三类:模式分析、交易分析和计划识别。在接下来的几节中,我们将快速了解一些实际应用。
模式分析
从模式中检测异常和可疑行为的活跃区域是基于视觉模态的,例如照相机。张等人(2007)提出了一种从视频序列中进行视觉人体运动分析的系统,该系统基于行走轨迹识别异常行为;林等人(2009)描述了一种基于颜色特征、距离特征和计数特征的视频监控系统,其中进化技术用于测量观察相似性。该系统跟踪每个人,并通过分析他们的轨迹模式对他们的行为进行分类。该系统在图像的不同部分提取一组视觉低级特征,并用支持向量机进行分类,以便检测攻击性、愉快、陶醉、紧张、中性和疲劳行为。
交易分析
与连续观察相反,事务分析假设离散的状态/事务。一个主要的研究领域是入侵检测 ( ID ),目的是检测对信息系统的攻击。有两种类型的 ID 系统,基于特征的和基于异常的,大体上遵循前面部分描述的可疑和异常模式检测。Gyanchandani 等人(2012 年)发表了一篇关于 ID 方法的综合综述。
此外,基于可穿戴传感器的环境辅助生活中的应用也适合于交易分析,因为感测通常是基于事件的。林贝洛保罗斯等人(2008)提出了一个系统,用于自动提取用户的时空模式,编码为来自他们家中部署的传感器网络的传感器激活。所提出的方法基于位置、时间和持续时间,能够使用 Apriori 算法提取频繁模式,并以马尔可夫链的形式编码最频繁的模式。相关工作的另一个领域包括隐马尔可夫模型 ( HMMs ),这些模型在传统的活动识别中被广泛用于对一系列动作进行建模,但这些主题已经超出了本书的范围。
计划识别
计划识别侧重于一种机制,用于识别一个代理的不可观察状态,给定其与环境的相互作用的观察(Avrahami-Zilberbrand,2009)。大多数现有的调查假设离散的观察活动的形式。为了执行异常和可疑行为检测,计划识别算法可以使用混合方法。符号计划识别器用于过滤一致的假设,并将它们传递给评估引擎,该引擎关注排名。
这些是应用于各种现实生活场景的高级方法,旨在发现异常。在接下来的几节中,我们将深入探讨更基本的可疑和异常模式检测方法。
使用 ELKI 的异常检测
ELKI 代表采用 KDD 应用索引结构的环境,其中 KDD 代表数据库中的知识发现 。这是一个开源软件,主要用于数据挖掘,重点是无监督学习。它支持聚类分析和离群点检测的各种算法。以下是一些异常值算法:
- 基于距离的异常值检测:用于指定两个参数。对于距离 c 大于 d 的所有数据对象,如果其分数 p,则对象被标记为离群值,有许多算法,如
DBOutlierDetection、DBOutlierScore、KNNOutlier、KNNWeightOutlier、ParallelKNNOutlier、ParallelKNNWeightOutlier、ReferenceBasedOutlierDetection等等。 - LOF 家族方法:计算特定参数上基于密度的局部异常因子。包括
LOF、ParallelLOF、ALOCI、COF、LDF、LDOF等算法。 - 基于角度的异常值检测:使用角度的方差分析,主要使用高维数据集。常见的算法有
ABOD、FastABOD、LBABOD。 - 基于聚类的离群点检测:使用 EM 聚类;如果该对象不属于某个聚类,则将其视为异常值。这包括
EMOutlier和KMeansOutlierDetection等算法。 - 子空间离群点检测:这使用轴平行子空间的离群点检测方法。它有
SOD、OutRankS1、OUTRES、AggrawalYuNaive、AggrawalYuEvolutionary等算法。 - 空间异常值检测:这是一个大型数据集,基于从不同来源收集的位置和相对于邻居极端的数据点。它有
CTLuGLSBackwardSearchAlgorithm、CTLuMeanMultipleAttributes、CTLuMedianAlgorithm、CTLuScatterplotOutlier等算法。
使用 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作为值。以下屏幕截图显示了所有已填写的选项:
单击 Run Task 按钮,它将处理并生成以下输出:
这显示了聚类和可能的异常值。
保险索赔中的欺诈检测
首先,我们将看看可疑行为检测,其目标是了解欺诈模式,这对应于对已知知识进行建模。
资料组
我们将使用描述保险交易的数据集,该数据集在 Oracle 数据库在线文档中公开提供,网址为docs . Oracle . com/CD/b 28359 _ 01/data mine . 111/b 28129/anomalies . htm。
该数据集描述了一家未披露的保险公司对车辆事故的保险索赔。它包含 15,430 项索赔;每个索赔由 33 个属性组成,描述了以下组件:
- 客户人口统计信息(年龄、性别、婚姻状况等)
- 购买的保单(保单类型、车辆类别、补充数量、代理类型等)
- 索赔情况(索赔日/月/周、提交的保单报告、证人在场、事故-保单报告、事故索赔之间的过去天数等)
- 其他客户数据(汽车数量、以前的索赔、驾驶评级等)
- 发现欺诈(是或否)
以下屏幕截图中显示的数据库示例描述了已经加载到 Weka 中的数据:
现在的任务是创建一个模型,将来能够识别可疑的索赔。这项任务的挑战性在于,只有 6%的声明是可疑的。如果我们创建一个虚拟分类器,说没有索赔是可疑的,它将在 94%的情况下是准确的。因此,在这个任务中,我们将使用不同的准确性度量:精确度和召回率。
让我们回忆一下第一章、应用机器学习快速入门中的结果表,其中有四种可能的结果,分别表示为真阳性、假阳性、假阴性和真阴性:
| | | 归类为 | | 实际 | | 诈骗 | 没有欺诈 | | 诈骗 | TP -真阳性 | FN -假阴性 | | 没有欺诈 | FP -假阳性 | TN -真阴性 |
精确度和召回率定义如下:
- 精度等于正确发出警报的比例,如下:
- 召回等于异常签名的比例,正确识别如下:
- 通过这些衡量标准——我们的虚拟分类器得分——我们发现 Pr = 0 和 Re = 0 ,因为它从未将任何实例标记为欺诈( TP = 0 )。在实践中,我们希望通过两个数字来比较分类器;因此,我们使用 F 值。这是一个事实上的度量,它计算精度和召回率之间的调和平均值,如下所示:
现在,让我们继续设计一个真正的分类器。
模拟可疑模式
要设计一个分类器,我们可以遵循标准的监督学习步骤,如第一章、应用机器学习快速入门中所述。在这个方法中,我们将包括一些额外的步骤来处理不平衡的数据集,并基于精度和召回率评估分类器。计划如下:
- 加载
.csv格式的数据。 - 分配类属性。
- 将所有属性从数值转换为标称值,以确保没有错误加载的数值。
- 实验一:用 k 重交叉验证评估模型。
- 实验二:将数据集重新平衡到更均衡的类分布,手动进行交叉验证。
- 通过召回率、精确度和 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-fold 交叉验证。
首先,让我们定义一些我们想要测试的分类器,如下所示:
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 重交叉验证,提供precision、recall和fMeasure作为输出:
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%,这意味着在 9/10 的情况下,当一个索赔被标记为欺诈时,该模型是正确的。
数据集重新平衡
因为与正面例子相比,反面例子(即欺诈的实例)的数量非常少,所以学习算法难以进行归纳。我们可以通过给他们一个数据集来帮助他们,在这个数据集里,正面和负面例子的比例是可以比较的。这可以通过数据集重新平衡来实现。
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%被标记为欺诈的索赔确实是欺诈的。如果未被发现的欺诈比调查假警报的成本高得多,那么处理越来越多的假警报是有意义的。
整体性能很可能仍有一些改进的空间;我们可以执行属性选择和特征生成,并应用更复杂的模型学习,我们在第三章、中讨论了基本算法——分类、回归、聚类。
网站流量中的异常检测
在第二个例子中,我们将重点关注与前一个例子相反的建模。我们将讨论系统的正常预期行为,而不是讨论什么是典型的无欺诈案例。如果某些东西不能与我们预期的模型相匹配,它将被认为是异常的。
资料组
我们将使用雅虎发布的公开数据集。实验室,这对于讨论如何检测时间序列数据中的异常非常有用。对雅虎来说,主要的用例是检测雅虎服务器上不寻常的流量。
即使雅虎已经宣布他们的数据是公开的,你必须申请使用它,并且在批准之前需要大约 24 小时。数据集在webscope.sandbox.yahoo.com/catalog.php?datatype=s&did = 70可用。
该数据集由雅虎服务的真实流量和一些合成数据组成。该数据集总共包含 367 个时间序列,每个时间序列包含 741 至 1,680 个观测值,这些观测值是定期记录的。每个系列都写在自己的文件中,每行一个观察值。一个系列伴随着第二个列指示器,如果观察值是异常值,则使用 1,否则使用 0。真实数据中的异常是由人的判断确定的,而合成数据中的异常是通过算法生成的。下表显示了合成时间序列数据的一个片段:
在下一节中,您将了解如何将时间序列数据转换为属性表示,以便我们应用机器学习算法。
时间序列数据中的异常检测
检测原始流时间序列数据中的异常需要一些数据转换。最明显的方法是选择一个时间窗口,以固定长度对时间序列进行采样。下一步,我们希望将新的时间序列与之前收集的时间序列进行比较,以检测是否有异常。
可以使用各种技术进行比较,如下所示:
- 预测最可能的后续值以及置信区间(例如,霍尔特-温特斯指数平滑)。如果一个新值超出了预测的置信区间,则被认为是异常的。
- 互相关将新样本与阳性样本库进行比较,寻找完全匹配的样本。如果没有找到匹配,则标记为异常。
- 动态时间缠绕类似于互相关,但相比之下允许信号失真。
- 将信号离散化为带,每个带对应一个字母。例如,
A=[min, mean/3]、B=[mean/3, mean*2/3]和C=[mean*2/3, max]将信号转换为字母序列,例如aAABAACAABBA....这种方法减少了存储空间,并允许我们应用文本挖掘算法,我们将在第十章、使用 Mallet 进行文本挖掘-主题建模和垃圾邮件检测中讨论这些算法。 - 基于分布的方法估计特定时间窗口中值的分布。当我们观察一个新的样本时,我们可以比较这个分布是否与之前观察到的分布相匹配。
这份清单绝非详尽无遗。不同的方法侧重于检测不同的异常(例如,值、频率和分布)。在这一章中,我们将集中讨论一种基于分布的方法。
对时间序列使用 Encog
我们必须从 solarscience.msfc.nasa.gov/greenwch/sp…](solarscience.msfc.nasa.gov/greenwch/sp…:
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>>();
我们将需要min和max值来进行直方图归一化;所以,让我们在这个数据通道中收集它们:
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/package metadata/localooutlierfactor/index . html。
LOF 算法有两个实现的接口:作为计算 LOF 值(已知未知)的非监督过滤器,以及作为监督的 k-最近邻分类器(已知已知)。在我们的例子中,我们想要计算异常因子,因此,我们将使用无监督过滤器接口:
import weka.filters.unsupervised.attribute.LOF;
该过滤器的初始化方式与普通过滤器相同。我们可以用-min和-max参数指定k个邻居(例如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 分数。LOF 值约为 1 表示密度大致相等,而较高的 LOF 值表示实例的密度远低于其相邻实例的密度。在这种情况下,实例可以被标记为异常。
摘要
在这一章中,我们研究了异常和可疑模式的检测。我们讨论了两种基本的方法,集中在库编码上,无论是积极的还是消极的模式。接下来,我们接触了两个真实的数据集,并讨论了如何处理不平衡的类分布以及如何对时间序列数据执行异常检测。
在下一章中,我们将更深入地研究模式和更高级的方法来构建基于模式的分类器,并讨论如何使用深度学习自动为图像分配标签。