前面一共介绍了五种分类算法:KNN,决策树,朴素贝叶斯,逻辑斯蒂回归和SVM,它们各有优缺点。我们自然可以将不同的分类器组合起来,而这种组合结果被称为集成方法或者元算法。
今天主要练习的内容是一个被称为AdaBoost的最流行的元算法。
其优缺点如下图所示:
在构造Adaboost的代码时,我们首先通过一个简单的数据集来确保在算法实现上一切就绪。
1.构造一个简单的数据集
def loadSimpData():
datMat = np.matrix([[ 1. , 2.1],
[ 2. , 1.1],
[ 1.3, 1. ],
[ 1. , 1. ],
[ 2. , 1. ]])
classLabels = [1.0, 1.0, -1.0, -1.0, 1.0]
return datMat,classLabels
2.单层决策树生成函数
首先先生成一个辅助函数,用于实现支持主体函数遍历所有特征值的所有分界点的所有可能性。
def stumpClassify(dataMatrix, dimen, threshVal, threshIneq): # 该函数用于遍历所有特征值的所有分界点的所有可能性
retArray = np.ones((np.shape(dataMatrix)[0], 1)) # 矩阵大小为:数据个数*1
if threshIneq == 'lt':
retArray[dataMatrix[:, dimen] <= threshVal] = -1.0 # 左负右正,左侧是w2
else:
retArray[dataMatrix[:, dimen] > threshVal] = -1.0 # 左正右负,右侧是w2
# 没错,因为只有一个分界点的时候,也是有两种可能的,左正右负或者左负右正
return retArray
接着基于以上函数,实现整个单层决策树的建立:
def buildStump(dataArr, classLabels, D): # 建立单层决策树
dataMatrix = np.array(dataArr) # m * n 阶矩阵
labelMat = np.array(classLabels) # m 阶一维矩阵矩阵
m, n = np.shape(dataMatrix) # m为样本数,n为每个样本的特征数
numSteps = 10.0 # 每个特征值取10个来看哪个最适合做分界点
bestStump = {}
bestClasEst = np.array(np.zeros((m, 1)))
minError = math.inf # inf代表正无穷
for i in range(n): # 遍历所有特征
rangeMin = dataMatrix[:, i].min() # 第i个特征的最小值
rangeMax = dataMatrix[:, i].max() # 第i个特征的最大值
stepSize = (rangeMax - rangeMin) / numSteps # 步长
for j in range(-1, int(numSteps) + 1): # 遍历所有可能的特征值分界点
for inequal in ['lt', 'gt']:
threshVal = (rangeMin + float(j) * stepSize) # 所有可能的分界点
predictedVals = stumpClassify(dataMatrix, i, threshVal,
inequal) # 预测值
errArr = np.array(np.ones((m, 1))) # 标记预测是否错误
errArr[predictedVals == labelMat.reshape([m,1])] = 0 # 正确分类的数据置0
weightedError = np.dot(np.array(D).reshape([1,m]), errArr) # D为各数据的权重,即该次分类中每个数据的重要性
if weightedError < minError:
minError = weightedError
bestClasEst = predictedVals.copy() # 预测值
bestStump['dim'] = i
bestStump['thresh'] = threshVal
bestStump['ineq'] = inequal
return bestStump, minError, bestClasEst
注意:因为原书为python2的代码,故在python3中有些需要修改的地方,其中最重要的就是关于矩阵的运算。
①. 在预测值与真实值比较的时候,一定要转换为同类型的矩阵,即将labelMat
这个一维矩阵也转换为二维的 m*1
型矩阵,同类型的矩阵才能进行比较是否相等的运算:
errArr[predictedVals == labelMat.reshape([m,1])] = 0
②. 在算加权误差时,要对矩阵作乘法运算,也需要将一维矩阵D
转换为二维矩阵,并对齐其内侧维度:np.array(D).reshape([1,m])
。
③. math中的math.inf
表示正无穷。
④. 学习心得:矩阵运算中尽量避免使用*
,因为*
在mat
和array
生成的矩阵中表示不同的运算,尽量使用dot
和multiply
函数使表达更清晰。
接下来我们运行一小段代码来测试一下程序的效果
datMat,classLabels = loadSimpData()
bestStump, minError, bestClasEst = buildStump(datMat, classLabels, [.2, .2, .2, .2, .2])
print('bestStump = ', bestStump)
print('minError = ', minError)
print('bestClasEst = ')
print(bestClasEst)
运行结果为:
bestStump = {'dim': 0, 'thresh': 1.3, 'ineq': 'lt'}
minError = [[ 0.2]]
bestClasEst =
[[-1.]
[ 1.]
[-1.]
[-1.]
[ 1.]]
可见以上程序可以得到某一权重下实现最佳分类的简单的单层决策树分类器。
3.完整Adaboost算法的实现
第二步中我们构建了一个能进行简单分类的决策器。现在,我们拥有了一个实现Adaboost算法所需要的所有信息,因此我们来实现Adaboost算法。
首先我们根据其伪代码大致了解一下Adaboost算法的思路:
其具体的训练过程如下所示:
def buildStump(dataArr, classLabels, D): # 建立单层决策树
dataMatrix = np.array(dataArr) # m * n 阶矩阵
labelMat = np.array(classLabels) # m 阶一维矩阵矩阵
m, n = np.shape(dataMatrix) # m为样本数,n为每个样本的特征数
numSteps = 10.0 # 每个特征值取10个来看哪个最适合做分界点
bestStump = {}
bestClasEst = np.array(np.zeros((m, 1)))
minError = math.inf # inf代表正无穷
for i in range(n): # 遍历所有特征
rangeMin = dataMatrix[:, i].min() # 第i个特征的最小值
rangeMax = dataMatrix[:, i].max() # 第i个特征的最大值
stepSize = (rangeMax - rangeMin) / numSteps # 步长
for j in range(-1, int(numSteps) + 1): # 遍历所有可能的特征值分界点
for inequal in ['lt', 'gt']:
threshVal = (rangeMin + float(j) * stepSize) # 所有可能的分界点
predictedVals = stumpClassify(dataMatrix, i, threshVal,
inequal) # 预测值
errArr = np.array(np.ones((m, 1))) # 标记预测是否错误
errArr[predictedVals == labelMat.reshape([m,1])] = 0 # 正确分类的数据置0
weightedError = np.dot(np.array(D).reshape([1,m]), errArr) # D为各数据的权重,即该次分类的每个数据的重要性
if weightedError < minError:
minError = weightedError
bestClasEst = predictedVals.copy() # 预测值
bestStump['dim'] = i
bestStump['thresh'] = threshVal
bestStump['ineq'] = inequal
return bestStump, minError, bestClasEst
与上面一样,运行的时候小心矩阵的维度不一致导致不能运算的错误。
运行下面代码测试一下其结果:
datMat,classLabels = loadSimpData()
weakClassArr,aggClassEst = adaBoostTrainDS(datMat,classLabels,numIt=9)
print(weakClassArr)
print(aggClassEst)
运行结果如下:
[[ 1.]
[ 1.]
[-1.]
[-1.]
[ 1.]]
total error: 0.2
[[ 1.]
[ 1.]
[-1.]
[-1.]
[ 1.]]
total error: 0.2
[[ 1.]
[ 1.]
[-1.]
[-1.]
[ 1.]]
total error: 0.0
[{'dim': 0, 'thresh': 1.3, 'ineq': 'lt', 'alpha': 0.6931471805599453}, {'dim': 1, 'thresh': 1.0, 'ineq': 'lt', 'alpha': 0.9729550745276565}, {'dim': 0, 'thresh': 0.90000000000000002, 'ineq': 'lt', 'alpha': 0.8958797346140273}]
[[ 1.17568763]
[ 2.56198199]
[-0.77022252]
[-0.77022252]
[ 0.61607184]]
可见经过三次迭代后,分类正确率达到了百分百,然后依次输出了我们最后得到的强分类器的形成(由三个弱分类器组成)和强分类器中m个数据对分类的贡献(权重)。
1.加载数据
采用如下代码实现:
def loadDataSet(fileName): # general function to parse tab -delimited floats
fr = open(fileName)
numFeat = len(fr.readline().split('\t')) # 每行多少字段(特征数+1)
dataMat = []
labelMat = []
for line in fr.readlines():
lineArr = []
curLine = line.strip().split('\t') # strip函数是删除开头和结尾的字符(默认是空字符)
for i in range(numFeat - 1): # 将每个特征存入列表lineArr中
lineArr.append(float(curLine[i]))
dataMat.append(lineArr) # 存储一组数据的特征
labelMat.append(float(curLine[-1])) # 存储数据对应的类别标签
return dataMat, labelMat
注意:其中strip()
函数的作用是删除字符串开头或者结尾的特定字符,例如:
str = '132shiashia123sai321'
print(str.strip('123'))
其运行结果是:
shiashia123sai
可见strip()
函数将开头和结尾处的字符1、2、3都删除掉了,但是中间的却未删除。
2.建立测试分类器
def adaClassify(datToClass,classifierArr):
dataMatrix = np.array(datToClass)
m = np.shape(dataMatrix)[0] # 数据的个数
aggClassEst = np.array(np.zeros((m,1)))
for i in range(len(classifierArr)): # 遍历所有待分类的数据
classEst = stumpClassify(dataMatrix,classifierArr[i]['dim'],
classifierArr[i]['thresh'],
classifierArr[i]['ineq'])
aggClassEst += classifierArr[i]['alpha']*classEst
return np.sign(aggClassEst)
与训练器类似,不再赘述。
3.调用我们第一板块构建的Adaboost代码,输入以下指令:
datMat, classLabels = loadDataSet('E:\学习资料\机器学习算法刻意练习\机器学习实战书电子版\machinelearninginaction\Ch07\horseColicTraining2.txt')
classifierArray = adaBoostTrainDS(datMat, classLabels,10)
print(classifierArray)
运行结果如下:
the 1 times' total error: 0.28523489932885904
the 2 times' total error: 0.28523489932885904
the 3 times' total error: 0.2483221476510067
the 4 times' total error: 0.2483221476510067
the 5 times' total error: 0.2483221476510067
the 6 times' total error: 0.24161073825503357
the 7 times' total error: 0.24161073825503357
the 8 times' total error: 0.2214765100671141
the 9 times' total error: 0.2483221476510067
the 10 times' total error: 0.2214765100671141
([{'dim': 9, 'thresh': 3.0, 'ineq': 'gt', 'alpha': 0.4593204546095544}, {'dim': 17, 'thresh': 52.5, 'ineq': 'gt', 'alpha': 0.31654488263333286}, {'dim': 3, 'thresh': 55.199999999999996, 'ineq': 'gt', 'alpha': 0.28402835050611847}, {'dim': 18, 'thresh': 62.300000000000004, 'ineq': 'lt', 'alpha': 0.23222873860913737}, {'dim': 10, 'thresh': 0.0, 'ineq': 'lt', 'alpha': 0.19836267426245105}, {'dim': 5, 'thresh': 2.0, 'ineq': 'gt', 'alpha': 0.18642416210017293}, {'dim': 12, 'thresh': 1.2, 'ineq': 'lt', 'alpha': 0.1496988869138094}, {'dim': 7, 'thresh': 1.2, 'ineq': 'gt', 'alpha': 0.15848275395378547}, {'dim': 5, 'thresh': 0.0, 'ineq': 'lt', 'alpha': 0.1370746524177519}, {'dim': 0, 'thresh': 1.0, 'ineq': 'lt', 'alpha': 0.12365372615766472}], array([[ 0.53994254],
[ 1.16499852],
[-0.02321395],
[-0.7070226 ],
[ 1.4643963 ],
[ 1.23465658],
[ 0.59314933],
[-0.39514964],
[ 0.29466419],
[ 0.27618382],
[ 0.59694182],
[ 1.53405435],
[ 0.8963396 ],
[ 1.0576068 ],
[-0.64245709],
[-1.27554685],
[ 0.81108938],
[ 0.34963437],
[-0.40899627],
...
[-0.39514964],
[-1.27554685],
[ 0.27618382],
[ 0.56378193]]))
由此可见,随着弱分类器个数的增加,Adaboost算法得到的分类器的分类能力越来越强。
准确率、召回率,精确率以及ROC曲线等分类器的性能度量指标已经在看统计学习方法和西瓜书的过程中多次学习,此处便不再耗费时间写此部分的笔记。