2.1 Iris 数据集
Iris数据集(Iris dateset, 也称鸢尾花卉数据集)是源自20世纪30年代的经典数据集。它是最早应用统计分类的现代示例之一。
数据中包含有不同种类的Iris花朵的数据,这些种类可以通过它们的形态来识别。时至今日,我们已经可以通过基因签名(genomic signature)来识别这些分类。但在20世纪30年代,人们还不确定DNA是不是基因信息的载体。
测量的是每个花朵的以下四个属性:
花萼长度;
花萼宽度;
花瓣长度;
花瓣宽度。
一般来说,我们把数据中所有的测量结果都叫做特征 。
此外,每个花朵所属的种类都已经标出。现在的问题是:如果看到这种植物的一个新花朵,我们能否通过它的四个特征来预测出它的种类?
这就是监督学习 或分类 问题;对于给定的带有类别标签的样本,我们要设计出一种规则,然后通过这种规则,最终实现对其他样本的预测。这跟垃圾邮件分类问题是一样的;根据用户标出的垃圾邮件和非垃圾邮件样本,我们能否判定一个新来的信息是否是垃圾邮件?
在这个时候,Iris数据集可以很好地满足我们的需求。它的规模很小(只包含150个样本,每个样本有4个特征),很容易可视化地显示出来并进行处理。
2.1.1 第一步是可视化
由于这个数据集的规模很小,我们很容易把所有的数据点,以及它们在二维空间中的映射画在一张纸上。我们由此可以形成一个直观认识,然后将其拓展到维度更高、数量更大的数据上。下图中的每一个子图都画出了所有数据点在其中两个维度上的映射。外面那一组点(三角形)代表的是山鸢尾花(Iris Setosa),中间的(圆圈)代表的是变色鸢尾花(Iris Versicolor),用x标记的是维吉尼亚鸢尾花(Iris Virginica)。我们可以看到,数据分成两大组:一组是山鸢尾花(Iris Setosa),另一组是变色鸢尾花(Iris Versicolor)和维吉尼亚鸢尾花(Iris Virginica)的混合。
我们使用Matplotlib这个最著名的Python绘图包来绘制图形。这里展示了生成左上子图的代码。而生成其他子图的代码与此类似:
from matplotlib import pyplot as plt from sklearn.datasets import load_iris import numpy as np # 我们用sklearn中的load_iris读取数据 data = load_iris() features = data['data'] feature_names = data['feature_names'] target = data['target'] for t,marker,c in zip(xrange(3),">ox","rgb"): # 我们画出每个类别,它们各自采用不同的颜色标识 plt.scatter(features[target == t,0], features[target == t,1], marker=marker, c=c)
2.1.2 构建第一个分类模型
如果我们的目标是区分这三种花朵的类型,那么根据已经绘制出的图形,答案显而易见。例如,根据花瓣长度似乎就可以将山鸢尾花(Iris Setosa)跟其他两类花朵区分开。我们写一点代码来寻找切分点在哪里,如下所示:
plength = features[:, 2] # 用numpy操作来获取setosa的特征 is_setosa = (labels == 'setosa') # 这是重要的一步 max_setosa =plength[is_setosa].max() min_non_setosa = plength[~is_setosa].min() print('Maximum of setosa: {0}.'.format(max_setosa)) print('Minimum of others: {0}.'.format(min_non_setosa))
它打印输出了1.9 和3.0 。因此,我们可以构造一个简单的模型:如果花瓣长度小于2,那么它是山鸢尾花(Iris Setosa);否则,它不是维吉尼亚鸢尾花(Iris Virginica)就是变色鸢尾花(Iris Versicolor) 。
if features[:,2] < 2: print 'Iris Setosa' else: print 'Iris Virginica or Iris Versicolour'
这是我们的第一个模型。通过它可以将山鸢尾花跟其他两类花区分开,而且效果非常好,不会出现任何错误。
目前这个模型只是一个简单结构;它是某个维度上的一个简单阈值。我们还可以通过一些计算,可视化地寻找最佳的阈值;当我们编写代码实现它的时候,机器学习就派上用场了。
将山鸢尾花(Iris Setosa)跟其他两类花区分开这个例子是很简单的。然而,我们却无法立即找到区分维吉尼亚鸢尾花(Iris Virginica)和变色鸢尾花(Iris Versicolor)的最佳阈值。我们甚至会发现永远都不可能找到完美的划分。不过,我们还是可以尝试一下最有可能成功的方式。对此,需要进行一点计算。
首先,只选择非山鸢尾花(Iris Setosa)的特征和标签:
features = features[~is_setosa] labels = labels[~is_setosa] virginica = (labels == 'virginica')
在这里,我们经常会用到NumPy对数组进行一些操作。is_setosa 是一个布尔型的数组,我们用它从另外两个数组(features 和labels )中选取出一个子集来。最后,我们对标签进行比较看它们是否相等,并构建出一个新的布尔型数组virginica 。
现在我们对所有可能的特征和阈值进行遍历,去寻找更高的正确率。正确率 就是模型正确分类的那部分样本所占的比例:
best_acc = -1.0 for fi in xrange(features.shape[1]): # 我们将要针对这个特征生成所有可能的阈值 thresh = features[:,fi].copy() thresh.sort() # 现在测试所有阈值 for t in thresh: pred = (features[:,fi] > t) acc = (pred == virginica).mean() if acc > best_acc: best_acc = acc best_fi = fi best_t = t
代码最后几行选出最佳模型。首先,我们要比较预测结果(pred )和真实标签(virginica )。这里有一个小技巧,那就是通过计算比较次数的平均值,可以得到正确结果所占的比例,也就是正确率。在for 循环的最后,所有特征上的所有可能阈值都测试过了,best_fi 和best_t 变量就代表了我们选出的模型。要将它应用到新的样本上,我们需要进行如下操作:
if example[best_fi] > t: print 'virginica' else: print 'versicolor'
这个模型生成后是什么样子?如果在全部数据上运行,我们得到的最佳模型就是一个在花瓣长度上的划分。我们可以把判别边界画出来。在下图中,我们可以看到两个区域:一个是白色的,另一个被灰色阴影覆盖。白色区域中的任何点代表的都是维吉尼亚鸢尾花(Iris Virginica),阴影那边的任何点代表的都是变色鸢尾花(Iris Versicolor)。
在阈值模型中,判别边界常常是一条与坐标轴平行的直线。上图中包含了判别边界和两个区域,这些数据点不是在白色区域就是在灰色区域中。同时,上图也显示出(见虚线)另一个阈值恰好也可以得到同样的正确率。我们的模型选择了第一个阈值,但那是随便选的。
评估:留存数据和交叉验证
我们在前一节中讨论了一个简单模型;它在训练集上达到了94%的正确率。然而,这个评估也许过于乐观了。因为我们用这些数据去确定阈值,然后又用同样的数据评估了这个模型。该模型的效果当然比其他所有我们在这个数据集上尝试过的模型都好。这在逻辑上犯了循环论证的错误。
我们真正想做的事情是衡量模型对新样本的泛化能力。所以,这里应该用训练中未出现过的数据来评估模型的性能。因此,我们将要进行一个更严格的评估,并且使用留存数据。对此,我们把数据分成两部分,一部分用于训练模型,一部分(从训练集拿出来的数据)用于测试模型效果。输出如下所示:
Training error was 96.0%. Testing error was 90.0% (N = 50).
测试集上的正确率低于训练集上的正确率。这可能会让一个没有经验的机器学习初学者感到惊讶,但这是符合预期的,而且是一个典型情况。要了解原因,可以回过头来看一下画出的判别边界。仔细观察一下,一些离边界很近的样本是否不在那里了,或者两条线中间的点是否消失了。我们很容易想象,这时的边界将会向右或向左移动一点,从而导致这些点被放在“错误”的一边。
注意 训练数据上的误差叫做训练误差 ,它对算法效果的估计常常过于乐观。我们总是应该测量和报告测试误差 ,也就是在未用于训练的样本集合上的误差。
随着模型越来越复杂,这些概念也变得越来越重要。在这个示例中,这两种误差的差距并不是很大。但当使用一个复杂的模型时,训练集上的正确率很可能达到100%,但在测试集上的效果却跟随机猜测差不多。
我们之前采用了从训练集中留存数据的方式,这里有一个潜在的问题,即在训练中只使用了部分数据(在这个例子中,我们使用了一半的数据)。另一方面,如果我们在测试中使用了过少的数据,误差估计将只在很少的一部分样本上进行。在理想情况下,我们希望在训练和测试中都能使用所有的数据。
我们可以通过交叉验证 (cross-validation)达到类似的效果。交叉验证的一个极端(但有时很有用)形式叫做去一法(leave-one-out)。从训练集中拿出一个样本,并在缺少这个样本的数据上训练一个模型,然后看模型是否能对这个样本正确分类:
error = 0.0 for ei in range(len(features)): # 选择除了ei以外的所有位置: training = np.ones(len(features), bool) training[ei] = False testing = ~training model = learn_model(features[training], virginica[training]) predictions = apply_model(features[testing], virginica[testing], model) error += np.sum(predictions != virginica[testing])
至这一循环的末尾,我们在所有样本上对一系列模型进行了测试。然而,这里并没有循环影响的问题,这是因为测试每个样本的模型,在构建的时候并没有把这个样本考虑进去。因此,这个综合评估就是对模型泛化能力的一个可靠估计。
交叉验证去一法最主要的问题是,我们必须进行100次或者更多的训练。事实上,针对每个样本,我们都要去学习一个全新的模型。工作量会随着数据集变大而增加。
我们可以通过x 折交叉验证以部分代价获得去一法的大部分收益。这里x代表一个小数字,例如5。为了进行5折交叉验证,我们会把数据分成5份,这就是5折的由来。
然后我们训练了5个模型,每次训练分别把其中一份数据拿出去。实现代码跟本节前面给出的类似,但这里我们是把20%的数据拿出去,而不仅仅是1个元素。我们在留存数据上测试这些模型的效果,并对结果取平均值:
上图阐释了这个数据5分块的过程(数据集被分成了5块)。对于每一折,你将其中一块保留下来用于测试,而在其余4块上进行训练。其实你想用多少折都可以。5或10折是比较常见的;也就是在训练中使用80%或者90%的数据,这样得出的结果与使用所有数据的效果比较接近。在极端情况下,如果你用的折数跟数据个数一样多,那么简单地进行去一交叉验证即可。
在生成数据折的时候,你需要谨慎地保持数据分布的平衡。例如,如果某一折中所有的样本都属于同一类,那在这个数据集上得到结果就不具有代表性。我们并不想深入介绍如何来做这个事情,因为机器学习工具包可处理好此事。
我们现在已经生成了几个模型,而非仅仅一个。那最终模型 是什么呢?如何将它用在新数据上呢?最简单的方法就是在所有训练数据上使用一个综合模型。交叉验证循环会帮你评价一个模型的泛化能力。
注意 交叉验证计划允许你使用所有的数据去衡量你的方法效果如何。在交叉验证循环的末尾,你可以用所有数据来训练一个最终模型。
尽管这在机器学习发展之初并没有被适当地指出,而在今天,甚至讨论分类系统的训练误差都会被看做一个非常不好的迹象,因为它的结果非常具有误导性。我们希望使用留存数据上的误差,或者用交叉验证计划衡量出的误差来进行效果评估和比较。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论