Kaggle&TianChi分类问题相关算法快速实现



17/12/30-update :很多朋友私密我想要代码,甚至利用金钱诱惑我,好吧,我沦陷了。因为原始代码涉及到公司的特征工程及一些利益trick,所以我构造了一个数据集后复现了部分算法流程,需要看详细代码实现朋友可以移步Ensemble_Github


更多代码内容欢迎follow我的个人Github,如果有任何算法、代码疑问都欢迎通过stw386@sina.com联系我。


导读

无论是在TianChi,还是在Kaggle上,通常会出现类似0-1分类,多分类这样的问题,比如:

Kaggle Competition Past Solutions
O2O优惠券使用预测
移动推荐算法

除此之外,在金融、风控、交通领域,也会有比较相近的分类问题:
Kesci“魔镜杯”风控算法大赛
DataCastle微额借款用户人品预测

如果仔细阅读就会发现,很多很多case,但是总结下来都是一个套路:

  • Bagging
  • Boosting
  • Stacking
  • Ensemble

我想写这篇文章的目的就是让大家在比如竞赛、项目push的情况下,在较短时间内,快速的构造出一个效果中上的算法集合。

理论解析

Bagging

Bagging通过构造一个预测函数系列,然后以一定的方式将它们组合成一个预测函数。更加形象的理解就是,我们考试,大家都有各种擅长的科目,都有自己弱势的科目,我们每个人都尽力把我们擅长的科目考好,然后最后把各自答案互相借鉴,这样每一科都能有不错的结果。
如果以图形理解就是:

Boosting

Boosting通过改变训练样本的权重(增加分错样本的权重,减小分对样本的权重),学习多个分类器,并将这些分类器线性组合,提高分类器性能。更加形象的理解就是,我们考试,每次我们数学都考的不好,然后我们在平时学习的时候投入更多精力去学数学,然后再看什么考的不好,再去投入精力学什么。

Stacking

Stacking通过用初始训练数据学习出若干个基学习器后,将这几个学习器的预测结果作为新的训练集,来学习一个新的学习器。更加形象的理解就是,我们考试,先让学霸去考,然后把每个学霸的答案按照他们历史上这科目考的成绩加权打分,最后确定每个学霸的各科目成绩加权的变化,重复再让学霸去考试修正这个各科目成绩加权。
注意:其实,Stacking和Boosting的思想是很近的,其中很大的差异在于Stacking一般用的都是完善的模型中间件结果当作入参,生成了新的数据分布,算法设计核心应该考虑的Bias-Vars-Balance;而Boosting用的简单的模型结果当做权重分布优化原始入参,并没有生成新数据,算法核心的目的还是在考虑降低Bias。
所以,在多数情况下,Stacking的模型应该更稳定,有更好的泛化性能。

Ensemble

Ensemble通过对新的数据实例进行分类的时候,通过训练好多个分类器,把这些分类器的的分类结果进行某种组合(比如投票)决定分类结果。更加形象的理解就是,先让学霸去考试,尽可能的让每个学霸都考好,然后根据历史考的分数高低投票出每个科目对应的考的最好的那个学霸,决定每科抄对应的那个学霸。
注意:对的,正如你们所想,Ensemble的思想就和Bagging的思想近似。Bagging的关键集中在,我不管我每个弱鸡的分类器的效果,只要我数量足够多,一定能得到不错的组合效果,相对而言简单,效果稍弱;而我们在Ensemble的时候,设计更加复杂而且要注意:a.尽可能让每个子分类器能识别出不同分布的数据,如果针对同一份数据做再多的子分类器,也只是只能起到堆砌的作用。b.加权方式的不同:简单平均(Simple Average),加权平均(Weight Average),概率投票(Soft vote)等等。

说了这么多,我们其实有个前提,就是加权或者组合或者堆砌能够提升准确度,不然等于以上都不合理,下面简单证明下:

假设,我们有5个分类器的正例率分别是{0.7,0.7,0.7,0.9,0.9}:
a.如果直接判断的话,最好的一个子分类器的准确率也只能达到0.9
b.采用简单投票方法(必须有三个以上分类正确),那么根据二项分布:
3个分类器正确:0.73*0.1*0.1+3*0.72*2*0.9*0.1+3*0.7*0.32*0.9*0.9
4个分类器正确:0.73*0.9*0.1*2+3*0.72*0.3*0.92
5个分类器正确:0.73*0.92
把这几个相加可得p≈0.933>0.9。

这些都是是非常基础的,也不是本文的重点,如果有任何疑问,请自行百度了解或者邮件我。

Bagging的代表作有randomforest,Boosting的代表作有Adaboost及GBDT,Stacking和Ensemble就需要我们自己设计了。

如何设计一个Stacking|Ensemble的模型?

先看一个Stacking的经典之作:FaceBook基于gbdt+lr下stacking的CTR预估。不得不说,虽然实测下来对Valid的准确率提升只有2-3pp,但是在泛化性能上稳定程度上升了若干个档次,后面我们细讲。Ensemble就更不用举例子了,随便打开一个互联网公司的算法库,99.9%的都已经有完善的成功案例了。我呆过的滴滴、电信、hp等等,还没有一个公司不用的。

讲理论之前,先看一个概念:Bias–variance tradeoff。我知道每多一个公式,会少一半的读者,我尽可能的用叙述的方式才阐述。
首先,Error = Bias + Variance,这个公式请刻在脑子里。
如果记不住,上面这个公式有个对应的图:

我们来解释Error = Bias + Variance,公式中的Error就是我们需要考虑的我们设计模型中的Loss;Bias就是我们预测数据距离真实数据的距离程度,比如你今年25,我预测你23,BIas=2;Variance就是我预测的结果的波动程度,比如,我预测三个人的年龄A、B组分别为(23,22,24)及(10,20,80),很明显B组波动程度要狠狠大于A。我们形象的来看,如果Error = Bias + Variance中Bias过大是什么样?

结果就是红色线条,预测的结果几乎没有任何参考意义,偏离正常值非常远。

如果Error = Bias + Variance中Variance过大是什么样?

结果就是红色线条,预测的结果几乎完全拟合了所有数据,预测的结果波动非常不稳定。

所以,我们在设计Stacking和Ensemble的过程需要避免上述的两个问题,最简单的举个例子:
如果,我们在做Stacking模型,Model_1我们用的Adaboost的算法,我们improve了正例的权重,纠正了正负样本比,训练出了叶子结点。接下来,我们是选择做XGBOOST的时候,参数设置如下:

1
2
3
4
5
6
7
8
9
estimator=XGBClassifier(
learning_rate=0.1,
n_estimators=500,
max_depth=3,
min_child_weight=7,
objective='binary:logistic',
scale_pos_weight=0.707653,
gamma=0.6,
seed=27)

参数中scale_pos_weight是否应该更改为1?答案是yes的,这边建议大家自己思考一下。如果再做一次scale_pos_weight = negative /positive ,是不是相当于我们2次提高了正样本的权重?对正样本的拟合过度是的bias下降,Variance上升,Vaild的泛化能力就会非常的弱,极其不稳定。

除此之外,如果我们用了random forest的作为Model_1,后接一个Xgboost中的subsample还有必要设置为0.4或者0.5么?这边考虑到randomforest已经控制每棵树,随机采样的比例,也控制了每个feature,每个样本的被随机选取的前提,在后面追加模型stacking的过程就需要更注重拟合,此时就算接一个nernual network 都是可以的,所以后面可以但是不建议再追加注重Bagging的算法。

其实,核心在于不论我们如何组合一个stacking或者ensemble模型,需要时时刻刻考虑的是平衡bias和variable。上面这些描述很抽象,我自己返回的阅读也觉得不是解释的很清晰,但是建议各位自己好好想一下,如何搭建一个stacking和ensemble不需要考虑上面这些,但是要如何搭建一个stacking和ensemble模型,最核心的就是上面这些。

案例复现

先看结果,我借着公司case,训练了常规的方法其中stacking中基于tensorflow下的deepFM和FNN当时没有记录,就没有留下,其他的都如下:

整体上,我写了包括sample、ensemble、stacking、deepFM、TF-FNN前后一共花了3天的时间,所以真的可以说是高效快速的方法而且可复制性极高。效果上,基本上比简单的处理完直接random forest,accuracy要高10-15pp,如果愿意深挖,效果应该还可以提升。

这边就着重和大家捋一遍Facebook15年出品的xgboost+sparse+lr这个思路吧,这边只贴了核心的代码段,后面看大家需求再考虑是不是GitHub共享吧,如果想要知道其他的模型或者其他什么想法,可以邮件我~

  • 数据预处理

修复一些DBA没有处理好的数据,这样需要在做数据处理之前纵览整体数据质量。

1
2
3
4
5
6
7
8
make_new_data = []
for i in range(train_data['crm__crm_user_wechat_info__we_chat'].shape[0]):
if train_data['crm__crm_user_wechat_info__we_chat'][i].replace('\"', '').replace("[", '').replace(']', '') == str(
0):
make_new_data.append(0)
else:
make_new_data.append(1)
train_data['crm__crm_user_wechat_info__we_chat'] = make_new_data

离散化连续特征,这边也可以保留一些连续变量,我这边两种都尝试了,离散化的效果是要优于保留连续变量的。

1
2
3
4
5
6
7
8
# separate the classification data : define that if the set is under 10,the columns can be treated as classification
class_set = []
continue_set = []
for key in arrange_data_col:
if arrange_data_col[key] >= 10 and key != 'uid':
continue_set.append(key)
class_set = [x for x in train_data.columns if
x not in continue_set and x != 'uid' and x != 'label' and arrange_data_col[x] > 1]

删除低方差的feature,我这边用的是我之前写的一个包,理论:特征工程代码模版,包地址:data_preprocessing,这个是我自己写的,也不难,大家嫌麻烦也可以自己写。

1
2
3
4
5
6
7
# remove the low variance columns
meaningful_col = ['uid', 'label']
for i in cbind_classed_data_columns:
if i != 'uid' and i != 'label':
if arrange_data_col[i] >= 2:
meaningful_col.append(i)
meaningful_data = cbind_classed_data[meaningful_col]

同理,计算了互信量,删除低贡献的feature,也是上面包data_preprocessing.feature_filter()里面有的。

1
2
ff = data_preprocessing.feature_filter()
res = ff.mic_entroy(reshaped_data.iloc[:, 1:], 'label')

然后,我自己定义了评价函数,根据importance删选了feature,特征由最开始的243个减少到最后的64个。

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
def metrics_spec(actual_data, predict_data, cutoff=0.5):
actual_data = np.array(actual_data)
predict_data = np.array(predict_data)
bind_data = np.c_[actual_data, predict_data]
res1 = 1.0 * (bind_data[bind_data[:, 0] == 1][:, 1] >= cutoff).sum() / bind_data[bind_data[:, 0] == 1].shape[0]
res2 = 1.0 * (
(bind_data[bind_data[:, 0] == 1][:, 1] >= cutoff).sum() + (
bind_data[bind_data[:, 0] == 0][:, 1] < cutoff).sum()) / \
bind_data.shape[0]
return res1, res2


# define the initial param
clf = XGBClassifier(
learning_rate=0.01,
n_estimators=500,
objective='binary:logistic',
)

# best cutoff : 223 , more details follow the train_doc_guide
filter_columns = ['uid', 'label'] + [x[0] for x in res[-223:]]
reshaped_data = reshaped_data[filter_columns]
X_train = reshaped_data.iloc[:, 2:]
y_train = reshaped_data.iloc[:, 1]
model_sklearn = clf.fit(X_train, y_train)

# calculate the importance ,best cutoff : 0.0022857142612338 , more details follow the train_doc_guide
importance = np.c_[X_train.columns, model_sklearn.feature_importances_]
train_columns = [x[0] for x in importance if x[1] > 0.0022857142612338]

这样,数据预处理就完成了,接下来就是模型设计部分了,但是上面的过程很重要,请务必重视!


  • 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
    # update the values in the model
    # scale_weight_suggestion = (Y_train.count() - Y_train.sum()) / Y_train.sum()
    param_test = {
    'n_estimators': [100, 250, 500, 750]
    }
    gsearch = GridSearchCV(
    estimator=XGBClassifier(
    learning_rate=0.1,
    objective='binary:logistic',
    scale_pos_weight=0.707653,
    seed=27),
    param_grid=param_test,
    scoring='roc_auc',
    n_jobs=4,
    iid=False,
    cv=5)
    gsearch.fit(X_train, Y_train)
    print(gsearch.best_params_)
    # {'n_estimators': 500}

    # define the final param
    clf = XGBClassifier(
    learning_rate=0.01,
    n_estimators=500,
    max_depth=3,
    min_child_weight=7,
    objective='binary:logistic',
    scale_pos_weight=0.707653,
    gamma=0.6,
    reg_alpha=1,
    seed=27
    )

    # train the values
    model_sklearn = clf.fit(X_train, Y_train)
    y_bst = model_sklearn.predict_proba(X_test)[:, 1]
    metrics_spec(Y_train, model_sklearn.predict_proba(X_train)[:, 1])
    metrics_spec(Y_test, y_bst)

为了避免冗长,我删除了调参数细节,留了一个case做guide,下面就是拿出xgboost的叶子结点,并enhotencoding的过程。

1
2
3
4
5
6
7
8
9
10
11
12
# 叶子结点获取
train_new_feature = clf.apply(X_train)
test_new_feature = clf.apply(X_test)
# enhotcoding
enc = OneHotEncoder()
enc.fit(train_new_feature)
train_new_feature2 = np.array(enc.transform(train_new_feature).toarray())
test_new_feature2 = np.array(enc.transform(test_new_feature).toarray())
res_data = pd.DataFrame(np.c_[Y_train, train_new_feature2])
res_data.columns = ['f' + str(x) for x in range(res_data.shape[1])]
res_test = pd.DataFrame(np.c_[Y_test, test_new_feature2])
res_test.columns = ['f' + str(x) for x in range(res_test.shape[1])]

到此为止,将叶子结点获取过程就结束了,这边细心的人会发现,这个是一个非常稀疏的矩阵,我这边追加的是常规的LR,但是如果就单纯从数据特征的角度来讲,神经网络和FFM对这类数据类型有更好的表现,如果需要写FM收尾的同学,可以参考我写的这个FM包,理论:FM理论解析及应用,代码在:FM快速实现Github


  • logistics模块python实现
    1
    2
    3
    4
    5
    6
    7
    lr = LogisticRegression(C=1, penalty='l2', max_iter=1000, solver='sag', multi_class='ovr')
    model_lr = lr.fit(res_data.iloc[:,1:], res_data['f0'])
    y_train_lr = model_lr.predict_proba(res_data.iloc[:,1:])[:, 1]
    y_test_lr = model_lr.predict_proba(res_test.iloc[:,1:])[:, 1]
    res = metrics_spec(Y_test, y_test_lr)
    correct_rank = X_train.columns
    # (0.80, 0.71)

简单易上手,实现了下图的流:

顺带附上ks值计算逻辑:

1
2
3
4
5
6
7
8
9
10
# 算法评估
# ks_xgb_lr = np.c_[Y_test,y_test_lr]
# ks_xgb_lr = sorted(ks_xgb_lr , key = lambda x : x[1],reverse = True)
# ks_xgb_lr = pd.DataFrame(ks_xgb_lr)
# for i in range(9):
# end = (i+1)*break_cut
# res1 = 1.0*ks_xgb_lr.iloc[:end,:][ks_xgb_lr.iloc[:end,0]==0].shape[0]/ks_xgb_lr[ks_xgb_lr.iloc[:,0]==0].shape[0]
# res2 = 1.0*ks_xgb_lr.iloc[:end,:][ks_xgb_lr.iloc[:end,0]==1].shape[0]/ks_xgb_lr[ks_xgb_lr.iloc[:,0]==1].shape[0]
# res = res2-res1
# print(res1,res2,res)

只要重复上述的案例浮现中的code流程,可以快速复刻出一份效果还不错的分类算法,但是如果想能够拿到很好的名次或者有更优秀的表现,等待下一篇文章中的原理剖析。

最后,给大家分享一下之前和Kaggle大神在算法竞赛或者解决项目问题的时候总结出来需要尤其注意的点:

  • 请务必重视数据集构造,你能不能上榜或者得到leader的重视,这一点最关键,没有之一
  • 如果条件允许,尽可能的离散化数据尝试一下,多做两次特征筛选这些预处理的步骤,收益是非常大的
  • 请善于使用gridsearch,最后能进前十还是前三很大程度上相差的就是那零点几
  • 在集群或者资源充足的情况下,利用交叉检验代替Valid test,管中窥豹的结果会让自己更加固步自封,离真相越走越远
  • 乐于分享,表达出自己的观点,反过来在驳斥自己的观点,直到可以完全说服自己

没啥广告要打,就这样吧。

打赏的大佬可以联系我,赠送超赞的算法资料