1、方法论
adaboost(adaptive boosting, 自适应提升法)是用多个弱分类器组合成强分类器的一种算法,其中这些弱分类器只需要满足分类效果比随机猜测好即可(当然弱分类器越强,需要的弱分类器数量也越少)。用俗话说,人多力量大,或者三个臭皮匠赛过诸葛亮。
adaboost的弱分类器可以使用很多种,常见的是单层决策树。其实很简单,就是只使用特征的一个维度来划分数据,如何用这个特征来划分,则可以用穷举法,使得分类正确的数量最大。
adaboost训练的时候每次只训练一个弱分类器,一个个训练,然后线性组合起来,所以可以认为本文讨论的adaboost就是若干个单层决策树的线性组合。训练时,本次训练,会更加重视上一次训练时被分错的那些数据,而分对了的那些数据则没那么重视。用数学的方法描述是,在总权重为1的条件下,初始权重为相等的,而若干次训练后,总是加大本错分的那些数据的权重,而减小被分对的那些数据的权重。每一次训练,将错分了的那些数据的权重加起来,就是本次训练的损失,或者误差。因此adaboost的训练任务可以定义为:每一轮训练,都new一个单层决策树来划分现有数据集的数据,使得所有数据的权重和最小,然后增加错误分类样本的权重而减小正确分类样本的权重以准备下一轮训练。
上面说了,adaboost是若干单层决策树的线性组合,那么线性组合的系数如何计算呢?错误分类的样本和正确分类的样本的权重又是如何增加和减小呢?对于adaboost,我不研究具体的推导方法,直接方法论。
设错误率定义为:
则本次训练的决策树的线性组合系数为(弱分类总比随机猜测好,因此e<0.5):
被正确分类的样本的权值变化递推公式为(e<0.5,alfa>0,于是exp(-alfa)<1):
被错误分类的样本的权值变化的递推公式为(e<0.5,alfa>0,于是exp(alfa) > 1):
上面两个公式中的Sum(D)都是对D(t+1)而言的,其实就是把计算后的数据归一化。两式中权值D的下标i指的是对每一个样本,每一个样本都要单独计算一下权值。
2、代码实现
2.1测试数据
首先了解下要做的事情,如下图,目标是将左下角的两个点和右边的三个点分开。显然,仅仅通过一个维度是无法将数据分类的。
if __name__ == "__main__": pd.set_option("display.width", 5000) d = np.array([[1.0, 1.0], [1.3, 1.0], [1.0, 2.1], [1.5, 1.6], [2.0, 1.0], ]) label = np.array([1, 1, -1, -1, -1])
这里直接输入了数据和对应的样本标签
2.2单层决策树的构建
# 对data按照dimen特征来分类,阈值是thr,threshdeh表示大于或小于判定为1 def stump_classify(data, dimen, thr, thresh_deh): retarr = np.ones((data.shape[0], 1)) if thresh_deh == "lt": retarr[data[:, dimen] <= thr] = -1.0 else: retarr[data[:, dimen] > thr] = -1.0 return retarr def build_stump(data, labels, d): dm = np.mat(data) y = np.mat(labels).T # print(y) m, n = np.shape(dm) num_steps = 10.0 best_stumps = {} best_class_estimate = np.mat(np.zeros((m, 1))) min_err = np.inf for i in range(n): # 对n个特征 rangemin = dm[:, i].min() rangemax = dm[:, i].max() step_size = (rangemax - rangemin) / num_steps for j in range(-1, int(num_steps) + 1): # 按照一定的精度搜索该特征,把搜索范围扩大到了min和max外 for inequal in {"lt", "gt"}: thr = (rangemin + float(j) * step_size) predict_arr = stump_classify(dm, i, thr, inequal) err_arr = np.mat(np.ones((m, 1))) err_arr[y == predict_arr] = 0 weighted_err = d.T * err_arr # print("split:", i, ", thr:", thr, ", inequal:", inequal, ", err:", weighted_err) if weighted_err < min_err: min_err = weighted_err best_class_estimate = predict_arr.copy() best_stumps["dim"] = i best_stumps["thr"] = thr best_stumps["ineq"] = inequal return best_stumps, min_err, best_class_estimate
用到的子函数stump_classify获得指定维度和指定阈值下数据集内所有的数据的分类标签,增加"gt"和"lt"是因为我们不知道数据大于阈值是负类还是小于阈值是负类,要穷举。
build_stump中也很简单,就是对每个特征,按照某种方式划分阈值(这种阈值划分的方式比较粗糙)来进行穷举,找到误差(每个样本的权重和)最小的那一种划分方式。最后将最好的划分方式用字典表示,并返回字典、最小误差以及对应的数据划分结果。
2.3完整的adaboost代码(权值计算和系数计算)
每个样本的初始权值是相等的,然后每次迭代更新一次权重,期间还要根据错误率计算对应若分类器的决策系数。
def adaboost_train(data, labels, num_iter=40): weak_classifier_arr = [] m = data.shape[0] w = np.mat(np.ones((label.shape[0], 1)) / m) agg_class_est = np.mat(np.zeros((m, 1))) for i in range(num_iter): best_stump, err, best_clsss_est = build_stump(data, labels, w) print("weight:", w.T, end=" ") alfa = np.float(0.5 * np.log((1 - err) / max(err, 1e-16))) best_stump["alfa"] = alfa weak_classifier_arr.append(best_stump) print("class_est:", best_clsss_est.T, end=" ") expon = np.multiply(-1*alfa*np.mat(labels).T, best_clsss_est) # 正确分类为-alfa,错误分类为alfa w = np.multiply(w, np.exp(expon)) w = w / w.sum() agg_class_est += alfa * best_clsss_est # 将若分类器累加求和得到最终结果 print("aggclassest:", agg_class_est, end=" ") agg_err = np.mat(np.ones((m, 1))) agg_err[np.sign(agg_class_est) == np.mat(labels).T] = 0 err_rate = agg_err.sum() / m print("errrate:", err_rate) if err_rate == 0.0: print(np.sign(agg_class_est.T)) break return weak_classifier_arr
几个较难理解的点在代码中已经备注了,注意下expon的计算,这个其实只是为每个样本计算自己的alfa,正确分类的是-alfa,错误分类的是alfa,仅此而已.
有一个值得注意的点是,best_clsss_est是当前弱分类器取得的最优分类结果,agg_class_est才是全局最优,是多个若分类器乘以系数后累加,也就是多个若分类器的线性组合。
最后调用如下:
weak_classifiers = adaboost_train(d, label, 40) res = adaboost_test(d, weak_classifiers) print(res) plt.scatter(d[:, 0], d[:, 1], color="blue", alpha=0.5) plt.show()
运行结果:
2.4用Adaboost预测马疝病
这个测试比较简单,数据储存为txt格式,读入也很简单。数据分为训练集和测试集
def load_dataset(filename): num_f = len(open("D:\\leran\\adaboost\\"+filename).readline().split("\t")) data_m = [] label_m = [] fr = open("D:\\leran\\adaboost\\"+filename) for line in fr.readlines(): li = [] curren_line = line.strip().split("\t") for i in range(num_f-1): li.append(float(curren_line[i])) data_m.append(li) label_m.append(float(curren_line[-1])) return np.array(data_m), np.array(label_m)
文件路径可以根据实际情况修改,我这里直接写死了。
d, label = load_dataset("horseColicTraining2.txt") weak_classifiers = adaboost_train(d, label, 50) # res = adaboost_test(d, weak_classifiers) d, label = load_dataset("horseColicTest2.txt") res = adaboost_test(d, weak_classifiers) err_arr = np.mat(np.ones((res.shape[0], 1))) err_arr[np.mat(label).T == np.sign(res)] = 0 weighted_err = err_arr.sum() / res.shape[0] print("test res:", weighted_err)
使用50个若分类器,最后在训练样本上得到的错误率为0.194:
如果继续增加弱分类器数量,训练错误率会更加小,不过测试错误率却不会随着弱分类器的增加而下降,当弱分类器数量大约大于500个时训练错误率很小,但测试错误率反而上升了。这是过拟合了。