最近工作上需要用到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
现有的 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的计算公式:
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 Positive | FN: False Negative |
| 实际反例 | FP: False Positive | TN: 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都需要补充,实现当中