FlinkML: 浅谈二分类预估的实现

285 阅读9分钟

最近工作上需要用到FlinkML做实时训练和流式机器学习。对于卡口车辆图像,人员的二分类有着比较大的作用,在此分析实现.

我会分成三个部分来讲述: 应用场景、FlinkML对于二分类的实现, 如何评估指标

应用场景

比如我们有这样一个数据集

  • 病例历史行动数据集(训练集1) 1M+
  • 确诊病例数据 (测试集1) 500+
  • 实时病例行动数据集(测试集2) 1000+
  • 人脸特征512维

需求:

  • 根据测试集1每条数据的特征向量,在训练集1中找出该病例(人)对应的所有记录。
  • 对测试集2的每条数据,根据其特征向量进行实时分类(人)。

限制条件:

  • 对每条实时数据完成实时分类的响应时间不能超过500ms

FlinkML对于二分类的实现

FlinkML对于二分类选择的实现在BinaryClassificationEvaluator这个类当中. 先讲一下提案中FlinkML关于这块的设计,然后再回归源码看一看

对于模型而言,分为三个阶段,编排,训练,预测.在FLIP-173当中提到了相关实现

编排

FlinkML 提供了Pipeline/PipelineModel,其在功能和使用方式上与SparkML的Pipeline/PipelineModel类似。保持训练和预测过程中数据处理的一致性

管道(Pipeline)的概念源于Scikit-learn。可以将数据处理的过程看成数据在“管道”中流动。管道分为若干个阶段(PipelineStage),数据每通过一个阶段就发生一次变换,数据通过整个管道,也就依次经历了所有变换。

如果要在管道中加入分类器,对数据进行类别预测,就涉及模型的训练和预测,这需要分两个步骤完成。所以,管道也会被细分为管道定义与管道模型(PipelineModel)。在管道定义中,每个PipelineStage会按其是否需要进行模型训练,分为估计器(Estimator)和转换器(Transformer)。随后,可以对其涉及模型的部分,即对估计器(Estimator)进行估计,从而得到含有模型的转换器。该转换器被称为Model,并用Model替换相应的估计器,从而每个阶段都可以直接对数据进行处理。我们将该Model称为PipelineModel

image5454.png

现有的 Flink ML 库允许用户从一个 Estimator/Transformer 的流水线(即线性序列)组成一个 Estimator/Transformer,每个 Estimator/Transformer 有一个输入和一个输出.什么意思呢? 可以看到下面这张图

  • 转换器(Transformer):Pipeline中的处理模块,用于处理Table。输入的是待处理的数据,输出的是处理结果数据。比如,向量归一化是一个常用的数据预处理操作。它就是一个转换器,输入向量数据,输出的数据仍为向量,但是其范数为1。
  • Model:派生于转换器(Transformer)。其可以存放计算出来的模型,用来进行模型预测。其输入的是预测所需的特征数据,输出的是预测结果数据。
  • 估计器(Estimator):估计器是对输入数据进行拟合或训练的模型计算模块,输出适合当前数据的转换器,即模型。输入的是训练数据,输出的是Model。

Pipeline与PipelineModel构成了完整的机器学习处理过程,可以分为三个子过程:定义过程,模型训练过程,数据处理过程。

  • 定义过程:按顺序罗列Pipeline所需的各个阶段。Pipeline由若干个Transformer和Estimator构成,按用户指定的顺序排列,并在逻辑上依次执行。
  • 模型训练过程:使用Pipeline的fit方法,对Pipeline中的Estimator进行训练,得到相应的Model。Pipeline执行fit方法后得到的结果是PipelineModel。
  • 数据处理过程:该过程指的是通过PipelineModel直接处理数据。

二分类模型的用法

<标签,特征向量>
private static final List<Row> INPUT_DATA =
        Arrays.asList(
                Row.of(1.0, Vectors.dense(0.1, 0.9)),
                Row.of(1.0, Vectors.dense(0.2, 0.8)),
                Row.of(1.0, Vectors.dense(0.3, 0.7)),
                Row.of(0.0, Vectors.dense(0.25, 0.75)),
                Row.of(0.0, Vectors.dense(0.4, 0.6)),
                Row.of(1.0, Vectors.dense(0.35, 0.65)),
                Row.of(1.0, Vectors.dense(0.45, 0.55)),
                Row.of(0.0, Vectors.dense(0.6, 0.4)),
                Row.of(0.0, Vectors.dense(0.7, 0.3)),
                Row.of(1.0, Vectors.dense(0.65, 0.35)),
                Row.of(0.0, Vectors.dense(0.8, 0.2)),
                Row.of(1.0, Vectors.dense(0.9, 0.1)));

.......

@Setup
public void setUp() {
    inputDataTable =
        tEnv.fromDataStream(env.fromCollection(INPUT_DATA)).as("label", "rawPrediction");
}

public void testEvaluate() {
    BinaryClassificationEvaluator eval =
            new BinaryClassificationEvaluator()
                    .setMetricsNames(
                            BinaryClassificationEvaluatorParams.AREA_UNDER_PR,
                            BinaryClassificationEvaluatorParams.KS,
                            BinaryClassificationEvaluatorParams.AREA_UNDER_ROC);
    Table evalResult = eval.transform(inputDataTable)[0];
    List<Row> results = IteratorUtils.toList(evalResult.execute().collect());
    assertArrayEquals(
            new String[] {
                BinaryClassificationEvaluatorParams.AREA_UNDER_PR,
                BinaryClassificationEvaluatorParams.KS,
                BinaryClassificationEvaluatorParams.AREA_UNDER_ROC
            },
            evalResult.getResolvedSchema().getColumnNames().toArray());
    Row result = results.get(0);
    for (int i = 0; i < EXPECTED_DATA.length; ++i) {
        assertEquals(EXPECTED_DATA[i], result.getFieldAs(i), EPS);
    }
}
训练

在训练阶段,FLinkML中由AIgoOperator#transform实现.需要计算出该样本的ROC曲线判断模型训练的好坏

假设需要对上述训练的数据出现的次数,在该特征中的得分值做统计. 根据测试集1每条数据的特征向量,在训练集1中找出该病例(人)对应的所有记录。 那么要怎么做呢? 现已经得出一系列样本被划分为正类的概率,然后按照大小排序,下面代码中“label”一栏表示每个测试样本真正的标签(isPositive为true表示正样本,isPositive为false表示负样本),“Score”表示每个测试样本属于正样本的概率。

ROC的计算公式:
fdsgfg.PNG

public Table[] transform(Table... inputs) {
    Preconditions.checkArgument(inputs.length == 1);
    StreamTableEnvironment tEnv =
            (StreamTableEnvironment) ((TableImpl) inputs[0]).getTableEnvironment();
    // 标签列, 原始预测列的参数, 标签对应的打分权重
    DataStream<Tuple3<Double, Boolean, Double>> evalData =
            tEnv.toDataStream(inputs[0])
                    .map(new ParseSample(getLabelCol(), getRawPredictionCol(), getWeightCol()));


    final String boundaryRangeKey = "boundaryRange";
    final String partitionSummariesKey = "partitionSummaries";
    // <特正标签, 算子名称, 精确率,特证向量 >
    // 1.传递特征标签到估算器所输出的边当中
    // 每次训练100条数据
    DataStream<Tuple4<Double, Boolean, Double, Integer>> evalDataWithTaskId =
            BroadcastUtils.withBroadcastStream(
                    Collections.singletonList(evalData),
                    Collections.singletonMap(boundaryRangeKey, getBoundaryRange(evalData)),
                    inputList -> {
                        DataStream input = inputList.get(0);
                        return input.map(new AppendTaskId(boundaryRangeKey));
                    });

    /* Repartition the evaluated data by range. */
    //2.重新打散分区后的数据
    evalDataWithTaskId =
            evalDataWithTaskId.partitionCustom((chunkId, numPartitions) -> chunkId, x -> x.f3);

    /* 根据score分值本地排序的数据.*/
    DataStream<Tuple3<Double, Boolean, Double>> sortEvalData =
            DataStreamUtils.mapPartition(
                    evalDataWithTaskId,
                    new MapPartitionFunction<
                            Tuple4<Double, Boolean, Double, Integer>,
                            Tuple3<Double, Boolean, Double>>() {
                        @Override
                        public void mapPartition(
                                Iterable<Tuple4<Double, Boolean, Double, Integer>> values,
                                Collector<Tuple3<Double, Boolean, Double>> out) {
                            List<Tuple3<Double, Boolean, Double>> bufferedData =
                                    new LinkedList<>();
                            for (Tuple4<Double, Boolean, Double, Integer> t4 : values) {
                                bufferedData.add(Tuple3.of(t4.f0, t4.f1, t4.f2));
                            }
                            bufferedData.sort(Comparator.comparingDouble(o -> -o.f0));
                            for (Tuple3<Double, Boolean, Double> dataPoint : bufferedData) {
                                out.collect(dataPoint);
                            }
                        }
                    });

    // 3.计算每次训练的数据的分区累加值
    DataStream<BinarySummary> partitionSummaries =
            sortEvalData.transform(
                    "reduceInEachPartition",
                    TypeInformation.of(BinarySummary.class),
                    new PartitionSummaryOperator());

    //4.计算ROC曲线
    /* Sorts global data. Output Tuple4 : <score, order, isPositive, weight>. */
    // <分值,排序,正样本,权重>
    DataStream<Tuple4<Double, Long, Boolean, Double>> dataWithOrders =
            BroadcastUtils.withBroadcastStream(
                    Collections.singletonList(sortEvalData),
                    Collections.singletonMap(partitionSummariesKey, partitionSummaries),
                    inputList -> {
                        DataStream input = inputList.get(0);
                        return input.flatMap(new CalcSampleOrders(partitionSummariesKey));
                    });

    DataStream<double[]> localAreaUnderROCVariable =
            dataWithOrders.transform(
                    "AccumulateMultiScore",
                    TypeInformation.of(double[].class),
                    new AccumulateMultiScoreOperator());

    DataStream<double[]> middleAreaUnderROC =
            DataStreamUtils.reduce(
                    localAreaUnderROCVariable,
                    (ReduceFunction<double[]>)
                            (t1, t2) -> {
                                t2[0] += t1[0];
                                t2[1] += t1[1];
                                t2[2] += t1[2];
                                return t2;
                            });

    // 5.ROC计算公式 先对score从大到小排序,然后令最大score对应的sample 的rank为n,
    // 第二大score对应sample的rank为n-1,以此类推。
    // 然后把所有的正类样本的rank相加,再减去M-1种两个正样本组合的情况
    DataStream<Double> areaUnderROC =
            middleAreaUnderROC.map(
                    (MapFunction<double[], Double>)
                            value -> {
                        // <分值0,排序1,正样本2,权重3>
                                if (value[1] > 0 && value[2] > 0) {
                                    return (value[0] - 1. * value[1] * (value[1] + 1) / 2)
                                            / (value[1] * value[2]);
                                } else {
                                    return Double.NaN;
                                }
                            });

    Map<String, DataStream<?>> broadcastMap = new HashMap<>();
    broadcastMap.put(partitionSummariesKey, partitionSummaries);
    broadcastMap.put(AREA_UNDER_ROC, areaUnderROC);
    //6.收集之前打散的各个分区的评估数据
    DataStream<BinaryMetrics> localMetrics =
            BroadcastUtils.withBroadcastStream(
                    Collections.singletonList(sortEvalData),
                    broadcastMap,
                    inputList -> {
                        DataStream input = inputList.get(0);
                        return DataStreamUtils.mapPartition(
                                input, new CalcBinaryMetrics(partitionSummariesKey));
                    });

    DataStream<Map<String, Double>> metrics =
            DataStreamUtils.mapPartition(
                    localMetrics, new MergeMetrics(), Types.MAP(Types.STRING, Types.DOUBLE));
    metrics.getTransformation().setParallelism(1);

    final String[] metricsNames = getMetricsNames();
    TypeInformation<?>[] metricTypes = new TypeInformation[metricsNames.length];
    Arrays.fill(metricTypes, Types.DOUBLE);
    RowTypeInfo outputTypeInfo = new RowTypeInfo(metricTypes, metricsNames);

    DataStream<Row> evalResult =
            metrics.map(
                    (MapFunction<Map<String, Double>, Row>)
                            value -> {
                                Row ret = new Row(metricsNames.length);
                                for (int i = 0; i < metricsNames.length; ++i) {
                                    ret.setField(i, value.get(metricsNames[i]));
                                }
                                return ret;
                            },
                    outputTypeInfo);
    return new Table[] {tEnv.fromDataStream(evalResult)};
}

....

//更新二分类预估值指标
private static void updateBinaryMetrics(
        Tuple3<Double, Boolean, Double> cur,
        BinaryMetrics binaryMetrics,
        long[] countValues,
        double[] recordValues) {
// 正确预测为真,正确预测为假,全部预测为真,全部预测为假

    if (binaryMetrics.count == 0) {
        //正确预测为真/全部预测为真
        recordValues[0] = countValues[2] == 0 ? 1.0 : 1.0 * countValues[0] / countValues[2];
        //正确预测为假/全部预测为假
        recordValues[1] = countValues[3] == 0 ? 1.0 : 1.0 * countValues[1] / countValues[3];
        //正确预测为真的/ (正确预测为真 + 正确预测为假 )(精确率)
        recordValues[2] =
                countValues[0] + countValues[1] == 0
                        ? 1.0
                        : 1.0 * countValues[0] / (countValues[0] + countValues[1]);
        // 所有的预测正确(正类负类)/总样本 (准确率)
        recordValues[3] =
                1.0 * (countValues[0] + countValues[1]) / (countValues[2] + countValues[3]);
    }

    binaryMetrics.count++;
    //cur:
    // maximum score in this partition
    //   f0 -> public double maxScore;
    //  // real positives in this partition
    //  f1 -> public long curPositive;
    // // real negatives in this partition
    //  f2 -> public long curNegative;
    if (cur.f1) {
        countValues[0]++;
    } else {
        countValues[1]++;
    }

    double tpr = countValues[2] == 0 ? 1.0 : 1.0 * countValues[0] / countValues[2];
    double fpr = countValues[3] == 0 ? 1.0 : 1.0 * countValues[1] / countValues[3];
    double precision =
            countValues[0] + countValues[1] == 0
                    ? 1.0
                    : 1.0 * countValues[0] / (countValues[0] + countValues[1]);
    double positiveRate =
            1.0 * (countValues[0] + countValues[1]) / (countValues[2] + countValues[3]);

    binaryMetrics.areaUnderLorenz +=
            ((positiveRate - recordValues[3]) * (tpr + recordValues[0]) / 2);
    binaryMetrics.areaUnderPR += ((tpr - recordValues[0]) * (precision + recordValues[2]) / 2);
    binaryMetrics.precision += precision ;
    binaryMetrics.recall = tpr ;
    binaryMetrics.ks = Math.max(Math.abs(fpr - tpr), binaryMetrics.ks);

    recordValues[0] = tpr;
    recordValues[1] = fpr;
    recordValues[2] = precision;
    recordValues[3] = positiveRate;
}

AUC计算公式请见: www.plob.org/article/228…

如何评估指标

机器学习要建模,但是对于模型性能的好坏我们并不知道是怎样的,很可能这个模型就是一个差的模型,对测试集不能很好的预测。那么如何知道这个模型是好是坏呢?必须有个评判的标准,需要用某个指标来衡量,这就是性能度量的意义。有了一个指标,就可以对比不同模型了,从而知道哪个模型更好,或者通过这个指标来调参优化选用的模型。

评估一个二分类的分类器的性能指标有:准确率、查准率、查全率、F1值、AUC/ROC。如下所示

对于二分类模型,预测情况与实际情况会得出2*2=4种组合,形成混淆矩阵:

 预测正例预测反例
实际正例    TP: True PositiveFN: False Negative
实际反例FP: False PositiveTN: True Negative
真正(True  Positive , TP):被模型预测为正的正样本

假正(False Positive , FP):被模型预测为正的负样本

假负(False Negative , FN):被模型预测为负的正样本

真负(True  Negative , TN):被模型预测为负的负样本

性能指标

**准确率(Accuracy):  Accuracy = (TP+TN)/(TP+FN+FP+TN)**

即正确预测的正反例数 /预测总数。准确率是预测正确的结果占总样本的百分比,**是很自然就会想到的指标,但很多项目场景都不适用!最主要的原因是样本不平衡。** 举个简单的例子,比如在一个总样本中,正样本占90%,负样本占10%,样本是严重不平衡的。对于这种情况,我们只需要将全部样本预测为正样本即可得到90%的高准确率,但实际上我们并没有很用心的分类,只是随便无脑一分而已。这就说明了:由于样本不平衡的问题,导致了得到的高准确率结果含有很大的水分。**即如果样本不平衡,准确率就会失效。**

 

**精确率(Precision): Precision = TP/(TP+FP)**

即正确预测的正例数 /预测正例总数。可理解为查准率。在预测为正的记录中,有多少实际为正?

 

**召回率(Recall): Recall = TP/(TP+FN)**

即正确预测的正例数 /实际正例总数 。可理解为查全率。在实际为正的记录中,有多少预测为正?

 

**F1 score : 2/F1 = 1/Precision + 1/Recall**

精确率和召回率的调和值。由于Precision和Recall是一对不可调和的矛盾,很难同时提高二者,也很难综合评价。故提出F1来试图综合二者,F1是P和R的调和平均。F1更接近于两个数较小的那个,所以精确率和召回率接近时值最大。很多推荐系统会用的评测指标。

 

**ROC曲线: 以假正率FPR为横轴,以真正率TPR为纵轴,绘制的曲线**

FPR表示模型虚报的响应程度,而TPR表示模型预测响应的覆盖程度。我们希望:虚报的越少越好,覆盖的越多越好。总结一下就是FPR越低TPR越高(即ROC曲线越陡),那么模型就越好。ROC曲线无视样本不平衡。

画曲线的用意是:用假正率与真正率的变化趋势,来观察模型是否能在较低的假正率下得到较高的真正率。

 

**AUC: Area under the ROC curve**

**绘制ROC曲线时,横轴纵轴都是0~1,形成一个1*1的正方形。AUC就是在这个正方形里ROC曲线围成的面积。**

如果连接正方形的对角线,它的面积正好是0.5。对角线的实际含义是:随机判断响应与不响应,正负样本覆盖率都是50%,即AUC =0.5表示随机分类器。AUC < 0.5表示差于随机分类器,没有建模价值;AUC = 1表示完美分类器,不存在;0.5 < AUC < 1,优于随机分类器,大多模型都在这个区间里。

 

AUC的一般判断标准

0.5 - 0.7:效果较低,但用于预测股票已经很不错了

0.7 - 0.85:效果一般

0.85 - 0.95:效果很好

0.95 - 1:效果非常好,但基本不太可能

AUC与ROC的详细解释: https://www.jianshu.com/p/f07a79dfeb1d

对比了一下Alink的预估指标 在目前的二分类模型当中,评估指标只有Roc支持,准确率,召回率,F1,以及AUC都需要补充,实现当中

4676767.PNG