Ghouls, Goblins, and Ghosts
开篇
这篇文章描述了使用R语言利用决策树以及随机森林对kaggle的一个分类问题解题的全部过程。本文需要读者对机器学习中的决策树、随机森林的原理有所了解,并且知道基本的R语言语法。
背景
关于Kaggle
Kaggle是一个进行数据挖掘和预测竞赛的在线平台,任何人都可以参加竞赛。企业或者组织者将实际问题的背景、数据、目标发布到Kaggle,广大参与者可以分析、提交答案,最终根据准确率排名。 Kaggle的竞赛题目有些有奖金,而有些则是数据分析挖掘的练习场。我们这次练手的就是这样一个playground级别的题目Ghouls, Goblins, and Ghosts。
关于 Ghouls, Goblins, and Ghosts
题目网址: https://www.kaggle.com/c/ghouls-goblins-and-ghosts-boo 大致背景如下:闲来无事的研究员把研究院里面的900只怪物的特征都测量了一遍,根据特征已经对371只怪物进行了分类,分为ghouls、ghosts和goblins三类。而剩下的怪物的分类工作就交给我们了。
这是一个分类问题,后面我们用决策树来完成这个任务。
关于决策树
不知道决策树是啥的请自行google http://www.cnblogs.com/leoo2sk/archive/2010/09/19/decision-tree.html
观察数据
初步观察数据
从网站上下载训练数据train.csv和测试数据test.csv,放在目录D:/RData/Ghost/下 第一步当然是看看这些数据都长什么样子,有哪些特征可以处理。 首先将数据读取进来,合并成一个data.frame,通过增加一个变量data_set来区分训练数据集和测试数据集。 合并成同一个data.frame的好处就是后续所有的特征变量的转化操作都可以在训练数据集和测试数据集上同时生效。
代码语言:javascript复制# 加载包
library(dplyr)
#读取训练数据集和测试数据集
train <- read.csv("D:/RData/Ghost/train.csv")
train$data_set <- 'train'
test <- read.csv("D:/RData/Ghost/test.csv")
test$data_set <- 'test'
# 合并两个训练集
all <- bind_rows(train, test)
# 转化data_set为factor
all$data_set <- factor(all$data_set)
先来看一下数据集中的变量:
str(all)
结果如下:
'data.frame': 900 obs. of 8 variables:
$ id : int 0 1 2 4 5 7 8 11 12 19 ...
$ bone_length : num 0.355 0.576 0.468 0.777 0.566 ...
$ rotting_flesh: num 0.351 0.426 0.354 0.509 0.876 ...
$ hair_length : num 0.466 0.531 0.812 0.637 0.419 ...
$ has_soul : num 0.781 0.44 0.791 0.884 0.636 ...
$ color: Factor w/ 6 levels "black","blood",..: 4 5 1 1 5 5 6 4 3 6 ...
$ type : Factor w/ 3 levels "Ghost","Ghoul",..: 2 3 2 2 1 3 3 2 1 1 ...
$ data_set : chr "train" "train" "train" "train" ...
数据集中除了变量id和data_set外还有其他6个变量。
数据集中各字段含义:
- id 怪物的标号ID
- bone_length 骨头的平均长度(标准化到0-1)
- rotting_flesh -腐肉的百分比
- hair_length -毛发的平均长度(标准化到0-1)
- has_soul -灵魂的百分比
- color -体表颜色,分为’white’,’black’,’clear’,’blue’,’green’,’blood’六种
- type -怪物的类型,分为’Ghost’、’Goblin’和’Ghoul’三种 查看所有变量的描述性统计信息:
summary(all) 结果:
代码语言:javascript复制id bone_length rotting_flesh hair_length
Min. : 0.0 Min. :0.0000 Min. :0.0000 Min. :0.0000
1st Qu.:224.8 1st Qu.:0.3321 1st Qu.:0.4024 1st Qu.:0.3961
Median :449.5 Median :0.4268 Median :0.5053 Median :0.5303
Mean :449.5 Mean :0.4291 Mean :0.5050 Mean :0.5222
3rd Qu.:674.2 3rd Qu.:0.5182 3rd Qu.:0.6052 3rd Qu.:0.6450
Max. :899.0 Max. :1.0000 Max. :1.0000 Max. :1.0000
has_soul color type data_set
Min. :0.0000 black:104 Ghost :117 test :529
1st Qu.:0.3439 blood: 21 Ghoul :129 train:371
Median :0.4655 blue : 54 Goblin:125
Mean :0.4671 clear:292 NA's :529
3rd Qu.:0.5892 green: 95
Max. :1.0000 white:334
其中四个变量bone_length、rotting_flesh、hair_length、has_soul的原始数据都已经被标准化。 而变量color、type、data_set都是因子变量。 从date_set变量的分布中可以看到训练集有371个记录,测试集有529条记录。 变量type就是这次分类算法的响应变量。
整体数据很工整,也不存在缺失值,预示着我们很快就可以直接撸模型了。
数据初步分析
我们先看看各变量在各个怪物分类上的分布情况,来人工筛查一些各个变量和分类结果的相关度。
代码语言:javascript复制# 加载画图包
library(ggplot2)
# 取出训练集数据
train <- all[all$data_set == 'train', ]
# 对bone_length、rotting_flesh、hair_length、has_soul四个变量做分析
source('D:/RData/comm/multiplot.r')
plots <- list()
name_pic <- c("bone_length", "rotting_flesh", "hair_length", "has_soul")
for(i in 1:length(name_pic)) {
p <- ggplot(train, aes_string(x = "type", y = name_pic[i] , fill = "type") )
geom_boxplot()
guides(fill = FALSE)
ggtitle(paste(name_pic[i], " vs type"))
xlab("Creature")
plots[[i]] <- p
}
# 其中multiplot是一个自定义函数,用来将多张图画在一起。具体代码见附件。
multiplot(plotlist = plots, cols = 2)
通过图像可以看到怪物中的两类Ghost和Ghoul在四个变量上区分度比较高,而Goblin则处于中间位置,同其他两类的特征都有交集。
查看图形,可以初步有如下的推断:
最终Goblin的预测准确率会低一些。 hair_length和has_soul两个特征上Ghost和Ghoul区分度相对比较高,后续模型中的重要性应该会高一些。
而其他例如腐烂度等一类的指标看不出什么名堂。 我们继续看下颜色是否和怪物的类型有关系
代码语言:javascript复制# 查看颜色和类型的关系
ggplot(train, aes(x=color, fill=type)) geom_bar() theme(text=element_text(size=14))
这个结果充分说明,各种怪物对颜色也没有特别的偏好,基本所有颜色中三种怪物比例都差不多。
接下来再看看各变量之间的散点图,直观的感受一下各变量之间的相关性。
代码语言:javascript复制# 各个指标的散点图
pairs(~bone_length rotting_flesh hair_length has_soul, data=train, col = train$type, labels = c("Bone Length", "Rotting Flesh", "Hair Length", "Soul"))
三种怪物的变量相互交织在一起,人类已经没有什么好办法了,剩下的就交给计算机吧。
模型训练
基础模型
特征都已经处理好了(其实我们什么都没有做,汗),现在开始扔进模型里面看看吧。 我们用R语言的rpart包里面的CART决策树来对样本分类。
首先设置决策树的控制参数
代码语言:javascript复制# 加载rpart包
library(rpart)
library(rpart.plot)
# 设置决策树的控制参数
# minsplit -- 节点中样本数如果小于minsplit则分裂停止,否则节点继续分裂子节点
# minbucket -- 树中叶节点包含的最小样本数
# maxdepth -- 决策树最大深度
# xval -- 交叉验证的次数
# cp -- complexity parameter, 任何提升效果小于此值的分裂尝试都会被剪枝掉
tc <- rpart.control(minsplit=20,minbucket=10,maxdepth=10,xval=5,cp=0.005)
我们简单粗暴的将所有特征都加到模型中
代码语言:javascript复制# 设定随机种子,保证后续的计算一致性
set.seed(223)
# 模型公式
fm.base <- type ~ bone_length rotting_flesh hair_length has_soul color
# 训练模型
# method:根据因变量的数据类型选择相应的变量分割方法: 连续性method=“anova”,离散型method=“class”,计数型method=“poisson”,生存分析型method=“exp”
# control: rpart.control生成的提前剪枝的设置变量)
mod.base <- rpart(formula=fm.base, data=train, method="class", control=tc)
模型结果中有很多信息,其中比较重要的是一个变量重要度variable.importance,给出了各个模型特征在模型训练中的重要程度。
代码语言:javascript复制# 查看变量重要度
par(mai=c(0.5,1,0.5,0.5))
barplot(mod.base$variable.importance, horiz=T, cex.names=0.8, las=2)
通过条形图可以看到hair_length和has_soul的重要度要高一些,而color这个变量毫无贡献度,这点也和上面的箱线图表现一致。
我们把最终的决策树图画出来看一下。
代码语言:javascript复制#加载画图包
library(rpart.plot)
# 画图
rpart.plot(mod.base, branch=1, under=TRUE, faclen=0, type=0)
我们看一下模型在训练集上的准确度
代码语言:javascript复制# type: 预测的类型 取值class,则结果为分类factor
pred.base<-predict(mod.base, train, type='class')
# 查看结果
table(train$type, pred.base)
# 结果
Ghost Ghoul Goblin
Ghost 105 4 8
Ghoul 2 112 15
Goblin 17 27 81
# 准确率
sum(diag(prop.table( table(train$type, pred.base) )))
[1] 0.8032345
训练集上的准确度为80%,看起来还不错,我们趁热打铁,在测试集上训练一下结果,提交答案。
代码语言:javascript复制# 测试集上计算结果
test <- all[all$data_set == 'test', ]
pred.base<-predict(mod.base, test, type='class')
# 生成结果
res <- data.frame(id = test$id, type = pred.base)
write.csv(res, file = "D:/RData/Ghost/res_basic.csv", row.names = FALSE)
我们把结果文件res_basic.csv提交到Kaggle上,很快在Public Leaderboard上面获得了我们第一次提交的结果,准确率 0.68431。 这个准确率有点低,说明我们还有很长的路要走。
特征再加工
题目给的变量较少,我们在之前的重要度图上也看到了hair_length和has_soul的重要度比较高。我们组合hair_length和has_soul来得到一个新的特征。 我们依次将四个变量一一相乘获得新的10个变量,然后再重新用决策树训练一下。
代码语言:javascript复制# 两两组合生成新的变量
all$bone_rot = all$bone_length * all$rotting_flesh
all$bone_hair = all$bone_length * all$hair_length
all$bone_soul = all$bone_length * all$has_soul
all$rot_hair = all$rotting_flesh * all$hair_length
all$rot_soul = all$rotting_flesh * all$has_soul
all$hair_soul = all$hair_length * all$has_soul
# 增加新的次方来生成新的变量
all$bone2 = all$bone_length ^ 2
all$rot2 = all$rotting_flesh ^ 2
all$hair2 = all$hair_length ^ 2
all$soul2 = all$has_soul ^ 2
重新观察一下新的变量的分布,可以看到最右下角的hair_soul变量几乎将Ghost和Ghoul给完全分开,同时和Goblin的区分度也增加了。
重新训练模型,并在测试集上计算,提交验证。
代码语言:javascript复制
# 重新获取训练集和测试集
train <- all[all$data_set == 'train', ]
test <- all[all$data_set == 'test', ]
# 将新的特征量加入模型
fm.more <- type ~ bone_length rotting_flesh hair_length has_soul color bone_rot bone_hair bone_soul rot_hair rot_soul hair_soul rot2 hair2 soul2 bone2
# 训练新的模型
mod.more <- rpart(formula=fm.more, data=train, method="class", control=tc)
我们看一下新的模型的变量的重要度。
可以看到我们新加入的变量hair_soul的重要度最高,说明对模型的贡献度最高。
代码语言:javascript复制# 重新训练以及预测
pred.more <-predict(mod.more, test, type='class')
# 生成结果
res <- data.frame(id = test$id, type = pred.more)
write.csv(res, file = "D:/RData/Ghost/res_more.csv", row.names = FALSE)
这次提交上去之后,准确率提高到0.71078.
随机森林
俗话说,三个臭皮匠顶个诸葛亮。机器学习里面也有类似的技术,就是模型组合。对于决策树来说,随机森林则是一个简单易行的模型组合方法。 使用bagging的方式建立一个森林,森林里面有很多的决策树组成,随机森林的每一课决策树之间是没有关联的。在得到森林之后,当有一个新的输入样本进入,就让森林中的每一颗决策树分别进行判断,看看这个样本属于那个类,然后看看哪一类被选择多,就预测为那一类。
代码语言:javascript复制#加载随机森林包
library(randomForest)
library(caret)
#设定种子
set.seed(223)
# 设定控制参数
# method = "cv" -- k折交叉验证
# number -- K折交叉验证中的K, number=10 则是10折交叉验证
# repeats -- 交叉验证的次数
# verboseIter -- 打印训练日志
ctrl <- trainControl(method = "cv", number = 10, repeats = 20, verboseIter = TRUE)
# 训练模型
mod.rf <- train(fm.more, data = train, method = "rf", trControl = ctrl, tuneLength = 3)
预测结果
代码语言:javascript复制# 重新训练以及预测
pred.rf <-predict(mod.rf, test)
# 生成结果
res <- data.frame(id = test$id, type = pred.rf)
write.csv(res, file = "D:/RData/Ghost/res_rf.csv", row.names = FALSE)
此次提交结果是 0.72023
参考书籍
- Introduction to Data Mining
- Machine Learning With R Cookbook
- Machine Learning using R