K最近邻算法基于实例的学习法,它假定实例可以被表示为欧氏空间中的点,即,可以由欧几里得距离求得两实例之间的距离,以此来确定所谓的K个最近邻(这在后面的算法中有所体现)。
对于每个实例,K最近邻并没有确定一个通用的目标函数好以之求得实例的目标值。而是,对不同的实例,根据其相邻的最近K个实例的不同而产生不同的目标函数。这就是基于实例的学习法。
为了描述方便,以下使用来自《Programing Collective Intelligence》中的葡萄酒例子。说是有葡萄酒,其价格收不同要素的影响,诸如等级,年代etc。我们已知一系列的葡萄酒的相关信息,要对价格建模并预测价格。
构造数据集
所有数据均为随机生成。所构造的数据放置在一个字典中。
这是关于葡萄酒价格的一个字典。所构造的数据集如下:
[{'input': (98.29602476812006, 44.01823179710145), 'result': 245.6692612796455}, {'input': (80.33840526889543, 41.81735415220468), 'result': 0.0}]
这里的问题是,我们知道这么些关于葡萄酒年代,等级及其价格的数据,我们要预测其余的普通酒的价格。
我们通过寻找与当前所关注的商品情况相似的一组商品,对这些商品的价格求平均,进而进行价格预测。这就是:K-最近邻算法。
注意,从这里可以看出:
K-最近邻算法与其他机器学习方法有个显著的不同:K-最近邻算法算法对训练数据只是存储起来而不进行诸如对其训练以产生一个目标函数之类的。。当遇到新的查询实例时,一系列相似的实例被从存储器中取出,并用来分类新的查询实例。
嗯,从此也可以看出,K-最近邻算法的所有计算几乎都发生在分类实例时,因此,分类实例的计算开销会很大。。。(这是第一个不足)
听起来是很简单很直接的算法。
不过有个问题是:怎么取合适的K值?如果太小,那么价格通常是过于特殊的而与实际不符;如果太多则偏移太大。具体的解决方案参考本博中部
话题回来,为了计算“K-邻近”,我们首先要计算出这些个邻近的究竟是哪些条目(我们可以先假设一个k值,比如k==5)。
于是我们要计算不同样例之间的相似度。可以用欧几里得算法来计算。(实际上,K-最近邻算法假定所有实例均位于n维欧式空间内。这也即是K-最近邻算法的归纳偏置:。它的归纳偏置对应于假定:一个实例的分类x最相似于在欧氏空间中它附近的实例的分类。)
定义相似度:使用欧几里得距离
比如,我们这里的关于葡萄酒的价格即是2维欧式空间中的一个点。
这里要倒叙一下:我们的数据是个字典,其中有input和result两项,input是在该葡萄酒案例中决定葡萄酒价格的两个因素(相当于两个点)而result是价格。因此,我们判断的距离即是这些因素之间的距离。
看得出,在欧几里得算法中,所有因素的权值都是1,即,他们对结果的影响都是相同的。但实际上,这各个因素的权值未必就是相同的,比如,可能等级较之年代,对价格的影响更甚。
这是KNN算法的又一缺陷。
KNN
KNN算法较为简单,可以是:针对当前实例,确定其值为K个样例中最普遍的训练样例中的值;也可以是,K个样例的平均值。
这里采用求平均值的方法。(PS,记得前面说过,计算量是相当可观的。)
为取得这K个值,算法进行了一个sort操作。不知效率如何。。。
紧邻权重
对k-近邻算法的一个显而易见的改进是对k个近邻的贡献加权,根据它们相对查询点x q 的距离,将较大的权值赋给较近的近邻。
在此例中,不同距离的邻居对最终价格的影响程度理应是不同的。于是我们要为不同距离的邻居赋予不同的权值。他们之间的距离可以很方便的算出,于是本质上,我们只需找到一种方法将距离上的差距转化为权重就可以了。
方法有很多。
反函数
num是计算倒数的分子。应该注意的是const,const是为了避免算法为很紧邻或相似项赋予过大的权重。(因为距离很近,则dist会很小,其倒数必然很大甚至是无穷大。。。)
注意inverseweight中,可能存在两个实例完全相同的情况,那么此时的dist可能为0,但由于我们的分母是:dist+const,因此不会引发异常。
反函数的问题在于:反函数倾向于为很紧邻赋予很大的权重而为稍远的赋予较小的权重,且,权重的变化很大。如此,可能算法对噪声会变得过于敏感。
减法函数
该算法虽然克服了反函数的紧邻权重过大的缺陷,但因为权重最终会跌至0,如距离足够大而const足够小,那么可能关于权重的计算没有任何意义(因为权重总为0),或根本找不到合适的权重。
高斯函数有效的克服了上述不足。
说明:该函数来自《集体智慧编程》一书。根据查到的高斯函数资料,个人觉得这算法貌似有问题。。。
加权KNN
算法求的是加权平均。
我们应该要注意,如果在weightedKNN和knnestimate中,算出某一个dist为0,那么我们应该直接让程序返回该项的值。(这表明训练数据中有一项和该实例是相等的,那么价格也理应相同。)
测试一下:
结果是:
31.2838056203
30.1185287381
以上k-近邻算法的所有变体都只考虑k个近邻以分类查询点。如果使用按距离加权,那么允许所有的训练样例影响x q 的分类事实上没有坏处,因为非常远的实例对结构的影响很小。
交叉验证
那,我并不知道这样的结果哪一个更正确,甚至我不知道是否算法weightedKNN所得到的就是更精确的结果。我们使用交叉验证来做测试(交叉验证会在以后的算法中大量使用)。
首先将数据进行拆分,为训练数据和测试数据。
(该代码参考自《集体智慧编程》一书,个人觉得不太多。按作者原意,是要将数据拆为5%的测试数据和95%的训练数据,但这里貌似是在用一个概率值判断是否进行拆分,这样应该无法保证合理的拆分吧。。。也许这里的原意就是:按照某个概率来拆分数据而不是将数据拆分为百分之几的百分之几。。。)
整个交叉验证过程如下:
我们的测试样例(我们要测试的是算法crossvalidate的k值,即上文提到的“什么样的K才是最合适的?”):
注意算法的流程、在crossvalidate中,我们进行trials次的测试。每次会从已知的样例集中划分训练集合测试集(注意,这里无论是训练集还是测试集本质上都是样例集,都是已知数据,我们所进行的测试是:将一大部分样例作为训练集来训练学习器,而将另外的一部分作为测试数据来测试学习器,对学习器给出的答案与这些数据中存在的答案(因为这些测试数据也是已知的样例集)进行比较从而得到一个相关的比较结果,这点很重要),并累加testalgorithm返回值,最后return一个关于trials的平均值。
在testalgorithm中,我们对testset中的每一条数据(该数据包括input和result两部分)用K-最近邻算法(K是我们本次摇测试的一个K值,比如3),求得其result(实际该result是存在的),我们再与实际的result比较(这样就能知道此处算法是否正确,误差是多少?),并将二者的误差放大并进行累加,这样,总体的效果就是:crossvalidate的返回值越大,则误差越多,K算法的正确率越低,本次测试项目的表现越差。
比如,关于K值的测试中,我的测试结果是:
这说明,当K取值为3左右时,所得的效果最佳。注意,由于我们的训练集合测试集的划分是基于概率的,因此,多进行几次的测试是必要的。
这下可以解答上面提出的关于knnestimate和weightedKNN的比较了。
结果是:
可以看出:weightedKNN确实好过knnestimate。(但也没有想象中的好。。。)
补:
从这里的测试中,学到:
1.Python中定义的函数均有一内建的__name__属性,但lambda没有,lambda的原意即是:“匿名函数”,因此lambda函数的__name__ == 'lambda'
2.关于字典取值
字典中,可以通过键获取值,但不能通过值获取键。因此,这里的kdic不能定义做kdic = { 'KNN':KNN, 'WKNN':WKNN },因为后面可能发生以值取键的错误
3.字典是{},列表是[],元组是(),集合是{}
另,WingIDE写代码确实爽,不过总觉得IDE占的屏幕太大,导致代码可视区小,这可能是所有IDE的通病了。而且总不习惯在WingIDE里调试,还是喜欢用最简单的IDLE来调试。。。