Overview
互联网金融行业主要涉及以下方面:保险、理财、基金、信贷、虚拟信用卡、催收等,主要用到以下类型的模型:
- 营销模型、召回模型、排序模型(支付宝蚂蚁财富基金推荐红包发放等)
- 新用户反欺诈模型
- 新老用户信用额度模型(蚂蚁借呗,腾讯微粒贷,京东金条,微博钱包,美团借钱,各类小贷等)
- 虚拟信用卡贷中风控模型(蚂蚁花呗,京东白条等)
- 贷后催收模型
我在互联网金融行业已经工作两年了,也积累了不少信贷数据特征挖掘和信贷模型开发经验。现在将这些经验记录一下,算是对两年时间的一个总结。总体上,这个领域和其他领域在机器学习模型构建的流程上区别不大,主要精力都是在特征挖掘和模型构建上。
1. 特征挖掘
显而易见,这个领域,用户的资产和负债数据是最有效的数据,但是除了银行或者蚂蚁金服和京东金融等大金融平台之外,我们一般情况下很难拿到用户的资产数据,我们最多能拿到用户的第三方征信数据,即用户负债数据(国内常见为同盾,百融,百行征信等),结合用户授权我们爬取的用户的通讯录,APP安装列表,淘宝京东APP订单数据等,以及用户在本公司的借贷历史行为记录和用户登录APP的埋点数据,我们可以做出很多特征,三方多头征信特征是最有效的,其次对于老用户,用户的借贷历史行为也很有效。其中,时间序列数据是特征工程的重头戏,对于和时间相关的订单类,行为类等数据,我们可以用类似的方法开发出一大批时间序列特征,其形式均为“某时间段内 + 某种行为发生的次数/金额/比例/时长间隔/持续时间 + 最值/q1/q2/q3/均值/众数/中位数”
,例如"三个月内用户在本平台贷款金额序列的均值"
。这部分特征由三部分组成:
- 时间长度
- 行为类型
- 统计量
另外,用户授权我们拿到他们的通讯录之后,我们可以通过他的一二度人脉在本公司的表现,来做关系图特征。我们取得用户的APP安装列表之后,可以将APP按照风险等级给分为3或者4类:714高炮类,小贷类,普通消费金融与正常金融类,学习以及其他正向类。另外,如果是贷后催收模型,还需要加入用户的催收表现特征。
2. 特征处理
2.1 样本不均衡问题
我们在业务上遇到的很多数据,几乎都是正负样本不平衡的,一般情况下是好样本数量远大于坏样本,用户客群质量越高,样本差异越大。小贷平台好坏样本比例一般在3:1
到20:1
之间,大平台差异更大,这样设计一个课题:样本不均衡问题
。
样本不均衡时,我们有以下这些处理方式(通用方式,不只信贷领域),但基本思想就是"再缩放(rescaling)"
:
- 较多类欠采样
- 较少类过采样
- 预测时阈值移动
- 合成数据或无
label
样本学习 - 权重缩放或惩罚权重
- 异常检测的方式(如果好坏样本数量悬殊,例如欺诈检测)
- 组合/集成学习
个人以为,我们在信贷工作当中,业务样本真实性还是很重要的,因此,不建议用
SMOTE
等方法进行插值产生坏样本。当然像图像,语音,文本等具象的数据,人可以轻松判断正负样本的领域除外。
2.2 空值问题
我们的特征中是有不少空值的,我们可以进行零值填充、均值填充或者众数填充,不过我们一般不会对这些空值进行填充,因为填充大概率会引入误差。另外,某些机器学习算法,其思想是计算距离来划分类别,例如SVM
和KNN
,对于这些算法,有空值的情况是不适合的,填充空值也是不适合的,因为填充不同的数据会对结果产生较大影响。而且,我们业务上特征维度是非常高的,即便进行了特征选择和降维,因此有很多算法效率是很低的,不推荐使用,这是后话。
因此,我们原则上,保留空值不处理。
2.3 类别特征处理
类别特征大家一般都是做one-hot
处理,因为大部分算法是只支持数字型特征的。但是有少部分算法是支持类别型特征,而不用做独热编码的,例如LightGBM
。
2.4 归一化和标准化
归一化和标准化都属于无量纲化,其主要目的就是为了避免数值问题和加快模型收敛速度。有些模型最好要进行无量纲化,例如线性回归
,SVM
,kNN
,逻辑回归
,k-Means
,神经网络
等;而XGBoost
,lightGBM
,朴素贝叶斯
,决策树
和随机森林
等算法通常是不需要无量纲化的。
归一化和标准化之前的文章数据挖掘之数据标准化(Normalization)写过,这里就不赘述了。
2.5 特征选择和降维
特征选择的主要作用是去掉冗余的特征加速学习过程和减轻过拟合,降维也有类似的目的。但是某些降维算法使用之后,得到的低维度特征和降维之前的特征的量纲就发生了变化,模型解释性会受到影响,因为我们不知道得到的特征是什么意义。这是降维和特征选择的一点区别。而且,我们在业务当中,可能同类型的三方数据会有好几种,这些数据效果相差无几,同时使用也不会有更多增益,所以我们需要选择合适的特征,以节约数据成本。
特征选择主要有以下三类:
- 过滤式
- 包裹式
- 嵌入式
降维有两大类:
- 无监督降维(
PCA
等) - 有监督降维(
LDA
等)
降维这个领域,Chris
博士有较深研究。
2.6 特征评估
我们如果选择好了特征,想知道哪些特征对模型作用更大,除了训练模型之后查看gain
和weight
,也可以在训练模型之前用以下几个指标去初步判断一下:
- WOE
- IV
- 单特征AUC
- 单特征KS
3. 模型构建
3.1 数据集准备
互联网金融领域受政策,市场,经济等外部因素影响较大,因此客群变化也是很快的。例如2018年1月左右和2019年3月15日左右,受政策影响,互联网金融市场发生两次“风暴期”,导致坏账率急剧提高,因为我们选用样本的时候要考虑这些因素。
我们离线跑机器学习模型,一般情况下会将数据集按时间先后顺序划分为:训练集,验证集和测试集。此外,还要准备最新的业务数据(尚未产生表现的样本)作为回归集以作为监控模型稳定性的benchmark
。原则上,我们最好保持几个数据集的客群分布一致,即其间未发生重大市场震荡和较大业务调整。如果不能满足以上条件,我们至少需要满足验证集,测试集,回归集和当前线上业务的用户分布一致。且训练集可以抽样,验证集和测试集最好不要抽样。
经典的训练集,验证集和测试集的划分为7 : 1.5 : 1.5
,但是如果我们有几十万到几千万条样本,再这样按比例划分就不太合适了。我们最好使验证集和测试集保证足够的正样本和负样本,有稳定的统计学表现,将偶然性降至合理范围。且原则上如果机器性能足够,我们建议保留所有训练集而不采样,这样会尽最大可能保留样本的多样性,带来的不平衡问题可以用合适的机器学习算法包来解决(例如SVM
和XGBoost
算法工程实现中分别有class_weight
和scale_pos_weight
等参数调整样本权重)。
另外,信贷领域label
通常是这样定义的:该笔贷款逾期天数是否超过n
天,n
可以选择0,3,7,30
等等。
3.2 机器学习算法选择
如果业务不是新开展的,那么我们一般要沿用前人使用的算法,之后再慢慢迭代尝试同类型更好的算法(XGBoost
替换为lightGBM
或CatBoost
),或者数据量积累足够大时尝试神经网络类算法。如果是新开展的业务,那我们可以这么选择:
- 样本规模较小,选
SVM
(不适合稀疏数据) - 样本规模适中,
XGBoost
或lightGBM
或CatBoost
- 样本规模较大,
XGBoost
或lightGBM
或CatBoost
或神经网络
互联网金融领域,XGBoost
还是比较流行的算法,包括蚂蚁金服,京东金融,360金融都在大规模使用。我们下面就以此算法为例,做记录。
3.3 过拟合与欠拟合
过拟合和欠拟合是建模当中经常出现又非常重要的知识点,过拟合出现更多一些。我们主要有以下手段去缓解过拟合和欠拟合的影响。
3.3.1 过拟合
过拟合就是模型能够很好的拟合训练集,但是对于验证集和测试集的泛化效果非常差,这种情况也叫高方差。
- 更多训练样本
- 特征选择
- 减小模型规模,修改模型架构
L1
和L2
正则化early stop
dropout
- 权重衰减
- 加入噪音
bagging
集成
3.3.2 欠拟合
欠拟合就是模型对训练集不能很好拟合,这种情况也叫高偏差。
- 增加模型规模,修改模型架构
- 挖掘更好的特征
- 增加特征
- 较小正则化
值得注意的是,欠拟合和过拟合在实际任务当中有可能会同时存在的。
3.4 XGBoost
调参训练demo
本demo
不含任何敏感数据。
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 35 36 37 38 39 40 41 42 | # 引入数据处理和模型训练必须的包 import numpy as np import pandas as pd import xgboost as xgb # 加载特征列表 name_list = pd.read_csv( 'feature_list.txt' , header = None , index_col = 0 ) my_feature_names = list (name_list.transpose()) print ( len (my_feature_names)) # 加载样本 df_total = pd.read_csv( 'data_total.csv' ) print (df_total.shape) # 划分数据集 print (df_total.apply_time. min ()) print (df_total.apply_time. max ()) df_train = df_total[df_total.apply_time < '2020-01-21 00:00:00' ] df_val = df_total[(df_total.apply_time > = '2020-01-21 00:00:00' ) & (df_total.apply_time < '2020-02-01 00:00:00' )] df_test = df_total[df_total.apply_time > = '2020-02-01 00:00:00' ] df_train.label.mean() df_val.label.mean() df_test.label.mean() # 数据处理 train_x = df_train[my_feature_names] train_y = df_train.get( 'label' ) val_x = df_val[my_feature_names] val_y = df_val.get( 'label' ) test_x = df_test[my_feature_names] test_y = df_test.get( 'label' ) class_weight = ( 1 - train_y.mean()) / train_y.mean() #负样本(0样本)与正样本(1样本)的数量比 data_train_X = train_x.astype( float ) data_val_X = val_x.astype( float ) data_test_X = test_x.astype( float ) xgb_train = xgb.DMatrix(data_train_X, label = train_y) xgb_val = xgb.DMatrix(data_val_X, label = val_y) xgb_test = xgb.DMatrix(data_test_X, label = test_y) |
将我们需要的数据处理完成之后,就可以导入模型进行调参了。
调参方式有很多种,比如网格搜索和贝叶斯调参等,我这里就用我经常用的贝叶斯调参,因为XGBoost
参数较多,用网格搜索不太划算。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | import functools from bayes_opt import BayesianOptimization scale_pos_weight_ratio = class_weight from sklearn.metrics import roc_curve # 计算KS def ks_measure(preds, label): fpr, tpr, thresholds = roc_curve(label, preds) threshold = None if threshold is None : score = np. max (np. abs (tpr - fpr)) else : idx = np.digitize(threshold, thresholds) - 1 score = np. abs (tpr[idx] - fpr[idx]) return score def new_eval_xgb_ks(is_cv, metrics, X_train, y_train, X_validation, y_validation, X_test, y_test,eta, gamma, max_depth, min_child_weight, subsample, colsample_bytree, alpha,lambda_s): ''' 我们用验证集效果最好的参数作为最终训练的参数 ''' params = dict () params[ 'eta' ] = eta params[ 'gamma' ] = gamma params[ 'max_depth' ] = int (max_depth) params[ 'min_child_weight' ] = min_child_weight params[ 'subsample' ] = subsample params[ 'colsample_bytree' ] = colsample_bytree params[ 'alpha' ] = alpha params[ 'lambda' ] = lambda_s params[ 'silent' ] = 1 params[ 'seed' ] = 696 params[ 'objective' ] = 'binary:logistic' params[ 'scale_pos_weight' ] = scale_pos_weight_ratio params[ 'eval_metric' ] = [ 'auc' ] n_estimators = 800 print (params) # 是否对这组参数做cross validation if is_cv: pass else : dtr = X_train dva = X_validation eval_list = [(dtr, "train" ), (dva, "validation" )] clf = xgb.train(params, dtr, n_estimators, eval_list, verbose_eval = 30 , early_stopping_rounds = 20 ) y_pred = clf.predict(dva, ntree_limit = clf.best_ntree_limit) dtest = X_test y_pred_test = clf.predict(dtest, ntree_limit = clf.best_ntree_limit) # 用ks选参 if metrics = = "ks" : result = ks_measure(y_pred, y_validation) print ( "test ks: " , ks_measure(y_pred_test, y_test)) return result def new_model_selection_bayes_opt(X_train, y_train, X_validation, y_validation, X_test, y_test,classifier = 'xgb' , metrics = "ks" , is_cv = False ): num_iter = 150 init_points = 50 if classifier = = 'xgb' : eval_xgb_ks_part = functools.partial(new_eval_xgb_ks, is_cv, metrics, X_train, y_train, X_validation, y_validation, X_test, y_test) classifierBO = BayesianOptimization(eval_xgb_ks_part, { 'eta' : ( 0.01 , 0.5 ), 'gamma' : ( 0 , 20 ), 'max_depth' : ( 3 , 5 ), 'min_child_weight' : ( 1 , 200 ), 'subsample' : ( 0.6 , 1 ), 'colsample_bytree' : ( 0.6 , 1 ), 'alpha' : ( 1 , 100 ), 'lambda_s' :( 1 , 200 )}) classifierBO.maximize(init_points = init_points, n_iter = num_iter) best_params = classifierBO. max [ 'params' ] best_val = classifierBO. max [ 'target' ] print (best_val) print (best_params) return classifierBO classifier_max = new_model_selection_bayes_opt(xgb_train, train_y, xgb_val, val_y, xgb_test, test_y, classifier = 'xgb' ) |
调参结束之后,我们查看验证集上最好的表现和对应的参数。
1 2 | target_bst = classifier_max. max [ 'target' ] params_bst = classifier_max. max [ 'params' ] |
然后开始训练模型:
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 | # 参数 params = { 'booster' : 'gbtree' , 'objective' : 'binary:logistic' , 'alpha' : params_bst[ 'alpha' ], 'colsample_bytree' : params_bst[ 'colsample_bytree' ], 'eta' : params_bst[ 'eta' ], 'gamma' : params_bst[ 'gamma' ], 'lambda' : params_bst[ 'lambda_s' ], 'max_depth' : int (params_bst[ 'max_depth' ]), 'min_child_weight' : params_bst[ 'min_child_weight' ], 'subsample' : params_bst[ 'subsample' ], 'silent' : 1 , 'seed' : 696 , 'scale_pos_weight' : scale_pos_weight_ratio, 'eval_metric' : [ 'auc' ] } num_boost = bst_num_boost # 从输出日志中可以找到最好的树棵数 watchlist = [(xgb_train, 'train' ), (xgb_val, 'val' ), (xgb_test, 'test' )] # 训练 model = xgb.train(params, xgb_train, num_boost, watchlist) # 保存模型 model.save_model( 'result_model.model' ) # 用于存储训练出的模型 |
评估模型,我们一般用KS
,其实AUC
也可以,但是不如KS
通用。KS
的主要意义就是评估模型对好坏用户的区分度,值越高,区分度越好。相应的,我们的信用额度策略同事会利用我们的模型打分,对不同区间的用户赋予不同的额度,以实现业务利润最大化。
评估函数如此定义
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 35 36 37 38 39 40 41 | import numpy as np import pandas as pd from sklearn.metrics import roc_curve, auc def model_evaluation(y, y_pred): """ 输入真实的y和预测的y的概率 输出ks和auc """ result = {} result[ 'ks' ] = ks_score(y, y_pred) result[ 'auc' ] = auc_score(y, y_pred) return result def ks_score(y, y_pred, threshold = None ): """ 计算KS """ fpr, tpr, thresholds = roc_curve(y, y_pred) if threshold is None : score = np. max (np. abs (tpr - fpr)) else : idx = np.digitize(threshold, thresholds) - 1 score = np. abs (tpr[idx] - fpr[idx]) return score def auc_score(y, y_pred): """ 计算AUC """ fpr, tpr, thresholds = roc_curve(y, y_pred) roc_auc = auc(fpr, tpr) return roc_auc train_pred = model.predict(xgb_train) val_pred = model.predict(xgb_val) test_pred = model.predict(xgb_test) model_evaluation(train_y, train_pred) model_evaluation(val_y, val_pred) model_evaluation(test_y, test_pred) |
如果验证集和测试集KS
接近,在0.2
以上,且和训练集的KS
差距不大,那么这个模型的效果就是可以接受的。当然实际工作当中,我们评估的步骤更加严格,我们需要看特征重要性,看看排名在前面的特征是不是符合我们的认知,单特征的表现以及单特征分bin
表现等等。
3.5 模型预测
我们模型保存之后,在线上应用,需要这样导入并预测。
1 2 3 4 | model = xgb.Booster() model.load_model( 'result_model.model' ) reg_pred = model.predict(xgb_reg) # 假设xgb_reg是回归集处理过的数据 online_pred = model.predict(xgb_online) # 假设xgb_online是上线后的数据集处理过的数据 |
我们可以将回归集的预测结果reg_pred
保存下来,然后在线上监控模型预测的稳定性,主要指标是PSI
。
1 2 3 4 5 6 7 8 9 10 11 12 | def psi(bench, target, group = None ): if group is None : group = int ( len (bench) * * 0.5 ) labels_q = np.percentile(bench,[( 100.0 / / group) * i for i in range (group + 1 )],interpolation = "nearest" ) ben_pct = (pd.cut(bench, bins = np.unique(labels_q),include_lowest = True ).value_counts()) / len (bench) target_pct = (pd.cut(target,bins = np.unique(labels_q),include_lowest = True ).value_counts()) / len (target) target_pct = target_pct.sort_index() ben_pct = ben_pct.sort_index() psi = sum ((target_pct - ben_pct) * np.log(target_pct / ben_pct)) return psi |
评估模型稳定性:
1 | psi(reg_pred, online_pred, group = 10 ) # 10或者20都可以,但是得保证每个bin的数量充足 |
如果psi < 0.1
那么表示模型稳定,一般会远小于0.1
。
3.6 多模型融合
实际业务当中,模型的效果还是可以有更大的提升的,这种技术手段就是多模型的融合(stacking)
。关于模型融合,可以有这样两种方式:
3.6.1 不同类型的样本产生的多个模型进行融合
例如我们的业务当中,多条不同的产品线有可能会有一定的用户重合,我们可以将其他不同产品线用户样本作为不同的训练集,验证集和测试集则使用相同的本产品线的样本。这样,产生的不同的模型,对相同的测试集进行预测,生成的预测结果(概率值)作为一个新的逻辑回归模型(或者别的模型)的训练集,再进行模型训练。
3.6.2 不同类型的机器学习算法产生的多个模型进行融合
这种情况可能在kaggle
等比赛当中更常见一些。相同的训练集,用不同的机器学习算法建模(最好是不同类型的算法,例如用了XGBoost
之后最好就不要用LightGBM
了),然后用训练好的模型对相同的测试集进行预测,生成的预测结果(概率值)作为一个新的逻辑回归模型(或者别的模型)的训练集,再进行模型训练。
当然以上两种方式可以结合使用,进一步提升模型效果。