← 所有文章

Kaggle DonorsChoose.org Application Screening【Top 10%】

0. 习惯的碎碎念

距离上一篇博文已经过去4个月,闯斯还在微信上问我为何不写博客了(囧)。一方面是自从开年以来,感觉时间特别得紧,主R了一个项目,倒也不难,但是特别的费精力,改了好几次需求,所以下班以后只有input的时间和精力,没时间output,笔记里面攒了好多好多的想法和实践;另一方面也是学得越多,越觉得自己水,不太愿意写博文。现在时间稍微多了一些,而且觉得写博文主要就是自我总结,然后说不定能帮助到其他人,所以把写博文又提上了Todo List.

工作一年多了,现在还处在每天大量吸收知识的状态,也不知道是好还是不好😂。

今天想要分享的是在2018-04~2018-05利用业余时间参加的一个kaggle比赛,取得的成绩是Public Leaderboard Top 4%, Private Leaderboard Top 10%.

1. 题目解析

原题地址:DonorsChoose.org Application Screening

DonorsChoose.org是一个教学资助机构,美国的教师可以向该机构申请教学基金和物品。越来越多的教师向该机构发起申请,现在每年大概有50W+的申请数量,过多的申请数量导致了3个问题

  • 如何调整当前人工审核的流程,才能高效且快速地得出基金是否发放的结果
  • 如何统一不同审核人员对于同一个项目的看法
  • 如何让审核人员的精力花费在有意义的项目审核上

本次比赛的目标就是预测一个申请被批准的概率,审核人员会优先审核预测通过率较高的项目。

评价函数:AUC

数据集

train.csv和test.csv文件字段及说明如下表所示:

名称 说明
id 申请id
teacher_id 申请教师id
teacher_prefix 教师名称前缀(Ms., Mr., etc.)
school_state 学校所在州
project_submitted_datetime 申请时间戳
project_grade_category 所在年级(PreK-2, 3-5, 6-8, and 9-12)
project_subject_categories 主题(e.g., “Music & The Arts”)
project_subject_subcategories 子主题(e.g., “Visual Arts”)
project_title 申请题目
project_essay_1 第1份材料,主要讲述学生
project_essay_2 第2份材料,主要讲述资金和物品用途
project_resource_summary 所需资金和物品总结
teacher_number_of_previously_posted_projects 该教师之前提交过的次数
project_is_approved 申请是否通过

注:其实还有project_essay_3和project_essay_4,经过处理后可以和project_essay_1和project_essay_2合并,为了书写方便,直接就从合并后开始讲

resource.csv文件字段及说明如下表所示:

名称 说明
id 申请id
description 申请物品描述
quantity 申请物品数量
price 申请物品价格

2. 数据清洗和特征工程

本次比赛提供了诸多特征,有数值特征,类别特征和文本特征。

2.1 resource.csv特征处理

  • 根据quantity和price计算每一项物品的申请总价price_all
  • 根据id进行groupby操作,计算本次申请所需要的物品种类数items,物品总申请数quantity,单价和price_sum,总价和price_all_sum,平均价price_avg,产出5个统计特征
  • 把max(), min(), mean()作用于quantity, price, price_all,产出9个统计特征
  • 根据id进行groupby操作,把属于同一个id的所有申请物品描述使用空格连接起来,特征类型是文本

把处理后的resource数据通过id特征与train/test进行join.

2.2 统计特征处理

  • 根据teacher_prefix推断申请教师的性别teacher_gender
  • 统计teacher_id,teacher_gender,school_state,project_grade_category,project_grade_category,project_subject_categories,teacher_number_of_previously_posted_projects特征每一个数值的占比

举个例子,一个list=[‘a’, ‘a’, ‘b’, ‘c’],则’a’的衍生特征为0.50,’b’的衍生特征为0.25,’c’的衍生特征为0.25

2.3 情感特征处理

2.3.1 情感分析知识补充

文本一般情况下都有自己的情感倾向,一般分为褒义和贬义两种。使用textblob可以进行简单的情感分析,TextBlob(string).sentiment返回一个元组(polarity, subjectivity)

  • 第一个元素polarity是区间[-1, 1]内的一个浮点数,表示消极情感(负数)或积极情感(正数)的程度
  • 第二个元素subjectivity是区间[0, 1]内的一个浮点数,表示主客观程度,0表示客观,1表示主观
1
2
3
4
5
6
7
8
9
from textblob import TextBlob

sent1 = TextBlob('There is nothing more enjoyable than reading.').sentiment
sent2 = TextBlob("Can you stop watching such a boring movie?").sentiment
print(sent1)
print(sent2)
# output:
# Sentiment(polarity=0.5, subjectivity=0.55)
# Sentiment(polarity=-0.5, subjectivity=0.75)

2.3.2 情感特征处理

一般情况下,文本的情感倾向是一个较为重要的特征。文本特征中的标点符号,另类表达方式,I或者we,甚至是段落的划分都有可能影响阅读者的情感,具体怎么划分褒贬现在并没有一个统一的方法。

  • 本文使用textblob包进行情感分析,调用该包提供的情感分析方法去判断每一个文本特征(project_title, project_essay_1, project_essay_2, project_resource_summary, description)的情感倾向,并将其作为新特征,表示对应特征的褒贬特性及可信度
  • 由于符号和常用表达短语也可能影响阅读者的体验,所以还需要统计这些符号和常用表达短语在每一个文本特征中出现的次数,作为新的特征
  • 文本的长度也会影响阅读者的体验,所以每个文本特征又可以衍生出其长度的特征
  • 重复出现的词汇数量也会影响阅读者的体验,所以两两文本特征之间可以衍生出共有词汇数量的特征

统计的特殊字符如下列所示:
KeyChars = [‘!’, ‘\?’, ‘@’, ‘#’, ‘$‘, ‘%’, ‘&’, ‘*‘, ‘(‘, ‘[‘, ‘\{‘, ‘|‘, ‘-‘, ‘_’, ‘=’, ‘+‘, ‘.‘, ‘:’, ‘;’, ‘,’, ‘/‘, ‘\\\\r’, ‘\\\\t’, ‘\\”‘, ‘...‘, ‘etc’, ‘http’, ‘poor’, ‘military’, ‘traditional’, ‘charter’, ‘head start’, ‘magnet’, ‘year-round’, ‘alternative’, ‘art’, ‘book’, ‘basics’, ‘computer’, ‘laptop’, ‘tablet’, ‘kit’, ‘game’, ‘seat’, ‘food’, ‘cloth’, ‘hygiene’, ‘instraction’, ‘technolog’, ‘lab’, ‘equipment’, ‘music’, ‘instrument’, ‘nook’, ‘desk’, ‘storage’, ‘sport’, ‘exercise’, ‘trip’, ‘visitor’, ‘my students’, ‘our students’, ‘my class’, ‘our class’]

经过以上特征处理之后,可以明显的发现感叹号较多的申请被拒绝的次数更多。

2.4 时间特征

  • 根据申请时间戳project_submitted_datetime生成year, month, day, dayofweek, hour, days等特征
  • 生成时间特征的统计特征

2.5 组合特征

  • 类似于FM,两两数值特征之间进行相乘组合,有可能会产生更有用的特征,使用pearson相关系数筛选组合后的特征,保留与目标值相似性较高的组合特征。

比如max_price与mean_price组合的特征和目标值相似度就比单独的特征高


2.6 类别特征

由于类别特征多为文本类特征,因此使用词向量的方式。

2.6.1 词向量知识补充

词向量是处理文本一种常见的方式。词向量的维度数等于所有文本中不重复的单词数,每个位置对应一个单词,单词对应位置的数值表示单词在该文本中出现的频数。在sklearn中可以使用CountVectorizer进行词向量转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from sklearn.feature_extraction.text import CountVectorizer

corpus = [
'This is the first document.',
'This is the second second document.',
'And the third one.',
'Is this the first document?'
]

count_vectorizer = CountVectorizer()
X = count_vectorizer.fit_transform(corpus)
print(count_vectorizer.get_feature_names())
print(X.toarray())
# output:
# ['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']
# [[0 1 1 1 0 0 1 0 1]
# [0 1 0 1 0 2 1 0 1]
# [1 0 0 0 1 0 1 1 0]
# [0 1 1 1 0 0 1 0 1]]

2.6.2 类别特征转化为词向量

  • 使用原始特征teacher_prefix, school_state, project_grade_category, project_subject_categories, project_subject_subcategories计算词向量

2.7 文本特征

这一部分的文本特征主要是指长文本特征,即project_title,project_essay_1,project_essay_2,project_resource_summary,description

  • 清除单词时态,单复数变化等
  • 使用词向量的方式处理长文本特征
  • 使用TF-IDF的方式处理长文本特征
  • 使用word2vec的方式处理长文本特征

2.7.1 TF-IDF知识补充

TF-IDF(term frequency–inverse document frequency)是一种用于信息检索与文本挖掘的常用加权技术。tf-idf是一种统计方法,用以评估一字词对于一个文件集或一个语料库中的其中一份文件的重要程度。维基百科TF-IDF中的定义较为常见


此处公式总是渲染不对,所以直接用截图代替了

维基百科举了一个很通俗易懂的例子,具体如下图所示

在sklearn中可以使用TfidfVectorizer进行词向量转化,不过sklearn中的算法进行了些许改动,具体可以看文档

  • 使用频数作为分子,而不是频率
  • 计算idf的时候,分子分母同时加1
  • 计算idf的时候,取对数以e为底,并且最后加1
  • 为了避免某文档中某个词的频数过高,计算出所有次的tf-idf后,进行了L2标准化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from sklearn.feature_extraction.text import TfidfVectorizer

corpus = [
'This is the first document.',
'This is the second second document.',
'And the third one.',
'Is this the first document?'
]

tfidf_vectorizer = TfidfVectorizer()
X = tfidf_vectorizer.fit_transform(corpus)

print(tfidf_vectorizer.get_feature_names())
# ['and', 'document', 'first', 'is', 'one', 'second', 'the', 'third', 'this']
print(X)# 此处X是稀疏矩阵

2.7.2 word2vec知识补充

word2vec也是常见的文本处理方式,可以使用固定维数的向量表示文本,具体可以参考[NLP] 秒懂词向量Word2vec的本质.

词向量的训练需要花费大量的时间,所以一般情况下,如果不是特别专业的方向,都是使用各大研究所或者互联网公司训练好的词向量。本文使用的是Facebook公开的fastText预训练词向量数据集,fastText词向量的语料是维基百科或者网络上爬取的文本,一共有三个版本,本文使用的是最大的预训练词向量数据集crawl-300d-2M.vec.zip.

3. 单模型训练

3.1 XGBoost

XGBoost自从被天奇大佬发明以后,风靡各个数据挖掘竞赛,相比于传统的机器学习算法,准确率高,速度快,但是在微软亚研院的LightGBM出现以后,风头开始降低。

XGBoost主要的调整参数有:

  • objective,训练目标
  • eta,更新权重的步长,越小训练越慢
  • num_round,迭代次数
  • subsample,训练数据的抽样比例
  • colsample_bytree,训练特征的抽样比例
  • max_depth,最大树深
  • eval_metric,评价标准
  • early_stopping_rounds,及早停止的训练次数

调参的基本套路:

  • 根据实际情况设置正确的objective和eval_metric
  • eta设置的大一些,如0.1~0.2,num_round设置的小一些,如300~500,使用GridSearch方法对其他参数进行搜索
  • 逐步降低eta,找到最佳值,这个时候训练时间会越来越长
  • 如果出现了过拟合现象,调低max_depth,early_stoppoing_rounds

XGBoost单模型的提交分数是0.81005.

3.2 LightGBM

LightGBM是微软亚洲研究院在2016年10开源的boosting模型,相比于XGBoost,内存占用减小至1/6~1/5,训练速度提升至5~10倍,但是精度没有影响,甚至还略有提升,所以在各大数据挖掘竞赛中露脸的频率越来越多,毕竟时间就是生命。LightGBM相比于传统的GBDT算法,主要是使用了直方图算法,大大减少了训练的时间。

主要的调整参数有:

  • boosting_type, 决策树类型,缺省值为gbdt
  • objective, 训练目标
  • metric, 评价函数
  • max_depth, 最大深度
  • learning_rate, 学习率
  • feature_fraction, 随机选择特征数,[0,1]之间
  • bagging_fraction, 类似于 feature_fraction, 但是它将在不进行重采样的情况下随机选择部分数据

调参的套路和XGBoost差不多。LightGBM单模型的提交得分是0.81322.

3.3 Capsule Network

Capsule Network是Hinton大佬在2017年10月提出的新型神经网络结构。Capsule Network和以往的神经网络最大的不同就是每一个节点不再是标量,而是矢量,即vector in vector out。Capsule在论文中就是最基本的向量单元。

Capsule Network的提出主要是为了解决CNN忽略结构信息的问题,如下图所示,更改了眼睛和嘴唇的位置,CNN还是把图像识别为人类,如果把图片旋转180度,CNN就不能识别出人类了,因此为了避免这种情况,训练CNN之前需要进行数据增强等操作。

Capsule Network论文关键词主要有动态路由算法,压缩函数,耦合系数,Capsule Network具体内容详见参考资料[4]和[5]。

Capsule Network单模型的提交得分是0.81800.

4. 模型融合

模型融合就是把之前的单模型进行堆叠,或者说是组合,是一种bagging的思想。最简单的模型融合就是直接取平均数(回归)或者投票(分类)。当然,简单的代价就是精度的牺牲,kaggle比赛到后期千分位级别分数的提升都能前进好几名,因此大家一般都是使用稍微复杂一点的stacking方法进行模型融合,即使用模型去进行模型融合。

4.1 有CV的stacking


一般情况下都是5折交叉验证,使用训练集其中的4份数据训练模型,然后将剩下的1份数据和测试集代入模型,得到对应的预测值,这样最终可以得到某个模型在整个训练集上的预测值和5份测试集的预测值,将训练集上的预测值拼接在一起,作为第二层训练集的一列特征;将5份测试集的预测值求一个平均数,作为第二层测试集的一列特征。

对于所有单模型进行这样的操作,每个单模型都可以产出一列数据,将各个列的数据拼接起来,最终可以得到第二层的训练集和测试集。

这里使用交叉验证的方式训练模型并得到第二层的数据集是非常关键的一步,这么做的目的是避免数据集污染。


以上图为例,是一个2层的stacking,第1层一共训练了3个模型,然后将这3个模型预测的结果在水平方向进行堆叠,得到第二层的数据集,第二层使用XGB进行5折交叉验证训练,得到最终的预测结果。

使用该方法堆叠本文训练的3个模型,提交得分为0.82278.

4.2 无CV的stacking

无CV的stacking其实有点无聊,并且有刷公榜的嫌疑(笔者无聊至极,靠着这个方法刷到过公榜Top 2%)。Kaggle参赛者分享的kernel中都有模型预测的测试集结果,可以直接下载。这个时候没有训练集对应的标签,可以做一个简易版的stacking,不过一般精度都不如有CV的stacking。

以本文为例,Kaggle上有众多kernel,挑选得分0.78以上预测结果下载,作为融合的准备数据(选取别的模型预测结果的时候,可以使用pearson相关系数来筛选)

  • 载入若干个模型预测的结果y[i]及其分数score[i],均为矢量
  • 设置参数power,多为4,8,16,32,64等
  • 计算权重weight[i] = score[i] ** power
  • 使用logit函数将y[i]展开到[-∞, +∞]
  • 根据weight[i]求加权平均数
  • 将求得平均数再使用sigmoid函数压缩到[0, 1]之间,得到最终结果


logit函数


sigmoid函数

本质就是一种bagging的思想,得分越高,占的权重就越大,提交得分为0.82633,该方法由于没有CV验证,所以很容易过拟合,private score也证实确实发生了过拟合。

5. 经验和心得

  • 留出1~2月的业余时间比较合理,尤其是比赛截止最后几天,会杀出很多程咬金,public leaderboard变化非常频繁,我最后就掉以轻心了从12th掉到22th
  • 一个有效的CV非常重要,一般情况下,CV比public leaderboard上面的分数更加接近private score,尤其是public score和CV score相差较大时,以CV score为准
  • 特征非常的重要,大概需要花费整个比赛的2/3~3/4的时间与精力
  • 大概在public leaderboard排到Top 10%的位置的时候,就可以进行模型融合了,现在的比赛异常激烈,基本上到最后就是疯狂地stacking,进行若干层stacking,即stacking of stacking of stacking of stacking…
  • 多看看比赛的kernel和评论区,即使是边边角角的评论也不要放过,很可能就有大神的讨论

6.参考资料

[1].https://www.kaggle.com/safavieh/ultimate-feature-engineering-xgb-lgb-nn/notebook
[2].http://xgboost.readthedocs.io/en/latest/parameter.html#general-parameters
[3].http://lightgbm.apachecn.org/cn/latest/Parameters.html
[4].DSabour S, Frosst N, Hinton G E. Dynamic Routing Between Capsules[J]. 2017.
[5].https://kexue.fm/archives/4819