机器学习(八)——集成学习

对于训练集数据,我们通过训练若干个个体学习器,通过一定的结合策略,就可以最终形成一个强学习器,以达到博采众长的目的。也就是说,集成学习有两个主要的问题需要解决,第一是如何得到若干个个体学习器,第二是如何选择一种结合策略,将这些个体学习器集合成一个强学习器。
集成学习潜在的思想是即便某一个弱分类器得到了错误的预测,其他的弱分类器也可以将错误纠正回来。集成学习通过构建并结合多个学习器来完成学习任务,有时也被称为多分类器系统、基于委员会的学习等。

对于个体学习器的选择,我们有两种选择:第一种就是所有的个体学习器都是一个种类的,或者说是同质的。比如都是决策树个体学习器,或者都是神经网络个体学习器。第二种是所有的个体学习器不全是一个种类的,或者说是异质的。比如我们有一个分类问题,对训练集采用支持向量机个体学习器,逻辑回归个体学习器和朴素贝叶斯个体学习器来学习,再通过某种结合策略来确定最终的分类强学习器。

同质个体学习器按照个体学习器之间是否存在依赖关系可以分为两类:
第一个是个体学习器之间存在强依赖关系,一系列个体学习器基本都需要串行生成,即:除了训练第一个之外,其他的学习器学习都需要依赖于前面生成的学习的结果。代表算法是boosting系列算法,第二个是个体学习器之间不存在强依赖关系,一系列个体学习器可以并行生成,代表算法是bagging和随机森林(Random Forest)系列算法。

一、bootstrap 自助法统计简介

bootstrap 的核心思想就是对样本数据再进行有放回抽样,得到新的样本,通过不断重复,得到若干个新的样本,然后统计这些样本数据分布情况,来估计总体的分布情况。一言以蔽之,子样本之于样本,可以类比样本之于总体。举个例子:
假设我们有一个样本数据:30,37,36,43,42,43,43,46,41,42,通过这个样本,我们知道样本的均值为:40.3。但是我们能说总体的均值也是40.3么?或者近似40.3么? 显然不能。为什么? 因为我们只有一个样本。一个样本并不能去说明总体。但是现在我们就只有这一个样本数据,那我们要想推断总体的均值,要这么办呢? 一个办法就是对这个样本再进行抽样,我们每次抽1个数,然后返回,然后再抽,总共抽10次,得到一个新的样本:
43,43,42,37,42,36,43,41,46,42,这个样本的均值为:35.7。
重复上述过程,做20次,我们就可以得到20个样本的均值,例如:第二次抽样为:
36,41,43,42,36,36,37,42,43,43, 这个样本的均值为:37.4
第三次抽样结果为:
46,37,37,43,43,42,41,30,42,43,样本均值为:38.0
……
因为我们每次都是有放回抽样,因此得到的均值都是不同的。重复20次后,我们得到20个不同的均值。我们计算出这20个均值相对于原始样本均值之间的差,并且按从小到大排序。而对于样本的抽样得到的新样本均值,与原始样本均值之间的偏离程度,与原始样本与总体样本之间的偏离程度近视相等。于是我们就可以大致知道总体样本的置信区间。

二、装袋(Bagging)

bagging 是一种个体学习器之间不存在强依赖关系、可同时生成的并行式集成学习方法。在Bagging算法中,模型集合中的每个成员学习器均从不同的训练集得到,而每个训练集通过bootstrapping方法得到。即给定包含n个样本的数据集,先随机从样本中取出一个样本放入采样集中,再把该样本返回初始数据集,使得下次采样时该样本仍可以被选中,这样,经过m次随机采样操作,就可以得到包含m个样本的采样集,初始数据集中有的样本多次出现,有的则未出现。每个样本不被选中的概率为:。 当n和m都非常大时,比如n=m=10000,一个样本不被选中的概率p = 36.8%。因此一个bootstrap约包含原样本63.2%,约36.8%的样本未被选中。
照上面的方式进行T次操作,采样出T个含有m个训练集的采样集,然后基于每个采样集训练出T个基学习器,再将这些基学习器进行结合,即可得到集成学习器。在对输出进行预测时,Bagging通常对分类进行简单投票法,对回归使用简单平均法。若出现形同,则任选其一。

在R中手工实现装袋的例子如下:

heart <- read.table("heart.dat", quote="\"")
names(heart) <- c("AGE", "SEX", "CHESTPAIN", "RESTBP", "CHOL", "SUGAR", "ECG",
                  "MAXHR", "ANGINA", "DEP", "EXERCISE", "FLUOR", "THAL", "OUTPUT")
#将一些特征因子化
heart$CHESTPAIN = factor(heart$CHESTPAIN)
heart$ECG = factor(heart$ECG)
heart$THAL = factor(heart$THAL)
heart$EXERCISE = factor(heart$EXERCISE)

#将输出范围限制在0和1之间(之前是1和2)
heart$OUTPUT = heart$OUTPUT-1

#划分测试集和训练集
library(caret)
set.seed(987954)
heart_sampling_vector <- createDataPartition(heart$OUTPUT, p = 0.85, list = FALSE)
heart_train <- heart[heart_sampling_vector,]
heart_train_labels <- heart$OUTPUT[heart_sampling_vector]
heart_test <- heart[-heart_sampling_vector,]
heart_test_labels <- heart$OUTPUT[-heart_sampling_vector]

#准备训练11个小型的训练集
M <- 11
seeds <- 70000 : (70000 + M - 1)
n <- nrow(heart_train) #心脏病数据的行数
#每次抽n行,有放回的抽样。抽M次。(抽出来的是行数)
sample_vectors<-sapply(seeds,function(x) { set.seed(x); return(sample(n,n,replace=T)) })
#根据序号,找到对应的数据,放到glm函数中进行逻辑回归
train_1glm <- function(sample_indices) { 
    data <- heart_train[sample_indices,]; 
    model <- glm(OUTPUT~., data=data, family=binomial("logit")); 
    return(model)
}
#生成11个逻辑回归的结果
models <- apply(sample_vectors, 2, train_1glm)##################################

#取出抽样数据中的非重复数据
get_1bag <- function(sample_indices) {
    unique_sample <- unique(sample_indices); 
    df <- heart_train[unique_sample, ]; 
    df$ID <- unique_sample; 
    return(df)
}
bags<-apply(sample_vectors, 2, get_1bag) ######################################

#对每个数据,计算11个模型的预测结果
glm_predictions <- function(model, data, model_index) {
    colname <- paste("PREDICTIONS",model_index);
    data[colname] <- as.numeric(predict(model,data,type="response") > 0.5); 
    return(data[,c("ID",colname), drop=FALSE])
}
#mapply跟sapply类似,只是是一个多变量版本。simplify=True表示返回一个矩阵,否则返回一个list
#由于glm_predictions需要传递多个参数,因此需要用mapply。
training_predictions <- mapply(glm_predictions,models,bags,1:M,SIMPLIFY=F)
training_predictions
#Reduce函数是将每次计算后的结果保留,并与下一个数字进行计算,这是和 apply 函数不同的
#这里是利用reduce函数将多个数据框按照同一列merge
train_pred_df <- Reduce(function(x, y) merge(x, y, by = "ID", all = T), training_predictions)
train_pred_df
#进行结果投票,对于每一行,去掉ID列后,求这一行的均值。若均值大于0.5,则返回1,否则返回0
train_pred_vote<-apply(train_pred_df[,-1],1,function(x) as.numeric(mean(x,na.rm=TRUE)>0.5))
train_pred_vote
#计算精度(准确度),准确度从原来的0.86提高到0.88
(training_accuracy<-mean(train_pred_vote==heart_train$OUTPUT[as.numeric(train_pred_df$ID)]))


# 用袋外数据来进行预测(类似于测试集)
get_1oo_bag <- function(sample_indices) {
  #注意,这里的n是个全局变量,上面定义过,是心脏病数据的行数。
  #使用setdiff,将统计从1到n的数中与sample_indices不同的数,得到袋外数据的行号
    unique_sample <- setdiff(1:n,unique(sample_indices)); 
    df <- heart_train[unique_sample,];  #根据行号取出这些数据
    df$ID <- unique_sample;  #赋予一个ID
    #如果ECG的数量小于3,则将ECG=1的数据的全部变为NA。
    #因为ECG=1的数量很少,很有可能训练集的时候没有。导致模型没有见过ECG的特征,所以将ECG判定为NA,忽略此特征
    if (length(unique(heart_train[sample_indices,]$ECG)) < 3) df[df$ECG == 1,"ECG"] = NA; 
    return(df)
}
#获得带外数据
oo_bags <- apply(sample_vectors,2, get_1oo_bag)
#对每个数据,计算11个模型的预测结果
oob_predictions<-mapply(glm_predictions, models, oo_bags, 1:M,SIMPLIFY=F)
#合并数据
oob_pred_df <- Reduce(function(x, y) merge(x, y, by="ID", all=T), oob_predictions)
#投票
oob_pred_vote<-apply(oob_pred_df[,-1], 1, function(x) as.numeric(mean(x,na.rm=TRUE)>0.5))
#查看投票后的精度(袋外数据的精度为0.8)
(oob_accuracy<-mean(oob_pred_vote==heart_train$OUTPUT[as.numeric(oob_pred_df$ID)],na.rm=TRUE))

#再来使用测试集进行验证
get_1test_bag <- function(sample_indices) {
     df <- heart_test; 
     df$ID <- row.names(df); 
     if (length(unique(heart_train[sample_indices,]$ECG)) < 3) 
         df[df$ECG == 1,"ECG"] = NA; 
     return(df)
}

test_bags <- apply(sample_vectors,2, get_1test_bag)
test_predictions<-mapply(glm_predictions, models, test_bags, 1 : M, SIMPLIFY = F)
test_pred_df <- Reduce(function(x, y) merge(x, y, by="ID",all=T), test_predictions)
test_pred_vote<-apply(test_pred_df[,-1],1,function(x) as.numeric(mean(x,na.rm=TRUE)>0.5))
#可以看到,测试集上的精度为0.925
(test_accuracy<-mean(test_pred_vote==heart_test[test_pred_df$ID,"OUTPUT"],na.rm=TRUE))

三、增强(Boosting)

Boosting算法的工作机制是首先从训练集用初始权重训练出一个弱学习器1,根据弱学习的学习误差率表现来更新训练样本的权重,使得之前弱学习器1学习误差率高的训练样本点的权重变高,使得这些误差率高的点在后面的弱学习器2中得到更多的重视。然后基于调整权重后的训练集来训练弱学习器2.,如此重复进行,直到弱学习器数达到事先指定的数目T,最终将这T个弱学习器通过集合策略进行整合,得到最终的强学习器。


即:原始数据集 -> 某种算法拟合,会产生错误 -> 根据上个模型预测结果,更新样本点权重。预测错误的结果权重增大 -> 再次使用更新了权重后的数据进行模型的拟合,得到一个新的模型 -> 重复上述过程,继续重点训练错误的预测样本点。每一次生成的子模型,都是在生成拟合结果更好的模型(用的数据点都是相同的,但是样本点具有不同的权重值)。

注意:增强每次都会用上所有的观测数据,而装袋只是抽样了部分数据;

AdaBoost:
AdaBoost 是Boosting中的经典算法,其主要应用于二分类问题(打当然也可以用于回归问题。但是主要是用于二分类问题)。Adaboost 算法采用调整样本权重的方式来对样本分布进行调整,即提高前一轮个体学习器错误分类的样本的权重,而降低那些正确分类的样本的权重,这样就能使得错误分类的样本可以受到更多的关注,从而在下一轮中可以正确分类,使得分类问题被一系列的弱分类器“分而治之”。对于组合方式,AdaBoost采用加权多数表决的方法,具体地,加大分类误差率小的若分类器的权值,减小分类误差率大的若分类器的权值,从而调整他们在表决中的作用。

我们从上面的表述可以看到,这里有两个权重:
一个是样本的权重(每一个样本的权重我们记为:,整体样本的权重集合记为:D): 样本权重越大,代表这个样本被正确分类的要求越高,可能性越大。
一个是基分类器的权重(我们记为:):一个分类器,分类的正确率越高,其权重越大,代表其在最后投票时所占的比重越大。

下面这张图对Ada-boost做了恰当的解释:


  • Box 1: 你可以看到我们假设所有的数据点有相同的权重(正号、负号的大小都一样),并用一个决策树桩D1将它们分为两部分。我们可以看到这个决策树桩将其中的三个正号标记的数据点分类错误,因此我们将这三个点赋予更大的权重交由下一个预测树桩进行分类。
  • Box 2: 在这里你可以看到三个未被正确分类的(+)号的点的权重变大。在这种情况下,第二个决策树桩D2试图将这三个错误的点准确的分类,但是这又引起新的分类错误,将三个(-)号标记的点识别错误,因此在下一次分类中,这三个(-)号标记的点被赋予更大的权重。
  • Box 3: 在这里三个被错误分类的(-)号标记的点被赋予更大的权重,利用决策树桩D3进行新的分类,这时候又产生了新的分类错误,图中用小圆圈圈起来的一个负号点和两个正号点
  • Box 4: 在这里,我们将D1、D2和D3三个决策器组合起来形成一个复杂的规则,你可以看到这个组合后的决策器比它们任何一个弱分类器表现的都足够好。

那么我们如何来确定样本的权重 和分类器的权重呢?AdaBoost采用如下算法:
1)初始时,假设所有样本的权重都相等,假设都为:,N代表样本的数量;
2)使用初始权重和初始数据训练第一个基学习器;
3)根据样本数据,计算的误差率。对于一个二元分类问题来说,误差率的计算公式为分类错误数量除以样本总数,即:。如果带上样本权重,则变为:
4)根据公式: 得到分类器的权重。可以看到,误差率越大, 越小,也越小。
5)更新样本的权值:
若分类正确,则新的权值为:,以减少权重;
若分类错误,则新的权值为:,以增大权重;
6)将更新后的权重进行归一化处理,确保所有权重之和等于1。(即用当前权值除以所有权值之和);
7)重复上述步骤2~6,直到基分类器数量达到规定的数量T为止。
8)最终输出模型:

在R中实现AdaBoost算法如下:

#预测大气中伽马射线的辐射(根据一系列特征来判断是放射性泄漏污染还是常规背景辐射)
#输出(class)是二元的,g代表是伽马射线,b代表背景辐射。总共有19020条数据
magic <- read.csv("study/graduate/机器学习/magic04.data", header=FALSE)
names(magic)=c("FLENGTH", "FWIDTH", "FSIZE", "FCONC", "FCONC1", "FASYM", "FM3LONG", 
               "FM3TRANS", "FALPHA", "FDIST", "CLASS")
#把输出变量进行因子化,g转化为1,b变为-1;
magic$CLASS = as.factor(ifelse(magic$CLASS=='g',1,-1))

#划分训练集和测试集
set.seed(33711209)
magic_sampling_vector <- createDataPartition(magic$CLASS, p = 0.80, list = FALSE)
magic_train <- magic[magic_sampling_vector,1:10]
magic_train_output <- magic[magic_sampling_vector,11]
magic_test <- magic[-magic_sampling_vector,1:10]
magic_test_output <- magic[-magic_sampling_vector,11]

#对数据进行归一化、中心化处理;
magic_pp <- preProcess(magic_train, method = c("center", "scale"))
magic_train_pp <- predict(magic_pp, magic_train)
magic_train_df_pp <- cbind(magic_train_pp, CLASS = magic_train_output)
magic_test_pp <- predict(magic_pp, magic_test)

#使用只有1个神经元的神经网络进行拟合
library(nnet)
n_model <- nnet(CLASS~., data = magic_train_df_pp, size = 1)
#在测试集上进行预测
n_test_predictions = predict(n_model, magic_test_pp, type = "class")
#计算测试集上的精度(精度为78.67%)
(n_test_accuracy <- mean(n_test_predictions == magic_test_output))

#构造AdaBoost
AdaBoostNN <- function(training_data, output_column, M, hidden_units) {
  #library和require都可以载入包,但二者存在区别。
  #在一个函数中,如果一个包不存在,执行到library将会停止执行,require则会继续执行。
  require("nnet")
  models<-list()
  alphas<-list()
  n <- nrow(training_data)
  model_formula <- as.formula(paste(output_column,'~.',sep=''))
  w <- rep((1/n),n) #初始化样本权重
  for (m in 1:M) {
    #构造hidden_units个隐藏单元的神经网络
    model<-nnet(model_formula,data=training_data,size=hidden_units, weights=w)
    models[[m]]<-model #将构造后的模型存入模型列表中
    #先将training_data中,输出变量去除,然后使用模型进行预测,预测后将结果转为数值型
    predictions<-as.numeric(predict(model,training_data[,-which(names(training_data) == output_column)],type = "class"))
    errors<-predictions!=training_data[,output_column] #如果分类正确,则返回false,分类错误返回true
    #as.numeric(errors),会是的分类正确变为0,分类错误变为1。然后再乘以权重,相当于是把所有分类错误的权重求和,再除以全部的权重之和,得到错误率。
    error_rate<-sum(w*as.numeric(errors))/sum(w)
    alpha<-0.5*log((1-error_rate)/error_rate) #计算分类器的权重
    alphas[[m]]<-alpha #将分类器的权重存入列表中
    temp_w<-mapply(function(x,y) if (y) {x*exp(alpha)} else {x*exp(-alpha)},w,errors) #更新样本权重
    w<-temp_w/sum(temp_w) #样本权重归一化处理
  }
  return(list(models=models, alphas=unlist(alphas)))  #返回模型列表和模型权重列表
}

#不同的模型有不同的权重,对测试集上的所有数据,应用所有模型,根据模型的权重,进行综合评分。
AdaBoostNN.predict<-function(ada_model,test_data) {
  models<-ada_model$models #获得模型列表
  alphas<-ada_model$alphas #获得模型权重列表
  #针对每一个模型,使用测试集数据进行预测,结果是一个矩阵。每一行是一个模型预测的结果。每一列是测试集的每一条观测数据的预测值
  prediction_matrix<-sapply(models,function (x) as.numeric(predict(x,test_data,type = "class")))
  #对每一行数据,乘以其对应的权重值
  weighted_predictions<-t(apply(prediction_matrix,1,function (x) mapply(function(y,z) y*z,x,alphas)))
  #对权值化后的预测值求和后,判断正负号
  final_predictions<-apply(weighted_predictions,1,function(x) sign(sum(x)))
  return(final_predictions)
}

ada_model <- AdaBoostNN(magic_train_df_pp, 'CLASS', 10, 1)
predictions <- AdaBoostNN.predict(ada_model, magic_test_pp)
mean(predictions == magic_test_output)

四、随机森林

随机森林=集成方法(bagging)+决策树
这里的随机,有两层含义:1)样本的随机(参考上面的bagging介绍);2)特征的随机。注意,随机森林不是使用全部的特征来参与计算,而是随机取全部特征的一些子集来参与运算。这样的抽样步骤可以强制让装袋的树结构互不同,可以有效避免因为样本本身有偏差所带来的模型在测试集上效果不理想的问题。
森林很好理解。一树成木,百树成林。当我们使用了多棵决策树时,自然也就了森林。

我们用R来比较几种算法的优劣(决策树、bagging、随机森林、回归决策树):

#计算SSE
compute_SSE <- function(correct,predictions) {
  return(sum((correct-predictions)^2))
}

skillcraft <- read.csv("SkillCraft1_Dataset.csv")
#数据预处理
skillcraft<-skillcraft[-1]
skillcraft$TotalHours <- factor(skillcraft$TotalHours)
skillcraft$HoursPerWeek <- factor(skillcraft$HoursPerWeek)
skillcraft$Age <- factor(skillcraft$Age)
skillcraft$TotalHours=as.numeric(levels(skillcraft$TotalHours))[skillcraft$TotalHours]
skillcraft$HoursPerWeek=as.numeric(levels(skillcraft$HoursPerWeek))[skillcraft$HoursPerWeek]
skillcraft$Age=as.numeric(levels(skillcraft$Age))[skillcraft$Age]
skillcraft<-skillcraft[complete.cases(skillcraft),] #去除空行

#划分测试集和训练集
library(caret)
set.seed(133)
skillcraft_sampling_vector <- createDataPartition(skillcraft$LeagueIndex, p = 0.80, list = FALSE)
skillcraft_train <- skillcraft[skillcraft_sampling_vector,]
skillcraft_test <- skillcraft[-skillcraft_sampling_vector,]

#使用rpart进行决策树分类
library(rpart)
regtree <- rpart(LeagueIndex~., data=skillcraft_train)
#在测试集上进行预测
regtree_predictions = predict(regtree, skillcraft_test)
(regtree_SSE <- compute_SSE(regtree_predictions, skillcraft_test$LeagueIndex))#评判测试集上的预测效果

#baggin函数默认使用rpart包的决策树方法来进行分类
#coob=T表示用袋外样本来评估误差
#nbagg表示用100个袋子,也就是生成100个小的模型
library("ipred")
baggedtree <- bagging(LeagueIndex~., data=skillcraft_train, nbagg=100, coob=T)
baggedtree_predictions <- predict(baggedtree, skillcraft_test)
(baggedtree_SSE <- compute_SSE(baggedtree_predictions, skillcraft_test$LeagueIndex))

#使用随机森林来进行预测
library("randomForest")
rf<-randomForest(LeagueIndex~., data=skillcraft_train)
rf_predictions = predict(rf,skillcraft_test)
(rf_SSE <- compute_SSE(rf_predictions, skillcraft_test$LeagueIndex))

#使用回归决策树来进行预测
library("gbm")
boostedtree <- gbm(LeagueIndex~., data=skillcraft_train, distribution="gaussian", n.trees=10000, shrinkage=0.1)
best.iter <- gbm.perf(boostedtree,method="OOB")
boostedtree_predictions = predict(boostedtree, skillcraft_test,best.iter)
(boostedtree_SSE <- compute_SSE(boostedtree_predictions, skillcraft_test$LeagueIndex))

可以看到,我们使用决策树,最后的SSE为799,使用bagging,SSE为677,使用随机森林,SSE为575,使用GBDT,SSE为549。

【参考资料】
Bootstrap方法详解——技术与实例
Bootstrap详解
Bootstrap采样
机器学习之集成学习(ensemble learning)
集成学习(Ensemble learning)
机器学习算法之Boosting
Boosting算法总结
AdaBoost算法
随机森林算法及其实现
集成学习之Boosting —— Gradient Boosting原理

你可能感兴趣的:(机器学习(八)——集成学习)