- 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的大小和正负样本的采样无关
- 数值型特征进行分桶,转化为类别型特征会更好一下,有利于模型分类