★ AdaBoost的理解:
对于二分类而言,我们通过一个阈值threshVal将数据集划分为两种分类(正类和负类),而这个阈值就是我们所说的分割线,只不过它总是平行于坐标轴(因为我们取的基分类器是单层决策树):
对于上述数据集,一个基分类器即可将其分类,但是对于大多数训练集而言,一个基分类器是不足够的,比如:
在图3中,我们找不到一条平行于坐标轴的直线将数据集正确的划分,因此我们使用了3个基分类器将其分开,由图4看出,3个分类器把数据集分成6个部分,每一部分我们公式计算出来它是正类还是负类,比如对第①部分:,所以第①部分是负类(蓝色点就是负类),再比如第⑥部分:,所以第⑥部分是正类(红色点就是正类)。依次计算这6个部分,画出图来就是:
由图我们可以看出,橘红色部分(②,③,⑥)是正类,绿色部分(①,④,⑤)是负类。
★ AdaBoost 分类器简介:
adaboost 就是把几个弱分类器,通过线性组合,组合成强分类器,它在训练集上有很高的准确率,但是在测试集上效果却没有那么好,因为它是逐步优化分错的样本,所以最终分类误差率会降到很低,这个组合分类器的数学描述是:
①
其中m是分类器个数,也就是当组合m个弱分类器后,误差率能满足我们的要求。
代表着该分类器的重要程度。
★ AdaBoost 算法流程:
(1) 初始化权值,每一个训练样本最开始时都被赋予相同的权值:1/N (N是样本个数),它代表着开始时每个样本的重要性都是相等的。
②
我们经过每一次的迭代,上述权值会不断变化,但初始时各权值是相等的。
(2) 使用具有权值分布的训练数据集学习,得到基本分类器(分类器可以选择SVM,决策树等,后面我们选择最简单的单层决策树作为基分类器):
这里就是第m个基分类器,它的取值只有-1或者+1。
(3) 计算 在训练集上的分类误差率:
③
其中:是第m个分类器的分类误差率,就是分类器分错的概率。
是第m个分类器在数据集x上使用的每个样本的权值,跟公式②的 一个意思
是第m个分类器对第i个样本的预测分类。它的取值是-1或+1
是第i个样本真实的分类。对于二分类来说,它的取值也是-1或+1
当括号里面条件为true的时候取值为1,反之,为false的时候取值为0,这里 可不是单位矩阵哈
(4) 计算的系数,代表着该分类器的重要程度,当这个分类器的分类误差率越低的时候,就越大。
④
从上式④可以看出,分类误差率越小,就越大,对应的分类器就越重要。
(5) 更新每个样本的权值:
⑤
其中: 是第m+1个分类器的各样本点权值分布
是第m+1个分类器的样本点权值的更新规则。
是归一化因子,他其实就是分子的和。
⑥
上式的公式⑥是有公式③、④代入得到的。
(6) 线性组合各弱分类器:
⑦
得到最终的分类器:
⑧
★ AdaBoost 算法的解释:
证明 AdaBoost 的损失函数是指数函数:(可通过前向分布算法来推adaboost,只要证明前向分布算法与adaboost都是加法模型,并当前向分布算法取指数损失函数时就是adaboost,那么就证明了adaboost使用的是指数损失函数)
✿ 首先我们先来看一下前向分布算法:
前向分布算法的加法模型是:
⑨
其中,为基函数, 为基函数的参数,为基函数的系数,M是基分类器个数由公式⑦可以看出,adaboost也是一个加法模型。
假设前向分布算法的损失函数是,那么算法的目标函数就是极小化损失函数:
⑩
上式中的N是样本个数。
前向分布算法求解这一优化问题的思想是:因为该学习是加法模型,如果能够从前向后,每一步只学习一个基函数及其系数,逐步逼近优化目标函数式⑩,那么就可以简化该优化问题的复杂度,因此每一步只需优化如下损失函数(即优化每一个基分类器的损失函数):
⑪
当前向分布算法的损失函数是指数损失函数时,即:
⑫
只需证明该学习的具体操作等价于AdaBoost的具体操作,就可证明AdaBoost也使用的是指数损失函数。假设我们经过m-1次迭代前向分布算法得到:
⑬
在第m轮迭代得到,和:
⑭
目标是使前向分布算法得到的和 使得在训练集上的指数损失函数最小,即:
⑮
公式⑮可以表示为:
⑯
其中: 。 因为 既不依赖也不依赖于G,所以与最小化无关。但依赖于,随着每一轮迭代而发生改变。
现证使公式⑯达到最小时,,,的取值就是AdaBoost算法得到的,,。
对于任意,使公式最小的由下式得到:
⑰
我们要求极小值,所以将上式对求偏导并令其为0:
⑱
上式的两边同时除以 ,且令:
⑲
而AdaBoost的为:
③
对比公式③、⑲可以看出,AdaBoost的和前向分布算法的是一致的。
故求导的原式为:
解上式得:
⑳
而AdaBoost的 :
④
对比公式④、⑳可以看出,AdaBoost的和前向分布算法的是一致的。
我们再来推算前向分布算法的每个样本的权值更新,由公式:
㉑
将公式㉑的第一个式子,等式两边同时乘以 -y 再取以e为底数:
㉒
对比公式⑤、㉒,可以看出AdaBoost的和前向分布算法的只差一个归一化因子,因而等价。
因此,AdaBoost算法使用的是指数损失函数。
★ 代码实践:
from numpy import * import matplotlib.pyplot as plt import matplotlib from matplotlib.font_manager import * def loadDataSet(): # 加载测试数据 dataMat = mat([[1.,2.1], [2.,1.1], [1.3,1.], [1.,1.], [1.1,1.2], [2.,1.], ]) labelList = [1.0,1.0,-1.0,-1.0,1.0,1.0] return dataMat, labelList # 数据集返回的是矩阵类型,标签返回的是列表类型 def stumpClassify(dataMat,dimen,threshVal,threshIneq): # dimen:第dimen列,也就是第几个特征, threshVal:是阈值 threshIneq:标志 retArray = ones((shape(dataMat)[0],1)) # 创造一个 样本数×1 维的array数组 if threshIneq == 'lt': # lt表示less than,表示分类方式,对于小于等于阈值的样本点赋值为-1 retArray[dataMat[:,dimen] <= threshVal] = -1.0 else: # 我们确定一个阈值后,有两种分法,一种是小于这个阈值的是正类,大于这个值的是负类, #第二种分法是小于这个值的是负类,大于这个值的是正类,所以才会有这里的if 和else retArray[dataMat[:,dimen] > threshVal] = -1.0 return retArray # 返回的是一个基分类器的分类好的array数组 def buildStump(dataArr,classLabels,D): dataMat = mat(dataArr) labelMat = mat(classLabels).T m,n = shape(dataMat) numStemp = 10 bestStump = {} bestClassEst = mat(zeros((m,1))) minError = inf # 无穷 for i in range(n): # 遍历特征 rangeMin = dataMat[:,i].min() # 检查到该特征的最小值 rangeMax = dataMat[:,i].max() stepSize = (rangeMax - rangeMin)/numStemp # 寻找阈值的步长是最大减最小除以10,你也可以按自己的意愿设置步长公式 for j in range(-1, int(numStemp)+1): for inequal in ['lt', 'gt']: # 因为确定一个阈值后,可以有两种分类方式 threshVal = (rangeMin+float(j) * stepSize) predictedVals = stumpClassify(dataMat,i,threshVal,inequal) # 确定一个阈值后,计算它的分类结果,predictedVals就是基分类器的预测结果,是一个m×1的array数组 errArr = mat(ones((m,1))) errArr[predictedVals==labelMat] =0 # 预测值与实际值相同,误差置为0 weightedEroor = D.T*errArr # D就是每个样本点的权值,随着迭代,它会变化,这段代码是误差率的公式 if weightedEroor<minError: # 选出分类误差最小的基分类器 minError=weightedEroor # 保存分类器的分类误差 bestClassEst = predictedVals.copy() # 保存分类器分类的结果 bestStump['dim']=i # 保存那个分类器的选择的特征 bestStump['thresh']=threshVal # 保存分类器选择的阈值 bestStump['ineq']=inequal # 保存分类器选择的分类方式 return bestStump,minError,bestClassEst def adaBoostTrainDS(dataMat, classLabels,numIt=40): # 迭代40次,直至误差满足要求,或达到40次迭代 weakClassArr = [] # 保存每个基分类器的信息,存入列表 m =shape(dataMat)[0] D = mat(ones((m,1))/m) aggClassEst = mat(zeros((m,1))) for i in range(numIt): bestStump,error,classEst = buildStump(dataMat,classLabels,D) print('D: ',D) alpha = float(0.5 * log((1.0-error)/max(error,1e-16))) # 对应公式 a = 0.5* (1-e)/e bestStump['alpha']=alpha weakClassArr.append(bestStump) # 把每个基分类器存入列表 print('classEst: ',classEst.T) expon = multiply(-1 * alpha * mat(classLabels).T, classEst) # multiply是对应元素相乘 D = multiply(D,exp(expon)) # 根据公式 w^m+1 = w^m (e^-a*y^i*G)/Z^m D = D/D.sum() # 归一化 aggClassEst += alpha * classEst # 分类函数 f(x) = a1 * G1 print("aggClassEst: ",aggClassEst.T) aggErrors = multiply(sign(aggClassEst) != mat(classLabels).T, ones((m,1))) # 分错的矩阵 errorRate = aggErrors.sum() /m # 分错的个数除以总数,就是分类误差率 print('total error: ',errorRate) if errorRate == 0.0: # 误差率满足要求,则break退出 break return weakClassArr,aggClassEst def adaClassify(datToClass, classifierArr): # 预测分类 dataMat = mat(datToClass) # 测试数据集转为矩阵格式 m = shape(dataMat)[0] aggClassEst = mat(zeros((m,1))) for i in range(len(classifierArr)): classEst = stumpClassify(dataMat, classifierArr[i]['dim'], classifierArr[i]['thresh'],classifierArr[i]['ineq']) # 可以对比文章开头的图,其实就是那个公式 aggClassEst += classifierArr[i]['alpha']*classEst print(aggClassEst) return sign(aggClassEst) def draw_figure(dataMat,labelList,weakClassArr): # 画图 # myfont = FontProperties(fname='/usr/share/fonts/simhei.ttf') # 显示中文 matplotlib.rcParams['axes.unicode_minus'] = False # 防止坐标轴的‘-’变为方块 matplotlib.rcParams["font.sans-serif"]=["simhei"] # 第二种显示中文的方法 fig = plt.figure() # 创建画布 ax = fig.add_subplot(111) # 添加子图 red_points_x = [] # 红点的x坐标 red_points_y = [] # 红点的y坐标 blue_points_x = [] # 蓝点的x坐标 blue_points_y = [] # 蓝点的y坐标 m,n = shape(dataMat) # 训练集的维度是 m×n ,m就是样本个数,n就是每个样本的特征数 dataSet_list = array(dataMat) # 训练集转化为array数组 for i in range(m): # 遍历训练集,把红点,蓝点分开存入 if labelList[i] == 1: red_points_x.append(dataSet_list[i][0]) # 红点x坐标 red_points_y.append(dataSet_list[i][1]) else: blue_points_x.append(dataSet_list[i][0]) blue_points_y.append(dataSet_list[i][1]) line_thresh = 0.025 # 画线阈值,就是不要把线画在点上,而是把线稍微偏移一下,目的就是为了让图更加美观直接 annotagte_thresh = 0.03 # 箭头间隔,也是为了美观 x_min = y_min = 0.50 # 自设的坐标显示的最大最小值,这里固定死了,应该是根据训练集的具体情况设定 x_max = y_max = 2.50 v_line_list = [] # 把竖线阈值的信息存起来,包括阈值大小,分类方式,alpha大小都存起来 h_line_list = [] # 横线阈值也是如此,因为填充每个区域时,竖阈值和横阈值是填充边界,是不一样的,需各自分开存贮 for baseClassifier in weakClassArr: # 画阈值 if baseClassifier['dim'] == 0: # 画竖线阈值 if baseClassifier['ineq'] == 'lt': # 根据分类方式,lt时 ax1=ax.vlines(baseClassifier['thresh']+line_thresh, y_min, y_max, colors='green',label='阈值') # 画直线 ax.arrow(baseClassifier['thresh']+line_thresh,1.5,0.08,0,head_width=0.05,head_length=0.02) # 显示箭头 ax.text(baseClassifier['thresh']+annotagte_thresh,1.5+line_thresh, str(round(baseClassifier['alpha'],2))) # 画alpha值 v_line_list.append([baseClassifier['thresh'],1,baseClassifier['alpha']]) # 把竖线信息存入,注意分类方式,lt就存1,gt就存-1 else: # gt时,分类方式不同,箭头指向也不同 ax.vlines(baseClassifier['thresh']+line_thresh, y_min, y_max, colors='green',label="阈值") ax.arrow(baseClassifier['thresh']+line_thresh,1.,-0.08,0,head_width=0.05,head_length=0.02) ax.text(baseClassifier['thresh']+annotagte_thresh, 1.+line_thresh, str(round(baseClassifier['alpha'],2))) v_line_list.append([baseClassifier['thresh'],-1,baseClassifier['alpha']]) else: # 画横线阈值 if baseClassifier['ineq'] == 'lt': # 根据分类方式,lt时 ax.hlines(baseClassifier['thresh']+line_thresh,x_min,x_max,colors='black',label="阈值") ax.arrow(1.5+line_thresh, baseClassifier['thresh']+line_thresh,0.,0.08,head_width=0.05,head_length=0.05) ax.text(1.5+annotagte_thresh, baseClassifier['thresh']+0.04,str(round(baseClassifier['alpha'],2))) h_line_list.append([baseClassifier['thresh'],1,baseClassifier['alpha']]) else: # gt时 ax.hlines(baseClassifier['thresh']+line_thresh,x_min,x_max,colors='black',label="阈值") ax.arrow(1.0+line_thresh,baseClassifier['thresh'],0.,0.08,head_width=-0.05,head_length=0.05) ax.text(1.0+annotagte_thresh,baseClassifier['thresh']+0.04,str(round(baseClassifier['alpha'],2))) h_line_list.append([baseClassifier['thresh'],-1,baseClassifier['alpha']]) v_line_list.sort(key=lambda x: x[0]) # 我们把存好的竖线信息按照阈值大小从小到大排序,因为我们填充颜色是从左上角开始,所以竖线从小到大排 h_line_list.sort(key=lambda x: x[0],reverse=True) # 横线从大到小排序 v_line_list_size = len(v_line_list) # 排好之后,得到竖线有多少条 h_line_list_size = len(h_line_list) # 得到横线有多少条 alpha_value = [x[2] for x in v_line_list]+[y[2] for y in h_line_list] # 把属性横线的所有alpha值取出来,这里也证实了上面的排序不是无用功 print('alpha_value',alpha_value) for i in range(h_line_list_size+1): # 开始填充颜色,(横线的条数+1) × (竖线的条数+1) = 分割的区域数,然后开始往这几个区域填颜色 for j in range(v_line_list_size+1): # 我们是左上角开始填充直到右下角,所以采用这种遍历方式 list_test = list(multiply([1]*j+[-1]*(v_line_list_size-j),[x[1] for x in v_line_list]))+list(multiply([-1]*i+[1]*(h_line_list_size-i),[x[1] for x in h_line_list])) # 上面是一个规律公式,后面会用文字解释它 # print('list_test',list_test) temp_value = multiply(alpha_value,list_test) # list_test其实就是加减号,我们知道了所有alpha值,可是每个alpha是相加还是相加,这就是list_test作用了 reslut_test = sign(sum(temp_value)) # 计算完后,sign一下,然后根据结果进行分类 if reslut_test==1: # 如果是1,就是正类红点 color_select = 'orange' # 填充的颜色是橘红色 hatch_select = '.' # 填充图案是。 # print("是正类,红点") else: # 如果是-1,那么是负类蓝点 color_select = 'green' # 填充的颜色是绿色 hatch_select = '*' # 填充图案是* # print("是负类,蓝点") if i == 0: # 上边界 现在开始填充了,用fill_between函数,我们需要得到填充的x坐标范围,和y的坐标范围,x范围就是多条竖线阈值夹着的区域,y范围是横线阈值夹着的范围 if j == 0: # 左上角 ax.fill_between(x=[x for x in arange(x_min,v_line_list[j][0]+line_thresh,0.001)],y1=y_max,y2=h_line_list[i][0]+line_thresh,color=color_select,alpha=0.3,hatch=hatch_select) elif j== v_line_list_size: # 右上角 ax.fill_between(x=[x for x in arange(v_line_list[-1][0]+line_thresh,x_max,0.001)],y1=y_max,y2=h_line_list[i][0]+line_thresh,color=color_select,alpha=0.3,hatch=hatch_select) else: # 中间部分 ax.fill_between(x=[x for x in arange(v_line_list[j-1][0]+line_thresh,v_line_list[j][0]+line_thresh,0.001)],y1=y_max,y2=h_line_list[i][0]+line_thresh,color=color_select,alpha=0.3,hatch=hatch_select) elif i == h_line_list_size: # 下边界 if j == 0: # 左下角 ax.fill_between(x=[x for x in arange(x_min,v_line_list[j][0]+line_thresh,0.001)],y1=h_line_list[-1][0]+line_thresh,y2=y_min,color=color_select,alpha=0.3,hatch=hatch_select) elif j== v_line_list_size: # 右下角 ax.fill_between(x=[x for x in arange(v_line_list[-1][0]+line_thresh,x_max,0.001)],y1=h_line_list[-1][0]+line_thresh,y2=y_min,color=color_select,alpha=0.3,hatch=hatch_select) else: # 中间部分 ax.fill_between(x=[x for x in arange(v_line_list[j-1][0]+line_thresh,v_line_list[j][0]+line_thresh,0.001)],y1=h_line_list[-1][0]+line_thresh,y2=y_min,color=color_select,alpha=0.3,hatch=hatch_select) else: if j == 0: # 中左角 ax.fill_between(x=[x for x in arange(x_min,v_line_list[j][0]+line_thresh,0.001)],y1=h_line_list[i-1][0]+line_thresh,y2=h_line_list[i][0]+line_thresh,color=color_select,alpha=0.3,hatch=hatch_select) elif j== v_line_list_size: # 中右角 ax.fill_between(x=[x for x in arange(v_line_list[-1][0]+line_thresh,x_max,0.001)],y1=h_line_list[i-1][0]+line_thresh,y2=h_line_list[i][0]+line_thresh,color=color_select,alpha=0.3,hatch=hatch_select) else: # 中间部分 ax.fill_between(x=[x for x in arange(v_line_list[j-1][0]+line_thresh,v_line_list[j][0]+line_thresh,0.001)],y1=h_line_list[i-1][0]+line_thresh,y2=h_line_list[i][0]+line_thresh,color=color_select,alpha=0.3,hatch=hatch_select) ax.scatter(red_points_x,red_points_y,s=30,c='red', marker='s',label="red points") # 画红点 ax.scatter(blue_points_x,blue_points_y,s=40,label="blue points") # 画蓝点 ax.set_xlabel("x") ax.set_ylabel("y") ax.legend() # 显示图例 如果你想用legend设置中文字体,参数设置为 prop=myfont ax.set_title("图 5 AdaBoost分类",position=(0.5,-0.175)) # 设置标题,改变位置,可以放在图下面,这个position是相对于图片的位置 plt.show() if __name__ =='__main__': # 运行函数 dataMat, labelList = loadDataSet() # 加载数据集 weakClassArr, aggClassEst = adaBoostTrainDS(dataMat,labelList) draw_figure(dataMat,labelList,weakClassArr) # 画图 print('weakClassArr',weakClassArr) print('aggClassEst',aggClassEst) classify_result =adaClassify([0.7,1.7],weakClassArr) # 预测的分类结果,测试集我们用的是[0.7,1.7]测试集随便选 print("结果是:",classify_result)
★ 运行结果:
★ 推算上述填充颜色的规律(以至于写出填充颜色的代码):
不失一般性,我们随便举一个例子如上图7,共有3条纵向阈值,2条横向阈值,将信息绘制成表格:
纵向阈值 0.4 0.6 0.8 分类方式 gt lt lt
横向阈值 1.2 1.0 分类方式 gt lt 注意:纵向阈值(竖线)按从小达到排序,横向阈值(横线)按从大到小排序,至于原因,可以看图7就知道了。
gt和lt对应上文的两种分类方式,gt箭头就朝左,lt箭头就朝右。可以看出,这3条竖线和2条横线把该区域分成了12个部分,这里分成的区域数就是: (横线条数+1)×(竖线条数+1)。
现在我们判别每一部分是正类还是负类,可以写出每一部分的计算公式,比如我先写出①②③④这个四个部分的计算公式:
①
当然了,上述式子是通过人观察图7,根据箭头指向来判断出加减号,然后写出来的计算式子。那么如何通过代码来代替人的观察以此来写出加减号呢?
其实加号或减号在代码中的体现就是+1或-1。我们把(1.),(2.),(3.),(4.)的符号提出来观察一下:
② == ==> ③
我把式子③分成左右两部分了(因为左边是竖线阈值提出来的符号,右部分是横线阈值提出来的符号)。gt和lt是字母表示的是分类方式,字母我们不能用于计算,所以我们规定:lt为+1,而gt为-1。(当然这是我自己规定的,你也可以反过来)。因此3条竖线阈值可以写为[-1,1,1],而2条横线阈值可以写为[-1,1]。
我们再来看图7,每一部分都跟这3条竖线和2条横线有关系,比如第①部分:它在竖线阈值0.4的左边,在竖线阈值0.6的左边,在竖线阈值0.8的左边,在横线阈值1.2的上边,在横线阈值1.0的上边。现在我们再次规定:在直线右边或上边的部分为+1,在直线左边或下边的部分为-1。因此由刚才叙述的第①部分与各直线的关系,我们可以写出来第①部分和3条竖线阈值的关系是[-1,-1,-1],与2条横线阈值的关系是:[1,1]。我们把阈值与区域和阈值的关系进行点乘(竖线和横线要分开,然后再合起来),比如还是第①部分:
④
上式④的+号是合并列表的意思,不是加减乘除的+号。对比式子③,可以看出与式子③的第一个式子一致,因此这样考虑是可行的,不信的话,可以根据上述规则写出第②部分的计算式子:
⑤
再把⑤和式子③的第二个式子对比,发现还是一致,现在小伙伴们可以相信这个做法是正确的。
可是还有一个问题:上述区域与竖线阈值(或横线阈值)的关系而写出的表达式使我们用肉眼看出来的,比如你看到第①部分在竖线阈值0.4的左边,你才根据规则写出的-1,可是程序不会肉眼看啊,现在我们再来发现一个规律:先把这些位置关系的表达式写出来,看看有没有规律可循,比如我先把①②③④这个四个区域与竖线阈值的关系写出来,你可以看出端倪:
我们可以看出第一行0个1,3个-1,而第二行1个1,2个-1,第三行2个1,1个-1,第四行3个1,0个-1。而在写代码时,我们遍历行的时候,就是遍历①②③④这四个部分来填充颜色,不正是第①部分j=0;第②部分j=1;第③部分j=2;第④部分j=3。那么上面矩阵的每一行就可以这么来制造,伪代码就是:
for j in range(竖线阈值的条数+1): # 因为看图7,第一行不正好4部分,对应区域①②③④
row_list = [1]*j + [-1]*(竖线阈值的条数)
同样的方法,我们也可以写出①⑤⑨这个3个区域与横线阈值的关系(为啥不继续写①②③④与横线的关系,因为①②③④与横线的关系是一样的,①⑤⑨在图7中是不同列的,他们的关系才是不同的):
我们同样看出了端倪:第一行0个-1,2个1;第二行1个-1,1个1;第三行2个-1,0个1。这正是我们写代码时遍历列,第一列 i = 0,第二列 i=1,第三列 i=2,因此伪代码是:
for i in range(横线阈值的条数+1):
column_list = [-1]*i + [1]*(横线阈值的条数)
因此我们把遍历行和遍历列的伪代码拼在一起,成为遍历3×4个区域,依次填充颜色。
✿ 我们写成伪代码(代码里的+号都是合并列表的意思,不是加减乘除的+号):
for i in range(横线阈值的条数+1):
for j in range(竖线阈值的条数+1):
symbol_list = row_list = [1]*j + [-1]*(竖线阈值的条数) + column_list = [-1]*i + [1]*(横线阈值的条数)
result = sign([alpha值的列表]*symbol_list):
if reslut == 1:
正类(红点)
else:
负类(蓝点)
★ 参考链接:
1. http://www.cnblogs.com/Tang-tangt/p/9557089.html
2. https://zhuanlan.zhihu.com/p/30035094
3. 统计学习与方法, 李航 P138-P146