作者|穆文
前言
Kaggle上有篇名为「Approaching (Almost) Any Machine Learning Problem」的博客(点击底部阅读原文
),作者是Kaggle比赛的专业户,博客是他参加Kaggle比赛的经验总结。在进入正题前随便扯几句:
本文并非原博客的翻译版,而是90%的原创,是在原博客基础上融合自己的经验,重写了大部分章节和代码。所以当你看到本文跟原博客差别很大时,请不要怀疑人生 ;-P 原博客题目直译过来是『解决(几乎)任一机器学习问题的方法』,但原博客内容更偏数据挖掘之『术』而非机器学习之『道』,因为讲解了很多实际操作的trick和代码,所以我给本文取名为『数据挖掘比赛通用框架』。为简化描述,后续用
ML
指代机器学习
,DM
指代数据挖掘
本文可以看做是一篇科普性质的文章,内容简单基础,关键在于结合实际实践这些想法,所谓 practice makes perfect. 本人连续多天利用数个晚上写成此文,请尊重原创,转载请注明。也希望本文能给各位带来收获,如有疏漏,欢迎后台留言积极指正,先行谢过
◆ ◆ ◆
背景
DM流程通常分两个阶段
Step1. 数据清洗,数据格式调整 Step2. 特征构建,模型选择,效果评估
Step1.是整个流程中最耗时的,这点想必大家早有耳闻,DM界有句名言 garbage in ,garbage out ,可见清洗数据非常重要。从我的经验看,这部分工作跟实际处理的业务问题关系很大,比较dirty,也没有统一流程,所以本文重点放在Step2.
◆ ◆ ◆
前期准备
1、数据变换
先把原始数据通过一定变换,变成通用的多列数据类型,作为ML模型的输入,也就是上面的Step1。用X代表样本及其特征集合,y代表样本标签集合,整个流程如下:
根据标签y的不同,可以把DM问题分为以下几类:
预测结果的好坏需要用一些指标来衡量,通常不同类型的DM问题有不同的评价指标。对于二分类问题,很多时候类别本身不均衡(比如正样本很多负样本极少),所以我们通常用AUC值——即ROC曲线下的面积——来评价二分类结果;在多分类或者多标签问题中,我们通常选取评价指标为交叉熵(cross-entropy)或者log损失(log loss);对于回归问题,则可以选用MSE(mean square error)
4、 工具
我跟原博客作者一样,提倡使用python
解决DM问题,因为python的第三方库非常齐全,以下是常见的、用于DM问题的python库:
这里我补充说一下python开发环境和上面几个库的安装方法。首先我跟原作者一样,因为追求自(装)由(逼),所以不用python IDE(比如Anaconda, Pycharm),当然,装IDE可能省很多事情,个人建议安装Pycharm。然后我自己的python开发环境(纯属个人习惯,仅供参考):
『一个神奇的脚本,一键运行各类程序』
,里面的nppexec脚本可一键执行Python。以及linux风格的shell: git bash (git bash是基于msys的,跟cygwin略有不同)再说库的安装,首先强烈建议安装64位python2.7,然后针对不同操作系统:
『在Windows下安装64位Python及数据挖掘相关库』
(后续我会完善该文,但只发送给指定分组,具体见文末Bonus)。大多数库的安装都类似,但xgboost稍微复杂些,不能直接pip install,而是要装VS来编译其中相关文件,再安装,遇到问题可以微信我。另外tensroflow目前没有windows版本◆ ◆ ◆
DM问题框架
终于到了最核心的部分,原作者总结了一个他参加各类DM比赛常用的ML流程图,真是一图胜千言
这里我擅自补充一下,这张图看着眼花缭乱,其实就两点,这两点也是DM比赛中最核心的
两点:
特征工程(包括各种离散化、组合、选择) 模型选择、模型融合(即ensemble)
能把这两点做好,实属不易,但其实在工业界,特征工程和模型融合是否需要做到极致,是要看具体问题的。有些业务的数据维度本身就很稀少,并不足以支撑庞大的特征体系;有些业务需要很强的可解释性(比如金融领域),于是很多模型不能直接用;有些业务则要系统的实时性和稳定性,过于复杂的ensemble虽然能提升一点指标,但也许得不偿失。
上图当中的粉色部分是最常用的一些步骤,简单梳理一下:先确定DM问题的类型,然后对数据集划分,接着对常见的数值变量和类别变量做相应处理,可以进行特征选择,最后选择合适的模型做预测,评估模型并输出结果。下面将详细展开。
1. 问题定义
首先搞清楚要解决的问题属于哪一类,结合上节所讲,我们一般通过观察y标签类来定义DM问题的类型。
在明确了问题的分类后,我们将对数据集划分成训练集(Training Data)和验证集(Validation Data)(补充:很多时候还要划分出测试集(Test Data),先用训练集验证集的交叉验证来寻找模型的最优超参数,模型调优完毕后,最终用测试集来评估模型最终效果,具体参考我之前在公众号发布的『新手数据挖掘中的几个常见误区』
第二节)。划分方式如下:
这里我用自己本地的一个小数据集(名为toy_data.txt
)做展示,获取方式见文末Bonus,加载以上小数据集的代码如下:
import pandas as pd
df = pd.read_csv("toy_data.txt",sep = "\t")
df.head()
运行结果:
最后一个字段Label
就是我们要预测的y,在我的数据集里取值0或1,所以是一个二分类问题。
对于分类问题,要根据标签来划分数据集,比如每种标签采样多少,这样才能保证训练集跟验证集的样本标签分布接近,另外采样方式也不限于随机采样,可以根据实际业务问题选择合适的采样方式。这里我们可以借助scikit-learn来实现分层的K折交叉验证,代码如下
X = df.ix[:,0:-1]
y = df.ix[:,-1]
from sklearn.cross_validation import StratifiedKFold
kf = StratifiedKFold(y,3) # 三折交叉验证
用以下代码验证一下训练集和验证集中的正样本的占比:
idx_train, idx_valid = next(iter(kf))
print float(sum(y[idx_train]))/len(idx_train)
print float(sum(y[idx_valid]))/len(idx_valid)
结果为0.69713 0.69565
,两者非常接近。
注意,不太推荐使用iter(kf)
,这里只是为了展示标签分布,具体我会在本文第五节『实战』中介绍如何高效地使用交叉验证。
如果是回归问题,则不存在分类问题中类别标签分布不均的情况,所以我们只需采用普通的K折交叉验证即可:
from sklearn.cross_validation import KFold
kf = KFold()
毫不夸张地说,特征工程是DM重要的一环,也是决定DM比赛的关键因素。纵观DM比赛,几年间已由追求模型是否fancy转向无尽的特征工程,主要得益于越来越标准化的ML模型,以及更好的计算能力。
特征工程可以做的很复杂很庞大,但受限于本人目前的水平,这里只结合原博客内容讲解一些最基本(也是最经典)的处理方法。
类别变量(categorial data)是一种常见的变量,在我之前写的
『新手数据挖掘中的几个常见误区』
一文的第三节中讨论过为何要对类别变量编码
在toy_data
当中,字段Continent, Country, Product, Brand, TreeID, Industry, Saler
都可以看做是类别变量。处理类别变量一般是先标签化,然后再二值化编码。标签化的目的是将字段的原始值(如字符串、不连续的数字等)转换成连续的整数值,再对整数值二值化编码,如果原始值是整数,则直接二值化即可
我们拿toy_data
前几个样本的Continent
字段举例,对其进行编码:
mapper = skp.DataFrameMapper([
('Continent', sklearn.preprocessing.LabelBinarizer()),
])
tempX = df[['Continent']].head()
print tempX
print mapper.fit_transform(tempX.copy())
运行结果如下
可以看到,原来的一列Continent
字段变成了三列,分别代表[ 'AM', 'EP', 'LA' ]
,取值1表明是,取值0表明否。这就是常说的one-hot编码。如果类别变量的取值是整数,则直接用sklearn.preprocessing.OneHotEncoder()
即可,把上面代码中LabelBinarizer()
替换掉
注意我们必须将对训练集上的变换原封不动的作用到测试集,而不能重新对测试集的数据做变换(详见我之前写的『新手数据挖掘中的几个常见误区』
第一节)。
一般而言,数值变量不用做太多处理,只需做正规化(normalization)和标准化(standardization)即可,分别对应scikit-learn中的Normalizer
和StandardScaler
。不过对于稀疏变量,在做标准化的时候要注意,选择不去均值。
其实数值型变量最好也进行离散化,离散手段从基本的等距离散法、按分隔点人为指定,到聚类、输入树模型等,手段很多,在此不详细展开,我会在后续文章中提及。
文本在实际问题中很常见,比如用户评论、新闻摘要、视频弹幕等等。我们用的toy_data
不包含文本变量,所以这里我参考了scikit-learn的文档,一个小的corpus作为我们的训练数据集。
corpus = [
'This is the first document.',
'This is the second second document.',
'And the third one.',
'Is this the first document?',
]
corpus有四句话,可以看做是四个样本。接下来我们先用一个简单的方法处理文本变量——统计corpus中每个词出现次数,代码如下:
from sklearn.feature_extraction.text import CountVectorizer
vectorizer1 = CountVectorizer(min_df=1)
temp1 = vectorizer1.fit_transform(corpus)
print vectorizer1.get_feature_names()
print temp1.toarray() # temp1是sparse类型, 转换成ndarray方便查看
运行结果:
第一行是corpus中所有词,下面的ndarray每行代表该词在该样本中出现次数,比如第2行第6列的2
代表second
这个词在第二句话中出现了2次。一般我们不会直接用这个结果,而是会将每行归一化之类。
这种处理方式简单粗暴,没有考虑词与词之间的关系。我们改进一下这个方法,除了考虑单个词之外,还考虑corpus中成对出现的词(类似NLP里n-gram的 bi-gram,具体请自行Google),代码如下
vectorizer2 = CountVectorizer(ngram_range=(1, 2))
temp2 = vectorizer2.fit_transform(corpus)
print vectorizer2.get_feature_names()
print temp2.toarray()
运行结果:
然而,这还不够,像a is this
这类的助词、介词等,词频将非常高(在NLP中又叫停止词 stop word),所以需要减小他们的权重。一种做法是,不再简单统计该词在文档中出现的词频,而且还要统计 出现该词的文档的占比,这在NLP中叫tfidf。说的有点绕,具体到我们的例子中可以写成如下表达式:
某单词x的tfidf = x在一个样本中出现的次数/出现x的文档占比
分子即tf,分母即1/idf,有时需要用log sqrt
之类的函数作用在tf或者 1/idf上,以减弱某项的影响。同样,我们可以:
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer3 = TfidfVectorizer(ngram_range=(1, 2))
temp3 = vectorizer3.fit_transform(corpus)
print vectorizer3.get_feature_names()
print temp3.toarray()
运行结果:
仔细观察不难发现,is this
这类的停止词在变换后数值变小了。
!!注意!!跟处理类别变量、数值变量一样,我们在处理文本变量时,必须将训练集上的变换方式原封不动地作用到验证集或测试集上,而不能重新对验证集或者测试集做变换。比如在得到上面的vectorizer3
后,我们将其作用在一个新的样本 ['a new sentence']
上,代码如下
print vectorizer1.transform(['a new sentence']).toarray()
我们可以看到,结果是 [[0 0 0 0 0 0 0 0 0]]
,因为这个样本里的三个词从未出现在训练集corpus中,这是正确的结果!
为了方便将变换作用在未来的测试集,我们可以先把vectorizer3
用pickle
保存到本地,用的时候再load,保存方式如下:
import cPickle as pickle
pickle.dump(vectorizer3, open('vectorizer3.pkl','w'))
用的时候再 vectorizer = pickle.load(open('vectorizer3.pkl','r'))
即可。
ToDo 区别对待稠密特征和稀疏特征,
ToDo PCA等
ToDo
ToDo
◆ ◆ ◆
实战
to do,欢迎各位留言交流实战经验。