= = 尽管试了很长时间,正确率也只能到75%,也许用其他的决策树方法能达到更高的效果吧。
这次主要用的是决策树方法去解决问题的,在机器学习实战的ID3决策树算法上进行扩展的。发现了机器学习实战决策树的一个bug,同时领会到决策树的一个局限性。都在文章里了,结构大致如下:
1 问题分析
2 解决方案
3 问题收获
1 问题分析
我们的问题和上一篇kaggle的一样,还是TitanicKaggle的预测问题。这次使用决策树方法。决策树的基本意图就是想通过不断分特征把所有的样本都分离开来,对应相应的分类结果,在这个题目里就是对应人员是否存货。然后把这个分离的方法提炼成模型,也就是我们的决策树,然后将新的数据按照同样的模型进行分类。
决策树的话,我们需要思考下面几个问题。
1.1 每个bin的混杂性。
根据决策树进行分类,分到最后的时候,所有的样本都被分到属于各自的bin里面。
从极限上考虑,最好的情况是,在利用各个特征进行分类的时候,每个样本都分到属于自己独特的那个bin里面,也就是一个萝卜一个坑,这样我们有新数据样本加 入的时候,就可以照猫画虎,对应到那个里面了。但是其实这个只是一个理想状况,真实的bin里面肯定是有这个类那个类的,举个例子,如果我们按照决策树分类的话,同样是一等舱,同样是妇女,同样是20-30岁,符合这样条件人里面,我们的rose活了,但是同样的一个少女却死掉了,唯独她缺少Jack这个人(但是我们的数据无法反应出来)这是很可能的。因此,基本上一个bin里面,有的符合有的不符合,但是我们决策树只能做到这个地步了,得给这个bin下个结论,要么这个bin是存活的,要么他们死掉。如何办呢?我们就计算这个bin的训练样本中存活的有多少,死亡的有多少。比如一等舱,妇女,20-30岁,这个bin里面有20个是存活的,3个是死掉的,那么我们就说这个bin是存活的。数据训练完,有新的测试样本如果进来,我们就判断她是活着的。
1.2 决策树分类次序的依据
决策树是根据多个特征进行分类的,比如像一等舱,妇女,20-30岁这样,我们是按照它是几等舱-->是什么性别的-->多大了.进行分类的。那么为什么是按照这个次序进行的?而不是先看年龄再看性别这种分类呢? 这涉及到决策树的贪心算法和熵判定方法。
决策树是贪心算法,什么是贪心算法,就是我保证每一步都是当前最好的决定。举个例子就是下棋的时候如果我的当前这个步最有利的话,我就走这一步,别人让给我个车,我吃了就好了,但是如果别人目的是为了让车去将军呢?那么我就玩完了。也就是说贪心算法保证每步最优,但是不一定整体是最优的。而且很可能根本不是最优的,也就是说我们废了这么大劲,也许到头来从模型上就不是最优的。这是完全有可能的,但是它至少有可能是次优的,这样做要比乱出牌好的多。
那么如何判定当前一步是最优的呢?我用熵计算方法作为依据,应用某个特征进行分类后,分类后集合的熵最小,就是我最好的方法.。为什么?熵是用来计
算混乱程度的。我们用这个公式计算熵:sum(-P*log2(P)),怎么理解?P是某个类别在集合里面出现概率,比如10个人的样本里面3个存活,7个走了。如果我按
照年龄特征将他们分成4个年少(2走2生)和6个年长(5走1生),那么分类后的熵就是-(0.5*log2(0.5)+0.5*log2(0.5))-(0.167*log2(0.167)+0.833*log2(0.833))=1.65, 而如果我们按照性别进行划分,3个女性(0走3生),7个男性(7走0生),这样计算分类后的熵是 -(1*log(1)-(1*log(1))=0 显然按照性别的划分更好(虽然实际数据中不太可能这样),因此这个是我们就根据熵选择按照性别这个特征先划分。
总之,熵就是计算混乱程度,一个集子里面各种类别越多,越杂,熵自然越大,如果越纯净,熵就越小。决策树就是找到当前最能使得划分后样本纯净度最小的那个特征作为这次划分的特征。
1.3 决策树是根据横纵特征进行划分。
决策树是根据特征进行划分的,比如Pclass舱等级,Sex性别,Age年龄等等几个横向维度进行划分。这个是横向的,而在特征中其实还有划分。离散变量特征比如Parch船上的父母数量,我们可以划分出0,1,2几种。对于连续变量,比如年龄,我们可以这样划分0-15岁幼年的,15-24青年,24-40成年 40+老年。这是在纵向上进行划分。决策树要做的就是利用横向和纵向的特征,联合进行划分。横向特征和纵向特征其实实质上都是划分的一个维度而已,只不过我们通过熵计算法计算横向维度的划分次序而已。
2 解决方案
说了这么多,开始解决。
2.1特征维度分析
数据中的字段有:Age年龄,Pclass舱等级,Name名字,Sex性别,Age年龄,SibSp船上姐妹兄弟数量,Parch船上父母数量,Ticket车票,Fare票价,Cabin包厢,Embarked登船地。
数据整理:通过python的pandas库中的dataframe.isnull.sum()函数,我们可以知道各个字段为空的状态。其中Age 177 Cabin 687 Embarked 2,Cabin数据空的数据比较多,可以剔除。Age和Embarked两个数据需要填充下。而Ticket和Name名字特征好像没法用,剔除。
连续离散字段分析:通过分析数据,我们知道连续字段有Age,Fare,离散字段有Pclass,SibSp,Parch,Embarked。我们决策树的子特征不能太多,需要划分。比如SibSp。我通过df.SibSp.hist() ,可以知道SibSp中为0的有800个,为1的有100多个,大于等于2的少多了,因此我们将SibSp特征划分为0个1个和大于等于2个这三种情况。而对于连续变量比如Age,通过该df.Age,hist()函数,我们知道大概0-16岁有200人,16-24岁有200人,24-32岁有两百人,大于32岁有两百人,这样划分的。
总之根据上面的意思,我们大致划分出的数据如下
2.2决策树
# -*- coding: utf-8 -*-
'''
Created on Oct 12, 2014
Decision Tree Source Code for kaggle titanic
@author: LI Ning
'''
from math import log
import operator
import numpy as np
import pandas as pd
from kaggleJudge import *
import kaggleJudge as kj
import treePlotter as tp
import random
#熵计算法
def calcShannonEnt(dataSet):
numEntries = len(dataSet)
labelCounts = {}
for featVec in dataSet: #the the number of unique elements and their occurance
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/numEntries
shannonEnt -= prob * log(prob,2) #log base 2
return shannonEnt
#根据特征和特征值切分数据集
def splitDataSet(dataSet, axis, value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] #chop out axis used for splitting
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
#选择最佳切分的特征
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #the last column is used for the labels
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain = 0.0; bestFeature = -1
for i in range(numFeatures): #iterate over all the features
featList = [example[i] for example in dataSet]#create a list of all the examples of this feature
uniqueVals = set(featList) #get a set of unique values
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet)/float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy #calculate the info gain; ie reduction in entropy
if (infoGain > bestInfoGain): #compare this to the best gain so far
bestInfoGain = infoGain #if better than current best, set to best
bestFeature = i
return bestFeature #returns an integer
#如果bin有多个类,按大多数确定最终分类
def majorityCnt(classList): #针对只剩最终分类结果的dataSet数据集,按大多数占优处理
classCount={}
for vote in classList:
if vote not in classCount.keys(): classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
#如果完全相同的特征对应的是不同的类,按照大多数确定最终分类
def majorityJudge(dataSet): #针对只剩最终分类结果的dataSet数据集,按大多数占优处理
classList=[ example[-1] for example in dataSet ]
classCount={}
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
else:
classCount[vote] += 1
sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
if sortedClassCount[0][1]==sortedClassCount[1][1]:
if(random.randint(0,2)==0):
return sortedClassCount[0][0]
else:
return sortedClassCount[1][0]
return sortedClassCount[0][0]
#根据数据集和标签集构件决策树
def createTree(dataSet,lab):
labels=list(lab)
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len(classList):
return classList[0]#如果都是一个类了,那么就自动返回这个类作为值。
if len(dataSet[0]) == 1: # 如果dataset只有结果live或dead项目了,那么就选哪个多的来作为这个vec是否存活的最终决定
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet) #最佳区分feature
if(bestFeat==-1):
return majorityJudge(dataSet)
bestFeatLabel = labels[bestFeat] #最佳feature对应的标签名字
myTree = {bestFeatLabel:{}}
del(labels[bestFeat])
featValues = [example[bestFeat] for example in dataSet] #纵向
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:] #copy all of labels, so trees don't mess up existing labels
retDataSet=splitDataSet(dataSet, bestFeat, value)
myTree[bestFeatLabel][value] = createTree(retDataSet,subLabels)
return myTree
#通信元素就是数据集
#根据决策树递归分类
def classify(inputTree,featLabels,testVec):
firstStr = inputTree.keys()[0]
key = testVec[featLabels.index(firstStr) ] #获取样本向量对应特征的值
secondDict = inputTree[firstStr]
valueOfFeat = secondDict[key]
if isinstance(valueOfFeat, dict):
classLabel = classify(valueOfFeat, featLabels, testVec)
else:
classLabel = valueOfFeat
return classLabel
#数据ETL函数
def trainDataPrd(trainOrNot):
if(trainOrNot):
df=pd.read_csv('data/train.csv') #训练数据整理
else:
df=pd.read_csv('data/test.csv') #测试数据整理
#判断字段的空值状况,自动化
df.isnull().sum() #通过判断为空的数量,来判断为空的数目,其中False=0,True=1嘛
#整理字段
df['Gender']=df['Sex'].map({'male':0,'female':1}).astype(int)
df=df.drop(['Cabin','Sex','Name','Ticket'],axis=1)
#填充空值字段
#按照gender,class进行groupby聚类然后填充这个分组的平均值
ageArr=np.zeros((2,3))
for i in range(2):
for j in range(3):
ageArr[i,j]=df[ (df['Gender']==i) & (df['Pclass']==j+1) ]['Age'].dropna().mean()
# 自我赋值df[ (df['Gender']==i) & (df['Pclass']==j+1) ]=df[ (df['Gender']==i) & (df['Pclass']==j+1) ].fillna(ageArr[i,j])
df.loc[(df['Gender']==i) & (df['Pclass']==j+1) &(df['Age'].isnull()), 'Age']=ageArr[i,j]
#填充test数据的Fare空字段
df.loc[(df['Fare'].isnull()),'Fare']=8
df[ (df['Gender']==1) & (df['Pclass']==1) & (df['Age']>30) & (df['Age']<40)].groupby(by='Embarked').count() #s
df.loc[df['Embarked'].isnull(),'Embarked']='S'
#特征拆列
#Age 0-13 13-37 >37
#Sibsp 0 1 2 >2
#Parch 0 1 >=2
#Embarked S C Q
#Gender 0 1
df['AgeJug'] = df['Age'].map(kj.judAge)
df['SibSpJug'] = df['SibSp'].map(kj.judSibSp)
df['ParchJug'] = df['Parch'].map(kj.judParch)
df['FareJug'] = df['Fare'].map(kj.judFare)
df['EmbarkedJug'] = df['Embarked'].map(kj.judEmbarked)
dfall=df
df=df.drop(['Age','SibSp','Parch','Fare','PassengerId','Embarked'],axis=1)
if (trainOrNot):
df['result']=df['Survived'].map({1:'live',0:'dead'})
datalist=df.values[:,1:].tolist()
else:
datalist=df.values[:,:].tolist()
labels =[ 'Pclass','Embarked','Gender','AgeJug','SibSpJug','ParchJug','FareJug']
return datalist, labels
#样本训练主函数
def run():
#训练阶段
dat,lab=trainDataPrd(True)
t=createTree(dat,lab) # 出错点在于lab是引用拷贝的
tp.createPlot(t)
#训练结果测试阶段
rows,cols=np.shape(dat)
correctCount=0
wrongCount=0
for r in range(rows):
vec=dat[r][:-1]
result=dat[r][-1]
predictResult=classify(t,lab,vec)
if predictResult==result:
correctCount+=1
else:
wrongCount+=1
total=correctCount+wrongCount
print correctCount/float(total)
#样本预测函数
def test():
#训练阶段
dat,lab=trainDataPrd(True)
t=createTree(dat,lab) # 出错点在于lab是引用拷贝的
#tp.createPlot(t)
#样本预测
datTest,labTest=trainDataPrd(False)
rows,cols=np.shape(datTest)
predictList=[]
for r in range(rows):
vec=datTest[r][:]
print vec
liveordead=classify(t,labTest,vec)
if liveordead=='live':
predictList.append(1)
else:
predictList.append(0)
df=pd.read_csv('data/test.csv')
series_passId=df['PassengerId']
series_survived=pd.Series(predictList)
r=pd.DataFrame()
r['PassengerId']=series_passId
r['Survived']=series_survived
r.to_csv('titanitDecisionTree.csv',index=False)
3 问题收获
3.1 决策树本身的局限性
我们是依靠决策树进行预测,如果我们根据之前的数据构建的决策树比较健全,那么没问题,如果我们的数据较小,而我们数据的特征又较多的话,那么很有可能我们添加来的新数据,根据特征集进行划分走的分类路径是决策树里所没有的,这个时候预测就会出现问题了。因此在数据集较小的时候,使用的数据的维度特征也不能太多,同时每个特征的纵向划分也不能太多,总之当数据特征和数据集样本数不对称的时候就会出现上述问题。
3.2 特征选择和分类的重要性,
之前我想特征越多越好,但是越多就会出现过拟合的现象,kaggle的效果不好。用logistic训练的时候只有79,但是测试有75的正确了,现在用决策树过拟合的时候有89的训练正确率,但是实际测试正确率还是只有74-75左右。
其实即使我们不增加特征,合理的选择特征也会非常有效。之前我根据我自己想的对年龄按照0-15,15-40,40+进行划分,训练精度有85,在kaggle跑的精度只有73%,但是我后来又通过hist看了下年龄的分布情况,将它调整为0-16,16-24,24-32,32+的时候,训练精度上升到88。kaggle跑的时候在75.5左右。
另外,挖掘新特征也是很重要的,开始的时候我们觉得姓名没办法挖掘,扔掉了,但是有的人利用名字的常见性推测他是不是新移民,从而将其与存活率挂钩。因为那个时候不像现在第n代移民这样都会英语啊,新来美国移民英语一般不太好,在那个关键时刻有可能影响到他信息获取和逃生的。总之,关键的数据创建,较好的对数据的处理,都特别重要。
尽管还是很讽刺的是,决策树的精度还是赶不上单变量的精度。这其中大半是我的选择还是有些问题,但是在这个过程中还是增加了不少对于决策树的实践的理解。
3.3 python和数据处理
在使用python和pandas进行数据分析的时候,我觉得很好的一点是dataframe和numpy的结合,对于小粒度的数据操作和使用特别方便,如果需要大粒度的使用可以用mat。我个人觉得在算法的实现上,matlab也是不弱的,但是python数据整理方面的功能特别好用,数据清理转换等等操作非常方便,这个对好的数据分析来说特别重要。当然如果对性能有要求,肯定还是C++更好。
3.4 决策树的目标
在对连续变量转化为标注数据的时候,我在想用一个什么目标指导我的这项工作。是让每个子类分到的人数尽可能相等,还是这些子类的存活差异尽可能较大。
如果按照子类存活差异大这个情况分类,比如Fare票价里面,>23有95的存活率,而<23的只有62%的存活。但是如果按照将人群尽可能均匀分配的话,我觉得0-10,10-20,20+这种情况分割更好。
的确以存活为目标进行分类可能效果好,但是我是担心是不是这个已经添加了人的智能在里面,而不是机器学习了,人是不是某种程度上snoop了这个数据结果呢?