机器学习中交叉验证的原理及实现
本文记录自己在参与一个比赛时深入理解交叉验证原理以及学习代码的过程。
交叉验证原理
在交叉验证中,验证数据取自训练数据,但不参与训练,这样可以相对客观的评估模型对于训练集之外数据的匹配程度。模型在验证数据中的评估常用的是交叉验证,又称循环验证。它将原始数据分成K组(K-Fold),将每个子集数据分别做一次验证集,其余的K-1组子集数据作为训练集,这样会得到K个模型。
交叉验证优势
优点是可以降低由一次随机划分带来的偶然性,提高其泛化能力。
StratifiedKFold和KFold
但K折还有个问题就是因为是随机划分,很有可能划分的过程中刚好把类别都划分开了,比如第一折训练集里全是0标签,第二折测试集里全是1标签,这样对模型训练就不太好,在其中的某个模型学习的时候就没有学习到测试集的分类特征。
在KFold中就是这样的,对于不平衡数据集,特别是一些比赛数据正类非常少,那么直接用KFold就可能出现这种问题。
所以对非平衡数据可以用分层采样StratifiedKFold,就是在每一份子集中都保持和原始数据集相同的类别比例
代码(三个模型)
def cv_model(clf, train_x, train_y, test_x, clf_name):
folds = 5
seed = 2023
kf = KFold(n_splits=folds, shuffle=True, random_state=seed)
oof = np.zeros(train_x.shape[0]) # 初始化一个大小为n(n=训练集行数),值全为0的数组 用于存放每折验证集的预测概率
predict = np.zeros(test_x.shape[0]) # 初始化一个大小为n(n=测试集行数),值全为0的数组 用于存放预测概率
cv_scores = []
# 遍历每一折(注意每折数据是从训练集中随机选取的)进行训练
for i, (train_index, valid_index) in enumerate(kf.split(train_x, train_y)):
print('************************************ {} ************************************'.format(str(i+1)))
trn_x, trn_y, val_x, val_y = train_x.iloc[train_index], train_y[train_index], train_x.iloc[valid_index], train_y[valid_index]
if clf_name == "lgb":
train_matrix = clf.Dataset(trn_x, label=trn_y) # 该折训练集矩阵
valid_matrix = clf.Dataset(val_x, label=val_y) # 该折验证集矩阵
params = {
'boosting_type': 'gbdt', # 基学习器
'objective': 'binary', # 采用的目标函数是binary,说明任务类型是二分类
'metric': 'auc', # 评估指标
'min_child_weight': 5, # 最小叶子节点样本权重和
'num_leaves': 2 ** 5, # 叶子数
'lambda_l2': 10, # l2正则化
'feature_fraction': 0.8, # 在训练时,对某一棵树,随机选取的特征比例,调小该参数可以防止过拟合,加快运算速度
'bagging_fraction': 0.8, # 训练样本的采样比例,调小该参数可以防止过拟合,加快运算速度
'bagging_freq': 4, # 表示执行bagging的频率
'learning_rate': 0.01, # 学习率
'seed': 2020, # 随机种子
'n_jobs':8 # 设定CPU运行情况,通常设置成与CPU内核数量一致,若不知道数量,则设置为-1
}
# 模型训练 valid_sets也可以只放valid_matrix verbose_eval表示打印信息的间隔 early_stopping_rounds表示早停,
# 防止过拟合,表示在验证集上,当连续n次迭代,分数没有提高后,提前终止训练
model = clf.train(params, train_matrix, 10000, valid_sets=[train_matrix, valid_matrix],
categorical_feature=[], verbose_eval=5, early_stopping_rounds=200)
val_pred = model.predict(val_x, num_iteration=model.best_iteration) # 预测该折验证集 最优迭代次数
test_pred = model.predict(test_x, num_iteration=model.best_iteration) # 该折训练下的模型来预测测试集
# 打印特征重要度
print(list(sorted(zip(cols, model.feature_importance("gain")), key=lambda x: x[1], reverse=True))[:20])
if clf_name == "xgb":
train_matrix = clf.DMatrix(trn_x , label=trn_y) # 该折该折训练集矩阵
valid_matrix = clf.DMatrix(val_x , label=val_y) # 该折验证集矩阵
test_matrix = clf.DMatrix(test_x) # 测试集矩阵
params = {'booster': 'gbtree',
'objective': 'binary:logistic',
'eval_metric': 'auc',
'gamma': 1, # 复杂度的惩罚项
'min_child_weight': 1.5,
'max_depth': 5,
'lambda': 10, # 正则化程度
'subsample': 0.7, # 子样本比例
'colsample_bytree': 0.7,
'colsample_bylevel': 0.7,
'eta': 0.05,
'tree_method': 'exact',
'seed': 2020,
'nthread': 8
}
watchlist = [(train_matrix, 'train'),(valid_matrix, 'eval')]
# num_boost_round为迭代次数 evals是一个列表,用于对训练过程中进行评估列表中的元素,形式就是watchlist
model = clf.train(params, train_matrix, num_boost_round=10000, evals=watchlist, verbose_eval=5, early_stopping_rounds=100)
val_pred = model.predict(valid_matrix, ntree_limit=model.best_ntree_limit) # 最优模型时对应树的个数
test_pred = model.predict(test_matrix , ntree_limit=model.best_ntree_limit)
if clf_name == "cat":
model = clf(
n_estimators=10000,
random_seed=1024,
eval_metric='AUC',
learning_rate=0.05,
max_depth=5,
early_stopping_rounds=200,
metric_period=500,
)
model.fit(trn_x, trn_y, eval_set=(val_x, val_y),
use_best_model=True,
verbose=1)
val_pred = model.predict_proba(val_x)[:,1]
test_pred = model.predict_proba(test_x)[:,1]
oof[valid_index] = val_pred # 将每一折验证集的预测结果放入原先的初始化矩阵中(每一折会对应索引valid_index)
predict += test_pred / kf.n_splits # 最终预测概率等于每一折预测概率/5再相加
cv_scores.append(roc_auc_score(val_y, val_pred))
print(cv_scores)
return oof, predict
说明
lgb模型参数params(部分)
‘objective’: ‘binary’, 说明采用的目标函数是binary loss,表明任务类型是二分类;若改成 ‘multiclass’,则表明任务类型是多分类
lgb模型训练时参数(部分)
- valid_sets = [train_matrix, valid_matrix],也可以只放valid_matrix
- verbose_eval 表示打印信息的间隔,即隔多少轮训练打印一次信息
- early_stopping_rounds 表示早停,防止过拟合,表示在验证集上,当连续n次迭代,分数没有提高后,提前终止训练
xgb模型参数params(部分)
参数说明参考:XGBoost参数
在xgb模型中,一般用’objective’: ‘binary:logistic’,预测的是概率,而lgb模型中一般用’objective’: ‘binary’
xgb模型训练时参数
- num_boost_round为迭代次数
- evals是一个列表,用于对训练过程中进行评估列表中的元素,形式就是watchlist: watchlist = [(train_matrix, ‘train’),(valid_matrix, ‘eval’)]
predict += test_pred / kf.n_splits
test_pred时每一折训练模型后预测的概率,因为共有五折数据,但只采用一折验证,故/5,然后再将每一折/5之后的概率相加作为最终测试集的预测概率,可以使结果更准确。(个人见解)
调用交叉验证代码
# xgb
xgb_oof, xgb_pred = cv_model
(xgb, train_label[cols], train_label['black_flag'], test_label[cols], 'xgb')
传入参数说明:xgb(xgboost实例化)、train_label[cols](训练集除去ID和标签)、train_label[‘black_flag’](训练集标签)、test_label[cols](测试集数据,每一个ID会对应一份特征数据,然后模型预测也是根据这些特征数据来预测,因此最终的预测概率也就是和ID一一对应的)
交叉验证之后的步骤
确定最优分类阈值
oof = xgb_oof
scores = []; thresholds = []
best_score = 0; best_threshold = 0
for threshold in np.arange(0.4,0.6,0.01): # 分类阈值 0.4-0.6(不一定就是按0.5)
preds = (oof.reshape((-1)) > threshold).astype('int') # oof.reshape((-1))表示改写成一串,没有行列
m = f1_score(train_label['black_flag'].values.reshape((-1)), preds, average='macro') # 计算f1
scores.append(m)
thresholds.append(threshold)
if m>best_score:
best_score = m
best_threshold = threshold
print(f'{threshold:.02f}, {m}')
print("==============================")
print(f'{best_threshold:.02f}, {best_score}')
说明:oof.reshape((-1))表示改写成一串,没有行列。astype(‘int’) 表示若大于阈值,则分类为1;若小于阈值,则分类为0
确定最优阈值后为测试集打上标签
pred = xgb_pred
test_label['black_flag'] = (pred.reshape((-1))>best_threshold).astype('int')
生成提交文件
test_label[['zhdh','black_flag']].to_csv('./submission/submission.csv', index=False)
代码(一个模型,以lgb模型为例)
def lgb_model(train, target, test, k, seed):
feats = [f for f in train.columns if f not in ['zhdh', 'black_flag']]
print('Current num of features:', len(feats))
oof_probs = np.zeros((train.shape[0],))
output_preds = 0
offline_score = [] # 线下分数
feature_importance_df = pd.DataFrame() # 特征重要度
parameters = {
'boosting_type': 'gbdt',
'objective': 'binary',
'tree_learner':'serial',
'metric': 'auc',
'min_child_weight': 4,
'num_leaves': 64,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 4,
'learning_rate': 0.02,
'seed': seed,
'nthread': 32,
'n_jobs':8,
'silent': True,
'verbose': -1,
}
seeds = [2]
for seed in seeds:
folds = StratifiedKFold(n_splits=k, shuffle=True, random_state=seed)
for i, (train_index, test_index) in enumerate(folds.split(train, target)):
train_y, test_y = target.iloc[train_index], target.iloc[test_index]
train_X, test_X = train[feats].iloc[train_index, :], train[feats].iloc[test_index, :]
dtrain = lgb.Dataset(train_X,
label=train_y)
dval = lgb.Dataset(test_X,
label=test_y)
lgb_model = lgb.train(
parameters,
dtrain,
num_boost_round=8000,
valid_sets=[dval],
callbacks=[early_stopping(100), log_evaluation(100)],
)
oof_probs[test_index] = lgb_model.predict(test_X[feats], num_iteration=lgb_model.best_iteration) / len(
seeds) # 该折预测概率
offline_score.append(lgb_model.best_score['valid_0']['auc'])
# 测试集预测概率
output_preds += lgb_model.predict(test[feats],
num_iteration=lgb_model.best_iteration) / folds.n_splits / len(seeds)
print(offline_score)
# feature importance 一折
fold_importance_df = pd.DataFrame()
fold_importance_df["feature"] = feats
fold_importance_df["importance"] = lgb_model.feature_importance(importance_type='gain')
fold_importance_df["fold"] = i + 1
feature_importance_df = pd.concat([feature_importance_df, fold_importance_df], axis=0)
print('OOF-MEAN-AUC:%.6f, OOF-STD-AUC:%.6f' % (np.mean(offline_score), np.std(offline_score)))
print('feature importance:')
print(feature_importance_df.groupby(['feature'])['importance'].mean().sort_values(ascending=False).head(50))
return output_preds, oof_probs, np.mean(offline_score), feature_importance_df
调用交叉验证代码
print('开始模型训练train')
lgb_preds, lgb_oof, lgb_score, feature_importance_df = lgb_model(train=train[feature_names],
target=train['label'],
test=test[feature_names], k=5,seed=2020)
确定最优分类阈值与上述类似
参考文档: