Python机器学习实战:K近邻算法原理、加权策略与优化详解
本文深入解析Python中K最近邻(KNN)算法的实现。从基础分类原理出发,详细讲解欧几里得距离计算、K值选择及加权平均策略(反函数、减法、高斯函数)。通过白酒价格预测案例,演示数据预处理、交叉验证评估、属性缩放优化及概率分布分析,帮助开发者掌握KNN在实际项目中的应用与调优技巧。
K最近邻(KNN)是一种直观的分类算法,其核心逻辑可概括为“近朱者赤,近墨者黑”。判断一个新样本的类别,主要观察其最接近的邻居类别。具体实现涉及多个关键要素:如何定义样本特征以衡量相似性(特征选择),如何量化“接近”程度(距离函数),如何依据邻居类别判定目标类别(分类规则),是否需考虑邻居的远近差异赋予不同权重(距离权重),以及应选取多少个邻居进行决策(K值设定),最终结果是否以概率形式呈现(概率分布)。
K最近邻概念:
KNN的工作原理基于一个已标记的训练样本集。每个样本都拥有明确的分类标签。当输入一个未标记的新数据时,算法会将新数据的各个特征与训练集中的样本特征进行比对,找出最相似的几个样本(即最近邻),并提取这些邻居的分类标签。通常,我们仅选取距离最近的K个样本,K一般是不超过20的整数。最终,统计这K个样本中出现频率最高的分类,将其作为新数据的预测类别。
接下来,我们将利用K最近邻算法构建一个白酒价格预测模型。
构造数据集
首先构建一个葡萄酒样本数据集。白酒的价格主要受等级和年份两个因素影响。
from random import random,randint
import math
# 根据等级和年代对价格进行模拟
def wineprice(rating,age):
peak_age=rating-50
# 根据等级计算价格
price=rating/2
if age>peak_age:
# 经过“峰值年”,后续5年里其品质将会变差
price=price*(5-(age-peak_age)/2)
else:
# 价格在接近“峰值年”时会增加到原值的5倍
price=price*(5*((age+1)/peak_age))
if price<0: price=0
return price
# 生成一批模式数据代表样本数据集
def wineset1():
rows=[]
for i in range(300):
# 随机生成年代和等级
rating=random()*50+50
age=random()*50
# 得到一个参考价格
price=wineprice(rating,age)
# 添加一些噪音
price*=(random()*0.2+0.9)
# 加入数据集
rows.append({'input':(rating,age),'result':price})
return rows
数据间的距离
使用KNN算法的前提是能够识别“最近邻”,这依赖于计算数据点之间的距离。本文采用欧几里得距离(Euclidean Distance)作为衡量标准。
# 使用欧几里得距离,定义距离 def euclidean(v1,v2): d=0.0 for i in range(len(v1)): d+=(v1[i]-v2[i])**2 return math.sqrt(d)
获取与新数据距离最近的k个样本数据
# 计算给预测商品和原数据集中任一其他商品间的距离。data原数据集,vec1预测商品 def getdistances(data,vec1): distancelist=[] # 遍历数据集中的每一项 for i in range(len(data)): vec2=data[i]['input'] # 添加距离到距离列表 distancelist.append((euclidean(vec1,vec2),i)) # 距离排序 distancelist.sort() return distancelist #返回距离列表
根据距离最近的k个样本数据预测新数据的属性
1、简单求均值
# 对距离值最小的前k个结果求平均 def knnestimate(data,vec1,k=5): # 得到经过排序的距离值 dlist=getdistances(data,vec1) avg=0.0 # 对前k项结果求平均 for i in range(k): idx=dlist[i][1] avg+=data[idx]['result'] avg=avg/k return avg
2、求加权平均
若直接对前K个最近邻的结果求均值,可能会引入偏差。因为即使距离较远,只要排在K个之内仍会被计入。为解决此问题,采用加权平均法:距离较近的节点赋予较大权重,距离较远的节点赋予较小权重,从而降低远距离样本对预测结果的影响。
具体如何根据距离分配权值?这里介绍三种递减函数作为权值分配方案。
2.1、使用反函数为近邻分配权重。
def inverseweight(dist,num=1.0,const=0.1): return num/(dist+const)
2.2、使用减法函数为近邻分配权重。注意:当最近距离大于常数const时,该方法不可用。
def subtractweight(dist,const=1.0): if dist>const: return 0 else: return const-dist
2.3、使用高斯函数为距离分配权重。
def gaussian(dist,sigma=5.0): return math.e**(-dist**2/(2*sigma**2))
确定权值分配方法后,即可计算加权平均值以得出预测结果。
# 对距离值最小的前k个结果求加权平均 def weightedknn(data,vec1,k=5,weightf=gaussian): # 得到距离值 dlist=getdistances(data,vec1) avg=0.0 totalweight=0.0 # 得到加权平均 for i in range(k): dist=dlist[i][0] idx=dlist[i][1] weight=weightf(dist) avg+=weight*data[idx]['result'] totalweight+=weight if totalweight==0: return 0 avg=avg/totalweight return avg
第一次测试
上述步骤完成了基于KNN的新数据预测功能,下面进行实际测试。
if __name__=='__main__': #只有在执行当前模块时才会运行此函数 data = wineset1() #创建第一批数据集 result=knnestimate(data,(95.0,3.0)) #根据最近邻直接求平均进行预测 print(result) result=weightedknn(data,(95.0,3.0),weightf=inverseweight) #使用反函数做权值分配方法,进行加权平均 print(result) result = weightedknn(data, (95.0, 3.0), weightf=subtractweight) # 使用减法函数做权值分配方法,进行加权平均 print(result) result = weightedknn(data, (95.0, 3.0), weightf=gaussian) # 使用高斯函数做权值分配方法,进行加权平均 print(result)
交叉验证
交叉验证用于评估算法或参数的优劣。例如,前文提到的三种权值分配算法,究竟哪种效果更佳?可通过交叉验证进行对比分析。
具体做法是:从样本数据集中随机选取95%作为训练集,剩余5%作为测试集(新数据)。对测试集进行预测,并与已知真实结果对比,以此评估算法效果。
实现交叉验证,首先需要具备将样本数据集划分为训练集和测试集两个子集的功能。
# 划分数据。test测试数据集占的比例。其他数据集为训练数据 def dividedata(data,test=0.05): trainset=[] testset=[] for row in data: if random()<test: testset.append(row) else: trainset.append(row) return trainset,testset
其次,需要能够应用算法,并计算预测结果与真实结果之间的误差。
# 使用数据集对使用算法进行预测的结果的误差进行统计,一次判断算法好坏。algf为算法函数,trainset为训练数据集,testset为预测数据集 def testalgorithm(algf,trainset,testset): error=0.0 for row in testset: guess=algf(trainset,row['input']) #这一步要和样本数据的格式保持一致,列表内个元素为一个字典,每个字典包含input和result两个属性。而且函数参数是列表和元组 error+=(row['result']-guess)**2 #print row['result'],guess #print error/len(testset) return error/len(testset)
结合数据拆分函数和算法性能误差统计函数,我们可以在原始数据集上执行多次实验,并统计平均误差。
# 将数据拆分和误差统计合并在一起。对数据集进行多次划分,并验证算法好坏 def crossvalidate(algf,data,trials=100,test=0.1): error=0.0 for i in range(trials): trainset,testset=dividedata(data,test) error+=testalgorithm(algf,trainset,testset) return error/trials
交叉验证测试
if __name__=='__main__': #只有在执行当前模块时才会运行此函数
data = wineset1() #创建第一批数据集
print(data)
error = crossvalidate(knnestimate,data) #对直接求均值算法进行评估
print('平均误差:'+str(error))
def knn3(d,v): return knnestimate(d,v,k=3) #定义一个函数指针。参数为d列表,v元组
error = crossvalidate(knn3, data) #对直接求均值算法进行评估
print('平均误差:' + str(error))
def knninverse(d,v): return weightedknn(d,v,weightf=inverseweight) #定义一个函数指针。参数为d列表,v元组
error = crossvalidate(knninverse, data) #对使用反函数做权值分配方法进行评估
print('平均误差:' + str(error))
不同类型、值域的变量、无用变量
样本数据的各个属性可能并非同类型或取值范围相同。例如,酒的属性可能包含档次(0-100)、年限(0-50)、容量(375.0ml、750.0ml、1500.0ml三种),甚至可能包含无关数据如生产流水线号(1-20之间的整数)。在计算样本距离时,取值范围大的属性变化会主导结果,导致取值范围小的属性被忽略。此外,无用属性的变化也会人为增加数据间的距离。
因此,必须对样本数据的属性进行缩放至合适范围,并能够剔除无效属性。
构造新的数据集
# 构建新数据集,模拟不同类型变量的问题
def wineset2():
rows=[]
for i in range(300):
rating=random()*50+50 #酒的档次
age=random()*50 #酒的年限
aisle=float(randint(1,20)) #酒的通道号(无关属性)
bottlesize=[375.0,750.0,1500.0][randint(0,2)] #酒的容量
price=wineprice(rating,age) #酒的价格
price*=(bottlesize/750)
price*=(random()*0.2+0.9)
rows.append({'input':(rating,age,aisle,bottlesize),'result':price})
return rows
实现按比例对属性取值进行缩放的功能
# 按比例对属性进行缩放,scale为各属性的值的缩放比例。
def rescale(data,scale):
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
接下来需解决的关键问题是:各属性应缩放多少?这是一个优化问题,可通过优化技术寻找最优解。优化的目标函数(成本函数)是缩放后进行预测的结果与真实结果之间的误差值,误差越小越好。误差计算方式与前文交叉验证时使用的crossvalidate函数相同。
下面构建成本函数
# 生成成本函数。闭包 def createcostfunction(algf,data): def costf(scale): sdata=rescale(data,scale) return crossvalidate(algf,sdata,trials=10) return costf weightdomain=[(0,10)]*4 #将缩放比例这个题解的取值范围设置为0-10,可以自己设定,用于优化算法
优化技术的相关参考可查阅:https://www.jb51.net/article/131719.htm
测试代码
if __name__=='__main__': #只有在执行当前模块时才会运行此函数 #========缩放比例优化=== data = wineset2() # 创建第2批数据集 print(data) import optimization costf=createcostfunction(knnestimate,data) #创建成本函数 result = optimization.annealingoptimize(weightdomain,costf,step=2) #使用退火算法寻找最优解 print(result)
不对称分布
当样本数据集包含多种分布情况时,我们希望输出结果不唯一,而是以概率形式表示,即输出每种可能结果及其出现的概率。
例如,葡萄酒可能来自折扣店,而样本数据集中未记录这一特性,导致样本数据中价格呈现两种形式的分布。
构造数据集
def wineset3(): rows=wineset1() for row in rows: if random()<0.5: # 葡萄酒是从折扣店购买的 row['result']*=0.6 return rows
若结果以概率形式存在,我们需要能够计算指定范围内的概率值。
# 计算概率。data样本数据集,vec1预测数据,low,high结果范围,weightf为根据距离进行权值分配的函数 def probguess(data,vec1,low,high,k=5,weightf=gaussian): dlist=getdistances(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
对于多种输出、以概率和值表示的结果,可以使用累积概率分布图或概率密度图进行可视化展示。
绘制累积概率分布图
from pylab import * # 绘制累积概率分布图。data样本数据集,vec1预测数据,high取值最高点,k近邻范围,weightf权值分配 def cumulativegraph(data,vec1,high,k=5,weightf=gaussian): t1=arange(0.0,high,0.1) cprob=array([probguess(data,vec1,0,v,k,weightf) for v in t1]) #预测产生的不同结果的概率 plot(t1,cprob) show()
绘制概率密度图
# 绘制概率密度图 def probabilitygraph(data,vec1,high,k=5,weightf=gaussian,ss=5.0): # 建立一个代表价格的值域范围 t1=arange(0.0,high,0.1) # 得到整个值域范围内的所有概率 probs=[probguess(data,vec1,v,v+0.1,k,weightf) for v in t1] # 通过加上近邻概率的高斯计算结果,对概率值做平滑处理 smoothed=[] for i in range(len(probs)): sv=0.0 for j in range(0,len(probs)): dist=abs(i-j)*0.1 weight=gaussian(dist,sigma=ss) sv+=weight*probs[j] smoothed.append(sv) smoothed=array(smoothed) plot(t1,smoothed) show()
测试代码
if __name__=='__main__': #只有在执行当前模块时才会运行此函数 data = wineset3() # 创建第3批数据集 print(data) cumulativegraph(data,(1,1),120) #绘制累积概率密度 probabilitygraph(data,(1,1),6) #绘制概率密度图
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













