1. AdaBoost 与集成学习
AdaBoost 算法是非常成功的集成学习算法之一。再详细的记录AdaBoost之前,先记录一些集成学习的相关概念。首先集成学习分为两种,同质集成和异质集成。前者所指的是基学习器是用相同的算法得到的,比如N个学习器都是使用的决策树。而异质学习器的个体学习器是有不同的算法组成的。集成学习通常分为两类,boosting和bagging。从AdaBoost的名字上就能够看出,它是属于前者的。集成学习的基本思路是想综合多个弱学习器的预测结果来提升总体的模型的表现。这里所谓的弱学习器指的是,非随机猜测但是准确度稍低的学习器。想让集成之后的模型具有更好的效果,并不是简单地将多个弱学习器结合起来。集成模型的基学习器需要具有一定的多样性才可以达到在集成之后提升性能的效果。同样的,基学习器的个数也有不可小嘘的作用。理论上,假设每一个基学习器都是相互独立的,随着个体分类器的数目T变得越来越大,集成的错误率也会呈现指数形式的下降。但是这里前提条件“每个基学习器相互独立” 在绝大多数的情况下都是不成立的。但是这也证明的基学习器的数量对于集成学习模型的重要性。
2. AdaBoost算法原理
AdaBoost 的推导和理解有很多种,但是其中相对比较好理解的一种就是将训练得到的基学习器做线性组合。每一个弱学习器的训练是基于数据集的多重抽样。换句话说,虽然训练训练数据是一样的,但是数据的分布不同。AdaBoost的训练思想类似残差训练。第t个弱学习器会去修正第t-1个模型的错误,一个理想的第t个弱学习器可以修正t-1个弱学习器的所有错误。换句话来说就是,第t个模型更加的重视第 t-1个模型出错的样本。既然AdaBoost可以理解成多个基学习器的线性叠加,它的表达式可以写成:
其中H表示的就是集成之后的模型,T代表训练的弱学习器的个数,
h
t
(
x
)
h_{t}(x)
ht(x)表示的是第t个弱学习器。
a
t
a_{t}
at代表着每一个弱学习器的权重。一个最好的集成是可以最小化指数损失函数的多个弱学习器的线性组合,指数损失函数定义成:
这里需要稍微的提一下,每一个弱分类器基于的数据分布其实是不一样的,每一次训练的数据分布会根据总体损失进行相应的调整,可以将其理解成多重抽样。
定义了损失函数之后,很明显我们的目的是想找到一个H来最小化损失函数,所以可以对式的H(x)求偏导,可以得到:
然后两边同时取sign。使用符号函数作为替代损失的原因是sign具有例如连续可微等较好的数学性质。
在了解了一些AdaBoost的一些基本知识之后。便可以深入的了解该算法的整个流程是什么样的了:
- 初始化每一个样本的权重,第一次初始化时,每一个样本的权重大小是一致的。
- 用选择好的算法来训练弱学习器 h t x h_t{x} htx。
- 计算误差
- 调整分类器的权重
- 调整样本分布
上述过程直到训练的弱学习器数量达到了预先设定的数量。
那如何来确定每一个弱学习器的权重呢? 弱学习器的权重可以定义成方程:
根据权重的计算公式可以发现,误差越低,准确率越高的弱学习器,它的占比就也高,误差越高的分类器,准确率越低的弱学习器,权重越低。若弱学习器是随机猜测结果,那么它的权重将趋近于0。
前面提及到,除了对弱学习器有一个权重之外,每一条数据也有一个权重值。起初数据的权重值都是一致的。之后每一条数据的权重会根据弱学习器是否预测正确来进行相应的更改。设D为每一个样本权重所组成的向量,样本权重的更新公式如下所示:
若原本预测正确,样本对应的权重大小减小:
若样本预测正确,则样本权重上升:
3. AdaBoost算法自实现
现在基于第二节所描述的算法原理来实现一个简单的AdaBoost算法。基学习算法选择最简单的决策树桩。它根据一个特征将数据划分为两类。因为决策树桩的分类能力很弱,所以先试用一个简单的数据集来观察算法的规律,之后在将自实现的AdaBoost算法使用到相对复杂的数据集上。
首先处理一下Iris数据集。首先只保留两个特征,并其取出类别为0,1的记录,将0类别更换成-1类别,便于之后的数学运算。最后提取出五条记录来做实验即可。数据处理的代码可以参考如下代码:
def load_data():
data = load_iris()
X = data.data
Y = data.target
x_train, x_test, y_train, y_test = train_test_split(X, Y, train_size=0.66, shuffle=True)
x_index = np.where(y_train == 0)
x_train_ = x_train[x_index][:2, :2]
y_train_ = y_train[x_index][:2]
y_train_[y_train_ == 0] = -1
x_index_1 = np.where(y_train == 1)
x_train_1 = x_train[x_index_1][:2, :2]
y_train_1 = y_train[x_index_1][:2]
X = np.vstack((x_train_, x_train_1))
Y = np.hstack((y_train_, y_train_1))
return X,Y
最后得到的简化数据如下:
X:
[[5.1 3.4]
[4.6 3.1]
[6.4 3.2]
[6.4 2.9]
Y:
[-1 -1 1 1]
简化之后的数据仅仅适用于测试和观察代码。在得到了数据之后,可以写出决策树树桩的代码。决策树树桩的分类方式是根据某一个特征将数据集一分为二。具体来说就是当用于判决的特征大于阈值将被分为正类,反之将被分为负类。根据上述原理,决策树桩的代码实现如下:
def decision_tree_stump(datamat,dim,split_,threshold):
datamat = np.mat(datamat)
print('mat.shape:{}'.format(datamat.shape))
classification = np.mat(np.ones((datamat.shape[0],1)))
print('classfication:{}'.format(classification.shape))
if split_ == 'A':
classification[datamat[:,dim]>threshold] = -1
else:
classification[datamat[:,dim]<=threshold] = -1
return classification
而我们想用作基学习算法的是最佳决策树树桩。这就意味着要循环数据集的每一个特征,决策树树桩将会用不同的特征进行分裂,并且得到对应的损失。损失最小的决策树树桩则是当前数据分布下的最佳决策树树桩。这里需要注意的是计算损失的方法,因为每一个数据是有自己的权重的,所以在计算损失的时候应该将每一条数据的权重都考虑进去。所以这里我们所计算的损失是误差的加权和。另外,对于当前的自定义数据集而言,并没有一个用于计算阈值的公式,所以我们可以将将其自定义为,每一个特征中的最小值加上当前循环的次数。循环完预设次数之后,返回最佳的决策树树桩,最小误差,以及最佳的分类结果。选择最佳的决策树树桩的代码可以用以下代码实现:
def best_decision_tree(x_train,y_train,D):
'''
x_train:训练数据集
y_train:训练标签
D:每一条数据的权重值,初始化时每一条数据的权重大小都是一致的。
:param x_train:
:param y_train:
:param D:
:return:
'''
x_train = np.mat(x_train)
y_train = np.mat(y_train).T
# prediction = np.mat(np.zeros((y_train.shape[0],1))) #prediction的数量和样本量一致
best_ = {} #创建一个字典来记录,最佳的决策树以及最小的损失
min_error = np.inf #将最小的误差初始化为 无穷大
num_sample, num_feature = x_train.shape
for i in range(num_feature): #遍历数据集中的所有特征
for split_ in ['A','B']: #假设两个特征的为A 和 B
min_ = x_train[:,i].min()
threshold = min_ + i
stump_prediction = decision_tree_stump(x_train,i,split_,threshold) #使用决策树树桩进行预测
# print('prediction.shape:{}'.format(stump_prediction.shape))
# print('y_train.shape:{}'.format(y_train.shape))
errorArr = np.mat(np.ones((x_train.shape[0],1))) #初始化误差向量
errorArr[stump_prediction == y_train] = 0 # 若预测正确,将其误差置为零
# print('D.shape:{}'.format(D.shape))
# print('errorArr.shape:{}'.format(errorArr.shape))
weightError = np.sum(np.multiply(D,errorArr)) #将所有预测错误的误差相加
if weightError < min_error:
min_error = weightError #如果当前的加权损失之和小于记录的最小误差,则交换
best_['feature'] = i #将最佳的分裂特征记下
best_['threshold'] = threshold #记录下最佳的阈值
best_['split'] = split_ #记录下最好的分裂特征
best_prediction = np.copy(stump_prediction)
return best_,min_error,best_prediction
我们可以用简化的小数据集来评估一下 ‘最佳的决策树树桩’的accuracy。
X,Y = load_data()
# print(X,Y)
D = np.ones((X.shape[0],1))/X.shape[0]
best_, min_error, best_prediction = best_decision_tree(X,Y,D)
print('best_feature:{}'.format(best_))
print('min_error:{}'.format(min_error))
print('best_prediction:{}'.format(best_prediction))
correct_prediction = np.where(best_prediction.T == Y )
print('Accuracy:{}'.format(len(correct_prediction[1])/len(Y)))
将每一个样本的权重大小初始成样本数分之1。因为这里是测试是否能找出最佳的决策树树桩,所以样本的权重大小暂不更新。将准备好的数据传给best_decision_tree函数。
X,Y = load_data()
# print(X,Y)
D = np.ones((X.shape[0],1))/X.shape[0]
best_, min_error, best_prediction = best_decision_tree(X,Y,D)
print('best_feature:{}'.format(best_))
print('min_error:{}'.format(min_error))
print('best_prediction:{}'.format(best_prediction))
correct_prediction = np.where(best_prediction.T == Y )
print('Accuracy:{}'.format(len(correct_prediction[1])/len(Y)))
可以的到以下结果:
best_feature:{'feature': 0, 'threshold': 4.7, 'split': 'B'}
min_error:0.25
best_prediction:[[ 1.]
[-1.]
[ 1.]
[ 1.]]
Accuracy:0.75
目前为止,最佳的决策树树桩可以在简化的数据集上达到75%的准确率,这样一来构建AdaBoost的前提条件都已经满足了。接下来便开始实现完整的AdaBoost算法。
那么大致的思路如下:
- 根据数据集训练出弱学习器。
- 将弱学习器添加至一个列表中。
- 计算误差
- 更改弱学习器的权重以及每一个样本的权重。
- 当误差小于一个阈值,退出循环,训练结束。
def adaboost_(x_train,y_train,num_estimator):
classifier_array = [] #用于储存所有的弱学习器
x_train = np.mat(x_train)
y_train = np.mat(y_train).T
D = np.ones((X.shape[0],1))/X.shape[0] #初始化D
aggregate_predict = np.mat(np.zeros((x_train.shape[0],1)))
for i in range(num_estimator):
#退出循环的条件:1.已经生成了目标个数的弱学习器。2.误差为0
best_, min_error, best_prediction = best_decision_tree(x_train,y_train,D)
alpha = 0.5*np.log((1-min_error)/max(min_error,1e-5)) #得到最小误差
print('min_error:{}'.format(min_error))
print(f'alpha{i}:{alpha}') #打印alpha
# print('D.T:{}'.format(D.T)) #打印样本权重
best_['alpha'] = alpha #记录alpha值
classifier_array.append(best_) #将弱学习器加入到弱学习器的列表当中
#更新D
expon = np.multiply(-1 * alpha * y_train,best_prediction)
D = np.multiply(D,np.exp(expon))
D = D/D.sum()
#给每一个弱学习器的预测结果加上权重
aggregate_predict += alpha * best_prediction
# print('aggrate_predict:{}'.format(aggregate_predict))
sign_prediction = np.sign(aggregate_predict)#将累计的预测使用符号函数表示成 1 或 -1,将其作为集成模型预测的结果
# #计算正确率
# correct_prediction = np.where(sign_prediction[sign_prediction==y_train])
# acc = len(correct_prediction[1])/len(y_train)
# print(acc)
#计算误差
aggregate_error = np.multiply(sign_prediction!=y_train,np.ones((y_train.shape[0],1)))
print('aggregate: {}'.format(aggregate_error))
errorRate = aggregate_error.sum() / y_train.shape[0]
print('errorRate:{}'.format(errorRate))
if errorRate == 0 : break
return classifier_array
首先第一步是要创建一个列表用于存储训练出来的弱学习器。然后训练一个决策树树桩,并且计算误差,根据误差计算alpha(也就是每一个弱学习器的权重。)并将学习器加入到列表中。在得到alpha之后,将其用于对应的弱学习器的结果上。将每一个弱学习器的预测结果进行加权累加。累加之后结果的符号表示样本的类别,所以使用sign函数来取出每一个样本的符号来表示类别,并将结果与原标签进行比较(与原标签相同的预测被标记为True,反之则是False)将比较后的结果与一个全是1的数组相乘(例如:True与1相乘 为 1,与0相乘为0)。这样做的目的是为了将损失进行累加,方便求得平均损失。需要提及的一点是D 的更新,因为D的更新公式根据弱学习器预测的情况而改变,所以为了避免每一次预测都进行一次判断,直接使用代码中的方式可以更加简单。最后使用准确率或者误差来作为停止循环的条件。
我们可以观察一下样本权重D的变化过程。
D.T:[[0.25 0.25 0.25 0.25]]
D.T:[[0.16666667 0.5 0.16666667 0.16666667]]
D.T:[[0.125 0.375 0.25 0.25 ]]
D.T:[[0.1 0.5 0.2 0.2]]
D.T:[[0.08333333 0.41666667 0.25 0.25 ]]
D.T:[[0.07142857 0.5 0.21428571 0.21428571]]
D.T:[[0.0625 0.4375 0.25 0.25 ]]
D.T:[[0.05555556 0.5 0.22222222 0.22222222]]
D.T:[[0.05 0.45 0.25 0.25]]
D.T:[[0.04545455 0.5 0.22727273 0.22727273]]
第一轮循环的时候每一个样本的权重都是一致的。但是第一轮预测之后,第二个样本出现了错误,其余的三个样本预测正确。这是其余三个样本的权重从0.25下降值了0.167左右,但是第2个样本的权重从0.25上升至0.5。这是我们期望的效果。
4. 总结
AdaBoost的原理其实并不难理解,但是在算法的实现上会有一些小技巧需要发掘。比如如何使用每一个弱分类器的权权重?将分类结果乘上分类器的权重并将所有的计算结果进行累加, 使用每一个值的符号作为分类的结果是一个很聪明的方法。其次,巧妙的运用矩阵运算可以简化编程难度。