- 1引言
- 2需求背景
- 3特征评估
- 3.1特征表维护
- 3.2样本频率分布直方图概览
- 3.3Pearson相关系数计算
- 3.4缺失率计算
 
- 4模型评估
- 4.1模型离线AUC评估
- 4.2TensorBoard可视化
- 4.2.1项目中集成tensorboard
- 4.2.2启动tensorboard命令
- 4.2.3可视化效果
 
- 4.3离线实验记录表
- 4.3.1训练测试集切分代码
 
- 4.4线上推理接口时间评估
- 4.5HTTP接口并发测试
 
- 5AB实验
- 6工业Tricks分享
- 6.1AUC提升和业务指标不一致问题
- 6.2经验汇总
 
引言
该文档阐述了算法从开始迭代到上线前的一些工作流程。
首先第一需求背景。我们需要清楚本次迭代的需求背景。
第二特征评估。抽取样本,进行特征评估,主要用单特征AUC和缺失率着两个指标进行评估,确保数据没有异常。
第三模型评估,包括多次实验,对模型的AUC和接口的耗时这两个指标进行评估
第四AB实验,主要通过AB实验来对比评估,用CTR进行评估。
Demo代码地址:http://172.29.28.203:8888/lab/tree/wangyongpeng/global_search_rank_ctr/model_eval.ipynb
需求背景
描述本次上线的改动点、背景。例如:
为了捕捉用户对物料的兴趣偏好,加入了用户对物料的点击率特征(user_item_ctr)
特征评估
特征评估有很多种方式,主要包括两大类,特征和特征直接的相关性分析,特征和标签之间的相关性分析,特征和标签的分析方法包括:单特征AUC评估、Pearson系数、GBDT训练得到特征重要性等方法。
特征表维护
需要维护一张模型用到的特征列表,包含四个信息(特征名,特征含义,单特征AUC,特征缺失率)。
这张表可以帮助我们在每次迭代的时候,发现数据问题。比如缺失率突然升高,说明我们数据关联发生问题。不容易漏掉特征。
实例如下:
| 特征名 | 含义 | Pearson相关性系数 | 缺失率 | 是否上线 | 备注 | 
|---|---|---|---|---|---|
| age | 用户的年龄 | -0.04009939333393429 | 100% | N | 缺失率100%,不作为特征使用 | 
| sex | 性别 | -0.039342348680731327 | 17.47% | Y | |
| role | 角色 | -0.033291028680731327 | 0.0% | Y | |
| total_show | 展示次数 | -0.02418875134961658 | 0.0% | Y | |
| total_click | 物料总点击 | -0.07942286768376286 | 0.0% | Y | |
| total_vote | 物料总点赞 | -0.04246059351624654 | 0.0% | Y | |
| total_collection | 物料总收藏 | -0.043361133083929235 | 0.0% | Y | |
| total_comment | 物料总评论 | -0.023291028680731327 | 0.0% | Y | |
| total_share | 物料总分享 | -0.04204700458418045 | 0.0% | Y | 
样本频率分布直方图概览
如果想看细节的一些具体数值,就可以用以下代码仔细的看。
|-----------|---------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 | %``matplotlib inline import matplotlib.pyplot as plt import pandas as pd num_train_data[``'total_share'``].hist(bins``=``100``) |
可视化完的结果如下,因为样本异常差异太大,可视化出来不美观。其表示的是我们应该在后续的特征工程阶段,将这些异常值进行范围限定

Pearson相关系数计算
Pearson相关系数的范围是在[-1,1]之间,下面给出Pearson相关系数的应用理解:
假设有X,Y两个变量,那么有:
(1) 当相关系数为0时,X变量和Y变量不相关;
(2) 当X的值和Y值同增或同减,则这两个变量正相关,相关系数在0到1之间;
(3) 当X的值增大,而Y值减小,或者X值减小而Y值增大时两个变量为负相关,相关系数在-1到0之间。
注:相关系数的绝对值越大,相关性越强,相关系数越接近于1或-1,相关度越强,相关系数越接近于0,相关度越弱。通常情况下通过以下取值范围判断变量的相关强度:
0.8-1.0 极强相关
0.6-0.8 强相关
0.4-0.6 中等程度相关
0.2-0.4 弱相关
0.0-0.2 极弱相关或无相关
|----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | %``matplotlib inline import seaborn as sns import matplotlib.pyplot as plt from pyhive ``import hive import pandas as pd import numpy as np conn ``= hive.Connection(host``=``'172.30.2.60'``, port``=``10000``, database``=``'recsys'``) train_sql ``= """select * from recsys.search_full_link_30d where resource_type='VIDEO' limit 10000""" train_data ``= pd.read_sql(train_sql, conn) conn.close() train_data.columns ``= [i.split(``'.'``)[``1``] ``for i ``in list``(train_data.columns)] # 输入特征列和标签列,输出列名对应的皮尔逊系数 # 分别计算每个特征与标签的相关系数 from scipy.stats ``import pearsonr def calc_pearsonr(dataframe, col_name, label_name):    ``x ``= dataframe[col_name].values  ``label ``= dataframe[label_name].values  ``p ``= pearsonr(x, label)[``0``]  ``print``(``"%s 和Label的系数 = %s"``%``(col_name, p)) train_data_fillna ``= train_data.fillna(``0``) featureCols ``=``[``'total_show'``, ``'total_click'``, ``'total_share'``, ``'total_vote'``,  ``'total_collection'``, ``'total_comment'``] for col ``in featureCols:  ``calc_pearsonr(train_data_fillna, col, ``'click_label'``) # total_show 和Label的系数 = -0.02418875134961658 # total_click 和Label的系数 = -0.07942286768376286 # total_share 和Label的系数 = -0.04204700458418045 # total_vote 和Label的系数 = -0.04246059351624654 # total_collection 和Label的系数 = -0.043361133083929235 # total_comment 和Label的系数 = -0.023291028680731327 |
缺失 率计算
计算样本中,某列值得缺少情况,可以判断样本中存在的问题
|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pandas as pd mHeader ``= [``'age'``,``'city_id'``,``'hospital_id'``] rowDF ``= pd.read_csv(``'./data/raw_data.csv'``, sep``=``','``, skiprows``=``[``0``],names``=``mHeader)   # 传入列表头,输出特征的缺失率 def get_null_rate(featureCol, DF):  ``# 统计每列的缺失率,获取总数  ``totalRows ``= DF.shape[``0``]  ``tempCount ``= 1``-``(DF[featureCol].where(DF[featureCol] !``= -``1.0``).count()``/``totalRows)  ``return round``(tempCount``*``100``, ``2``)   featureCols ``= [``'age'``,``'city_id'``,``'hospital_id'``]   for col ``in featureCols:  ``print``(``'{}={}%'``.``format``(col, get_null_rate(col, rowDF))) # 输出结果 # age=14.65% # city_id=15.27% # hospital_id=14.44% |
模型评估
模型离线AUC评估
先简单介绍一些AUC的概念
什么是AUC
- 
其本身含义是正负样本之间预测的gap越大,auc越大. 
- 
随机抽出一对样本(一个正样本,一个负样本),然后用训练得到的分类器来对这两个样本进行预测,预测得到正样本的概率大于负样本概率的概率。 
AUC的优势:
AUC的计算方法同时考虑了分类器对于正例和负例的分类能力,在样本不平衡的情况下,依然能够对分类器作出合理的评价。且AUC对均匀正负样本采样不敏感。
以下代码是模型中设置评估指标的代码,metrics可以自己改动根据相应的场景进行设置。
|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 | model ``= DeepFM(linear_feature_columns,dnn_feature_columns,task``=``'binary'``, dnn_hidden_units``=``(``64``, ``32``, ``16``))  ``model.``compile``(``"adam"``, ``"binary_crossentropy"``,  ``metrics``=``[``'binary_crossentropy'``,``'AUC'``], )  ``history ``= model.fit(train_model_input, train[``'click_label'``].values,  ``batch_size``=``32``, epochs``=``10``, verbose``=``2``, validation_split``=``0.2``, ) |
TensorBoard可视化
项目中集成 tensorboard
下面代码是将模型训练的过程保存在"./tensorboard_logs"这个目录中。
|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 | model ``= DeepFM(linear_feature_columns,dnn_feature_columns,task``=``'binary'``, dnn_hidden_units``=``(``64``, ``32``, ``16``)) model.``compile``(``"adam"``, ``"binary_crossentropy"``,  ``metrics``=``[``'binary_crossentropy'``,``'AUC'``], ) tf_callback ``= tf.keras.callbacks.TensorBoard(log_dir``=``"./tensorboard_logs"``) history ``= model.fit(train_model_input, train[``'click_label'``].values,  ``batch_size``=``512``, epochs``=``20``, verbose``=``1``, validation_split``=``0.1``, callbacks``=``[tf_callback]) |
启动tensorboard命令
tensorboard --logdir tensorboard_logs/ --port 8081
可视化效果
下图显示的是在每次迭代中,AUC的变化曲线

离线实验记录表
这部分主要的目的就是新旧模型的PK,新增特征前后的PK,前提是在同一份测试集上。这部分可能会有很多次实验。在训练阶段,我们应该形成如下的记录,其中:
- 训练集:54天的数据作为训练数据
- 测试集:一周,7天的数据进行测试
训练测试集切分代码
如下代码是按照天数进行对数据进行切分,先获取数据集中的最大日期,然后7天前的作为训练集,7天后的作为测试集。
|----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # 切分时间的函数 def split_data_by_dt(dataframe):  ``# 先获取最大的时间  ``max_dt ``= dataframe[``'log_date'``].``max``()  ``# 得到分割时间  ``dt ``= datetime.datetime.strptime(max_dt, ``"%Y-%m-%d"``)  ``split_date ``= (dt ``+ datetime.timedelta(days``=``-``7``)).strftime(``"%Y-%m-%d"``)  ``# where条件区分训练测试集, 去空  ``test ``= dataframe.where(dataframe[``'log_date'``] > split_date)  ``test ``= test.dropna(how``=``'all'``)  ``train ``= dataframe.where(dataframe[``'log_date'``] <``= split_date)  ``train ``= train.dropna(how``=``'all'``)  ``# 返回训练测试集  ``print``(``"训练数据量 = " + str``(train.shape))  ``print``(``"测试数据量 = " + str``(test.shape))  ``return train, test |
| 实验 | 改动点 | 参数 | 样本数据 | 离线AUC | 备注 | 
|---|---|---|---|---|---|
| Base组 | 线上版本 | hidden_units=[64, 32, 16] epochs=20 batch_size=512 | 训练:20220906~2022-10-30 测试:2022-10-30~20221106 | 0.7323 | 线上基础版本的情况 | 
| 实验组 | 增加特征 | hidden_units=[64, 32, 16] epochs=20 batch_size=512 | 训练:20220906~2022-10-30,(54天),302431条数据 测试:2022-10-30~20221106,(7天),37403条数据 | 0.7486 | AUC提升+0.0163,提升巨大,可以上线 | 
线上推理接口时间评估
模型性能主要包括接口线上预测的响应时长。响应时长在200ms以内可接受。
该时间是指接口整体的响应时间,在测试环境中,响应时长是指五次请求的平均响应时间(不包含第一次请求)
记录响应时间的代码如下,通过time.time()方法实现:
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 | @app``.route(``"/citizen_search_rank_ctr"``, methods``=``[``"GET"``]) def search_rank_ctr():  ``total_start_time ``= time.time()  ``aa ``= rank(user_id, resource_ids, resource_type)  ``total_end_time ``= time.time()  ``print``(``"====================== total time = %s======================"``%``int``((total_end_time``-``total_start_time)``*``1000``))  ``return aa   if __name__ ``=``= '__main__'``:  ``# 模型部署  ``app.run(host``=``'0.0.0.0'``, threaded``=``True``, port``=``5012``) |
响应时长在现有场景主要包含三部分:从redis获取特征、模型预测、其他三个方面的耗时。响应时长如下可以从控制台日志得到。
|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 | curl -G -d ``'user_id=61110e6ce4b0fe63c65be1c2' -d ``'resource_id_list=31662,31767,31672,33381,33742,31292,31706,33559,31613,31516,32569,24908,33731,33734,33774,33128,32546,31731,25866,33808' -d ``'resource_type=VIDEO' http:``//172``.16.68.209:6058``/global_search_ranking # ====================== get feature from redis time = 19====================== # ====================== pridict time = 12====================== # ====================== total time = 34====================== |
我们将上面的数据进行整理可以得到如下表格,可以从数据我们得到,模型在测试环境相应时间为34ms, 符合200ms要求,可以上线。
| | 总耗时 | redis获取特征 | 推理预测 | 其他 |
| 耗时 | 34ms | 19ms | 12ms | 3ms | 
|---|
HTTP接口并发测试
压测我们采用ApacheBench,简称ab,压测命令如下,从压测结果看,我们的接口可以支持5000日活的没有压力,但是想更多,就需要更多的机器进行负载。
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 | ab -n 10000 -c 5000 -r http:``//172``.16.68.209:6058``/global_search_ranking``?user_id=61110e6ce4b0fe63c65be1c2&resource_id_list=31662,31767,31672,33381,33742,31292,31706,33559,31613,31516,32569,24908,33731,33734,33774,33128,32546,31731,25866,33808&resource_type=VIDEO |
其中:
- -n后面的1000,表示总共发出1000 0个请求;
- -c后面的5000 ,表示采用5000个并发(模拟 5000个人同时访问)
测试完后会有如下评估报告:
| 指标 | 值 | 备注 | 
|---|---|---|
| Complete requests | 10000 | 总请求数 | 
| Failed requests | 2 | 失败次数(Connect: 0, Receive: 0, Length: 2, Exceptions: 0) | 
| Time per request | 0.591 [ms] | 请求平均时长 | 
AB实验
线上AUC计算
线上AUC计算,是全量之前重要的一步,线上AUC可以直接单向的反映模型上线后收益是正向还是负向。
计算方式是拿到线上点击标签,模型线上打分分数,根据predict分数和label来计算线上的AUC
|-------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import pandas as pd import numpy as np from sklearn.metrics ``import * pd.set_option(``'display.max_columns'``, ``None``) pd.set_option(``'display.max_rows'``, ``None``) from sklearn.metrics ``import roc_auc_score mHeader ``= [``'label'``, ``'score'``] expDF ``= pd.read_csv(``'./data/exp2022-11-24.csv' , sep``=``'\t'``, skiprows``=``[``0``,``1``], names``=``mHeader) baseDF ``= pd.read_csv(``'./data/base2022-11-24.csv'``, sep``=``'\t'``, skiprows``=``[``0``,``1``], names``=``mHeader) # 删除空值和设置label expDF ``= expDF.dropna() baseDF ``= baseDF.dropna() y ``= expDF[``'label'``].values pred ``= expDF[``'score'``].values.reshape(``-``1``,``1``) exp_auc ``= roc_auc_score(y, pred) y_base ``= baseDF[``'label'``].values pred_base ``= baseDF[``'score'``].values.reshape(``-``1``,``1``) base_auc ``= roc_auc_score(y_base, pred_base) print``(``"计算实验组合Base组的AUC : base_auc = %s, exp_auc = %s" %``(base_auc, exp_auc)) |
实验收益统计
在全量上线之前,应该先小流量进行对比,先上50%流量进行统计计算,连续观察4天(上线当天不算),如果点击率高,实验正向,就可以上线全量。
| 日期 | AB组 | show | UV | click | CTR(click/show) | 
|---|---|---|---|---|---|
| 20221101 | Base | 383904 | 50000 | 4950 | 0.011205926 | 
| 20221102 | Base | 394050 | 49837 | 4302 | 0.007636087 | 
| 20221103 | Base | 339876 | 39920 | 3009 | 0.009983053 | 
| 20221104 | Base | 339874 | 40092 | 3393 | 0.014564221 | 
| 20221101 | Exp | 499877 | 56700 | 4950 | 0.010198509 | 
| 20221102 | Exp | 49838 | 50987 | 5098 | 0.080440628 | 
| 20221103 | Exp | 59987 | 50988 | 4009 | 0.099821628 | 
| 20221104 | Exp | 399874 | 40052 | 5988 | 0.014974717 | 
| 平均 | Base | 364426 | 44962.25 | 3913.5 | 0.010847322 | 
| 平均 | Exp | 499502.25 | 49681.75 | 5261.25 | 0.011301451 | 
| 预计全量后 | 点击率提升+4.19% | 
工业Tricks分享
AUC提升和业务指标不一致问题
在实际的工作中,常常是模型迭代的auc比较,即新模型比老模型auc高,代表新模型对正负样本的排序能力比老模型好。理论上,这个时候上线abtest,应该能看到ctr之类的线上指标增长。
但是经常会出现线下AUC提升明显,线上AUC或者业务指标负向的情况,这个时候可以排查线上线下一致性。
经验汇总
- learning_rate:学习率(learning_rate)越小,模型AUC越高,但是训练速度会变慢,我们需要取一个平衡。我会通常设置learning_rate=0.0007
- epoch:epoch是指对训练集反复训练多少次,一般理想情况下AUC随着迭代的次数而升高,我的经验值是epoch = 20
- 神经网络层的大小只会影响训练的时长,并不会影响线上预测的时长
- AUC的大小和正负样本的采样无关
- 数值型特征进行分桶,转化为类别型特征会更好一下,有利于模型分类