K最近邻(k-Nearest Neighbor,KNN)分类算法,是一个理论上比较成熟的方法,也是最简单的机器学习算法之一。该方法的思路是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。KNN算法中,所选择的邻居都是已经正确分类的对象。该方法在定类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。 KNN方法虽然从原理上也依赖于极限定理,但在类别决策时,只与极少量的相邻样本有关。由于KNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,KNN方法较其他方法更为适合。
KNN算法不仅可以用于分类,还可以用于回归。通过找出一个样本的k个最近邻居,将这些邻居的属性的平均值赋给该样本,就可以得到该样本的属性。更有用的方法是将不同距离的邻居对该样本产生的影响给予不同的权值(weight),如权值与距离成正比(组合函数)。
该算法在分类时有个主要的不足是,当样本不平衡时,如一个类的样本容量很大,而其他类样本容量很小时,有可能导致当输入一个新样本时,该样本的K个邻居中大容量类的样本占多数。 该算法只计算“最近的”邻居样本,某一类的样本数量很大,那么或者这类样本并不接近目标样本,或者这类样本很靠近目标样本。无论怎样,数量并不能影响运行结果。可以采用权值的方法(和该样本距离小的邻居权值大)来改进。该方法的另一个不足之处是计算量较大,因为对每一个待分类的文本都要计算它到全体已知样本的距离,才能求得它的K个最近邻点。目前常用的解决方法是事先对已知样本点进行剪辑,事先去除对分类作用不大的样本。该算法比较适用于样本容量比较大的类域的自动分类,而那些样本容量较小的类域采用这种算法比较容易产生误分。
要预测某个特征向量所对应的价格,首先应当计算该特征向量与所有训练样本的距离。并对这些距离进行排序,从而获得其最近的K个近邻。
- 选取较大的K会降低准确率
- 选取较小的K会使得计算结果容易受噪声的影响
直接计算与特征向量Vec相聚最近的K个近邻的价格的均值。
权值随距离的增大而减小,此处的权值衰减函数有三类:
1)反函数:权值 = 1/(距离 + 常数) 其中常数的作用是防止分母为0.
2)减法函数: 权值 = 常数 - 距离
3)高斯函数: 权值 = exp(- (x ^ 2) / (2 * sigma^2)) 其中均值为0,表示距离为0时权值最大,随着距离的增加,权值按照高斯函数的形式衰减。
按照一定的比例,将数据集划分为训练集和测试集
- 根据测试集中的每一项,调用KNN计算价格,累计其平方误差。
- 重复前面两步,多次划分,取误差的平均值作为交叉验证的均值。
消除不同特征尺度的影响,同时识别干扰变量。
此处可以利用前面的博客中讲述的最优化方法,通过模拟退火算法或遗传算法,寻找缩放因子的最优值。其中,需要优化的代价函数即为交叉验证的误差值。
优化算法博客链接
在《集体智慧编程》上所描述的葡萄酒价格预测问题的源代码实现如下(其中优化算法采用模拟退火算法):
# -*- coding:utf-8 -*-
__author__ = 'Bai Chenjia'
from random import random, randint
import math
# 产生训练数据集,参数是训练数据的两个特征,根据两个特征计算价格
def winprice(rating, age):
peak_age = rating - 50
# 根据等级计算价格
price = rating / 2
if age > peak_age:
# 经过峰值年之后,后继5年内品质将会变差
price = price * (5 - (age - peak_age))
else:
# 价格在接近峰值年时会增加到原值的5倍
price = price * (5 * ((age + 1) / peak_age))
if price < 0:
price = 0
return price
# 生成300条训练数据,价格在winprice函数的基础上上下变动20%
def wineset1():
rows = []
for i in range(300):
# 随机生成年代和等级
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 euclidean(v1, v2):
d = 0.0
for i in range(len(v1)):
a = v1[i]
b = v2[i]
d += pow(a - b, 2)
return math.sqrt(d)
# 获取要预测的向量vec1与数据集data中所有元素的距离
def getdistances(data, vec1):
distancelist = []
for i in range(len(data)):
vec2 = data[i]['input']
distancelist.append((euclidean(vec1, vec2), i))
distancelist = sorted(distancelist, key=lambda x: x[0])
return distancelist
# 对vec1的K个近邻的训练数据的价格取平均值作为对vec1价格的估计
def knnestimate(data, vec1, k=5):
# 经过得到排序的距离值
dlist = getdistances(data, vec1)
avg = 0.0
# 对K个近邻的结果去平均
for i in range(k):
avg += data[dlist[i][1]]['result']
avg /= k
return avg
# 优化knnestimate函数,对不同距离的近邻进行距离加权
# 其中权值与距离成反比关系,以下是三种权值随距离衰减的方式
# 1.反函数
def inverseweight(dist, num=1.0, const=0.1):
return num / (dist + const)
# 2.减法函数
def subtractweight(dist, const=1.0):
if dist > const:
return 0
else:
return const - dist
# 3.高斯函数
def gaussian(dist, sigma=10.0):
return math.e**(-dist**2 / sigma**2)
# 加权KNN算法,根据距离对K个近邻加权,权值乘以对应的价格作累加最后除以权值之和
# 参数weightf是函数,指示使用哪一种权值衰减方式
def weightedknn(data, vec1, k=5, weightf=gaussian):
dlist = getdistances(data, vec1)
result = 0.0
weight = 0.0
for i in range(k):
price = data[dlist[i][1]]['result'] # 价格
result += price * weightf(dlist[i][0]) # 距离加权,累加价格和
weight += weightf(dlist[i][0]) # 统计权值和
return result / weight
# 交叉验证
# 1. 随机划分数据集,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
# 2. 为算法提供训练集,针对测试集中的每一项内容调用算法,返回误差
# 其中参数algf是一个函数,可以是 knnestimate, weightedknn或者其他根据KNN算法算法计算价格的函数
def testalgorithm(algf, trainset, testset):
error = 0.0
for row in testset:
guess = algf(trainset, row['input']) # 预测
error += (row['result'] - guess) ** 2 # 累计平方误差
return error / len(testset)
# 3. 交叉验证。 多次调用dividedata函数对数据进行随机划分,并计算误差,取所有随机划分误差的均值
def crossvalidate(algf, data, trials=100, test=0.05):
error = 0.0
# trials代表随机划分的次数
for i in range(trials):
trainset, testset = dividedata(data, test)
error += testalgorithm(algf, trainset, testset)
return error / trials
# 重新生成数据集,加入干扰变量
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, 3000.0][randint(0, 3)]
price = winprice(rating, age)
price *= (bottlesize / 750)
price *= random() * 0.9 + 0.2
rows.append({'input': (rating, age, aisle, bottlesize), 'result': price})
return rows
# 缩放,参数scale的长度与训练数据特征的长度相同. 每个参数乘以训练数据中的特征以达到缩放特征的目的
def rescale(data, scale):
scaledata = []
for row in data:
scaled = [scale[i] * row['input'][i] for i in range(len(scale))]
scaledata.append({'input': scaled, 'result': row['result']})
return scaledata
def knn3(d, v):
return knnestimate(d, v, k=3)
def knn1(d, v):
return knnestimate(d, v, k=1)
# 构造 优化搜索算法 的代价函数
def createcostfunction(algf, data):
def costf(scale):
sdata = rescale(data, scale)
return crossvalidate(algf, data)
return costf
# 搜索算法4:模拟退火算法
# 参数:T代表原始温度,cool代表冷却率,step代表每次选择临近解的变化范围
# 原理:退火算法以一个问题的随机解开始,用一个变量表示温度,这一温度开始时非常高,而后逐步降低
# 在每一次迭代期间,算法会随机选中题解中的某个数字,然后朝某个方向变化。如果新的成本值更
# 低,则新的题解将会变成当前题解,这与爬山法类似。不过,如果成本值更高的话,则新的题解仍
# 有可能成为当前题解,这是避免局部极小值问题的一种尝试。
# 注意:算法总会接受一个更优的解,而且在退火的开始阶段会接受较差的解,随着退火的不断进行,算法
# 原来越不能接受较差的解,直到最后,它只能接受更优的解。
# 算法接受较差解的概率 P = exp[-(highcost-lowcost)/temperature]
def annealingoptimize(schedulecost, domain, T=10000.0, cool=0.9, step=2):
# 随机初始化值
vec = [randint(domain[i][0], domain[i][1]) for i in range(len(domain))]
# 循环
while T > 0.1:
# 选择一个索引值
i = randint(0, len(domain) - 1)
# 选择一个改变索引值的方向
c = randint(-step, step) # -1 or 0 or 1
# 构造新的解
vecb = vec[:]
vecb[i] += c
if vecb[i] < domain[i][0]: # 判断越界情况
vecb[i] = domain[i][0]
if vecb[i] > domain[i][1]:
vecb[i] = domain[i][1]
# 计算当前成本和新的成本
cost1 = schedulecost(vec)
cost2 = schedulecost(vecb)
# 判断新的解是否优于原始解 或者 算法将以一定概率接受较差的解
if cost2 < cost1 or random() < math.exp(-(cost2 - cost1) / T):
vec = vecb
T = T * cool # 温度冷却
print vecb[:], "代价:", schedulecost(vecb)
self.printschedule(vec)
print "模拟退火算法得到的最小代价是:", schedulecost(vec)
return vec
if __name__ == '__main__':
"""
data = wineset1()
price = knnestimate(data, (95.0, 5.0))
print price
price = weightedknn(data, (95.0, 5.0))
print price
print crossvalidate(knnestimate, data)
"""
"""
data = wineset2()
print crossvalidate(weightedknn, data)
data = rescale(data, [10, 10, 0, 0.5])
print crossvalidate(weightedknn, data)
"""
weightdomain = [(0, 20)] * 4
data = wineset2()
costf = createcostfunction(knnestimate, data)
annealingoptimize(costf, weightdomain)