学习安排(8月24日-8月26日)
1.主要学习视频Week8
链接(http://www.xuetangx.com/courses/MITx/6_00_2x/2014_T2/courseware/d39541ec36564a88af34d319a2f16bd7/)
2.参考书第21、24章
chapter24 分类方法
最常见的监督式学习应用就是建立分类模型。分类模型也称为分类器,用于对样本进行标注,标明这个样本属于一个有限的类别集合中的哪个类。
在单分类学习中,训练集中的数据仅来自一个类别,目标是学习一个模型以预测某个样本是否属于这个类别。当难以找到不属于这个类别的训练数据时,单分类学习是比较有用的,它通常用于建立异常检测机制,例如在计算机网络中检测未知攻击。
在二分类学习(常称为二元分类)中,训练集中的样本全部来自两个类别(通常称为阳性和阴性) ,目标是找到一个可以区分两个类别的边界。 多分类学习的目标则是找到可以将多个类别区分开来的边界。
本章将介绍两种广泛使用的监督式学习方法来解决分类问题: K最近邻方法和回归方法。介
绍这些方法之前,我们先要解决一个问题:如何评价由这些方法产生的分类器?
分类器评价
对于二分类问题,可将样例根据其真是类别与分类器预测类别的组合划分为真阳性、假阳性、真阴性、假阴性四种情况,可用混淆矩阵表示模型的分类结果。
每种分类器在训练数据上的准确度可以计算如下:
当两个类的大小差不多时,用准确度评价分类器是非常合适的。存在严重的类别不平衡时,用准确度评价分类器会得到非常糟糕的结果。想像一下,如果你负责评价这样一种分类器,它用来预测某个人是否患有某种潜在的致命疾病,这种疾病的发病率大约是0.1%。这时,准确度就不是一个合适的统计量,因为只要简单地宣布所有患者都没有病,就可以得到99.9%的准确度。这种分类器对于那些要为治疗付钱的人来说真是太好了(因为没有人需要治疗!),但对于那些对自己可能患病忧心忡忡的人来说,就太不公平了。
类别不平衡时,可用如下统计量评价分类器:
预测跑步者的性别
我们的任务是通过跑步者的年龄和完成时间来预测跑步者的性别。
函数getBMData从一个文件读出数据并建立一个样本集合。每个样本都是Runner类的一个实例(instance)。每名跑步者都有一个标签(性别)和一个特征向量(年龄和完成时间)。 Runner类中的方法featureDist返回两名跑步者特征向量之间的欧氏距离。下一步就是,将样本划分为一个训练集和一个先保留不用的测试集。最常用的做法是,使用80%的数据训练模型,使用剩余的20%数据对模型进行测试。函数divide80_20可以完成这一任务。请注意,训练数据是随机选取的。
class Runner(object):
def __init__(self, gender, age, time):
self.featureVec = (age, time)
self.label = gender
def featureDist(self, other):
dist = 0.0
for i in range(len(self.featureVec)):
dist += abs(self.featureVec[i] - other.featureVec[i]) ** 2
return dist ** 0.5
def getTime(self):
return self.featureVec[1]
def getAge(self):
return self.featureVec[0]
def getLabel(self):
return self.label
def getFeatures(self):
return self.featureVec
def __str__(self):
return str(self.getAge()) + ', ' + str(self.getTime()) \
+ ', ' + self.label
def buildMarathonExamples(fileName):
data = getBMData(fileName)
examples = []
for i in range(len(data['age'])):
a = Runner(data['gender'][i], data['age'][i],
data['time'][i])
examples.append(a)
return examples
def divide80_20(examples):
sampleIndices = random.sample(range(len(examples)),
len(examples) // 5)
trainingSet, testSet = [], []
for i in range(len(examples)):
if i in sampleIndices:
testSet.append(examples[i])
else:
trainingSet.append(examples[i])
return trainingSet, testSet
现在我们已经做好了准备,可以通过各种不同的方法使用训练集来建立分类器,以预测跑步者的性别。通过对数据的检查,我们知道训练集中有58%的跑步者是男性。所以,如果我们总是猜测跑步者是男性,将会得到58%的准确度。
K最近邻方法
K最近邻方法可能是最简单的分类算法。通过这种方法“学习”的模型就是训练集本身。对新样本进行标注时,就是根据它们与训练集样本的相似度而进行的。
如下实现了一个K最近邻分类器,可以基于跑步者的年龄和完成时间对其性别进行预测。这个实现其实是一种暴力算法。函数 findKNearest 的复杂度与 exampleSet 中的样本数量 成 线 性 关系 , 因 为 它要 计 算example 与 exampleSet 中 每 个元 素 之 间 的特 征 距 离 。函 数 KNearestClassify 使用简单的多数票胜出原则来进行分类,它的复杂度是 O(len(training) * len(testSet)),因为它要对函数 findNearest 进行总共 len(testSet) 次调用。
def findKNearest(example, exampleSet, k):
kNearest, distances = [], []
# 建立一个列表,包含最初7个样本和它们的距离
for i in range(k):
kNearest.append(exampleSet[i])
distances.append(example.featureDist(exampleSet[i]))
maxDist = max(distances) # 找出最大距离
# 检查其余样本
for e in exampleSet[k:]:
dist = example.featureDist(e)
if dist < maxDist:
# 替换距离更远的邻居
maxIndex = distances.index(maxDist)
kNearest[maxIndex] = e
distances[maxIndex] = dist
maxDist = max(distances)
return kNearest, distances
def KNearestClassify(training, testSet, label, k):
"""假设training和testSet是两个样本列表, k是整数
使用K最近邻分类器预测testSet中的每个样本是否具有给定的标签
whether each example in testSet has the given label
返回真阳性、假阳性、真阴性和假阴性的数量"""
truePos, falsePos, trueNeg, falseNeg = 0, 0, 0, 0
for e in testSet:
nearest, distances = findKNearest(e, training, k)
# 进行投票
numMatch = 0
for i in range(len(nearest)):
if nearest[i].getLabel() == label:
numMatch += 1
if numMatch > k // 2: # 具有标签
if e.getLabel() == label:
truePos += 1
else:
falsePos += 1
else: # 不具有标签
if e.getLabel() != label:
trueNeg += 1
else:
falseNeg += 1
return truePos, falsePos, trueNeg, falseNeg
基于回归的分类器
下面使用线性回归,根据训练集数据为男性和女性分别建模。
如下代码使用LogisticRegression类为波士顿马拉松数据建立了一个模型,并进行了测试。实现applyModel时,代码首先使用列表推导式(参见5.3.2节)建立一个列表,列表中的元素是testSet中样本的特征向量。然后,代码调用model.predict_proba方法得到一个数组,数组的元素是一个值对,对应每个特征变量的预测值。最后,代码将预测值与具有该特征向量的样本的标签进行比较,记录并返回真阳性、假阳性、真阴性和假阴性结果的数量。
def applyModel(model, testSet, label, prob = 0.5):
#为所有测试样本创建一个包含特征向量的向量
testFeatureVecs = [e.getFeatures() for e in testSet]
probs = model.predict_proba(testFeatureVecs)
truePos, falsePos, trueNeg, falseNeg = 0, 0, 0, 0
for i in range(len(probs)):
if probs[i][1] > prob:
if testSet[i].getLabel() == label:
truePos += 1
else:
falsePos += 1
else:
if testSet[i].getLabel() != label:
trueNeg += 1
else:
falseNeg += 1
return truePos, falsePos, trueNeg, falseNeg
examples = buildMarathonExamples('bm_results2012.txt')
training, test = divide80_20(examples)
featureVecs, labels = [], []
for e in training:
featureVecs.append([e.getAge(), e.getTime()])
labels.append(e.getLabel())
model = sklearn.linear_model.LogisticRegression().fit(featureVecs, labels)
print('Feature weights for label M:',
'age =', str(round(model.coef_[0][0], 3)) + ',',
'time =', round(model.coef_[0][1], 3))
truePos, falsePos, trueNeg, falseNeg = \
applyModel(model, test, 'M', 0.5)
getStats(truePos, falsePos, trueNeg, falseNeg)
我们可以在applyModel函数中调整概率阈值,使灵敏度与KNN方法的灵敏度近似相等。要找到这个阈值概率,我们可以对prob的值进行遍历,直到得到一个与KNN方法非常接近的灵敏度。
如果使用prob = 0.578——而不是0.5——调用applyModel,会得到如下结果:
准确度=0.659
灵敏度=0.714
特异度=0.586
阳性预测值=0.695
可以看出,这两个模型的性能几乎是一样的。
对于线性回归模型,知道改变决策阈值所带来的影响非常容易。因此,人们通常使用受试者工作特征曲线,或称ROC曲线,来形象地表示灵敏度和特异度之间的折衷关系。这种曲线可以绘制出多个决策阈值的真阳性率(灵敏度)和假阳性率(1 – 特异度)之间的关系。
通过计算曲线下面积,可以在多个ROC曲线之间进行比较。这个面积实际上是个概率,对于一个随机选择的阳性样本,一个好的模型将其标注为阳性的概率应该高于将一个随机选择的阴性样本标注为阳性的概率。这就是人们所说的模型的判别能力。请一定注意,判别能力和准确度是不同的,它也常被称为对概率的校准。例如,我们可以将所有估计出的概率都除以2,这时不会
改变模型的判别能力,但显然改变了模型估计的准确度。
def buildROC(model, testSet, label, title, plot = True):
xVals, yVals = [], []
p = 0.0
while p <= 1.0:
truePos, falsePos, trueNeg, falseNeg =\
applyModel(model, testSet, label, p)
xVals.append(1.0 - specificity(trueNeg, falsePos))
yVals.append(sensitivity(truePos, falseNeg))
p += 0.01
auroc = sklearn.metrics.auc(xVals, yVals, True)
if plot:
pylab.plot(xVals, yVals)
pylab.plot([0,1], [0,1,], '--')
pylab.title(title + ' (AUROC = '+ str(round(auroc, 3)) + ')')
pylab.xlabel('1 - Specificity')
pylab.ylabel('Sensitivity')
return auroc
buildROC(model, test, 'M', 'ROC for Predicting Gender')