Bootstrap

集成方法预测客户关系

近接上一篇文章的内容,使用集成方法进行高级建模预测客户关系

创建工程

与上一篇文章的工程保持一致。

集成方法

前面实现了一个定位基准,接下来让我们把关注点放在“重型机械”上。我们将采用 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 个因素引 起:数据预处理与属性选择、多种学习方法的探索以及集成创建技术(该技术可以充分利用各种 基本分类器而不会过度拟合)的应用。然而,性能提升意味着处理时间有明显增加,占用的内存 也会增加。
;