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
不含任何敏感数据。
# 引入数据处理和模型训练必须的包
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
参数较多,用网格搜索不太划算。
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')
调参结束之后,我们查看验证集上最好的表现和对应的参数。
target_bst = classifier_max.max['target']
params_bst = classifier_max.max['params']
然后开始训练模型:
# 参数
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
的主要意义就是评估模型对好坏用户的区分度,值越高,区分度越好。相应的,我们的信用额度策略同事会利用我们的模型打分,对不同区间的用户赋予不同的额度,以实现业务利润最大化。
评估函数如此定义
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 模型预测
我们模型保存之后,在线上应用,需要这样导入并预测。
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
。
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
评估模型稳定性:
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
了),然后用训练好的模型对相同的测试集进行预测,生成的预测结果(概率值)作为一个新的逻辑回归模型(或者别的模型)的训练集,再进行模型训练。
当然以上两种方式可以结合使用,进一步提升模型效果。