上一节讲了决策树算法,虽然存在一些局限性,但是正是这种局限性造就了集成学习中的随机森林算法。
八、集成学习与随机森林
假设要解决一个复杂的问题,让众多学生去回答,然后汇总他们的答案。在许多情况下,会发现这个汇总的答案比一个老师的答案要好。同样,如果汇总了一组预测变量(例如分类器或回归因子)的预测结果,则通常会得到比最佳个体预测变量得到更好的预测结果。这种技术被称为集成学习(Ensemble Learning)。
1、投票分类器(Voting Classifiers)
创建集成分类器的一个非常简单的方法是聚合多个分类器(如Linear,Logistic,SVM,k-近邻、Ramdom forest等)的预测结果并预测得到最多选票的类。这个多数投票分类器被称为硬投票(hard voting)分类器。
这个投票分类器通常比集合中最好的分类器实现更高的准确性。事实上,即使每个分类器是一个弱学习器(weak learner)(意味着它只比随机猜测略好一点),但如果有足够的数量,集合仍然是一个强大的学习器(达到高精度)。比如每个学习器能到达51%的精度,1000个这样的分类器,通过投票取最大的分类,则能达到接近75%的精度。分类器越多精度更高。
需要注意:上面的情况只有每个分类器完全独立才能达到这种程度,相似的分类器很可能会犯同样的错误,所以会有大部分错误的选票,降低集体的准确性。因此获得不同分类器的一种方法是使用非常不同的算法来训练。这增加了会犯很多不同类型错误的机会,从而提高了整体的准确性。
下面是一个集成Logistic回归,SVM分类,Random forest分类的投票分类器,实验数据由moon产生,由于是硬投票,所以voting要设置为hard。
#产生moon数据并分开训练测试集
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
(X,y)=make_moons(1000,noise=0.5)
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size=0.2, random_state=42)
#构造模型和集成模型
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
log_clf = LogisticRegression()
rnd_clf = RandomForestClassifier()
svm_clf = SVC()
voting_clf = VotingClassifier(
estimators=[('lr', log_clf), ('rf', rnd_clf), ('svc', svm_clf)],
voting='hard'
)
voting_clf.fit(X_train, y_train)
#训练并预测
from sklearn.metrics import accuracy_score
for clf in (log_clf, rnd_clf, svm_clf, voting_clf):
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print(clf.__class__.__name__, accuracy_score(y_test, y_pred))
从结果可以看到集成的投票分类器的正确率比其中最好的分类器要高(但并不是每次都会更好,不过一般也能接近最好)。
如果所有分类器都能够估计分为每一类的概率,即都有predict_proba()方法,那么可以对每个分类器的概率取平均,再预测具有最高类概率的类,这被称为软投票(soft voting)。它通常比硬投票(hard voting)取得更好的效果,因为它给予了高度信任的投票更多的权重。 因此把voting =“soft”替换voting =“hard”,并确保所有分类器都可以估计类的概率即可。
由于默认情况下SVC类没有predict_proba()方法,所以需要将它的probability参数设置为True(这将使SVC类使用交叉验证来估计类概率,减慢训练速度,并且会增加一个predict_proba() 方法)。
#修改SVC类使得有predict_proba()方法,并软投票
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
log_clf = LogisticRegression()
rnd_clf = RandomForestClassifier()
svm_clf = SVC(probability=True)
voting_clf = VotingClassifier(
estimators=[('lr', log_clf), ('rf', rnd_clf), ('svc', svm_clf)],
voting='soft'
)
voting_clf.fit(X_train, y_train)
#训练并预测
from sklearn.metrics import accuracy_score
for clf in (log_clf, rnd_clf, svm_clf, voting_clf):
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print(clf.__class__.__name__, accuracy_score(y_test, y_pred))
可以看到软投票比硬投票确实要好一点。
2、Bagging and Pasting
上述获得多种分类器的方式是使用不同的训练算法。另一种方法是对每个分类器使用相同的算法,但是要在训练集的不同随机子集上进行训练。如果抽样时有放回,称为Bagging;当抽样没有放回,称为Pasting。抽样与培训过程如图所示。
与投票分类器一样,最后结果预测为被预测最多的类(分类问题)或平均值(回归问题)。一般来说,相比在完整的训练数据上得到的结果,集成学习得到的结果具有类似的偏差和较低的方差。而且这种算法具有并行性。
下面使用bagging算法训练模型,选择决策树分类器作为训练算法;n_estimators表示产生分类器的数目;max_samples为每个分类器分得的样本数;bootstrap=True表示使用bagging算法,否则为pasting算法;n_jobs表示使用CPU核的数目,-1代表把能用的都用上。
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
bag_clf = BaggingClassifier(
DecisionTreeClassifier(), n_estimators=500,
max_samples=100, bootstrap=True, n_jobs=-1
)
bag_clf.fit(X_train, y_train)
y_pred = bag_clf.predict(X_test)
print(bag_clf.__class__.__name__, accuracy_score(y_test, y_pred))
可以看到算法比前面三个算法集成的投票分类器正确率还要高一点。相比pasting算法,bagging算法偏差会稍微高一些,但是方差更小,通常能取得不错的效果,所以通常情况下人们更愿意用bagging算法。
Out-of-Bag Evaluation
由于bagging算法采用有放回的抽样方式(自助采样法),假设训练集有m个样本,每次抽取一个后放回,直到抽到m个样本,那么样本始终没有被抽到的概率为 (1−1m)m ,取极限得
bag_clf = BaggingClassifier(
DecisionTreeClassifier(), n_estimators=500,
bootstrap=True, n_jobs=-1, oob_score=True)
bag_clf.fit(X_train, y_train)
print("oob", bag_clf.oob_score_)
from sklearn.metrics import accuracy_score
y_pred = bag_clf.predict(X_test)
print("test", accuracy_score(y_test, y_pred))
可以看到两个精度还是比较相似的,因此可以用与模型的验证。
3、Random Patches and Random Subspaces
除了能随机选样本创建多个子分类器以外还能够随机选择特征来创建多个子分类器,通过参数max_features和bootstrap_features实现,其含义与max_samples和bootstrap类似。对特征进行采样能够提升模型的多样性,增加偏差,减少方差。
当处理高维(多特征)数据(例如图像)时,这种方法比较有用。同时对训练数据和特征进行抽样称为Random Patches,只针对特征抽样而不针对训练数据抽样是Random Subspaces。
随机森林(Random forest)
随机森林算法是以决策树算法为基础,通过bagging算法采样训练样本,再抽样特征,3者组合成的算法。对应的scikit-learn中为RandomForestClassifier(RandomForestRegression),它有这决策树(DecisionTreeClassifier)的所有参数,以及bagging(BaggingClassifier)的所有参数。下面是一个例子:
from sklearn.ensemble import RandomForestClassifier
rnd_clf = RandomForestClassifier(n_estimators=500, max_leaf_nodes=16, n_jobs=-1)
rnd_clf.fit(X_train, y_train)
y_pred_rf = rnd_clf.predict(X_test)
Extra-Trees
随机森林算法每个分类器是从随机抽样部分特征,然后选择最优特征来划分。如果在此基础上使用随机的阈值分割这个最优特征,而不是最优的阈值,这就是Extra-Trees(Extremely Randomized Trees)。这会再次增加偏差,减少方差。一般来说,Extra-Trees训练速度优于随机森林,因为寻找最优的阈值比随机阈值耗时。对应scikit-learn的类为ExtraTreesClassifier(ExtraTreesRegressor),参数与随机森林相同。
Extra-tree和随机森林哪个更好不好比较,只能通过交叉验证两种算法都实验一次才能知道结果。
特征重要性(Feature Importance)
由于决策树算法根据最优特征分层划分的,即根部的特征更为重要,而底部的特征不重要(不出现的特征更不重要)。根据这个可以判断特征的重要程度,在Scikit-learn可以通过feature_importance获得特征的重要程度。下面iris数据训练一个随机森林模型,输出特征的重要程度。
from sklearn.datasets import load_iris
iris = load_iris()
rnd_clf = RandomForestClassifier(n_estimators=500, n_jobs=-1)
rnd_clf.fit(iris["data"], iris["target"])
for name, score in zip(iris["feature_names"], rnd_clf.feature_importances_):
print(name, score)
可以看到petal length的重要性为44%,petal width为43%;
随机森林还能对图像中像素(特征)的重要程度,下面以MNIST图像为例子。
import matplotlib.pyplot as plt
import matplotlib
from sklearn.datasets import fetch_mldata
mnist = fetch_mldata('MNIST original')
rnd_clf = RandomForestClassifier(random_state=42)
rnd_clf.fit(mnist["data"], mnist["target"])
importances = rnd_clf.feature_importances_.reshape(28, 28)
plt.imshow(importances, cmap = matplotlib.cm.hot)
plt.colorbar(ticks=[rnd_clf.feature_importances_.min(), rnd_clf.feature_importances_.max()])
4、Boosting
Boosting是将弱学习器集成为强学习器的方法,主要思想是按顺序训练学习器,以尝试修改之前的学习器。Boosting的方法有许多,最为有名的方法为AdaBoost(Adaptive Boosting)和Gradient Boosting。
AdaBoost
一个新的学习器会更关注之前学习器分类错误的训练样本。因此新的学习器会越来越多地关注困难的例子。这种技术成为AdaBoost。
下面介绍AdaBoost算法是怎么工作的:
(1)假设有m个训练样本,初始化每个样本的权值 w(i) 为 1m ,经过第j个学习器训练以后,对训练样本计算加权错误率 rj :
(3)更新样本权值 w(i) ,将没有预测出来的样本权值变大,以便后续的学习器重点训练。当然这个计算完以后需要归一化。
(5)最终得到N个学习器,计算每个学习器对样本的加权和,并预测为加权后最大的一类。
可以看到上述的AdaBoost是二分类学习器,Scikit-learn中对应为AdaBoostClassifier类,如果要多分类,则可以设置参数algorithm=”SAMME”,如果需要predict_proba()方法,则设置参数algorithm=”SAMME.R”,下面为以决策树为基学习器,的多分类任务例子。
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier
ada_clf = AdaBoostClassifier(
DecisionTreeClassifier(max_depth=1), n_estimators=200,
algorithm="SAMME.R", learning_rate=0.5
)
ada_clf.fit(X_train, y_train)
需要注意:SVM算法由于训练速度慢且不稳定,所以不适合AdaBoost的基算法;如果产生过拟合可以减少学习器的数目;AdaBoost的缺点为不能并行,由于每一个学习器依赖上一个学习器。
Gradient Boosting
和AdaBoost类似,Gradient Boosting也是逐个训练学习器,尝试纠正前面学习器的错误。不同的是,AdaBoost纠正错误的方法是更加关注前面学习器分错的样本,Gradient Boosting(适合回归任务)纠正错误的方法是拟合前面学习器的残差(预测值减真实值)。
下面训练以决策树回归为基算法,根据上一个学习器的残差训练3个学习器。数据使用二次加噪声数据。
import numpy.random as rnd
from sklearn.tree import DecisionTreeRegressor
#准备数据
X = rnd.rand(200, 1) - 1
y = 3*X**2 + 0.05 * rnd.randn(200,1)
#训练第一个模型
tree_reg1 = DecisionTreeRegressor(max_depth=2)
tree_reg1.fit(X, y)
#根据上一个模型的残差训练第二个模型
y2 = y - tree_reg1.predict(X)
tree_reg2 = DecisionTreeRegressor(max_depth=2)
tree_reg2.fit(X, y2)
#再根据上一个模型的残差训练第三个模型
y3 = y2 - tree_reg2.predict(X)
tree_reg3 = DecisionTreeRegressor(max_depth=2)
tree_reg3.fit(X, y3)
#预测
X_new=0.5
y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3))
如果将结果画成图,我们可以看到左侧为每个学习器的决策线,有图为相加后的结果,可以发现拟合效果越来越好。对应Scikit-learn中的函数为GradientBoostingRegressor(决策树为基)。
from sklearn.ensemble import GradientBoostingRegressor
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=1.0)
gbrt.fit(X, y)
参数learning_rate表示每个学习器的贡献程度,如果设置learning_rate比较低,则需要较多的学习器拟合训练数据,但是通常会得到更好的效果。下图展示较低学习率的训练结果,左图学习器过少,欠拟合;右图学习器过多,过拟合。
为了找到最优学习器的数量,可以使用early stopping方法(在第5节讲到)。对应可以使用staged_predict()方法,该方法能够返回每增加一个学习器的预测结果。下面为一个实例:
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
X_train, X_val, y_train, y_val = train_test_split(X, y)
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=120)
gbrt.fit(X_train, y_train)
errors = [mean_squared_error(y_val, y_pred) for y_pred in gbrt.staged_predict(X_val)]
bst_n_estimators = np.argmin(errors)
gbrt_best = GradientBoostingRegressor(max_depth=2,n_estimators=bst_n_estimators)
gbrt_best.fit(X_train, y_train)
还可以与第5节中一样通过设置warm_start = True使模型继续训练,当认为不能再下降时停止,而不是训练完最大数目的学习器再找最小错误的。下面为连续迭代五次的错误没有改善时,停止训练:
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=1, learning_rate=0.1, random_state=42, warm_start=True)
min_val_error = float("inf")
error_going_up = 0
for n_estimators in range(1, 120):
gbrt.n_estimators = n_estimators
gbrt.fit(X_train, y_train)
y_pred = gbrt.predict(X_val)
val_error = mean_squared_error(y_val, y_pred)
if val_error < min_val_error:
min_val_error = val_error
error_going_up = 0
else:
error_going_up += 1
if error_going_up == 5:
break # early stopping
print gbrt.n_estimators
GradientBoostingRegressor类可以通过设置参数subsample,来确定每棵树训练的样本比例,这如上面的bagging一样,可以获得较低的方差和高的偏差。但也大大加快训练速度。这种技术被称为Stochastic Gradient Boosting。
需要注意:Gradient Boosting还可以选择其他损失函数,通过设置参数loss来代替残差
5、Stacking
上述的模型都是通过训练多个学习器后分别得到结果后整合为最终结果,整合的过程为投票、求平均、求加权平均等统计方法。那为什么不把每个学习器得到的结果作为特征进行训练(Blend),再预测出最后的结果,这就是Stacking的思想。
为了上述思想,需要将训练数据分为两部分。第1部分用于训练多个基学习器。
第2部分(hold-out set)用于训练blender。blender的输入为第2部分数据在第一部分数据训练好的多个模型的预测结果。
实际上可以训练多个blender(如一个Logistic回归,另一个Ramdomforest)实现这个思想的诀窍是将训练集分成三份,第一份用于训练多个基学习器,第二份用于训练第二个层(使用第一个层的预测器进行的预测作为输入),第三份用于训练第三层(使用第二层的预测器进行的预测作为输入)。如图所示。
不幸的是,scikit-learn没有提供stacking的实现,但是实现并不困难,可以参见(https://github.com/viisar/brew)。
上一篇:机器学习实战(用Scikit-learn和TensorFlow进行机器学习)(七)
下一篇:机器学习实战(用Scikit-learn和TensorFlow进行机器学习)(九)