[关闭]
@hanxiaoyang 2017-12-14T12:28:44.000000Z 字数 21874 阅读 2532

机器学习系列(3)_逻辑回归应用之Kaggle泰坦尼克之灾

个人博客


作者: 寒小阳
时间:2015年11月。
出处:http://blog.csdn.net/han_xiaoyang/article/details/49797143
声明:版权所有,转载请注明出处,谢谢。

1.引言

先说一句,年末双十一什么的一来,真是非(mang)常(cheng)欢(gou)乐(le)!然后push自己抽出时间来写这篇blog的原因也非常简单:

OK,OK,这就来了咯,同学们别着急,我们先找个简单的实际例子,来看看,所谓的数据挖掘或者机器学习实际应用到底是怎么样一个过程。

『喂,那几个说要看大数据上机器学习应用的,对,就是说你们!别着急好么,我们之后拉点大一点实际数据用liblinear或者spark,MLlib跑给你们看,行不行?咱们先拿个实例入入门嘛』

好了,我是一个严肃的技术研究和分享者,咳咳,不能废话了,各位同学继续往下看吧!

2.背景

2.1 关于Kaggle

艾玛,逗逼模式开太猛了。恩,不闹,不闹,说正事,Kaggle是一个数据分析建模的应用竞赛平台,有点类似KDD-CUP(国际知识发现和数据挖掘竞赛),企业或者研究者可以将问题背景、数据、期望指标等发布到Kaggle上,以竞赛的形式向广大的数据科学家征集解决方案。而热爱数(dong)据(shou)挖(zhe)掘(teng)的小伙伴们可以下载/分析数据,使用统计/机器学习/数据挖掘等知识,建立算法模型,得出结果并提交,排名top的可能会有奖金哦!

2.2 关于泰坦尼克号之灾

3.说明

接触过Kaggle的同学们可能知道这个问题,也可能知道RandomForest和SVM等等算法,甚至还对多个模型做过融合,取得过非常好的结果,那maybe这篇文章并不是针对你的,你可以自行略过。

我们因为之前只介绍了Logistic Regression这一种分类算法。所以本次的问题解决过程和优化思路,都集中在这种算法上。其余的方法可能我们之后的文章里会提到。

说点个人的观点。不一定正确。
『解决一个问题的方法和思路不止一种』
『没有所谓的机器学习算法优劣,也没有绝对高性能的机器学习算法,只有在特定的场景、数据和特征下更合适的机器学习算法。』

4.怎么做?

手把手教程马上就来,先来两条我看到的,觉得很重要的经验。

  1. 印象中Andrew Ng老师似乎在coursera上说过,应用机器学习,千万不要一上来就试图做到完美,先撸一个baseline的model出来,再进行后续的分析步骤,一步步提高,所谓后续步骤可能包括『分析model现在的状态(欠/过拟合),分析我们使用的feature的作用大小,进行feature selection,以及我们模型下的bad case和产生的原因』等等。

  2. Kaggle上的大神们,也分享过一些experience,说几条我记得的哈:

    • 『对数据的认识太重要了!』
    • 『数据中的特殊点/离群点的分析和处理太重要了!』
    • 『特征工程(feature engineering)太重要了!在很多Kaggle的场景下,甚至比model本身还要重要』
    • 『要做模型融合(model ensemble)啊啊啊!』

更多的经验分享请加讨论群,具体方式请联系作者,或者参见《“ML学分计划”说明书》

5.初探数据

先看看我们的数据,长什么样吧。在Data下我们train.csv和test.csv两个文件,分别存着官方给的训练和测试数据。

  1. import pandas as pd #数据分析
  2. import numpy as np #科学计算
  3. from pandas import Series,DataFrame
  4. data_train = pd.read_csv("/Users/Hanxiaoyang/Titanic_data/Train.csv")
  5. data_train

pandas是常用的python数据处理包,把csv文件读入成dataframe各式,我们在ipython notebook中,看到data_train如下所示:

训练数据

这就是典型的dataframe格式,如果你没接触过这种格式,完全没有关系,你就把它想象成Excel里面的列好了。
我们看到,总共有12列,其中Survived字段表示的是该乘客是否获救,其余都是乘客的个人信息,包括:

逐条往下看,要看完这么多条,眼睛都有一种要瞎的赶脚。好吧,我们让dataframe自己告诉我们一些信息,如下所示:

  1. data_train.info()

看到了如下的信息:
数据信息

上面的数据说啥了?它告诉我们,训练数据中总共有891名乘客,但是很不幸,我们有些属性的数据不全,比如说:

似乎信息略少啊,想再瞄一眼具体数据数值情况呢?恩,我们用下列的方法,得到数值型数据的一些分布(因为有些属性,比如姓名,是文本型;而另外一些属性,比如登船港口,是类目型。这些我们用下面的函数是看不到的):

数值型数据基本信息

我们从上面看到更进一步的什么信息呢?
mean字段告诉我们,大概0.383838的人最后获救了,2/3等舱的人数比1等舱要多,平均乘客年龄大概是29.7岁(计算这个时候会略掉无记录的)等等...

6.数据初步分析

每个乘客都这么多属性,那我们咋知道哪些属性更有用,而又应该怎么用它们啊?说实话这会儿我也不知道,但我们记得前面提到过

重要的事情说三遍,恩,说完了。仅仅最上面的对数据了解,依旧无法给我们提供想法和思路。我们再深入一点来看看我们的数据,看看每个/多个 属性和最后的Survived之间有着什么样的关系呢。

6.1 乘客各属性分布

脑容量太有限了...数值看花眼了。我们还是统计统计,画些图来看看属性和结果之间的关系好了,代码如下:

  1. import matplotlib.pyplot as plt
  2. fig = plt.figure()
  3. fig.set(alpha=0.2) # 设定图表颜色alpha参数
  4. plt.subplot2grid((2,3),(0,0)) # 在一张大图里分列几个小图
  5. data_train.Survived.value_counts().plot(kind='bar')# 柱状图
  6. plt.title(u"获救情况 (1为获救)") # 标题
  7. plt.ylabel(u"人数")
  8. plt.subplot2grid((2,3),(0,1))
  9. data_train.Pclass.value_counts().plot(kind="bar")
  10. plt.ylabel(u"人数")
  11. plt.title(u"乘客等级分布")
  12. plt.subplot2grid((2,3),(0,2))
  13. plt.scatter(data_train.Survived, data_train.Age)
  14. plt.ylabel(u"年龄") # 设定纵坐标名称
  15. plt.grid(b=True, which='major', axis='y')
  16. plt.title(u"按年龄看获救分布 (1为获救)")
  17. plt.subplot2grid((2,3),(1,0), colspan=2)
  18. data_train.Age[data_train.Pclass == 1].plot(kind='kde')
  19. data_train.Age[data_train.Pclass == 2].plot(kind='kde')
  20. data_train.Age[data_train.Pclass == 3].plot(kind='kde')
  21. plt.xlabel(u"年龄")# plots an axis lable
  22. plt.ylabel(u"密度")
  23. plt.title(u"各等级的乘客年龄分布")
  24. plt.legend((u'头等舱', u'2等舱',u'3等舱'),loc='best') # sets our legend for our graph.
  25. plt.subplot2grid((2,3),(1,2))
  26. data_train.Embarked.value_counts().plot(kind='bar')
  27. plt.title(u"各登船口岸上船人数")
  28. plt.ylabel(u"人数")
  29. plt.show()

数据基本信息图示

bingo,图还是比数字好看多了。所以我们在图上可以看出来,被救的人300多点,不到半数;3等舱乘客灰常多;遇难和获救的人年龄似乎跨度都很广;3个不同的舱年龄总体趋势似乎也一致,2/3等舱乘客20岁多点的人最多,1等舱40岁左右的最多(→_→似乎符合财富和年龄的分配哈,咳咳,别理我,我瞎扯的);登船港口人数按照S、C、Q递减,而且S远多于另外俩港口。

这个时候我们可能会有一些想法了:

口说无凭,空想无益。老老实实再来统计统计,看看这些属性值的统计分布吧。

6.2 属性与获救结果的关联统计

  1. #看看各乘客等级的获救情况
  2. fig = plt.figure()
  3. fig.set(alpha=0.2) # 设定图表颜色alpha参数
  4. Survived_0 = data_train.Pclass[data_train.Survived == 0].value_counts()
  5. Survived_1 = data_train.Pclass[data_train.Survived == 1].value_counts()
  6. df=pd.DataFrame({u'获救':Survived_1, u'未获救':Survived_0})
  7. df.plot(kind='bar', stacked=True)
  8. plt.title(u"各乘客等级的获救情况")
  9. plt.xlabel(u"乘客等级")
  10. plt.ylabel(u"人数")
  11. plt.show()

各乘客等级的获救情况

啧啧,果然,钱和地位对舱位有影响,进而对获救的可能性也有影响啊←_←
咳咳,跑题了,我想说的是,明显等级为1的乘客,获救的概率高很多。恩,这个一定是影响最后获救结果的一个特征。

  1. #看看各性别的获救情况
  2. fig = plt.figure()
  3. fig.set(alpha=0.2) # 设定图表颜色alpha参数
  4. Survived_m = data_train.Survived[data_train.Sex == 'male'].value_counts()
  5. Survived_f = data_train.Survived[data_train.Sex == 'female'].value_counts()
  6. df=pd.DataFrame({u'男性':Survived_m, u'女性':Survived_f})
  7. df.plot(kind='bar', stacked=True)
  8. plt.title(u"按性别看获救情况")
  9. plt.xlabel(u"性别")
  10. plt.ylabel(u"人数")
  11. plt.show()

各乘客等级的获救情况

歪果盆友果然很尊重lady,lady first践行得不错。性别无疑也要作为重要特征加入最后的模型之中。

再来个详细版的好了。

  1. #然后我们再来看看各种舱级别情况下各性别的获救情况
  2. fig=plt.figure()
  3. fig.set(alpha=0.65) # 设置图像透明度,无所谓
  4. plt.title(u"根据舱等级和性别的获救情况")
  5. ax1=fig.add_subplot(141)
  6. data_train.Survived[data_train.Sex == 'female'][data_train.Pclass != 3].value_counts().plot(kind='bar', label="female highclass", color='#FA2479')
  7. ax1.set_xticklabels([u"获救", u"未获救"], rotation=0)
  8. ax1.legend([u"女性/高级舱"], loc='best')
  9. ax2=fig.add_subplot(142, sharey=ax1)
  10. data_train.Survived[data_train.Sex == 'female'][data_train.Pclass == 3].value_counts().plot(kind='bar', label='female, low class', color='pink')
  11. ax2.set_xticklabels([u"未获救", u"获救"], rotation=0)
  12. plt.legend([u"女性/低级舱"], loc='best')
  13. ax3=fig.add_subplot(143, sharey=ax1)
  14. data_train.Survived[data_train.Sex == 'male'][data_train.Pclass != 3].value_counts().plot(kind='bar', label='male, high class',color='lightblue')
  15. ax3.set_xticklabels([u"未获救", u"获救"], rotation=0)
  16. plt.legend([u"男性/高级舱"], loc='best')
  17. ax4=fig.add_subplot(144, sharey=ax1)
  18. data_train.Survived[data_train.Sex == 'male'][data_train.Pclass == 3].value_counts().plot(kind='bar', label='male low class', color='steelblue')
  19. ax4.set_xticklabels([u"未获救", u"获救"], rotation=0)
  20. plt.legend([u"男性/低级舱"], loc='best')
  21. plt.show()

各性别和舱位的获救情况

恩,坚定了之前的判断。

我们看看各登船港口的获救情况。

  1. fig = plt.figure()
  2. fig.set(alpha=0.2) # 设定图表颜色alpha参数
  3. Survived_0 = data_train.Embarked[data_train.Survived == 0].value_counts()
  4. Survived_1 = data_train.Embarked[data_train.Survived == 1].value_counts()
  5. df=pd.DataFrame({u'获救':Survived_1, u'未获救':Survived_0})
  6. df.plot(kind='bar', stacked=True)
  7. plt.title(u"各登录港口乘客的获救情况")
  8. plt.xlabel(u"登录港口")
  9. plt.ylabel(u"人数")
  10. plt.show()

各登船港口的获救情况

下面我们来看看 堂兄弟/妹,孩子/父母有几人,对是否获救的影响。

  1. g = data_train.groupby(['SibSp','Survived'])
  2. df = pd.DataFrame(g.count()['PassengerId'])
  3. print df
  4. g = data_train.groupby(['SibSp','Survived'])
  5. df = pd.DataFrame(g.count()['PassengerId'])
  6. print df

堂兄弟/妹影响

父母/孩子影响

好吧,没看出特别特别明显的规律(为自己的智商感到捉急...),先作为备选特征,放一放。

  1. #ticket是船票编号,应该是unique的,和最后的结果没有太大的关系,先不纳入考虑的特征范畴把
  2. #cabin只有204个乘客有值,我们先看看它的一个分布
  3. data_train.Cabin.value_counts()

部分结果如下:
Cabin分布

这三三两两的...如此不集中...我们猜一下,也许,前面的ABCDE是指的甲板位置、然后编号是房间号?...好吧,我瞎说的,别当真...

关键是Cabin这鬼属性,应该算作类目型的,本来缺失值就多,还如此不集中,注定是个棘手货...第一感觉,这玩意儿如果直接按照类目特征处理的话,太散了,估计每个因子化后的特征都拿不到什么权重。加上有那么多缺失值,要不我们先把Cabin缺失与否作为条件(虽然这部分信息缺失可能并非未登记,maybe只是丢失了而已,所以这样做未必妥当),先在有无Cabin信息这个粗粒度上看看Survived的情况好了。

  1. fig = plt.figure()
  2. fig.set(alpha=0.2) # 设定图表颜色alpha参数
  3. Survived_cabin = data_train.Survived[pd.notnull(data_train.Cabin)].value_counts()
  4. Survived_nocabin = data_train.Survived[pd.isnull(data_train.Cabin)].value_counts()
  5. df=pd.DataFrame({u'有':Survived_cabin, u'无':Survived_nocabin}).transpose()
  6. df.plot(kind='bar', stacked=True)
  7. plt.title(u"按Cabin有无看获救情况")
  8. plt.xlabel(u"Cabin有无")
  9. plt.ylabel(u"人数")
  10. plt.show()

有无Cabin记录影响

咳咳,有Cabin记录的似乎获救概率稍高一些,先这么着放一放吧。

7.简单数据预处理

大体数据的情况看了一遍,对感兴趣的属性也有个大概的了解了。
下一步干啥?咱们该处理处理这些数据,为机器学习建模做点准备了。

对了,我这里说的数据预处理,其实就包括了很多Kaggler津津乐道的feature engineering过程,灰常灰常有必要!

『特征工程(feature engineering)太重要了!』
『特征工程(feature engineering)太重要了!』
『特征工程(feature engineering)太重要了!』

恩,重要的事情说三遍。

先从最突出的数据属性开始吧,对,Cabin和Age,有丢失数据实在是对下一步工作影响太大。

先说Cabin,暂时我们就按照刚才说的,按Cabin有无数据,将这个属性处理成Yes和No两种类型吧。

再说Age:

通常遇到缺值的情况,我们会有几种常见的处理方式

本例中,后两种处理方式应该都是可行的,我们先试试拟合补全吧(虽然说没有特别多的背景可供我们拟合,这不一定是一个多么好的选择)

我们这里用scikit-learn中的RandomForest来拟合一下缺失的年龄数据(注:RandomForest是一个用在原始数据中做不同采样,建立多颗DecisionTree,再进行average等等来降低过拟合现象,提高结果的机器学习算法,我们之后会介绍到)

  1. from sklearn.ensemble import RandomForestRegressor
  2. ### 使用 RandomForestClassifier 填补缺失的年龄属性
  3. def set_missing_ages(df):
  4. # 把已有的数值型特征取出来丢进Random Forest Regressor中
  5. age_df = df[['Age','Fare', 'Parch', 'SibSp', 'Pclass']]
  6. # 乘客分成已知年龄和未知年龄两部分
  7. known_age = age_df[age_df.Age.notnull()].as_matrix()
  8. unknown_age = age_df[age_df.Age.isnull()].as_matrix()
  9. # y即目标年龄
  10. y = known_age[:, 0]
  11. # X即特征属性值
  12. X = known_age[:, 1:]
  13. # fit到RandomForestRegressor之中
  14. rfr = RandomForestRegressor(random_state=0, n_estimators=2000, n_jobs=-1)
  15. rfr.fit(X, y)
  16. # 用得到的模型进行未知年龄结果预测
  17. predictedAges = rfr.predict(unknown_age[:, 1::])
  18. # 用得到的预测结果填补原缺失数据
  19. df.loc[ (df.Age.isnull()), 'Age' ] = predictedAges
  20. return df, rfr
  21. def set_Cabin_type(df):
  22. df.loc[ (df.Cabin.notnull()), 'Cabin' ] = "Yes"
  23. df.loc[ (df.Cabin.isnull()), 'Cabin' ] = "No"
  24. return df
  25. data_train, rfr = set_missing_ages(data_train)
  26. data_train = set_Cabin_type(data_train)

处理Cabin和Age之后

恩。目的达到,OK了。

因为逻辑回归建模时,需要输入的特征都是数值型特征,我们通常会先对类目型的特征因子化。
什么叫做因子化呢?举个例子:

以Cabin为例,原本一个属性维度,因为其取值可以是['yes','no'],而将其平展开为'Cabin_yes','Cabin_no'两个属性

我们使用pandas的"get_dummies"来完成这个工作,并拼接在原来的"data_train"之上,如下所示。

  1. dummies_Cabin = pd.get_dummies(data_train['Cabin'], prefix= 'Cabin')
  2. dummies_Embarked = pd.get_dummies(data_train['Embarked'], prefix= 'Embarked')
  3. dummies_Sex = pd.get_dummies(data_train['Sex'], prefix= 'Sex')
  4. dummies_Pclass = pd.get_dummies(data_train['Pclass'], prefix= 'Pclass')
  5. df = pd.concat([data_train, dummies_Cabin, dummies_Embarked, dummies_Sex, dummies_Pclass], axis=1)
  6. df.drop(['Pclass', 'Name', 'Sex', 'Ticket', 'Cabin', 'Embarked'], axis=1, inplace=True)
  7. df

离散/因子化之后

bingo,我们很成功地把这些类目属性全都转成0,1的数值属性了。

这样,看起来,是不是我们需要的属性值都有了,且它们都是数值型属性呢。

有一种临近结果的宠宠欲动感吧,莫急莫急,我们还得做一些处理,仔细看看Age和Fare两个属性,乘客的数值幅度变化,也忒大了吧!!如果大家了解逻辑回归与梯度下降的话,会知道,各属性值之间scale差距太大,将对收敛速度造成几万点伤害值!甚至不收敛! (╬▔皿▔)...所以我们先用scikit-learn里面的preprocessing模块对这俩货做一个scaling,所谓scaling,其实就是将一些变化幅度较大的特征化到[-1,1]之内。

  1. import sklearn.preprocessing as preprocessing
  2. scaler = preprocessing.StandardScaler()
  3. age_scale_param = scaler.fit(df['Age'])
  4. df['Age_scaled'] = scaler.fit_transform(df['Age'], age_scale_param)
  5. fare_scale_param = scaler.fit(df['Fare'])
  6. df['Fare_scaled'] = scaler.fit_transform(df['Fare'], fare_scale_param)
  7. df

scaling

恩,好看多了,万事俱备,只欠建模。马上就要看到成效了,哈哈。我们把需要的属性值抽出来,转成scikit-learn里面LogisticRegression可以处理的格式。

8.逻辑回归建模

我们把需要的feature字段取出来,转成numpy格式,使用scikit-learn中的LogisticRegression建模。

  1. from sklearn import linear_model
  2. # 用正则取出我们要的属性值
  3. train_df = df.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*')
  4. train_np = train_df.as_matrix()
  5. # y即Survival结果
  6. y = train_np[:, 0]
  7. # X即特征属性值
  8. X = train_np[:, 1:]
  9. # fit到RandomForestRegressor之中
  10. clf = linear_model.LogisticRegression(C=1.0, penalty='l1', tol=1e-6)
  11. clf.fit(X, y)
  12. clf

good,很顺利,我们得到了一个model,如下:
modeling

先淡定!淡定!你以为把test.csv直接丢进model里就能拿到结果啊...骚年,图样图森破啊!我们的"test_data"也要做和"train_data"一样的预处理啊!!

  1. data_test = pd.read_csv("/Users/Hanxiaoyang/Titanic_data/test.csv")
  2. data_test.loc[ (data_test.Fare.isnull()), 'Fare' ] = 0
  3. # 接着我们对test_data做和train_data中一致的特征变换
  4. # 首先用同样的RandomForestRegressor模型填上丢失的年龄
  5. tmp_df = data_test[['Age','Fare', 'Parch', 'SibSp', 'Pclass']]
  6. null_age = tmp_df[data_test.Age.isnull()].as_matrix()
  7. # 根据特征属性X预测年龄并补上
  8. X = null_age[:, 1:]
  9. predictedAges = rfr.predict(X)
  10. data_test.loc[ (data_test.Age.isnull()), 'Age' ] = predictedAges
  11. data_test = set_Cabin_type(data_test)
  12. dummies_Cabin = pd.get_dummies(data_test['Cabin'], prefix= 'Cabin')
  13. dummies_Embarked = pd.get_dummies(data_test['Embarked'], prefix= 'Embarked')
  14. dummies_Sex = pd.get_dummies(data_test['Sex'], prefix= 'Sex')
  15. dummies_Pclass = pd.get_dummies(data_test['Pclass'], prefix= 'Pclass')
  16. df_test = pd.concat([data_test, dummies_Cabin, dummies_Embarked, dummies_Sex, dummies_Pclass], axis=1)
  17. df_test.drop(['Pclass', 'Name', 'Sex', 'Ticket', 'Cabin', 'Embarked'], axis=1, inplace=True)
  18. df_test['Age_scaled'] = scaler.fit_transform(df_test['Age'], age_scale_param)
  19. df_test['Fare_scaled'] = scaler.fit_transform(df_test['Fare'], fare_scale_param)
  20. df_test

modeling

不错不错,数据很OK,差最后一步了。
下面就做预测取结果吧!!

  1. test = df_test.filter(regex='Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*')
  2. predictions = clf.predict(test)
  3. result = pd.DataFrame({'PassengerId':data_test['PassengerId'].as_matrix(), 'Survived':predictions.astype(np.int32)})
  4. result.to_csv("/Users/Hanxiaoyang/Titanic_data/logistic_regression_predictions.csv", index=False)

预测结果

啧啧,挺好,格式正确,去make a submission啦啦啦!

在Kaggle的Make a submission页面,提交上结果。如下:
Kaggle排名

0.76555,恩,结果还不错。毕竟,这只是我们简单分析处理过后出的一个baseline模型嘛。

9.逻辑回归系统优化

9.1 模型系数关联分析

亲,你以为结果提交上了,就完事了?
我不会告诉你,这只是万里长征第一步啊(泪牛满面)!!!这才刚撸完baseline model啊!!!还得优化啊!!!

看过Andrew Ng老师的machine Learning课程的同学们,知道,我们应该分析分析模型现在的状态了,是过/欠拟合?,以确定我们需要更多的特征还是更多数据,或者其他操作。我们有一条很著名的learning curves对吧。

不过在现在的场景下,先不着急做这个事情,我们这个baseline系统还有些粗糙,先再挖掘挖掘。

上面只是我瞎想的,who knows是不是这么回事呢,老老实实先把得到的model系数和feature关联起来看看。

  1. pd.DataFrame({"columns":list(train_df.columns)[1:], "coef":list(clf.coef_.T)})

LR模型系数

首先,大家回去前两篇文章里瞄一眼公式就知道,这些系数为正的特征,和最后结果是一个正相关,反之为负相关。

我们先看看那些权重绝对值非常大的feature,在我们的模型上:

噢啦,观察完了,我们现在有一些想法了,但是怎么样才知道,哪些优化的方法是promising的呢?

因为test.csv里面并没有Survived这个字段(好吧,这是废话,这明明就是我们要预测的结果),我们无法在这份数据上评定我们算法在该场景下的效果...

而『每做一次调整就make a submission,然后根据结果来判定这次调整的好坏』其实是行不通的...

9.2 交叉验证

重点又来了:

『要做交叉验证(cross validation)!』
『要做交叉验证(cross validation)!』
『要做交叉验证(cross validation)!』

恩,重要的事情说三遍。我们通常情况下,这么做cross validation:把train.csv分成两部分,一部分用于训练我们需要的模型,另外一部分数据上看我们预测算法的效果。

我们用scikit-learn的cross_validation来帮我们完成小数据集上的这个工作。

先简单看看cross validation情况下的打分

  1. from sklearn import cross_validation
  2. #简单看看打分情况
  3. clf = linear_model.LogisticRegression(C=1.0, penalty='l1', tol=1e-6)
  4. all_data = df.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*')
  5. X = all_data.as_matrix()[:,1:]
  6. y = all_data.as_matrix()[:,0]
  7. print cross_validation.cross_val_score(clf, X, y, cv=5)

结果是下面酱紫的:
[0.81564246 0.81005587 0.78651685 0.78651685 0.81355932]

似乎比Kaggle上的结果略高哈,毕竟用的是不是同一份数据集评估的。

等等,既然我们要做交叉验证,那我们干脆先把交叉验证里面的bad case拿出来看看,看看人眼审核,是否能发现什么蛛丝马迹,是我们忽略了哪些信息,使得这些乘客被判定错了。再把bad case上得到的想法和前头系数分析的合在一起,然后逐个试试。

下面我们做数据分割,并且在原始数据集上瞄一眼bad case:

  1. # 分割数据,按照 训练数据:cv数据 = 7:3的比例
  2. split_train, split_cv = cross_validation.train_test_split(df, test_size=0.3, random_state=0)
  3. train_df = split_train.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*')
  4. # 生成模型
  5. clf = linear_model.LogisticRegression(C=1.0, penalty='l1', tol=1e-6)
  6. clf.fit(train_df.as_matrix()[:,1:], train_df.as_matrix()[:,0])
  7. # 对cross validation数据进行预测
  8. cv_df = split_cv.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*')
  9. predictions = clf.predict(cv_df.as_matrix()[:,1:])
  10. origin_data_train = pd.read_csv("/Users/HanXiaoyang/Titanic_data/Train.csv")
  11. bad_cases = origin_data_train.loc[origin_data_train['PassengerId'].isin(split_cv[predictions != cv_df.as_matrix()[:,0]]['PassengerId'].values)]
  12. bad_cases

我们判定错误的 bad case 中部分数据如下:
预测错误的原始数据

大家可以自己跑一遍试试,拿到bad cases之后,仔细看看。也会有一些猜测和想法。其中会有一部分可能会印证在系数分析部分的猜测,那这些优化的想法优先级可以放高一些。

现在有了"train_df" 和 "vc_df" 两个数据部分,前者用于训练model,后者用于评定和选择模型。可以开始可劲折腾了。

我们随便列一些可能可以做的优化操作:

大家接着往下挖掘,可能还可以想到更多可以细挖的部分。我这里先列这些了,然后我们可以使用手头上的"train_df"和"cv_df"开始试验这些feature engineering的tricks是否有效了。

试验的过程比较漫长,也需要有耐心,而且我们经常会面临很尴尬的状况,就是我们灵光一闪,想到一个feature,然后坚信它一定有效,结果试验下来,效果还不如试验之前的结果。恩,需要坚持和耐心,以及不断的挖掘。

我最好的结果是在『Survived~C(Pclass)+C(Title)+C(Sex)+C(Age_bucket)+C(Cabin_num_bucket)Mother+Fare+Family_Size』下取得的,结果如下(抱歉,博主君commit的时候手抖把页面关了,于是没截着图,下面这张图是在我得到最高分之后,用这次的结果重新make commission的,截了个图,得分是0.79426,不是目前我的最高分哈,因此排名木有变...):

做完feature engineering调整之后的结果

9.3 learning curves

有一个很可能发生的问题是,我们不断地做feature engineering,产生的特征越来越多,用这些特征去训练模型,会对我们的训练集拟合得越来越好,同时也可能在逐步丧失泛化能力,从而在待预测的数据上,表现不佳,也就是发生过拟合问题。

从另一个角度上说,如果模型在待预测的数据上表现不佳,除掉上面说的过拟合问题,也有可能是欠拟合问题,也就是说在训练集上,其实拟合的也不是那么好。

额,这个欠拟合和过拟合怎么解释呢。这么说吧:

而在机器学习的问题上,对于过拟合欠拟合两种情形。我们优化的方式是不同的。

对过拟合而言,通常以下策略对结果优化是有用的:

而对于欠拟合而言,我们通常需要更多的feature,更复杂的模型来提高准确度。

著名的learning curve可以帮我们判定我们的模型现在所处的状态。我们以样本数为横坐标,训练和交叉验证集上的错误率作为纵坐标,两种状态分别如下两张图所示:过拟合(overfitting/high variace),欠拟合(underfitting/high bias)

过拟合

欠拟合

我们也可以把错误率替换成准确率(得分),得到另一种形式的learning curve(sklearn 里面是这么做的)。

回到我们的问题,我们用scikit-learn里面的learning_curve来帮我们分辨我们模型的状态。举个例子,这里我们一起画一下我们最先得到的baseline model的learning curve。

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. from sklearn.learning_curve import learning_curve
  4. # 用sklearn的learning_curve得到training_score和cv_score,使用matplotlib画出learning curve
  5. def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None, n_jobs=1,
  6. train_sizes=np.linspace(.05, 1., 20), verbose=0, plot=True):
  7. """
  8. 画出data在某模型上的learning curve.
  9. 参数解释
  10. ----------
  11. estimator : 你用的分类器。
  12. title : 表格的标题。
  13. X : 输入的feature,numpy类型
  14. y : 输入的target vector
  15. ylim : tuple格式的(ymin, ymax), 设定图像中纵坐标的最低点和最高点
  16. cv : 做cross-validation的时候,数据分成的份数,其中一份作为cv集,其余n-1份作为training(默认为3份)
  17. n_jobs : 并行的的任务数(默认1)
  18. """
  19. train_sizes, train_scores, test_scores = learning_curve(
  20. estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes, verbose=verbose)
  21. train_scores_mean = np.mean(train_scores, axis=1)
  22. train_scores_std = np.std(train_scores, axis=1)
  23. test_scores_mean = np.mean(test_scores, axis=1)
  24. test_scores_std = np.std(test_scores, axis=1)
  25. if plot:
  26. plt.figure()
  27. plt.title(title)
  28. if ylim is not None:
  29. plt.ylim(*ylim)
  30. plt.xlabel(u"训练样本数")
  31. plt.ylabel(u"得分")
  32. plt.gca().invert_yaxis()
  33. plt.grid()
  34. plt.fill_between(train_sizes, train_scores_mean - train_scores_std, train_scores_mean + train_scores_std,
  35. alpha=0.1, color="b")
  36. plt.fill_between(train_sizes, test_scores_mean - test_scores_std, test_scores_mean + test_scores_std,
  37. alpha=0.1, color="r")
  38. plt.plot(train_sizes, train_scores_mean, 'o-', color="b", label=u"训练集上得分")
  39. plt.plot(train_sizes, test_scores_mean, 'o-', color="r", label=u"交叉验证集上得分")
  40. plt.legend(loc="best")
  41. plt.draw()
  42. plt.show()
  43. plt.gca().invert_yaxis()
  44. midpoint = ((train_scores_mean[-1] + train_scores_std[-1]) + (test_scores_mean[-1] - test_scores_std[-1])) / 2
  45. diff = (train_scores_mean[-1] + train_scores_std[-1]) - (test_scores_mean[-1] - test_scores_std[-1])
  46. return midpoint, diff
  47. plot_learning_curve(clf, u"学习曲线", X, y)

学习曲线

在实际数据上看,我们得到的learning curve没有理论推导的那么光滑哈,但是可以大致看出来,训练集和交叉验证集上的得分曲线走势还是符合预期的。

目前的曲线看来,我们的model并不处于overfitting的状态(overfitting的表现一般是训练集上得分高,而交叉验证集上要低很多,中间的gap比较大)。因此我们可以再做些feature engineering的工作,添加一些新产出的特征或者组合特征到模型中。

10.模型融合(model ensemble)

好了,终于到这一步了,我们要祭出机器学习/数据挖掘上通常最后会用到的大杀器了。恩,模型融合。

『强迫症患者』打算继续喊喊口号...
『模型融合(model ensemble)很重要!』
『模型融合(model ensemble)很重要!』
『模型融合(model ensemble)很重要!』
重要的事情说三遍,恩,噢啦。

先解释解释,一会儿再回到我们的问题上哈。
啥叫模型融合呢,我们还是举几个例子直观理解一下好了。

大家都看过知识问答的综艺节目中,求助现场观众时候,让观众投票,最高的答案作为自己的答案的形式吧,每个人都有一个判定结果,最后我们相信答案在大多数人手里。

再通俗一点举个例子。你和你班某数学大神关系好,每次作业都『模仿』他的,于是绝大多数情况下,他做对了,你也对了。突然某一天大神脑子犯糊涂,手一抖,写错了一个数,于是...恩,你也只能跟着错了。
我们再来看看另外一个场景,你和你班5个数学大神关系都很好,每次都把他们作业拿过来,对比一下,再『自己做』,那你想想,如果哪天某大神犯糊涂了,写错了,but另外四个写对了啊,那你肯定相信另外4人的是正确答案吧?

最简单的模型融合大概就是这么个意思,比如分类问题,当我们手头上有一堆在同一份数据集上训练得到的分类器(比如logistic regression,SVM,KNN,random forest,神经网络),那我们让他们都分别去做判定,然后对结果做投票统计,取票数最多的结果为最后结果

bingo,问题就这么完美的解决了。

模型融合可以比较好地缓解,训练过程中产生的过拟合问题,从而对于结果的准确度提升有一定的帮助。

话说回来,回到我们现在的问题。你看,我们现在只讲了logistic regression,如果我们还想用这个融合思想去提高我们的结果,我们该怎么做呢?

既然这个时候模型没得选,那咱们就在数据上动动手脚咯。大家想想,如果模型出现过拟合现在,一定是在我们的训练上出现拟合过度造成的对吧。

那我们干脆就不要用全部的训练集,每次取训练集的一个subset,做训练,这样,我们虽然用的是同一个机器学习算法,但是得到的模型却是不一样的;同时,因为我们没有任何一份子数据集是全的,因此即使出现过拟合,也是在子训练集上出现过拟合,而不是全体数据上,这样做一个融合,可能对最后的结果有一定的帮助。对,这就是常用的Bagging。

我们用scikit-learn里面的Bagging来完成上面的思路,过程非常简单。代码如下:

  1. from sklearn.ensemble import BaggingRegressor
  2. train_df = df.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass.*|Mother|Child|Family|Title')
  3. train_np = train_df.as_matrix()
  4. # y即Survival结果
  5. y = train_np[:, 0]
  6. # X即特征属性值
  7. X = train_np[:, 1:]
  8. # fit到BaggingRegressor之中
  9. clf = linear_model.LogisticRegression(C=1.0, penalty='l1', tol=1e-6)
  10. bagging_clf = BaggingRegressor(clf, n_estimators=20, max_samples=0.8, max_features=1.0, bootstrap=True, bootstrap_features=False, n_jobs=-1)
  11. bagging_clf.fit(X, y)
  12. test = df_test.filter(regex='Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass.*|Mother|Child|Family|Title')
  13. predictions = bagging_clf.predict(test)
  14. result = pd.DataFrame({'PassengerId':data_test['PassengerId'].as_matrix(), 'Survived':predictions.astype(np.int32)})
  15. result.to_csv("/Users/HanXiaoyang/Titanic_data/logistic_regression_bagging_predictions.csv", index=False)

然后你再Make a submission,恩,发现对结果还是有帮助的。

ensemble之后的结果

11.总结

文章稍微有点长,非常感谢各位耐心看到这里。
总结的部分,我就简短写几段,出现的话,很多在文中有对应的场景,大家有兴趣再回头看看。

对于任何的机器学习问题,不要一上来就追求尽善尽美,先用自己会的算法撸一个baseline的model出来,再进行后续的分析步骤,一步步提高

在问题的结果过程中:

本文中用机器学习解决问题的过程大概如下图所示:
机器学习解决问题的过程

12.关于数据和代码

本文中的数据和代码已经上传至github中,欢迎大家下载和自己尝试。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注