技术实践干货 | 初探大规模 GBDT 训练

本文作者: 字节,观远数据首席科学家。主导多个AI项目在世界500强的应用落地,多次斩获智能零售方向Hackathon冠军。曾就职于微策略,阿里云,拥有十多年的行业经验。

本文是此前评估在 Spark 上做大规模 GBDT 训练时写的一篇入门级教程与框架评估。目前市面上似乎没有多少使用 Spark 来跑 GBDT 的分享,故分享出来看看是否有做过类似场景的同学可以一道交流。

背景

在服务一些客户做商业问题的机器学习建模时,我们会碰到不少拥有非常大量数据且对模型 pipeline 运行有一定要求的情况。相比直接的单机 Python 建模,这类项目有一些难点:

1. 数据量大。 由于预测粒度较细,导致历史数据量非常巨大。一些场景的 pilot 项目中已经达到近千万级别的训练数据量,后续拓展到整个业务线,数据量会超过十亿甚至百亿行级别。

2. 整体流程运行时间有一定要求。 一般模型所依赖的上游数据会在半夜开始通过一系列 ETL 任务从业务系统导入到 Hive 数仓中,大约在凌晨 3 点后,各类预测所需数据会准备就绪。接下来运行整个取数,清洗,特征,训练,预测,业务系统对接全流程,需要在早上 8 点前完成并下发到业务系统中,整体运行时间必须控制在 5 小时以内。

3. 对监控运维等方面的高要求。 海量数据细粒度的预测,覆盖非常多业务人员的日常工作需求,因而也会受到更多的审视与挑战。如何确保模型预测输出的稳定性,在业务反馈问题后又如何快速定位排查,遇到数据不可用,服务器 down 机等突发情况,有什么样的备选方案确保整体流程的稳定运行,都是需要考虑的问题。

从前两点来看,我们之前习惯的单机 Pandas + lgb/xgb 建模思路已经难以适用(除非搞台神威·太湖之光之类的机器),所以我们需要引入目前大数据界的当红炸子鸡 – Spark 来协助完成此类项目。

部署 Spark

要玩 Spark,第一步是部署。如果是本机测试运行,一般跑一个 pip install pyspark 就能把一个 local 节点跑起来了,非常的方便。如果想部署一个相对完整一点的 standalone 集群,可以参考以下步骤:

  1. 到 Spark 官方网站[1] 下载 Spark。当时最新的稳定版本是 2.4.5,下载 pre-built for Apache Hadoop 2.7 就好。
  2. 解压缩,做一些简单的配置文件配置。在解压开的 spark 目录下,进入到 conf 目录里,会看到一系列配置的 template。把需要自定义的配置 copy 一份,例如:cp spark-env.sh.template spark-env.sh,然后进行编辑。我改的一些配置具体如下:
spark-env.sh
# 配置 master 和 worker

SPARK_MASTER_HOST=0.0.0.0
SPARK_DAEMON_MEMORY=4g
SPARK_WORKER_CORES=6
SPARK_WORKER_MEMORY=36g
slaves
# 指定 slaves 机器的列表,这里就选了本机

localhost
spark-defaults.conf
# 这个文件很多教程都会让你改,说是 spark-submit 命令会默认从这里读取相关配置
# 但要注意我们写的 PySpark 程序很多时候并不是通过 spark-submit 命令提交的,所以这里改了可能没用

spark.driver.memory 4g
  1. 启动集群。直接运行 sbin/start-all.sh 即可。或者也可以分别起 master 和 slave,运行./sbin/start-master.sh 和 ./sbin/start-slave.sh spark://127.0.0.1:7077 -c 6 -m 36G 即可。

  2. 停止集群。命令与上面非常类似,sbin/stop-all.sh ,或者分别停 slave 和 master 都行。

这样就算部署完了!其中 spark master 会有一个监听 8080 端口的 web-ui,worker 会监听 8081,后面提交 application 就会有监听 4040 端口的管理界面,功能强大,用户友好度强。

跑第一个 PySpark 程序

直接上代码:

from pyspark.sql import SparkSession

spark = (SparkSession.builder
         .master('spark://127.0.0.1:7077')
         .appName('zijie')
         .getOrCreate())
df = spark.read.parquet('data/the_only_data_i_ever_wanted.parquet')
df.show()

我们的大数据平台就跑起来了。

Spark 与 Pandas 的一些不同之处

在网上看一些 Spark 相关的介绍应该很快会有一些认识。有几个比较明显的区别点我大致列一下:

  1. Spark 里对 DataFrame 的操作大多是 lazy 的,也就是所谓的 transformation,只有少数的 action,例如 take, count, collect 等会真实进行计算返回结果。而 pandas 只要做了操作就会立刻执行。

  2. Pandas 里对性能方面的关注主要是这个操作能不能利用底层的计算库做 vectorize,而在 Spark 里需要关注的点就太多了,可能比较主要的是看怎么尽量减少 shuffle 这类宽依赖吧。当然还有什么数据倾斜等相关高级话题。

  3. 用 Spark 来做算法相关的应用时,要非常注意整体的计算逻辑(数据 lineage),对需要反复用到的数据集,一定要记得 cache/persist/checkpoint 才行(这条不知是否过时了)。

从实际操作来看,在 PySpark 中其实有很多操作长得跟 Pandas 非常类似,比如我们常用的 df[df['date'] > '2020-01-01'] 之类的写法。当然区别也有不少,所以后来 Databricks 干脆推出了一个 Koalas 的库来支持更平滑的切换。

Spark 特征工程

这里主要记录几个在项目过程中写的感觉比较好玩的,并对比 pandas 的版本方便大家理解。

日期填充

pandas version:

# 对每家店每个 SKU 历史无销售情况进行填零处理
def fill_dates(df):
    new_df = []
    for store_id in df.store_id.unique():
        for sku in df.query('store_id == @store_id').sku.unique():
            tmp = pd.DataFrame()
            cond = (df.store_id == store_id) & (df.sku == sku)
            min_date = df.loc[cond, 'date'].min()
            max_date = df.loc[cond, 'date'].max()
            dates_in_between = daterange(min_date, max_date)
            tmp['date'] = dates_in_between
            tmp['sku'] = sku
            tmp['store_id'] = store_id
            new_df.append(tmp)
    new_df = pd.concat(new_df)
    new_df = new_df.merge(df, on=['date', 'sku', 'store_id'], how='left').fillna(0)
    return new_df

可以看到整体逻辑就是取所有 store, sku 的组合,然后找到每个组合最小最大的售卖日期,把中间的日期都填上。

PS: 这段代码应该效率不高,后续我们又迭代了几个版本。

Spark version:

from pyspark.sql import functions as F

def fill_dates_spark(df):
    tmp = df.groupby(['store_id', 'sku']).agg(F.min('date').cast('date').alias('min_date'),
                                              F.max('date').cast('date').alias('max_date'))
    tmp = tmp.withColumn('date', F.explode(F.sequence('min_date', 'max_date'))).select(
        ['date', 'store_id', 'sku'])
    new_df = tmp.join(df, ['date', 'store_id', 'sku'], 'left').fillna(0, subset=['y'])
    return new_df

用了 sequence+explode 操作,代码简洁很多。其中 sequence 会自动生成从 start 到 end 的序列(时间,数字都支持),explode 操作直接把一行“炸开”成多行,省去了 join 操作,性能也更好。

Lag 特征

这个是我们最常用的一种特征了,在 pandas 里主要就是做循环 join:

def shift_daily_data(df, delay, shift_by='date', shift_value='y'):
    groupby_df = [x for x in df.columns if (x != shift_by) and (x != shift_value)]
    shift_df = df.copy()
    shift_df[shift_by] = shift_df[shift_by].apply(lambda x: x + relativedelta(days=delay))
    shift_df = shift_df.rename(columns={shift_value: '%s_%s_day_lag_%d' % ('_'.join(groupby_df), shift_value, delay)})
    return shift_df

def add_daily_shifts(df, days, categories, shift_by='date', shift_value='y'):
    merge_df = df.copy()
    for base_categories in categories:
        feat_cols = base_categories + [shift_by]
        base_df = df.groupby(feat_cols, as_index=False).agg({shift_value: sum})
        for i in days:
            delay_df = shift_daily_data(base_df, i, shift_by, shift_value)
            merge_df = pd.merge(left=merge_df, right=delay_df, how='left', on=feat_cols, sort=False).reset_index(
                drop=True).fillna(0)
            gc.collect()
    return merge_df

# 按照不同维度生成 lag 自回归时序特征
def add_lag_features(all_data_df, fcst_type):
    lag_days = list(range(1, 11)) + [14, 21, 28, 29, 30, 31]
    lag_days = [x for x in lag_days if x >= fcst_type]
    groupby_cats = [['sku'], ['store_id'], ['sku', 'store_id']]
    all_data_df = add_daily_shifts(all_data_df, lag_days, groupby_cats)
    return all_data_df

在迁移到 Spark 时第一版我也采用了类似的写法,不过发现性能比较差,而且随着 lag 数的增多,join 次数也增多了,数据血缘关系会拉得非常长。

第二版我们采用了 window function 的写法:

from pyspark.sql import functions as F
from pyspark.sql import Window

def add_date_index(df, date_col, start_day='2016-01-01'):
    df = df.withColumn(f'{date_col}_index'
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
数据分析职业是一个多金的职业,数据分析职位是一个金饭碗的职位,前景美好,但是要全面掌握大数据分析技术,非常困难,大部分学员的痛点是不能快速找到入门要点,精准快速上手。本课程采用项目驱动的方式,以Spark3和Clickhouse技术为突破口,带领学员快速入门Spark3+Clickhouse数据分析,促使学员成为一名高效且优秀的大数据分析人才。学员通过本课程的学习,不仅可以掌握使用Python3进行Spark3数据分析,还会掌握利用Scala/java进行Spark数据分析,多语言并进,力求全面掌握;另外通过项目驱动,掌握Spark框架的精髓,教导Spark源码查看的技巧;会学到Spark性能优化的核心要点,成为企业急缺的数据分析人才;更会通过Clickhouse和Spark搭建OLAP引擎,使学员对大数据生态圈有一个更加全面的认识和能力的综合提升。真实的数据分析项目,学完即可拿来作为自己的项目经验,增加面试谈薪筹码。课程涉及内容:Ø  Spark内核原理(RDD、DataFrame、Dataset、Structed Stream、SparkML、SparkSQL)Ø  Spark离线数据分析(千万简历数据分析、雪花模型离线数仓构建)Ø  Spark特征处理及模型预测Ø  Spark实时数据分析(Structed Stream)原理及实战Ø  Spark+Hive构建离线数据仓库(数仓概念ODS/DWD/DWS/ADS)Ø  Clickhouse核心原理及实战Ø  Clickhouse engine详解Ø  Spark向Clickhouse导入简历数据,进行数据聚合分析Ø  catboost训练房价预测机器学习模型Ø  基于Clickhouse构建机器学习模型利用SQL进行房价预测Ø  Clickhouse集群监控,Nginx反向代理Grafana+Prometheus+Clickhouse+node_exporterØ  Spark性能优化Ø  Spark工程师面试宝典       课程组件:集群监控:福利:本课程凡是消费满359的学员,一律送出价值109元的实体书籍.

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值