近接上一篇文章的内容,使用集成方法进行高级建模预测客户关系
创建工程
与上一篇文章的工程保持一致。
集成方法
前面实现了一个定位基准,接下来让我们把关注点放在“重型机械”上。我们将采用
KDD Cup 2009获奖方案(由
IBM Research
团队开发,
Niculescu-Mizil
等,
2009
)使用的方法。 应对挑战过程中,IBM Research
团队使用的是
集成选择
(
Ensemble Selection
)算法(
Caruana 与Niculescu-Mizil
,
2004
)。它是一个集成方法,创建了一系列模型,并且按照特定方式将其输出 组合在一起,形成最终分类结果。这个方法拥有几个优点,使得它非常适用于此次挑战。
这个方法非常健壮,并且拥有良好性能,这些优点已经得到证实。
它可以针对特定性能指标做优化,包括
AUC
。
它允许将不同分类器添加到库。
它是一个任何时候都适用的方法,这意味着,如果我们用完了时间,仍有解决方案可用。
本节大致根据他们在汇报中描述的步骤进行。请注意,我们并非要准确实现他们的方法,而 只实现方案的大概,包括必需的深入步骤。
步骤概述如下:
(1)
首先,对数据做预处理,剔除没有价值的属性,比如所有缺失值与常数值。修复缺失值 以帮助机器学习算法对其进行处理,将分类属性转换为数值;
(2)
接着,运行属性选择算法,选择属性的一个子集,用在任务预测中;
(3)
第三步,使用多种模型将集成选择算法实例化,最后评估其性能。
数据预处理
首先,使用
Weka
内置的
weka.filters.unsupervised.attribute.RemoveUseless
过 滤器移除无用属性。借助该过滤器,可以从数据中移除那些变化不大的属性,比如所有常量属性, 并且移除那些变化太过剧烈(变化接近随机)的属性。可以通过-M
参数指定最大方差(
maximum variance),它只应用于名义属性(
nominal attributes
)。其默认值是
99%
,这意味着,对于某个属 性,如果超过99%
的实例拥有唯一的属性值,那么该属性就会被移除
// 移除未选择属性
RemoveUseless removeUseless = new RemoveUseless(); // 创建移除无用属性的过滤器
removeUseless.setOptions(new String[]{"-M", "99"}); // 设置移除信息量低于 99% 的属性
removeUseless.setInputFormat(data); // 设置输入格式
data = Filter.useFilter(data, removeUseless); // 应用过滤器
接下来,通过
weka. filters.unsupervised.attribute.ReplaceMissingValues
过 滤器使用从训练数据得到的众数(名义属性)与平均数(数值属性),以此代替数据集中的所有 缺失值。一般来说,对缺失值做替换处理时要多加小心,同时也要考虑属性的含义与上下文环境。
// 填充缺失值
ReplaceMissingValues filterMissing = new ReplaceMissingValues(); // 创建填充缺失值的过滤器
filterMissing.setInputFormat(data); // 设置输入格式
data = Filter.useFilter(data, filterMissing); // 应用过滤器
最后,对数值型属性做离散化处理,也就是使用
weka.filters.unsupervised.attribute. Discretize过滤器将数值型属性变换为区间(
intervals
)。使用
-B
选项,将数值型属性划分为
4 个区间,使用-R
选项指定属性范围(只有数值型属性会被离散化):
// 离散化
Discretize discretize = new Discretize(); // 创建离散化过滤器
discretize.setOptions(new String[]{
"-O", "-M", "-1.0", "-B", "4", "-R", "first-last" // 设置离散化参数
});
filterMissing.setInputFormat(data); // 设置输入格式
data = Filter.useFilter(data, filterMissing); // 应用过滤器
属性选择
下面只选择包含有用信息的属性,也就是说,选择那些可能对预测更有帮助的属性。这个问 题的标准做法是,检查每个属性包含的信息增益。我们将使用weka.attributeSelection. AttributeSelection过滤器进行属性选择,它需要另外两个方法:一个是评估器,指出如何 计算属性的有用性;另一个是搜索算法,指出如何选择属性子集。
我们的示例中,先初始化
weka.attributeSelection.InfoGainAttributeEval
,它实 现了对信息增益的计算。
为了从高于某个阈值的属性中只选择顶级属性,初始化
weka. attributeSelection. Ranker,对高于特定阈值且带有信息增益的属性进行排列。使用
-T
参数进行指定,同时保持低 阈值,让属性至少带有一些信息。
// 评估和选择
InfoGainAttributeEval eval = new InfoGainAttributeEval(); // 创建信息增益评估器
Ranker ranker = new Ranker(); // 创建排序器
ranker.setOptions(new String[]{"-T", "0.001"}); // 设置排序器参数
接下来,初始化
AttributeSelection
类,设置评估器(
evaluator
)与排行器(
ranker
),并
且向数据集应用属性选择。
AttributeSelection attributeSelection = new AttributeSelection(); // 创建属性选择器
attributeSelection.setEvaluator(eval); // 设置评估器
attributeSelection.setSearch(ranker); // 设置搜索方法
attributeSelection.SelectAttributes(data); // 选择属性
最后,调用
reduceDimensionality(Instances)
方法,移除上次运行未被选中的那些属性。
data = attributeSelection.reduceDimensionality(data); // 降维
return data; // 返回预处理后的数据集
最终,从
230
个属性中保留了
214
个属性。
模型选择
多年来,活跃在机器学习领域的实践者们开发了各种各样的学习算法,并对一些现有算法做 了大量改进。独特的监督学习方法数量众多,很难全部记录。由于数据集的特征各不相同,所以 不可能有适用于所有情况的通用方法。不同算法可以利用一个给定数据集的不同特征与关系。集 成选择算法的特征就是尝试充分利用各种算法分析数据集(Jung
,
2005
)。
直观地说,集成选择算法的目标是自动检测并组合这些独特算法的力量,使整体效 果好于其中任何一个。这通过创建一个库来实现,其目的是尽可能多地利用这些不同的 学习方法。这种过量生成大量模型的范式与传统的集成方法有很大不同。到目前为止, 我们的结果非常鼓舞人心。
首先需要创建模型库。通过初始化
weka.classifiers. EnsembleLibrary
类实现,这个 类会帮助我们定义模型。
// 集成方法
EnsembleLibrary ensembleLibrary = new EnsembleLibrary();
接着,使用字符串向库添加模型及参数。比如使用不同参数向库添加两个决策树学习器
// 添加各种模型到集成库中
ensembleLibrary.addModel("weka.classifiers.trees.J48 -S -C 0.25 -B -M 2");
ensembleLibrary.addModel("weka.classifiers.trees.J48 -S -C 0.25 -B -M 2 -A");
为了完成示例,继续添加如下算法及其参数。
朴素贝叶斯算法,用作默认基准:
ensembleLib.addModel("weka.classifiers.bayes.NaiveBayes");
基于懒惰模型(
lazy models
)的
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");
决策树桩(
Decision stump
),基于单层决策树的集成方法:
ensembleLib.addModel("classifiers.trees.DecisionStump");
ensembleLibrary.addModel("weka.classifiers.bayes.NaiveBayes");
ensembleLibrary.addModel("weka.classifiers.lazy.IBk");
ensembleLibrary.addModel("weka.classifiers.meta.AdaBoostM1");
ensembleLibrary.addModel("weka.classifiers.meta.LogitBoost");
ensembleLibrary.addModel("weka.classifiers.functions.SMO");
ensembleLibrary.addModel("weka.classifiers.functions.Logistic");
ensembleLibrary.addModel("weka.classifiers.functions.SimpleLogistic");
由于
EnsembleLibrary
的实现主要针对
GUI
与控制台用户,所以必须调用
saveLibrary (File,EnsembleLibrary, JComponent)方法将模型存储到一个文件
// 获取资源路径并构造模型库文件路径
String PATH = ClassUtils.getDefaultClassLoader().getResource("orange_small_train.data").getPath();
String SUFFIX = PATH.substring(0, PATH.lastIndexOf("/")) + "/";
String libraryFilePath = SUFFIX + "ensembleLib.model.xml";
// 保存模型库到XML文件
EnsembleLibrary.saveLibrary(new File(libraryFilePath), ensembleLibrary, null);
// 打印模型库中的模型列表
System.out.println(ensembleLibrary.getModels());
接着实例化
weka. classifiers.meta.EnsembleSelection
类,对集成选择算法进行初 始化。先了解方法的各个选项,如下所示。
-L </path/to/modelLibrary>
:指定
modelLibrary
文件,延伸所有模型列表。
-W </path/to/working/directory>
:指定存储所有模型的工作目录。
-B <numModelBags>
:设置包(
bag
)数目,即执行集成选择算法的迭代次数。
-E <modelRatio>
:设置库中模型的比例,这些模型会被随机选择迁移到模型的每个包。
-V <validationRatio>
:设置保留用于验证的训练数据集的比例。
-H <hillClimbIterations>
:设置每个模型包上要执行的爬山迭代次数。
-I <sortInitialization>
:设置集成库的比例,分类初始化算法将从该集成库选出, 同时为每个模型包初始化集成方法。
-X <numFolds>
:设置交叉验证的折数。
-P <hillclimbMettric>
:指定爬山算法期间用作模型选择的指标,有效指标有 accuracy、
rmse
、
roc
、
precision
、
recall
、
fscore
等。
-A <algorithm>
:指定集成选择使用的算法,有效算法有
forward
(默认,前进选择法)、 backward(后退消去法)、
both
(前进与后退消去法)、
best
(简单打印集成库中的顶部执 行者)、library
(只训练集成库中的模型)。
-R
:该标记指示集成方法可否多次选择模型。
-G
:该项指定性能降低时分类初始化是否停止添加模型。
-O
:该选项用于详细输出。打印所有选中的模型性能。
-S<num>
:设置随机数种子值(默认
1
)。
-D
:开启该选项后,分类器将在调试模式下运行,并可能向控制台输出额外信息。 使用如下初始参数对算法进行初始化,并指定优化ROC
指标:
// 创建和配置集成选择模型
EnsembleSelection ensembleSelection = new EnsembleSelection();
ensembleSelection.setOptions(new String[]{
"-L", libraryFilePath, // 模型库文件路径
"-W", SUFFIX + "esTmp", // 工作目录
"-B", "10", // 袋的数量
"-E", "1.0", // 模型比例
"-V", "0.25", // 验证比例
"-H", "100", // 山顶爬升迭代次数
"-I", "1.0", // 排序初始化比例
"-X", "2", // 交叉验证折数
"-P", "roc", // 山顶爬升指标
"-A", "forward", // 算法类型
"-R", "true", // 允许模型重复选择
"-G", "true", // 排序初始化贪婪停止
"-O", "true", // 详细输出
"-S", "1", // 随机数种子
"-D", "true" // 调试模式
});
性能评估
做性能评估要耗费计算机大量计算力与内存,因此要保证使用额外的堆空间(比如 java–Xmx16g)对
JVM
做初始化。即便如此,整个计算可能也要耗费几小时或者几天,具体取决于添加到模型库的算法数量。我们的示例将耗费4
小时
22
分(
12-core Intel Xeon E5-2420 CPU
, 32 GB内存),平均需要占用
10% CPU
与
6 GB
内存。
// 评估集成选择模型并获取评估结果
double[] resultES = evaluate(ensembleSelection);
// 打印集成方法的评估结果
System.out.println("ensemble method评估结果:");
System.out.println("tchurn:" + resultES[0]);
System.out.println("tappetency:" + resultES[1]);
System.out.println("tupselling:" + resultES[2]);
System.out.println("平均模型评估结果:" + resultES[3]);
结果
Ensamble
churn: 0.7109874158176481
appetency: 0.786325687118347
up-sell: 0.8521363243575182
overall: 0.7831498090978378
完整代码
public static int PREDICT_CHURN = 0, PREDICT_APPETENCY = 1, PREDICT_UPSELLING = 3;
public static void main(String[] args) throws Exception {
// 集成方法
EnsembleLibrary ensembleLibrary = new EnsembleLibrary();
// 添加各种模型到集成库中
ensembleLibrary.addModel("weka.classifiers.trees.J48 -S -C 0.25 -B -M 2");
ensembleLibrary.addModel("weka.classifiers.trees.J48 -S -C 0.25 -B -M 2 -A");
ensembleLibrary.addModel("weka.classifiers.bayes.NaiveBayes");
ensembleLibrary.addModel("weka.classifiers.lazy.IBk");
ensembleLibrary.addModel("weka.classifiers.meta.AdaBoostM1");
ensembleLibrary.addModel("weka.classifiers.meta.LogitBoost");
ensembleLibrary.addModel("weka.classifiers.functions.SMO");
ensembleLibrary.addModel("weka.classifiers.functions.Logistic");
ensembleLibrary.addModel("weka.classifiers.functions.SimpleLogistic");
// 获取资源路径并构造模型库文件路径
String PATH = ClassUtils.getDefaultClassLoader().getResource("orange_small_train.data").getPath();
String SUFFIX = PATH.substring(0, PATH.lastIndexOf("/")) + "/";
String libraryFilePath = SUFFIX + "ensembleLib.model.xml";
// 保存模型库到XML文件
EnsembleLibrary.saveLibrary(new File(libraryFilePath), ensembleLibrary, null);
// 打印模型库中的模型列表
System.out.println(ensembleLibrary.getModels());
// 创建和配置集成选择模型
EnsembleSelection ensembleSelection = new EnsembleSelection();
ensembleSelection.setOptions(new String[]{
"-L", libraryFilePath, // 模型库文件路径
"-W", SUFFIX + "esTmp", // 工作目录
"-B", "10", // 袋的数量
"-E", "1.0", // 模型比例
"-V", "0.25", // 验证比例
"-H", "100", // 山顶爬升迭代次数
"-I", "1.0", // 排序初始化比例
"-X", "2", // 交叉验证折数
"-P", "roc", // 山顶爬升指标
"-A", "forward", // 算法类型
"-R", "true", // 允许模型重复选择
"-G", "true", // 排序初始化贪婪停止
"-O", "true", // 详细输出
"-S", "1", // 随机数种子
"-D", "true" // 调试模式
});
// 评估集成选择模型并获取评估结果
double[] resultES = evaluate(ensembleSelection);
// 打印集成方法的评估结果
System.out.println("ensemble method评估结果:");
System.out.println("tchurn:" + resultES[0]);
System.out.println("tappetency:" + resultES[1]);
System.out.println("tupselling:" + resultES[2]);
System.out.println("平均模型评估结果:" + resultES[3]);
}
public static Instances loadData(String pathData, String pathLabel) throws Exception {
// 加载数据
CSVLoader loader = new CSVLoader(); // 创建 CSV 加载器
loader.setFieldSeparator("\t"); // 设置字段分隔符为制表符
loader.setNominalAttributes("191-last"); // 设置第 191 列到最后一列为名义属性
loader.setSource(new File(pathData)); // 设置数据文件路径
Instances data = loader.getDataSet(); // 加载数据集
// 移除 String 属性类型
RemoveType removeType = new RemoveType(); // 创建移除特定类型属性的过滤器
removeType.setOptions(new String[]{"-T", "string"}); // 设置移除 String 类型属性
removeType.setInputFormat(data); // 设置输入格式
Instances filteredData = Filter.useFilter(data, removeType); // 应用过滤器
// 加载标签
loader = new CSVLoader(); // 重新创建 CSV 加载器
loader.setFieldSeparator("\t"); // 设置字段分隔符为制表符
loader.setNoHeaderRowPresent(true); // 设置没有标题行
loader.setNominalAttributes("first-last"); // 设置所有列为名义属性
loader.setSource(new File(pathLabel)); // 设置标签文件路径
Instances labels = loader.getDataSet(); // 加载标签数据
// 补充标签作为类值
Instances labeledData = Instances.mergeInstances(filteredData, labels); // 合并数据和标签
labeledData.setClassIndex(labeledData.numAttributes() - 1); // 设置最后一列为类值
return labeledData; // 返回带标签的数据集
}
public static Instances preProcessData(Instances data) throws Exception {
// 移除未选择属性
RemoveUseless removeUseless = new RemoveUseless(); // 创建移除无用属性的过滤器
removeUseless.setOptions(new String[]{"-M", "99"}); // 设置移除信息量低于 99% 的属性
removeUseless.setInputFormat(data); // 设置输入格式
data = Filter.useFilter(data, removeUseless); // 应用过滤器
// 填充缺失值
ReplaceMissingValues filterMissing = new ReplaceMissingValues(); // 创建填充缺失值的过滤器
filterMissing.setInputFormat(data); // 设置输入格式
data = Filter.useFilter(data, filterMissing); // 应用过滤器
// 离散化
Discretize discretize = new Discretize(); // 创建离散化过滤器
discretize.setOptions(new String[]{
"-O", "-M", "-1.0", "-B", "4", "-R", "first-last" // 设置离散化参数
});
filterMissing.setInputFormat(data); // 设置输入格式
data = Filter.useFilter(data, filterMissing); // 应用过滤器
// 评估和选择
InfoGainAttributeEval eval = new InfoGainAttributeEval(); // 创建信息增益评估器
Ranker ranker = new Ranker(); // 创建排序器
ranker.setOptions(new String[]{"-T", "0.001"}); // 设置排序器参数
AttributeSelection attributeSelection = new AttributeSelection(); // 创建属性选择器
attributeSelection.setEvaluator(eval); // 设置评估器
attributeSelection.setSearch(ranker); // 设置搜索方法
attributeSelection.SelectAttributes(data); // 选择属性
data = attributeSelection.reduceDimensionality(data); // 降维
return data; // 返回预处理后的数据集
}
public static double[] evaluate(Classifier model) throws Exception {
double[] results = new double[4]; // 存储评估结果
String[] labelFiles = new String[]{
"churn", "appetency", "upselling" // 标签文件名
};
double overallScore = 0.0; // 总体评分
// 获取数据路径
String PATH = ClassUtils.getDefaultClassLoader().getResource("orange_small_train.data").getPath();
String LABEL_SUFFIX = PATH.substring(0, PATH.lastIndexOf("/")) + "/";
// 遍历每个标签文件
for (int i = 0; i < labelFiles.length; i++) {
// 加载数据
Instances train_data = loadData(PATH, LABEL_SUFFIX + "orange_small_train_" + labelFiles[i] + ".labels.txt");
train_data = preProcessData(train_data); // 预处理数据
// 评估模型
Evaluation evaluation = new Evaluation(train_data); // 创建评估器
evaluation.crossValidateModel(model, train_data, 5, new Random(1), new Object[]{}); // 5 折交叉验证
results[i] = evaluation.areaUnderROC(train_data.classAttribute().indexOfValue("1")); // 计算 AUC
overallScore += results[i]; // 累加评分
}
results[3] = overallScore / labelFiles.length; // 计算平均评分
return results; // 返回评估结果
}
结论
总体来说,相比于本章开始时我们设计的初始参考基准,这个方法带来了明显的性能提升, 提升幅度超过15
个百分点。然而很难据此得出一个确切的结论,因为性能提升主要由
3
个因素引 起:数据预处理与属性选择、多种学习方法的探索以及集成创建技术(该技术可以充分利用各种 基本分类器而不会过度拟合)的应用。然而,性能提升意味着处理时间有明显增加,占用的内存 也会增加。