我们常说机器学习是一门实验科学。所以相比较传统工程而言,机器学习分成两个大的阶段:
- 实验结果
- 工程化阶段
实验阶段通常需要一个好的 Notebook 帮助机器学习的同学快速验证模型的可行性,这包括获取数据,处理数据,训练模型,这几个过程每个都需要反复进行。在实验阶段其实机器学习的同学已经解决了几个核心问题:
- 在哪里获取哪些数据对我们这个要解决的问题有价值
- 这些获得数据要经过怎样的处理才能对要解决的问题有价值
- 哪个模型对解决我们的问题最有价值
第二个阶段就是工程化,工程化也分成两个部分:
- 离线或者实时进行模型构建
- 模型的多场景部署,比如 流,批,API或者边缘部署(比如 iOS/Android等)
考虑到数据决定模型,所以模型需要数据更新才能延长寿命,所以这两个 Pipeline 需要有如下的基本要求:
- 模型需要可持续构建
- 模型更新也应该是日常工作
看着比较简单,实际上,以模型构建阶段为例,他需要一整套大数据系统来支撑数据的获取和特征处理,需要整套 Modern Data Stack 。多模型场景部署也是非常困难的事情,每个部署场景甚至都有可能需要有单独的团队去做。
每个阶段都有很大的Gap,比如实验阶段的代码和逻辑是很难面向大规模数据集,很难自动化运行的,他需要工程阶段的同学进行翻译。这种翻译又引出了另外一个问题:
逻辑一致性校验
这里的逻辑一致性校验又分成两个部分:
- 模型构建特征处理的逻辑一致性
- 预测时特征和离线时的逻辑一致性
每个阶段可能都需要极大人力成本消耗。
所以从上面我们看到,一套能够很好运转的机器学习系统需要有:
- 较好的 Infra
- 算法,研发,和运维 体系的支持
- 研发和算法的高 overhead 协作
而实际上,在现实生活中,这套体系会面临三个大问题:
- 开发周期太长,从开发到上线,需要以月计算。
- 硬件,人员成本可能远高于落地的算法的收益
- 就算愿意投钱,也很难招聘到满足需求的研发和算法
而这三个问题直接导致了在中小企业,AI 难以落地。
先姑且不论 实验阶段到工程阶段的翻译和一致性校验成本,单单模型构建和模型预测里的特征工程导致的开发成本就差不多要整整8人月, OpenMLDB 就给了一个很好的统计:
那如何降低成本呢?关键是要解决如下几个问题:
- 要有一套好的基础 Infra
- 实验阶段和工程化阶段要实现代码尽可能复用,避免完全重写,同时减轻的复杂逻辑校验过程。
- 特征在离线阶段和推理阶段特征处理代码也要尽可能复用,减少重写和减轻复杂的逻辑校验过程。
现在,我们来看看2,3 两个核心痛点是怎么产生的,方便我们理解问题。
首先是实验阶段和工程化阶段模型构建代码复用的问题:
- 实验阶段算法工程师可能会大量 hard-code 诸如路径地址,保存大量临时磁盘文件,甚至只处理部分数据。这显然是无法直接应用到生产环境里的。
- 工具能力不对等。今天 Python 是大部分数据科学家的首选工具;相反,工程化团队一般会首先尝试使用 SQL/Java/Scala等语言去翻译 Python 脚本 从而满足大数据量的吞吐,以及数据访问能力。因此两个工具在表达能力上并不对等。当然这里本质原因是,两个不同阶段的需求不一样,导致工具和能力上的不对等。
- 需求沟通的认知差。数据科学家以及工程化团队对于数据的定义和处理方式的认知可能会不一致,并且互相对对方的背景也很难获得共同。比如让研发工程师去理解算法工程师的一些思路是很困难的,意味他们不懂机器学习,甚至难以校验自己做的是不是对的。以美国的一家线上银行 Varo Bank 描述了一个他们在没有合适工具情况下,实时特征上线时碰到的一个不一致场景(具体可以参照他们工程化团队的博客 Feature Store: Challenges and Considerations)。在上线环境中,工程化团队很自然的认为“账户余额”的定义应该就是实时的账户里的余额;但是对于数据科学家来说,通过离线的数据去构建“实时账户余额”其实是一件相当复杂的事情,因此数据科学家使用了一个更加简单的定义,即昨天结束的时候的账户的余额。很明显,两者对于账户余额的认知差,直接造成了线上线下计算逻辑的不一致性。 当然,这里的本质是协作带来的 Overhead
接着我们来看看模型构建和模型推理中特征工程的代码复用问题。为什么这里也有特征代码复用的问题呢?首先推理阶段也是有 end-2-end需求的,也就是光有一个模型没有用,模型只接受向量或者张量,你也需要把数据先通过特征工程才能喂给模型,模型吐出来的结果,你可能还需要加工一下才能再给出去。其次,推理阶段有非常高的单次请求延迟要求,如果是用Python代码做特征处理的话,可能是没法满足响应延迟的。
我们举几个例子:
- 以实时特征,最近一小时用户的消费总额。算法同学如果是自己用 Python 写的一个逻辑,你就很难直接把用到线上预测中,基本上你肯定要用SQL、Java/C 改写下。
- 还有比如实现了一个 tf/idf 的特征算法,如果他没有写预测逻辑,那么研发是要自己写的。其次就算算法同学已经提供了一个预测的逻辑,但是这个逻辑也没办法保证性能。
那怎么解决这些痛点呢? 我这边给出几个大的指导原则,然后后续我们再看下 Byzer 和 OpenMLDB 是如何落地这几个指导原则的。
大的原则是:
- 使用 SQL 而不是 Python 去完成特征工程
- 尽可能减少 Python 的使用,Python 应该尽可能仅仅用于模型部分
先说这个原则带来的好处,再说说现在这个原则以前为什么没落地,最后我们再探讨我们现在是如何解决落地问题的。
先说说好处:
- SQL易于理解,不会像 Python 不同的人写出来的可读性完全不一样
- SQL 可以通过工程手段保证在实验阶段和工程阶段可移植,翻译的成本大幅度下降
- 从实验阶段到工程阶段能保证性能和吞吐
- 无需逻辑校验成本
说说落地的困难:
- SQL 可能无法完成非常复杂的特征处理逻辑
- Python 在某些时候在可视化亦或是特征处理上是不可避免的
这里值得注意的是,我们不是拒绝 Python 做特征工程,而是尽可能减少使用 Python 做特征工程,从而减少前面探讨的成本。
所以实际上解决方案也比较简单:
- SQL 应该是易于扩展的,从而满足越来越复杂的特征工程需求
- SQL 和 Python 可以应该可以做很好的融合,数据可以在 SQL 和 Python中自由流动
现在,我们来看看 Byzer OpenMLDB 是怎么解决用 SQL 帮助用户解决特征工程落地问题的。
首先我们来看看 SQL 的表达力问题, Byzer 很大程度上提升了 SQL 的表达力,并且非常易于扩展。
下面使用 Byzer 做一个文本分类的工作:
代码语言:javascript复制-- load data
load parquet.`${rawDataPath}` as orginal_text_corpus;
-- select only columns we care
select feature,label from orginal_text_corpus as orginal_text_corpus;
-- feature enginere moduel
train zhuml_orginal_text_corpus as TfIdfInPlace.`${tfidfFeaturePath}`
where inputCol="content"
and `dic.paths`="/data/dict_word.txt"
and stopWordPath="/data/stop_words"
and nGrams="2";
-- use RandomForest
train zhuml_orginal_text_corpus as RandomForest.`/tmp/model` where
keepVersion="true"
and evaluateTable="mock_data_validate"
and `fitParam.0.labelCol`="label"
and `fitParam.0.featuresCol`="features"
and `fitParam.0.maxDepth`="2"
and `fitParam.1.featuresCol`="features"
and `fitParam.1.labelCol`="label"
and `fitParam.1.maxDepth`="10"
;
这段小脚本脚本完成了数据加载,特征工程,最后的训练。其中所有
- 以train开头的,都是模块 ,用户可以自己扩展,从而提升 SQL的表达能力。
- 以select 开头的都是标准 SQL 处理逻辑
- 以load开头的则是各种数据源的加载
上边是模型训练阶段的代码,现在看看预测代码应该怎样的。在预测服务里执行如下两个代码:
代码语言:javascript复制register TfIdfInPlace.`${tfidfFeaturePath}` as tfidf;
register RandomForest.`/tmp/model` as model_predict;
此时,我们可以实现一个端到端的预测了:
代码语言:javascript复制curl -XPOST 'http://127.0.0.1:9004/model/predict' -d 'dataType=row&sql=select vec_argmax(model_predict(vec_dense(tfidf(doc))))&data=[{"doc":"Byzer is cool"}]';
从上面的实力代码,我们可以看到
- 用户实验阶段在 Byzer Notebook 中编写的代码可以几乎毫无更改的迁移到生产环境里。
- 训练时的特征工程也可以几乎毫无更改的转化为 SQL 函数应用于 预测阶段
我们很完美的解决了前面提到的诸多问题,无需工程师翻译,无需算法和工程师进行复杂的逻辑校验,算法几乎可以自己走完完整路径。
此外, Byzer 支持实时书写 UDF 函数,比如执行如下代码:
代码语言:javascript复制register ScriptUDF.`` as arrayLast
where lang="scala"
and code='''def apply(a:Seq[String])={
a.last
}'''
and udfType="udf";
马上就可以分别在模型训练和推理中使用这个函数,比如:
代码语言:javascript复制select arrayLast(array("a","b")) as lastChar as output;
这意味着,很多无法使用原生SQL实现的一些特征工程,用户既可以将其包装成类似
代码语言:javascript复制TfIdfInPlace
这种模块,该模块会自动产生一个SQL UDF 供后续的预测推理使用,也可以自己编写一个 SQL UDF 函数,然后函数就可以在各个场景里使用了。
从这里看到,为了复用训练时的SQL代码, Byzer 的预测变成了组装一堆的SQL UDF 函数,无论是特征工程还是模型预测,都可以转化为一个个UDF 函数,而整个 Pipeline 则是通过 SQL 语句组装这些 UDF, 从而实现了非常好的复用。
而且这些 SQL 函数可以很方便的应用于批,流,API 等场景,参看这篇文章: 祝威廉:如何将Python算法模型注册成Spark UDF函数实现全景模型部署
当然,这里大家也发现了一个问题,如果我的特征是需要从实时数据中实时计算的怎么办?比如一条数据过来了后,我们还要获取一个额外的特征比如最近一小时的用户消费额,此时这个逻辑是可能无法通过一个事先定义好的亦或是来源于某个模块的 UDF 函数来完成的,此外 Byzer 可能在这种实时大规模计算上无法保证毫秒级的响应时间,这个时候就可以引入 OpenMLDB了,我们可以封装一个 UDF 函数调用 OpenMLDB 接口来完成特征的获取,而不是通过 Byzer 自身来完成计算,这样就能很好的覆盖大部分场景了。
接着我们来看和Python的融合,比如在做特征探索的时候,我们会使用Python做数据可视化,这个是SQL 不容易做到的,Python有非常多的可视化库。此外,我们前面也说了,模型我们还是需要使用 Python来做的,所以SQL处理的数据也要很好的能够和 Python 衔接。Byzer 也很好的解决了这个问题。
首先,Byzer 提供了多种形态的 Notebook,而且是以SQL为中心的,帮助算法同学高效率使用 SQL。
- Byzer Notebook (Web 版本)
- Byzer Desktop (桌面版本)
- Byzer Shell (命令行版本)
先来看可视化的问题,在 Byzer Notebook 中,我通过SQL 获取到了一张表 day_pv_uv:
接着我希望对这个 day_pv_uv 进行一个可视化,那我直接在下一个 Cell 里直接操作这个表的数据:
我们用用一行代码将SQL中的数据转化为 Pandas,然后使用 matplotlib 绘制,下面是绘制结果:
所以是非常方便的。
我们可以用相同的方式来获得数据去做算法模型(Byzer-python里支持分布式获取数据,诸如使用 pandas on dask等)。通过 Python 训练好的模型可以直接保存到数据湖:
然后加载这个数据湖的模型,然后将模型注册成UDF 函数:
接着就可以在 SQL 中使用这个函数了:
最后我们来个总结, Byzer 事实上解决了使用 SQL 做特征工程的能力,这个能力不仅仅是完成,而是实现了实验阶段到工程阶段的无缝迁移,离线的特征到在线推理的特征的无缝衔接(当然,实际上 Byzer 也解决了机器学习模型的问题)。而能够使用 SQL 做特征工程,则可以很好的解决前面我们提到的种种问题,人天可能从10天缩短到一天,极大提升了AI落地的效率。配合 OpenMLDB, 可以很好的覆盖在线推理过程中实时大规模计算的特征生成问题。