从源码看机器学习平台Alink设计和架构 之 流水线

593 阅读7分钟

0x00 摘要

Alink 是阿里巴巴基于实时计算引擎 Flink 研发的新一代机器学习算法平台,是业界首个同时支持批式算法、流式算法的机器学习平台。本文是漫谈系列的第二篇,将从源码入手,带领大家具体剖析Alink设计思想和架构为何。

因为Alink的公开资料太少,所以均为自行揣测,肯定会有疏漏错误,希望大家指出,我会随时更新。

0x01 Alink设计原则

前文中 [Alink漫谈(一) : 从KMeans算法实现看Alink设计思想] 我们推测总结出Alink部分设计原则

  • 算法的归算法,Flink的归Flink,尽量屏蔽AI算法和Flink之间的联系。

  • 采用最简单,最常见的开发语言和思维方式。

  • 尽量借鉴市面上通用的机器学习设计思路和开发模式,让开发者无缝切换。

  • 构建一套战术打法(middleware或adapter),即屏蔽了Flink,又可以利用好Flink,还能让用户快速开发算法。

下面我们就针对这些设计原则,从上至下看看Alink如何设计自己这套战术打法。

为了能让大家更好理解,先整理一个概要图。因为Alink系统主要可以分成三个层面(顶层流水线, 中间层算法组件, 底层迭代计算框架),再加上一个Flink runtime,所以下图就是分别从这四个层面出发来看程序执行流程。

如何看待 pipeline.fit(data).transform(data).print();

// 从顶层流水线角度看
训练流水线  +-----> [VectorAssembler(Transformer)] -----> [KMeans(Estimator)]
          |      // KMeans.fit之后,会生成一个KMeansModel用来转换
          |      
转换流水线  +-----> [VectorAssembler(Transformer)] -----> [KMeansModel(Transformer)]


// 从中间层算法组件角度看    
训练算法组件 +-----> [MapBatchOp] -----> [KMeansTrainBatchOp]  
           |       // VectorAssemblerMapper in MapBatchOp 是业务逻辑
           |      
转换算法组件 +-----> [MapBatchOp] -----> [ModelMapBatchOp]
                   // VectorAssemblerMapper in MapBatchOp 是业务逻辑
                   // KMeansModelMapper in ModelMapBatchOp 是业务逻辑
 
  
// 从底层迭代计算框架角度看
训练by框架 +-----> [VectorAssemblerMapper] -----> [KMeansPreallocateCentroid / KMeansAssignCluster / AllReduce / KMeansUpdateCentroids in IterativeComQueue]   
          |       // 映射到Flink的各种算子进行训练
          |      
转换(直接) +-----> [VectorAssemblerMapper] -----> [KMeansModelMapper]    
                  // 映射到Flink的各种算子进行转换 
  
// 从Flink runtime角度看  
训练 +-----> map, mapPartiiton...
    |       // VectorAssemblerMapper.map等会被调用
    |      
转换 +-----> map, mapPartiiton...
            // 比如调用 KMeansModelMapper.map 来转换  

0x02 Alink实例代码

示例代码还是用之前的KMeans算法部分模块。

算法调用

public class KMeansExample {
     public static void main(String[] args) throws Exception {
        ......

        BatchOperator data = new CsvSourceBatchOp().setFilePath(URL).setSchemaStr(SCHEMA_STR);

        VectorAssembler va = new VectorAssembler()
            .setSelectedCols(new String[]{"sepal_length", "sepal_width", "petal_length", "petal_width"})
            .setOutputCol("features");

        KMeans kMeans = new KMeans().setVectorCol("features").setK(3)
            .setPredictionCol("prediction_result")
            .setPredictionDetailCol("prediction_detail")
            .setReservedCols("category")
            .setMaxIter(100);

        Pipeline pipeline = new Pipeline().add(va).add(kMeans);
        pipeline.fit(data).transform(data).print();
    }
}

算法主函数

public final class KMeansTrainBatchOp extends BatchOperator <KMeansTrainBatchOp>
 implements KMeansTrainParams <KMeansTrainBatchOp> {

 static DataSet <Row> iterateICQ(...省略...) {

  return new IterativeComQueue()
   .initWithPartitionedData(TRAIN_DATA, data)
   .initWithBroadcastData(INIT_CENTROID, initCentroid)
   .initWithBroadcastData(KMEANS_STATISTICS, statistics)
   .add(new KMeansPreallocateCentroid())
   .add(new KMeansAssignCluster(distance))
   .add(new AllReduce(CENTROID_ALL_REDUCE))
   .add(new KMeansUpdateCentroids(distance))
   .setCompareCriterionOfNode0(new KMeansIterTermination(distance, tol))
   .closeWith(new KMeansOutputModel(distanceType, vectorColName, latitudeColName, longitudeColName))
   .setMaxIter(maxIter)
   .exec();
 }
}  

算法模块举例

基于点计数和坐标,计算新的聚类中心。

// Update the centroids based on the sum of points and point number belonging to the same cluster.
public class KMeansUpdateCentroids extends ComputeFunction {
    @Override
    public void calc(ComContext context) {

        Integer vectorSize = context.getObj(KMeansTrainBatchOp.VECTOR_SIZE);
        Integer k = context.getObj(KMeansTrainBatchOp.K);
        double[] sumMatrixData = context.getObj(KMeansTrainBatchOp.CENTROID_ALL_REDUCE);

        Tuple2<Integer, FastDistanceMatrixData> stepNumCentroids;
        if (context.getStepNo() % 2 == 0) {
            stepNumCentroids = context.getObj(KMeansTrainBatchOp.CENTROID2);
        } else {
            stepNumCentroids = context.getObj(KMeansTrainBatchOp.CENTROID1);
        }

        stepNumCentroids.f0 = context.getStepNo();
        context.putObj(KMeansTrainBatchOp.K,
            updateCentroids(stepNumCentroids.f1, k, vectorSize, sumMatrixData, distance));
    }
}

0x03 顶层 -- 流水线

本部分实现的设计原则是 :尽量借鉴市面上通用的设计思路和开发模式,让开发者无缝切换。

1. 机器学习重要概念

一个典型的机器学习过程从数据收集开始,要经历多个步骤,才能得到需要的输出。这非常类似于流水线式工作,即通常会包含源数据ETL(抽取、转化、加载),数据预处理,指标提取,模型训练与交叉验证,新数据预测等步骤。

先来说一下几个重要的概念:

  • Transformer:转换器,是一种可以将一个数据转换为另一个数据的算法。比如一个模型就是一个 Transformer。它可以把一个不包含转换标签的测试数据集 打上标签,转化成另一个包含转换标签的特征数据。Transformer可以理解为特征工程,即:特征标准化、特征正则化、特征离散化、特征平滑、onehot编码等。该类型有一个transform方法,用于fit数据之后,输入新的数据,进行特征变换。

  • Estimator:评估器,它是学习算法或在训练数据上的训练方法的概念抽象。所有的机器学习算法模型,都被称为估计器。在 Pipeline 里通常是被用来操作 数据并生产一个 Transformer。从技术上讲,Estimator实现了一个方法fit(),它接受一个特征数据并产生一个转换器。比如一个随机森林算法就是一个 Estimator,它可以调用fit(),通过训练特征数据而得到一个随机森林模型。

  • PipeLine:工作流或者管道。工作流将多个工作流阶段(转换器和估计器)连接在一起,形成机器学习的工作流,并获得结果输出。

  • Parameter:Parameter 被用来设置 Transformer 或者 Estimator 的参数。

2. Alink中概念实现

从 Alink的目录结构中 ,我们可以看出,Alink提供了这些常见概念(其中有些代码借鉴了Flink ML)。

./java/com/alibaba/alink:
common  operator params  pipeline
  
./java/com/alibaba/alink/params:
associationrule evaluation nlp  regression statistics
classification feature  onlinelearning shared  tuning
clustering io  outlier  similarity udf
dataproc mapper  recommendation sql  validators

./java/com/alibaba/alink/pipeline:
EstimatorBase.java ModelBase.java  Trainer.java  feature
LocalPredictable.java ModelExporterUtils.java TransformerBase.java nlp
LocalPredictor.java Pipeline.java  classification  recommendation
MapModel.java  PipelineModel.java clustering  regression
MapTransformer.java PipelineStageBase.java dataproc  tuning

比较基础的是三个接口:PipelineStages,Transformer,Estimator,分别恰好对应了机器学习的两个通用概念 :转换器 ,评估器。PipelineStages是这两个的基础接口。

// Base class for a stage in a pipeline. The interface is only a concept, and does not have any actual functionality. Its subclasses must be either Estimator or Transformer. No other classes should inherit this interface directly.
public interface PipelineStage<T extends PipelineStage<T>> extends WithParams<T>, Serializable 

// A transformer is a PipelineStage that transforms an input Table to a result Table.
public interface Transformer<T extends Transformer<T>> extends PipelineStage<T> 
 
// Estimators are PipelineStages responsible for training and generating machine learning models.
public interface Estimator<E extends Estimator<E, M>, M extends Model<M>> extends PipelineStage<E>

其次是三个抽象类定义:PipelineStageBase,EstimatorBase,TransformerBase,分别就对应了以上的三个接口。其中定义了一些基础操作,比如 fit,transform。

// The base class for a stage in a pipeline, either an EstimatorBase or a TransformerBase.
public abstract class PipelineStageBase<S extends PipelineStageBase<S>>
    implements WithParams<S>, HasMLEnvironmentId<S>, Cloneable 
  
// The base class for estimator implementations.
public abstract class EstimatorBase<E extends EstimatorBase<E, M>, M extends ModelBase<M>>
    extends PipelineStageBase<E> implements Estimator<E, M>   
  
// The base class for transformer implementations.
public abstract class TransformerBase<T extends TransformerBase<T>>
    extends PipelineStageBase<T> implements Transformer<T>  

然后是Pipeline基础类,这个类就可以把Transformer,Estimator联系起来 。

// A pipeline is a linear workflow which chains EstimatorBases and TransformerBases to execute an algorithm 
public class Pipeline extends EstimatorBase<Pipeline, PipelineModel> {
 private ArrayList<PipelineStageBase> stages = new ArrayList<>();
  
   public Pipeline add(PipelineStageBase stage) {
  this.stages.add(stage);
  return this;
 }
}

最后是 Parameter 概念相关举例,比如实例中用到的 VectorAssemblerParams。

// Parameters for MISOMapper.
public interface MISOMapperParams<T> extends HasSelectedCols <T>,  HasOutputCol <T>,
 HasReservedCols <T> {}

// parameters of vector assembler.
public interface VectorAssemblerParams<T> extends MISOMapperParams<T> {
ParamInfo <String> HANDLE_INVALID = ParamInfoFactory
  .createParamInfo("handleInvalid", String.class)
  .setDescription("parameter for how to handle invalid data (NULL values)")
  .setHasDefaultValue("error")
  .build();
}

综合来说,因为模型和数据,在Alink运行时候,都统一转化为Table类型,所以可以整理如下:

  • Transformer: 将input table转换为output table。

  • Estimator:将input table转换为模型。

  • 模型:将input table转换为output table。

3. 结合实例看流水线

首先是一些基础抽象类,比如:

  • MapTransformer是 flat map 的Transformer。

  • ModelBase是模型定义,也是一个Transformer。

  • Trainer是训练模型定义,是EstimatorBase。

    // Abstract class for a flat map TransformerBase. public abstract class MapTransformer<T extends MapTransformer > extends TransformerBase implements LocalPredictable {

    // The base class for a machine learning model. public abstract class ModelBase<M extends ModelBase> extends TransformerBase implements Model

    // Abstract class for a trainer that train a machine learning model. public abstract class Trainer<T extends Trainer <T, M>, M extends ModelBase> extends EstimatorBase<T, M>

然后就是我们实例用到的两个类型定义。

  • KMeans 是一个Trainer,其实现了EstimatorBase;

  • VectorAssembler 是一个TransformerBase。

    // 这是一个 EstimatorBase 类型 public class KMeans extends Trainer <KMeans, KMeansModel> implements KMeansTrainParams , KMeansPredictParams { @Override protected BatchOperator train(BatchOperator in) { return new KMeansTrainBatchOp(this.getParams()).linkFrom(in); } }

    // 这是一个 TransformerBase 类型 public class VectorAssembler extends MapTransformer implements VectorAssemblerParams { public VectorAssembler(Params params) { super(VectorAssemblerMapper::new, params); } }

实例中,分别构建了两个流水线阶段,然后这两个实例就被链接到流水线上。

VectorAssembler va = new VectorAssembler()
KMeans kMeans = new KMeans()  
Pipeline pipeline = new Pipeline().add(va).add(kMeans);

// 能看出来,流水线上有两个阶段,分别是VectorAssembler和KMeans。

pipeline = {Pipeline@1201} 
 stages = {ArrayList@2853}  size = 2
   
  0 = {VectorAssembler@1199} 
   mapperBuilder = {VectorAssembler$lambda@2859} 
   params = {Params@2860} "Params {outputCol="features", selectedCols=["sepal_length","sepal_width","petal_length","petal_width"]}"
     
  1 = {KMeans@1200} 
   params = {Params@2857} "Params {vectorCol="features", maxIter=100, reservedCols=["category"], k=3, predictionCol="prediction_result", predictionDetailCol="prediction_detail"}"

下文将介绍算法组件。

0xFF 参考

k-means聚类算法原理简析

flink kmeans聚类算法实现

Spark ML简介之Pipeline,DataFrame,Estimator,Transformer

开源 | 全球首个批流一体机器学习平台

斩获GitHub 2000+ Star,阿里云开源的 Alink 机器学习平台如何跑赢双11数据“博弈”?|AI 技术生态论

Flink DataSet API

★★★★★★关于生活和技术的思考★★★★★★ 

 微信公众账号:罗西的思考