K-最近邻算法的一大缺陷是:维度危机(curse of dimensionality)。当有大量不相干的属性存在时,近邻间的距离会被大量的不相关属性所支配。
比如,在数据集方面。在《K-最近相邻(1)》中的数据集是单一的,值域范围相同的,且对结果有着同样影响力的;但可能实际的数据集比之更为复杂。
例如:可能存在值域范围不同的变量(例如,酒瓶的大小);或者完全不相干的变量(如,酒瓶的颜色etc。)在实际的学习器训练中,我们不应该对这些因素考虑(针对不相干的变量),或者因对之进行合理的处理(如,值域范围不同的变量)。
我们来构造一个全新的数据集
#---------------------------------------------------------------------- def wineset_x(wine_num): """build more complicated price set""" rows = [] for i in range(wine_num): rating = random() * 50 + 50 age = random() * 50 aisle = float(randint(1,20)) bottlesize = [375.0, 750.0, 1500.0, 3000.0][randint(0, 3)] price = winprice(rating, age) * (bottlesize/750) price *= random()*0.9 + 0.2 rows.append({'input':(rating,age,aisle,bottlesize), 'result':price})
这段代码构造了一乌七八糟什么都有的数据集。
如果使用我们前面的算法,会将这所有的数据,有用的无用的值域相同的值域不同的,同等对待,那么结果可能是这样的(幸亏我们可以交叉测试):
data = wineset(200) data_x = wineset_x(200) WKNN = lambda d,v: weightedKNN(d, v, k=3) for i in range(10): print( crossvalidate(WKNN, data) ) print( crossvalidate(WKNN, data_x) ) print()
594.899621942 8467.01121529 569.269136527 8691.51894409 677.876073646 9570.0476954 529.699699245 8667.28441348 589.597977734 9084.18326893 709.92912033 10442.1349148 547.270179483 11436.0060748 561.079841745 9498.04898803 601.720659638 8854.67502583 613.654765319 10449.3282605
嗯,这个结果,不太好。。。
我们应该对不同变量(要素)区别对待。
解决维数危机的方法可以有:
1.当计算两个实例间的距离时对每个属性加权。
2.从实例空间中完全消除最不相关的属性。
按比例缩放(这里顺带进行了不相干属性的消除操作)
对数值进行归一化处理,从而使所有变量都位于相同的值域之内。对每一维度上的数值乘以该维度上的一个常量。
如果要忽略某些值,则将这些值乘以0,这样就自然地忽略了。(这其实也是在按比例缩放,只不过缩放的比例是0。。。)
#---------------------------------------------------------------------- # 按比例缩放 def rescale(data, scale): """scaling dimensions""" scaleddata = [] for row in data: scaled = [ scale[i]*row['input'][i] for i in range(len(scale)) ] scaleddata.append( {'input':scaled, 'result':row['result']} ) return scaleddata
我们来测试一下:
data = wineset(200) data_x = wineset_x(200) sdata = rescale(data_x, [1,1,0,0.01]) # print(data[0]) print(data_x[0]) print(sdata[0]) # data_x = rescale(data_x, [1,1,0,0.05]) # print(data_x[0]) WKNN = lambda d,v: weightedKNN(d, v, k=3) KNN = lambda d,v: knnestimate(d, v, k=3) # for i in range(10): # print( crossvalidate(WKNN, data) ) # print( crossvalidate(WKNN, data_x) ) # print() print( crossvalidate(WKNN, data_x) ) print( crossvalidate(WKNN, sdata) )
结果大约如下:
缩放前: 6634.38530258 缩放后: 7280.93222132 缩放前: 8528.38773768 缩放后: 5451.28842881 缩放前: 8096.74622633 缩放后: 6710.65597041 缩放前: 5637.89616674 缩放后: 7714.30667587 缩放前: 7662.63614458 缩放后: 6570.56199408 缩放前: 7531.53444331 缩放后: 7668.44107909 缩放前: 6126.22586715 缩放后: 5640.87114486 缩放前: 7319.97627061 缩放后: 5054.53137966 缩放前: 6793.92133569 缩放后: 6724.26829326 缩放前: 5469.07057273 缩放后: 6977.01182822
可以看出,缩放后的效果总体上是好于所缩放前的。
(PS1:如何进行缩放是关键。这里所使用的缩放方法是乘以一个因子,我们的例子中使用的是:sdata = rescale(data_x, [1,1,0,0.01]),乘以1表示不缩放,乘以0,表示忽略该项。)
(PS2:其实这里的比较并没有多大的意义;因为算法是随机产生一个随机数来进行数据集的划分的,两次随机产生的数据集并不相同;但鉴于数据集被缩放过的,所以无论如何划分数据集,所进行的总是:未缩放的数据与已缩放的数据之间的比较,所以还是具有一定的意义的)
(PS3:书《集体智慧编程》上给出的例子中缩放前喝缩放后的比较那叫一个明显啊。。。)
缩放参数确定的自动化进行
以上的数据是我们自己生成的,但如果我们事先并不知道这些数据(例如特征etc。)那么我们该选择什么缩放参数呢?
嗯,我们的问题是:如何从一系列的答案中得到比较好的那个,那么很显然,应使用优化算法来自动确定优秀的缩放参数。
我们要做的只是写一个控制函数而已。
#---------------------------------------------------------------------- # 利用优化算法进行成本函数的封装 def createcostfuncton(algf, data): """optimizing the scaling""" constfunc = lambda scale: crossvalidate(algf, rescale(data, scale), trials=10 ) return constfunc weightdomain = [(0,20)]*4
貌似如果使用lambda来定义该函数的话会产生错误的结果。。。
另,注意weightdomain 的定义。(这个在优化算法中已有大量讲到)
我们来调用一下:
data_x = wineset_x(200) WKNN = lambda d,v: weightedKNN(d, v, k=3) costfunc = createcostfuncton(WKNN, data_x) print('Here We Go...') print( optimization.geneticoptimize_x(weightdomain, costfunc) )
geneticoptimize_x是以爬山法产生初始种群的遗传算法。geneticoptimize_x的运行效率显然是相当的可观。。。
运行结果大致如下:
[15, 8, 0, 2] 缩放前: 6993.97196586 缩放后: 5822.74897608 缩放前: 6552.64415819 缩放后: 5241.33544969 缩放前: 6901.51683037 缩放后: 5998.852073 缩放前: 6843.81841964 缩放后: 7406.39902386 缩放前: 7399.77968979 缩放后: 6553.51283337 缩放前: 6564.00568371 缩放后: 5502.11495222 缩放前: 6181.57919027 缩放后: 6413.02360567 缩放前: 6661.19941695 缩放后: 5494.88938746 缩放前: 6197.48800701 缩放后: 6482.08435538 缩放前: 6646.44901676 缩放后: 7706.1129552
(说大致是因为:我们定义的成本函数,从本质上讲,是个基于概率的函数,且在其中,概率发生的作用相当巨大,因此,在每次调用时,该成本函数对同样的序列并不能给出同样的结果,因此,geneticoptimize_x的种群的最优异者是每次都变化的。从本质上将,在geneticoptimize_x中,迭代N次与不迭代没有显著的区别(因为迭代的结果并不能保持到下次迭代中去,即是,优异者在种群的下次进化中死掉了。。。)。。。):
通过测试,该序列[15, 8, 0, 2]在geneticoptimize_x产生的成本结果是:
scores = (2647.7657340720734, [15, 8, 0, 2])
这在以后调同样的函数后,得到的成本结果却达不到该值。。。
基于以上的讨论,你可能认为,既然geneticoptimize_x的优异者并不能保持到下次种群进化中,也就是说,geneticoptimize_x中只有第一次爬山法产生的序列是真正有意义的,那么不如直接使用爬山法来得到序列得了。
但测试后发现,直接用爬山法得到的序列并不是很好。这大约是因为,geneticoptimize_x中我们产生的初始种群是由爬山法进行了N次获得的,且在geneticoptimize_x的main loop中叶并不是完全没有意义。。。
在测试中,遇到过这样的问题:
return error/len(testset)
ZeroDivisionError: int division or modulo by zero
原来是len(testset)中testset是基于概率产生的,那么完全可能产生一个长度为0的testset。。。(汗,这概率。。。)
修改后的testalgorithm如下:
#---------------------------------------------------------------------- def testalgorithm(algf, trainset, testset): """test function""" error = 0 # testset为0的情况下直接返回 if len(testset) == 0: return 0 for row in testset: guess = algf(trainset, row['input']) error += (row['result']-guess)**2 return error/len(testset)
这让我想起《蝙蝠侠2》中的台词:“只有概率是公平的”。。。
以此来确定最佳参数还有额外的一个好处便是:我们可以通过所优化得到的参数判断出哪些参数是重要的,哪些是不太重要的,哪些是可以忽略的。。。如此在搜集数据时候,就可以有针对性的进行(比如,我们不会去费力收集某些实际上无用的数据。。。)。
嗯,看起来这篇博文字数不太多。。。那就贴个K-最邻近算法的全部代码吧。
from random import randint,random import math import optimization #---------------------------------------------------------------------- # 构造价格 def winprice(rating, age): """to construct price""" peak_age = rating - 50 # 根据等级计算价格 price = rating / 2 if age > peak_age: # 经过巅峰年,后续五年内其品质将变差 price = price * (5 - (age-peak_age)) else: # 价格在逼近巅峰年时会增加原值的5倍 price = price * (5*((age+1)/peak_age)) if price < 0: price = 0 return price #---------------------------------------------------------------------- # 构造价格集 def wineset(wine_num): """build the price set""" rows = [] for i in range(wine_num): # 随机生成年代和等级 rating = random() * 50 + 50 age = random() * 50 # 得到参考价格 price = winprice(rating, age) # 增加噪声 price *= random()*0.4 + 0.8 # 加入数据集 rows.append({'input':(rating,age), 'result':price}) return rows #---------------------------------------------------------------------- def wineset_x(wine_num): """build more complicated price set""" rows = [] for i in range(wine_num): rating = random() * 50 + 50 age = random() * 50 aisle = float(randint(1,20)) bottlesize = [375.0, 750.0, 1500.0, 3000.0][randint(0, 3)] price = winprice(rating, age) * (bottlesize/750) price *= (random()*0.9 + 0.2) rows.append({'input':(rating,age,aisle,bottlesize), 'result':price}) return rows #---------------------------------------------------------------------- def wineset_xx(wine_num): """build Uneven Distributions""" rows = wineset(wine_num) for row in rows: # 葡萄酒是在折扣店中购买的 if random < 0.5: rows['result'] *= 0.5 return rows #---------------------------------------------------------------------- # 欧几里得算法计算相似度 def euclidean(v1, v2): """euclidean--Similarity""" d = 0.0 for i in range(len(v1)): d += (v1[i]-v2[i])**2 return math.sqrt(d) #---------------------------------------------------------------------- def getdistance(data, vec1): distancelist = [] for i in range(len(data)): vec2 = data[i]['input'] distancelist.append( (euclidean(vec1,vec2), i) ) distancelist.sort() return distancelist #---------------------------------------------------------------------- # KNN主体 def knnestimate(data, vec1, k=5): """KNN""" dlist = getdistance(data, vec1) avg = 0.0 # 对前K项结果求平均 for i in range(k): idx = dlist[i][1] avg += data[idx]['result'] avg /= k return avg #---------------------------------------------------------------------- # 紧邻权重 def inverseweight(dist, num=1.0, const=0.1): """inverse function for weighted neighbors""" return num/(dist+const) #---------------------------------------------------------------------- def subtractweight(dist, const=1.0): """subtraction function for weighted neighbors""" if dist > const: return 0 else: return const - dist #---------------------------------------------------------------------- def gaussian(dist, sigma=10.0): """gaussian function for weighted neighbors""" return math.e**(-dist**2/(2*sigma**2)) #---------------------------------------------------------------------- # Gaussian-based KNN def weightedKNN(data, vec1, k=5, weightedfunc=gaussian): """weighted KNN""" # 得到距离 dlist = getdistance(data, vec1) avg = 0.0 totalweight = 0.0 # 得到加权平均 for i in range(k): dist = dlist[i][0] idx = dlist[i][1] weight = weightedfunc(dist) avg += weight*data[idx]['result'] totalweight += weight avg /= totalweight return avg #---------------------------------------------------------------------- # 交叉验证 def dividedata(data, test=0.05): """Cross--Validation""" trainset = [] testset = [] for row in data: if random() < test: testset.append(row) else: trainset.append(row) return trainset,testset #---------------------------------------------------------------------- def testalgorithm(algf, trainset, testset): """test function""" error = 0 # testset为0的情况下直接返回 if len(testset) == 0: return 0 for row in testset: guess = algf(trainset, row['input']) error += (row['result']-guess)**2 return error/len(testset) #---------------------------------------------------------------------- def crossvalidate(algf, data, trials=100, test=0.05): """test control""" error = 0 for i in range(trials): trainset,testset = dividedata(data, test) error += testalgorithm(algf, trainset, testset) return error/trials #---------------------------------------------------------------------- # 按比例缩放 def rescale(data, scale): """scaling dimensions""" scaleddata = [] for row in data: scaled = [ scale[i]*row['input'][i] for i in range(len(scale)) ] scaleddata.append( {'input':scaled, 'result':row['result']} ) return scaleddata #---------------------------------------------------------------------- # 利用优化算法进行成本函数的封装 def createcostfuncton(algf, data): """optimizing the scaling""" def costfunc(scale): sdata = rescale(data, scale) return crossvalidate(algf, sdata, trials=10) return costfunc weightdomain = [(0,20)]*4 #---------------------------------------------------------------------- # 估计区间价格概率 def probguess(data, vec1, low, high, k=5, weightf=gaussian): """Estimating the Probability Density""" dlist = getdistance(data, vec1) nweight = 0.0 tweight = 0.0 for i in range(k): dist = dlist[i][0] idx = dlist[i][1] weight = weightf(dist) v = data[idx]['result'] # 当前数据点是否位于指定范围? if v >= low and v <= high: nweight += weight tweight += weight if tweight == 0: return 0 # 概率 == 位于指定范围内的权重/所有权重值 return nweight/tweight #====================================================================== # test: numpredict.py #====================================================================== if __name__ == '__main__': print('Test: numpredict.py') pass
(PS:其中很多部分可以单独成模块,或和其他模块整合在一起。)
嗯,关于K-最近邻算法大约就是这么些了。
K-最近邻算法属于基于实例的机器学习方法。但基于实例的机器学习算法还有很多。如局部加权回归,径向基函数,基于案例的推理等。具体可参看《机器学习》等书籍。
By Kewing