Overview1. 特征挖掘2. 特征处理2.1 样本不均衡问题2.2 空值问题2.3 类别特征处理2.4 归一化和标准化2.5 特征选择和降维2.6 特征评估3. 模型构建3.1 数据集准备3.2 机器学习算法选择3.3 过拟合与欠拟合3.3.1 过拟合3.3.2 欠拟合3.4 XGBoost调参训练demo3.5 模型预测3.6 多模型融合3.6.1 不同类型的样本产生的多个模型进行融合3.6.2 不同类型的机器学习算法产生的多个模型进行融合 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了），然后用训练好的模型对相同的测试集进行预测，生成的预测结果（概率值）作为一个新的逻辑回归模型（或者别的模型）的训练集，再进行模型训练。 当然以上两种方式可以结合使用，进一步提升模型效果。