KNN应用

  • 1、KNN简介
    • 1.1 KNN算法优缺点
  • 2、KNN算法的思想
  • 3、最佳K值的选择
  • 4、相似度的度量方法
    • 4.1 距离定义
    • 4.2 欧式距离
    • 4.3 曼哈顿距离
    • 4.4 余弦相似度
    • 4.5 杰卡德相似系数
  • 5、K-近邻的分类决策规则
  • 6、KNN算法从零实现(基于Python)
    • 6.1 伪代码
    • 6.2 Python代码实现
  • 7、近邻样本的搜寻方法
    • 7.1 KD搜寻树
      • 7.1.1 KD树的构建
      • 7.1.2 KD树的搜寻
  • 8、球树搜寻法
    • 8.1 球体的构造
    • 8.2 球树的搜寻
  • 9、KNN模型的应用
  • 10、参考资料:

1、KNN简介

KNN算法,中文名称为最近邻算法,k-近邻法是一种基本分类与回归算法。和其它有监督算法不同,KNN算法是一种“惰性”学习算法,即不会预先生成一个分类或预测模型,用于新样本的预测,而是将模型的构建与未知数据的预测同时进行。

KNN算法和决策树类似,即可以针对离散因变量做分类,又可以对连续因变量做预测,其核心思想就是比较已知y值的样本和未知y值样本的相似度,然后寻找最相似的k个样本用作未知样本的预测。

k值的选择、距离度量及分类决策规则是2k近邻法的三个基本要素,k-近邻法于1968年由Cover和Hart提出。

1.1 KNN算法优缺点

  • 优点:精度高、对异常值不敏感(由k个最近邻数据决定类别)、无数据输入假定(不需要输入数据符合某种要求);
  • 缺点:计算复杂度高(需要计算所有数据得到相互间的距离)、空间复杂度高(如果训练数据集很大,必须用大量的存储空间);无法给出任何数据的基础结构信息,无法知晓平均样例和典型实例样本具有什么特征;
  • 适用数据范围:数值型和标称型;
  • KNN算法并不会输出待预测数据属于某一类别的概率,而是直接输出其属于哪一个类别;

2、KNN算法的思想

K最近邻算法,顾名思义就是搜寻最近的k个已知类别样本用于未知类别样本的预测。

  1. “最近”的度量就是应用点之间的距离或者相似度。距离越小相似度越高,说明他们之间越近。
  2. “预测”,对于离散型的因变量来说,从k个最近的已知类别样本中挑选出频率最高的类别用于未知样本的判断;对于连续型的因变量来说,则是将k个最近的已知样本均值用作未知样本的预测。

KNN算法的具体步骤可以描述为:

  1. 确定未知样本近邻的个数k值;
  2. 根据某种度量样本间相似度的指标(如欧式距离)将每一个未知类别样本的最近k个已知样本搜寻出来,形成一个个簇;
  3. 对搜寻出来的已知样本进行投票,将各簇下类别最多的分类用作未知样本点的预测;

如果存在两个已知类别样本到未知类别样本的距离一样,但是两个已知类别样本的类别是矛盾的,则根据已知类别出现的先后顺序决定选择哪个。

KNN算法也能用于回归问题,假设离测试样本最近的k个训练样本的标签值为yiy_iyi​,则对样本的回归预测输出值为y^=(∑i=1kyi)/k\hat{y}=(\sum_{i=1}^ky_i)/ky^​=(i=1∑k​yi​)/k即所有邻居的标签均值,在这里最近的k个邻居的贡献被认为是相等的。同样也可以采用带权重的方案。带样本权重的回归预测函数为y^=(∑i=1kwiyi)/k\hat{y}=(\sum_{i=1}^kw_iy_i)/ky^​=(i=1∑k​wi​yi​)/k其中,wiw_iwi​为第iii个样本的权重,权重值可以人工设定,或者用其它方法来确定。例如,设置权重为与距离成反比。

通过上面步骤,能够理解为什么该算法被称为“惰性”算法,如果该算法仅仅接受已知类别的样本点,它是不会进行模型运算的,只有将未知类别样本加入到已知类别样本中,才会执行搜寻工作,并将最终的分类结果返回。

3、最佳K值的选择

根据经验发现,不同的K值对模型的预测准确性会有较大的影响,如果K值过于偏小,可能会导致模型的过拟合;如果K值过大,有可能会使模型进入欠拟合状态。

这个举个例子说明一下:

  1. 假设K值为1时,意味着未知样本点的类别将由最近的1个已知样本点决定,投票功能将不再起效。对于训练样本集本身来说,其训练误差几乎为0;但是对于未知的测试数据集来说,训练误差可能会很大,因为距离最近的1个已知样本点可以是异常观测也可以是正常观测。所以K值过于偏小可能会导致模型的过拟合。
  2. 假设K值为N,意味着未知样本点的类别将由所有已知样本点中频数最高的类别所决定。所以不管是训练数据集还是测试数据集,都会被判为一种类别,进而导致模型无法在训练数据集和测试数据集上得到理想的准确率。进而可以说明,K值越大,模型偏向于欠拟合的可能性就越大。

为了获得最佳的K值,可以考虑两种解决方案:

  1. 设置K近邻样本的投票权重,假设在使用KNN算法进行分类或预测时设置的K值比较大,担心模型发生欠拟合的现象,一个简单有效的处理办法就是设置近邻样本的投票权重,如果已知样本距离未知样本距离比较远,则对应的权重就设置地低一些,否则权重就设置得高一些,通常可以设置为距离的倒数。
  2. 采用多重交叉验证法,该方法是目前比较流行的方案,其核心是将K取不同的值,然后在每种值下执行m重的交叉验证,最后选出平均误差最小的k值。
  3. 当然还可以将两种方法的优点相结合,选出理想的k值。

4、相似度的度量方法

KNN分类算法的思想是计算位置分类的样本点与已知分类的样本点之间的距离,然后将位置分类最近的K个已知分类样本用作投票。所以该算法的一个重要步骤是计算他们之间的相似度。下面简单介绍一下欧氏距离,曼哈顿距离,然后拓展两种相似度的度量指标,一个是余弦相似度,另一个是杰卡德相似系数。

4.1 距离定义

KNN算法的实现依赖于样本之间的距离值,因此,需要定义距离的计算公式。两个向量之间的距离为d(xi,xj)d(x_i,x_j)d(xi​,xj​),这是一个将两个维数相同的向量映射为一个实数的函数。距离函数必须满足以下条件:

  1. 三角不等式:d(xi,xk)+d(xk,xj)≥d(xi,xj)d(x_i,x_k)+d(x_k,x_j)\geq d(x_i,x_j)d(xi​,xk​)+d(xk​,xj​)≥d(xi​,xj​)这与几何中的三角不等式吻合;
  2. 非负性,即距离不能是一个负数:d(xi,xj)≥0d(x_i,x_j)\geq 0d(xi​,xj​)≥0
  3. 对称性,即A到B的距离和B到A的距离必须相等:d(xi,xj)=d(xj,xi)d(x_i,x_j)=d(x_j,x_i)d(xi​,xj​)=d(xj​,xi​)
  4. 区分性,如果两点间的距离为0,则两个点必须相同:d(xi,xj)=0⇒xi=xjd(x_i,x_j)=0\Rightarrow x_i=x_jd(xi​,xj​)=0⇒xi​=xj​

满足上面4个条件的函数都就可以用作距离定义

4.2 欧式距离

该距离度量的是两点之间的直线距离,如果二维平面中存在两点A(x1,y1)A(x_1,y_1)A(x1​,y1​)、B(x2,y2)B(x_2,y_2)B(x2​,y2​),则它们之间的直线距离为:dA,B=(x1−x2)2+(y1−y2)2d_{A,B}=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2}dA,B​=(x1​−x2​)2+(y1​−y2​)2​该公式的几何意义实际上就是下图中的直角三角形的斜边。

如果将点扩展到n维空间,则点A(x1,x2,...,xn)A(x_1,x_2,...,x_n)A(x1​,x2​,...,xn​)、B(y1,y2,...,yn)B(y_1,y_2,...,y_n)B(y1​,y2​,...,yn​)之间的欧式距离可以表示成:dA,B=(y1−x1)2+(y2−x2)2+...+(yn−xn)2d_{A,B}=\sqrt{(y_1-x_1)^2+(y_2-x_2)^2+...+(y_n-x_n)^2}dA,B​=(y1​−x1​)2+(y2​−x2​)2+...+(yn​−xn​)2​

4.3 曼哈顿距离

该距离也称为“曼哈顿街区距离”,度量的是两点在轴上的相对距离总和。所以,二维平面中两点A(x1,y1)A(x_1,y_1)A(x1​,y1​)、B(x2,y2)B(x_2,y_2)B(x2​,y2​)之间的曼哈顿距离可以表示成:dA,B=∣x1−x2∣+∣y1−y2∣d_{A,B}=|x_1-x_2|+|y_1-y_2|dA,B​=∣x1​−x2​∣+∣y1​−y2​∣其具体几何意义可以看一下下面的图:

同样地,如果将点扩展到n维空间中,则点A(x1,x2,...,xn)A(x_1,x_2,...,x_n)A(x1​,x2​,...,xn​)、B(y1,y2,...,yn)B(y_1,y_2,...,y_n)B(y1​,y2​,...,yn​)之间的曼哈顿距离可以表示成:dA,B=∣y1−x1∣+∣y2−x2∣+...+∣yn−xn∣d_{A,B}=|y_1-x_1|+|y_2-x_2|+...+|y_n-x_n|dA,B​=∣y1​−x1​∣+∣y2​−x2​∣+...+∣yn​−xn​∣

4.4 余弦相似度

该相似度是计算两点所构成向量夹角的余弦值,夹角越小,则余弦值越接近于1,进而能够说明两点之间越相似。对于二维平面中的两点A(x1,y1)A(x_1,y_1)A(x1​,y1​)、B(x2,y2)B(x_2,y_2)B(x2​,y2​)来说,它们之间的余弦相似度可以表示成:SimilarityA,B=Cosθ=x1x2+y1y2x12+y12+x22+y22Similarity_{A,B}=Cos\theta=\frac{x_1x_2+y_1y_2}{\sqrt{x_1^2+y_1^2}+\sqrt{x_2^2+y_2^2}}SimilarityA,B​=Cosθ=x12​+y12​​+x22​+y22​​x1​x2​+y1​y2​​将A(x1,y1)A(x_1,y_1)A(x1​,y1​)、B(x2,y2)B(x_2,y_2)B(x2​,y2​)两点构成向量的夹角构成的图如下所示,就能够理解夹角越小,两点越相似的结论。

假设A、B代表两个用户从事某件事的意愿,意愿程度的大小用各自的夹角θ1\theta_1θ1​和θ2\theta_2θ2​表示,两个夹角之差越小,说明两者的意愿方向越一致,进而他们的相似度越高(不管是相同的高意愿还是低意愿)。

如果将点扩展到n维空间,则点A(x1,x2,...,xn)A(x_1,x_2,...,x_n)A(x1​,x2​,...,xn​)、B(y1,y2,...,yn)B(y_1,y_2,...,y_n)B(y1​,y2​,...,yn​)之间的余弦相似度可以用向量表示为SimilarityA,B=Cosθ=A.B∣∣A∣∣∣∣B∣∣Similarity_{A,B}=Cos\theta=\frac{A.B}{||A||||B||}SimilarityA,B​=Cosθ=∣∣A∣∣∣∣B∣∣A.B​其中,...代表两个向量之间的内积,符号∣∣∣∣||\space||∣∣ ∣∣代表向量的模,即L2正则化。

4.5 杰卡德相似系数

该相似系数与余弦相似度经常被用于推荐算法,计算用户之间的相似度。例如,用户A购买了10件不同的商品,B用户购买了15件不同的商品,则两者之间的相似系数可以表示为:J(A,B)=∣A⋂B∣∣A⋃B∣J(A,B)=\frac{|A\bigcap B|}{|A\bigcup B|}J(A,B)=∣A⋃B∣∣A⋂B∣​其中,∣A⋂B∣|A\bigcap B|∣A⋂B∣表示两个用户所购买相同商品的数量,∣A⋃B∣|A\bigcup B|∣A⋃B∣代表两个用户购买的所有商品的数量。杰卡德相似稀疏越大,代表样本之间越接近。

使用距离方法来度量样本间的相似度,必须注意两点,一个是所有变量的数值化,如果某些变量为离散型的字符串,它们是无法计算距离的,需要对其做数值化处理,如构造哑变量或强制数值编码(例如将受教育水平的高中、大学、硕士及以上三种离散值重编码为0,1,2);另一个是防止数值变量的量纲影响,在实际项目的数据中,不同变量的数值范围可能是不一样的,这样就会使计算的距离值收到影响,所以必须采用数据的标准化方法对其归一化,使得所有变量的数值具有可比性。

在确定好某种距离计算公式后,KNN算法就开始搜寻最近的K个已知类别样本点。实际上,该算法在搜寻过程中是非常耗内存的,因为它需要不停地比较每一个未知样本与已知样本之间的距离。因此在暴力搜寻法的基础上,提出一些效率提升的算法,如KD树搜寻法和球树搜寻法,使用不同的搜寻方法往往会提升模型的执行效率。

5、K-近邻的分类决策规则

k近邻法中的分类决策规则往往是多数表决,即由驶入实例的k个近邻的训练实例中的多数类决定输入实例的类;

多数表决规则有以下解释:如果分类的损失函数为0-1损失函数,分类函数为f:Rn→{c1,c2,...,cK}f:R^n\rightarrow \{c_1,c_2,...,c_K\}f:Rn→{c1​,c2​,...,cK​}那么误分类的概率是P(Y≠f(X))=1−P(Y=f(X))P(Y\neq f(X))=1-P(Y=f(X))P(Y​=f(X))=1−P(Y=f(X))对给定的实例x∈Xx\in Xx∈X,其最近邻的k个训练实例点构成集合Nk(x)N_k(x)Nk​(x),如果涵盖Nk(x)N_k(x)Nk​(x)的区域类别是cjc_jcj​,那么误分类率是1k∑xi∈Nk(x)I(yi≠cj)=1−1k∑xi∈Nk(x)I(yi=cj)\frac{1}{k}\sum_{x_i\in N_k(x)}I(y_i\neq c_j)=1-\frac{1}{k}\sum_{x_i\in N_k(x)}I(y_i=c_j)k1​xi​∈Nk​(x)∑​I(yi​​=cj​)=1−k1​xi​∈Nk​(x)∑​I(yi​=cj​)钥匙误分类率最小即经验风险最小,就要使∑xi∈Nk(x)I(yi=cj)\sum_{x_i\in N_k(x)}I(y_i=c_j)∑xi​∈Nk​(x)​I(yi​=cj​)最大,所以多数表决规则等价于经验风险最小化。

6、KNN算法从零实现(基于Python)

6.1 伪代码

对未知类别属性的数据集中的每个点依次执行以下操作:

  1. 计算已知类别数据集中的点与当前点之间的距离;
  2. 按照距离递增次序排序;
  3. 选取与当前点距离最小的k个点;
  4. 确定前k个点所在类别的出现频率;
  5. 返回前k个点出现频率最高的类别作为当前点的预测分类;

6.2 Python代码实现

方法(一)

from numpy import *
import operatordef GetData():  # 测试用例函数dataset = array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])label = ['A', 'A', 'B', 'B']return dataset, labeldef knn(index, dataset, label, k):# index: 待预测的数据# dataset: 训练数据# label: 训练数据的标签# k: 与待预测数据距离最近的k个训练数据 datasetSize = dataset.shape[0]  # 计算训练数据的数据量diffMat = tile(index, (datasetSize, 1)) - dataset  # tile用于将待预测数据按行复制,复制的行数为训练数据的个数,得到array数组与dataset训练数据进行相减,得到待预测数据与所有训练数据之间的差值sqDiffMat = diffMat ** 2  # 对上面计算得到的差值进行平方sqDistances = sqDiffMat.sum(axis=1)  # 对平方后的距离按行进行求和distance = sqDistances ** 0.5  # 使用欧式距离,计算得到距离的平方和后需要开方sortedDistIndices = distance.argsort()  # argsort()的作用是按距离值的大小从小到大返回对应的索引classCount = {}  # 建立一个字典用于保存K个数据的标签数量for i in range(k):  # 循环k次,因为只需要最近邻的k个训练数据voteLabel = label[sortedDistIndices[i]]  # 根据得到的距离最小的K个索引找到对应的K个训练数据的标签classCount[voteLabel] = classCount.get(voteLabel, 0) + 1  # get()函数是针对字典使用的,当字典中没有voteLabel关键字时,在字典中创建关键字voteLabel,并初始化为0。如果存在,则忽略,直接在原来基础上进行加一操作;sortedClassCount = sorted(classCount.items(), key=lambda x: x[1], reverse=True)  # Python 字典(Dictionary) items() 函数以列表返回可遍历的(键, 值) 元组数组,使用sorted()函数对列表进行排序,排序的依据是根据键值对的值从大往小进行排序return sortedClassCount[0][0]  # 返回列表中键值对值最大的元组对应的键if __name__ == "__main__":  # 测试KNN代码可行性group, labels = GetData()print(knn([0.0, 0.0], group, labels, 3))

代码中有三部分值得学习:

  1. tile()函数,作用是将数据根据训练数据的大小按行进行复制,这样可以直接对整个训练数据进行距离计算;
  2. argsort()函数,作用是根据数组中值的大小返回排序后的序列,比如数组中的数据为[1.3,1.1,1.5],使用argsort()返回的是[1,0,2],就是排序后返回其数据原来的索引位置;
  3. items()函数,作用是将字典的键值对转换为列表的键值对元组数组形式,这样就可以针对其标签数量进行排序,返回出现次数最多的标签;

方法(二)

# 代码来源:https://github.com/fengdu78/lihang-code
class KNN:def __init__(self, X_train, y_train, n_neighbors=3, p=2):"""parameter: n_neighbors 邻近点个数parameter: p 距离度量"""self.n = n_neighbors  # 邻近点个数,默认为3self.p = p  # 距离度量范数。默认为2self.X_train = X_train  # 训练数据self.y_train = y_train  # 训练数据labeldef predict(self, X):# 取出n个点knn_list = []for i in range(self.n): # 遍历X_train的前n个数dist = np.linalg.norm(X - self.X_train[i], ord=self.p)  # 等价于求X-self.X_train[i]的欧氏距离knn_list.append((dist, self.y_train[i]))  # 将X_train[i]和X的距离,X_train[i]的label保存到knn_list中for i in range(self.n, len(self.X_train)):  # 遍历X_train的剩余数据max_index = knn_list.index(max(knn_list, key=lambda x: x[0]))  # 找到knn_list中与X距离最远数据的indexdist = np.linalg.norm(X - self.X_train[i], ord=self.p)  # 计算当前数据X_train[i]与X的距离if knn_list[max_index][0] > dist:  # 如果dist小于最远距离knn_list[max_index] = (dist, self.y_train[i])  # 更新knn_list# 统计不同label的数量和种类knn = [k[-1] for k in knn_list]  count_pairs = Counter(knn)max_count = sorted(count_pairs.items(), key=lambda x: x[1])[-1][0]return max_countdef score(self, X_test, y_test):right_count = 0for X, y in zip(X_test, y_test):  # 遍历每个测试集数据label = self.predict(X)  # 预测结果比较if label == y:right_count += 1return right_count / len(X_test)  # 计算准确度

7、近邻样本的搜寻方法

搜寻的实际就是计算比比较未知样本和已知样本之间的距离,最简单粗暴的方法是全表扫描,该方法被称为暴力搜寻法。

例如,针对某个未知类别的测试样本,需要计算它与所有已知类别的样本点之间的距离,然后从中挑选出最近的k个样本,再基于这k个样本进行投票,将票数最多的类别用作未知样本的预测。该方法简单而直接,但是该方法只能适合小样本的数据集,一旦数据集的变量个数和观测个数较大时,KNN算法的执行效率就会非常低下。其运算过程就相当于使用了两层for循环,不仅要迭代每一个未知类别的样本,还需要迭代所有已知类别的样本。

为了避免全表扫描,提出了KD搜索树和球形搜寻法。每个算法的出现必定有其意义,只有知道其出现的意义才能更好地理解。下面我们介绍一下这两种提高KNN执行效率的搜寻方法:

7.1 KD搜寻树

KD树的英文名称为K-Dimension Tree,是一种二分支的树结构,这里的K表示训练集中包含的变量个数,而非KNN模型中的K个近邻样本。其最大的搜寻特点是先利用所有已知类别的样本点构造一个树模型,然后将未知类别的测试集应用在树模型上,实现最终的预测功能。先建树后预测的模式,能够避免全表扫描,提高KNN模型的运行速度。KD树搜寻法包含两个重要的步骤,第一个步骤是如何构建一颗二叉树,第二个步骤是如何实现最近邻的搜寻。

7.1.1 KD树的构建

实现k-近邻法时,主要考虑的问题是如何对训练数据进行快速的k近邻搜索。这在特征空间的维度大及训练数据容量大时尤其重要;

k近邻算法最简单的实现方法是线性扫描,这时要计算输入实例与每一个训练实例的距离。当训练集很大时,计算非常耗时,这种方法不可行。

为了提高k近邻搜索的效率,可以考虑使用特殊的结构存储训练数据,以减少计算距离的次数,下面介绍kd 树方法。

kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形结构。kd树是二叉树,表示对k维空间的一个划分。构造kd树相当于不断地用垂直于坐标轴的超平面将k维空间进行切分,构成一系列的k维超矩阵区域。kd树的每个结点对应于一个k维矩形区域。

构造kd树的方法如下:
构造根节点,使根节点对应于k维空间中包含所有实例点的超矩形区域;通过下面的递归方法,不断地对k维空间进行切分,生成子节点。在超矩形区域(结点)上选择一个坐标轴和在此坐标轴上的一个切分点,确定一个超平面,这个超平面通过选定的切分点并垂直于选定的坐标轴,将当前超矩形区域切分为左右两个子区域(子结点);这时,实例被分到两个子区域,这个过程直到子区域内没有实例时终止(终止时的结点为叶节点)。在此过程中,将实例保存在相应的结点上。

通常,依次选择坐标轴对空间切分,选择训练实例点在选定坐标轴上的中位数为切分点,这样得到的kd树是平衡的,注意,平衡的kd树搜索时的效率未必是最优的。

构造平衡kd树伪代码

输入:k维空间数据集T={x1,x2,...,xN}T=\{x_1,x_2,...,x_N\}T={x1​,x2​,...,xN​},其中xi={xi(1),xi(2),...,xi(k)}T,i=1,2,...,Nx_i=\{x_i^{(1)},x_{i}^{(2)},...,x_i^{(k)}\}^T,i=1,2,...,Nxi​={xi(1)​,xi(2)​,...,xi(k)​}T,i=1,2,...,N;
输出:kd树;

  1. 开始:构造根结点,根结点对应于包含T的k维空间的超矩形区域;选择x(1)x^{(1)}x(1)作为坐标轴,以T中所有实例的x(1)x^{(1)}x(1)坐标的中位数为切分点,将根结点对应的超矩形区域切分为两个子区域,切分由通过切分点并与坐标轴x(1)x^{(1)}x(1)垂直的超平面实现;

    由根结点生成深度为1的左、右子结点:左子节点对应坐标x(1)x^{(1)}x(1)小于切分点的子区域,右子结点对应于坐标x(1)x^{(1)}x(1)大于切分点的子区域;

    将落在切分超平面上的实例点保存在根结点。

  2. 重复:对深度为j的结点,选择x(l)x^{(l)}x(l)作为切分的坐标轴,l=j(modk)+1l=j(mod\space k)+1l=j(mod k)+1,以该结点的区域中所有实例的x(l)x^{(l)}x(l)坐标为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴x(l)x^{(l)}x(l)垂直的超平面实现。

    由该结点生成深度为j+1j+1j+1的左、右子结点:左子结点对应坐标x(l)x^{(l)}x(l)小于切分点的子区域,右子结点对应坐标x(l)x^{(l)}x(l)大于切分点的子区域。

    将落在切分超平面上的实例点保存在该结点;

  3. 直到两个子区域没有实例存在时停止,从而形成kd树的区域划分;

其Python代码实现如下:

# 代码来源:百度百科(https://baike.baidu.com/item/kd-tree/2302515?fr=aladdin)
from numpy import *class KDNode(object):def __init__(self, value, split, left, right):# value=[x,y]self.value = value  # value是当前结点的数据self.split = split  # split用于保存切分的特征self.right = right  # 保存左子树self.left = left  ¥ 保存右子树class KDTree(object):def __init__(self, data):# data=[[x1,y1],[x2,y2]...,]# 维度k = len(data[0])  # x的特征个数def CreateNode(split, data_set):  # split用于选择切分特征,data_set是要构建kd树的数据if not data_set:  # 如果数据集为空,返回None,用于停止kd树的构建return Nonedata_set.sort(key=lambda x: x[split])  # 在数据x的split维度进行排序# 整除2split_pos = len(data_set) // 2  # 选择中间的数据进行划分median = data_set[split_pos]  # 获得划分的节点数据split_next = (split + 1) % k  # 切分特征进行更新return KDNode(median, split, CreateNode(split_next, data_set[: split_pos]),CreateNode(split_next, data_set[split_pos + 1:]))  # 返回构造完成的kd树,这里对左右子树同样使用CreateNode进行子树构建self.root = CreateNode(0, data)  # 开始构建kd树,以第一个特征为初始划分split

7.1.2 KD树的搜寻

当一个未知类别的样本进入到KD树后,就会自顶向下地流淌到对应的叶子结点,并开始反向计算最近邻的样本。有关KD树的搜寻步骤可以描述为:

  1. 将测试集中某个数据点与当前结点(例如根结点或者某个中间结点)所在轴的数据进行运算比较,如果位置类别的样本点所对应的轴数据小于等于当前结点的轴数据,则将该测试点流入当前结点的左侧子结点中,否则流入当前结点的右侧子结点中;
  2. 重复步骤1,直到未知类别的样本点落入对应的叶节点中,此时从叶节点中搜寻到“临时”的最近邻点,然后以未知类别的测试点为中心,以叶节点中的最近距离为半径,构成球体;
  3. 按照起始流淌的顺序原路返回,从叶节点返回到上一层的父结点,检查步骤2中的球体是否与父结点构成的分割线相交,如果相交,需要从父结点和对应的另一侧叶节点中重新搜寻最近邻点;
  4. 如果在步骤3中搜寻到比步骤2中的半径还小的新样本,则将其更新为当前最近邻点,并重新构造球体。否则,就返回父结点的父结点,重新检查球体是否与分割线相交;
  5. 不断重复迭代步骤3和步骤4,当寻找出来的球体与分割线都没有相交时则停止迭代;最终从所有已知类别的样本中搜寻出最新的近邻样本;
# 代码来源:百度百科(https://baike.baidu.com/item/kd-tree/2302515?
from numpy import *class KDNode(object):def __init__(self, value, split, left, right):# value=[x,y]self.value = value  # value是数据self.split = split  # split是数据划分的特征所在维度值self.right = rightself.left = leftclass KDTree(object):def __init__(self, data):# data=[[x1,y1],[x2,y2]...,]# 维度k = len(data[0])def CreateNode(split, data_set):if not data_set:return Nonedata_set.sort(key=lambda x: x[split])  # 在数据x的split维度进行排序# 整除2split_pos = len(data_set) // 2  # 选择中间的数据进行划分median = data_set[split_pos]  # 获得划分的节点数据split_next = (split + 1) % kreturn KDNode(median, split, CreateNode(split_next, data_set[: split_pos]),CreateNode(split_next, data_set[split_pos + 1:]))self.root = CreateNode(0, data)def search(self, root, x, count=1):  # KD树的搜寻代码实现# root为kd树的根结点# count是寻找离x最近的count个数据nearest = []  # nearest用于保存最近的count个点的信息for i in range(count):nearest.append([-1, None])self.nearest = np.array(nearest)def recurve(node):if node is not None:axis = node.split  # 获得当前结点的划分特征daxis = x[axis] - node.value[axis]  # 在对应维度进行判断if daxis < 0:  # x[axis] < node.value[axis]recurve(node.left)  # 数据进入左子树 else:recurve(node.right)  # 数据进入右子树dist = sqrt(sum((p1 - p2) ** 2 for p1, p2 in zip(x, node.value)))  # 计算两点之间的欧式距离for i, d in enumerate(self.nearest):if d[0] < 0 or dist < d[0]:  # 如果当前nearest内i处未标记(-1),或者新点与x距离更近self.nearest = np.insert(self.nearest, i, [dist, node.value], axis=0)  # 插入比i处距离更小的self.nearest = self.nearest[:-1]  # 插入一个数据就得删除一个数据break  # 当找到插入位置,则打断循环# 找到nearest集合里距离最大值的位置,为-1值的个数n = list(self.nearest[:, 0]).count(-1)print(list(self.nearest[:,0]),list(self.nearest[:,0]).count(-1))# 切分轴的距离比nearest中最大的小(存在相交)if self.nearest[-n - 1, 0] > abs(daxis):  # 存在某个点与父结点的切分轴有交点if daxis < 0:  # 相交,x[axis]< node.data[axis]时,去右边(左边已经遍历了)recurve(node.right)else:  # x[axis]> node.data[axis]时,去左边,(右边已经遍历了)recurve(node.left)recurve(root)return self.nearest  # 返回距离x最近的count个点的信息# 最近坐标点、最近距离和访问过的节点数
# result = namedtuple("Result_tuple", "nearest_point nearest_dist nodes_visited")data = [[2, 3], [5, 4], [9, 6], [4, 7], [8, 1], [7, 2]]
kd = KDTree(data)#[3, 4.5]最近的3个点
n = kd.search(kd.root, [3, 4.5], 3)
print(n)#[[1.8027756377319946 list([2, 3])]
#  [2.0615528128088303 list([5, 4])]
#  [2.692582403567252 list([4, 7])]]

简单理解上面的代码,已有构建好的kd树,目的是寻找kd树中与数据x欧式距离最近的count个数据;从根结点开始进行遍历,找到与x距离最近的叶子结点,计算叶子结点中与x欧氏距离最近的点,并保存两点之间的距离;

更新nearset中距离最近的count个点的信息,如果当前结点与x的距离与划分轴有交叉,则对其左/右子树进行遍历。

8、球树搜寻法

尽管kd树搜寻法相比于暴力搜寻法要快得多,但是该方法在搜寻分布不均匀的数据集时,效率会下降很多,因为根据结点切分的超矩形体都含有“角”。如果构成的球体与“角”相交,必然会使搜寻路径扩展到“角”相关的超矩形体内,从而增加搜寻的时间。

球树搜寻法之所以能够解决kd树的缺陷,是因为球树将kd树中的超矩形体换成了超球体,没有了角,就不容易产生模棱两可的区域。对比球树的构造和搜寻过程,会发现与kd树的思想非常相似,所不同的是,球体的最优搜寻路径复杂度提高了,但是可以避免很多无谓样本点的搜寻。

8.1 球体的构造

不同的超球体囊括了对应的样本点,超球体相当于树中的结点,所以构造球体的过程就是构造树的过程,关键点在于球心的寻找和半径的计算。

球树的构造如下

  1. 首先构建一个超球体,这个超球体的球心就是某线段的中心,而该线段就是求内所有训练样本点中两辆距离最远的线段,半径就是最远距离的一半,从而得到的超球体就是囊括所有样本点的最小球体;
  2. 然后从超球体内寻找离球心最远的点p1p_1p1​,接着寻找离点p1p_1p1​最远的点p2p_2p2​,以这两个点为簇心,通过距离的计算,将剩余的样本点划分到对应的簇中心,从而得到两个数据块;
  3. 最后重复步骤1,将步骤2中的两个数据块构造成对应的最小球体,直到球体无法继续划分为止;

从上面的步骤可知,球树的根结点就是囊括所有训练数据集的最小超球体,根结点的两个子结点就是由步骤2中两个数据块构成的最小超球体。以此类推,可以不停地将数据划分到对应的最小超球体中,最终形成一颗球树;

8.2 球树的搜寻

球树在搜寻最近邻样本时与KD树非常相似,下面详细介绍球树在搜寻过程中的具体步骤:

  1. 从球树的顶端到底端,寻找能够包含未知类别样本点所属的叶节点,并从叶结点的球体中寻找离未知类别样本点最近的点,得到相应的最近距离d;
  2. 回流到另一支的叶结点中,此时不再比较未知类别样本点与叶结点中的其它样本点之间的距离,而是计算未知类别样本点与叶结点对应的球心距离D;
  3. 比较距离d、D和步骤2中叶结点球体的半径r,如果D>d+rD>d+rD>d+r,则说明无法从叶结点中找到未知类别样本点更近的点;如果D<d+rD<d+rD<d+r,则需要回流到上一层父结点所对应的球体,并从球体中搜寻更近的样本点;
  4. 重复步骤2和3,直到回流至根结点,最终搜寻到离未知类别样本点最近的样本;

球树搜寻法的Python代码实现:

# 代码来源于https://github.com/qzq2514/KDTree_BallTree
import numpy as np
import pandas as pd
from collections import Counter
import timeallow_duplicate = False
def load_data(csv_path):data = pd.read_csv(csv_path,sep=";")# data = data.sample(frac=1)# data = data.reset_index(drop=True)label = data["quality"]data = data.drop(["quality"], axis=1)return data.values,label,data.columns.valuesclass Ball():def __init__(self,center,radius,points,left,right):self.center = center  #使用该点即为球中心,而不去精确地去找最小外包圆的中心self.radius = radius  # 球半径self.left = left  # 左子球体self.right = right  # 右子球体self.points = pointsclass BallTree():def __init__(self,values,labels):self.values = values  # 训练数据self.labels = labels  # 训练数据的labelif(len(self.values) == 0 ):raise Exception('Data For Ball-Tree Must Be Not empty.')self.root = self.build_BallTree()self.KNN_max_now_dist = np.inf  # 距离为无穷大self.KNN_result = [(None,self.KNN_max_now_dist)]def build_BallTree(self):data = np.column_stack((self.values,self.labels))return self.build_BallTree_core(data)def dist(self,point1,point2):return np.sqrt(np.sum((point1-point2)**2))#data:带标签的数据且已经排好序的def build_BallTree_core(self,data):if len(data) == 0:return Noneif len(data) == 1:return Ball(data[0,:-1],0.001,data,None,None)#当每个数据点完全一样时,全部归为一个球,及时退出递归,不然会导致递归层数太深出现程序崩溃data_disloc =  np.row_stack((data[1:],data[0]))if np.sum(data_disloc-data) == 0:return Ball(data[0, :-1], 1e-100, data, None, None)cur_center = np.mean(data[:,:-1],axis=0)     #当前球的中心dists_with_center = np.array([self.dist(cur_center,point) for point in data[:,:-1]])     #当前数据点到球中心的距离max_dist_index = np.argmax(dists_with_center)        #取距离中心最远的点,为生成下一级两个子球做准备,同时这也是当前球的半径max_dist = dists_with_center[max_dist_index]root = Ball(cur_center,max_dist,data,None,None)point1 = data[max_dist_index]dists_with_point1 = np.array([self.dist(point1[:-1],point) for point in data[:,:-1]])max_dist_index2 = np.argmax(dists_with_point1)point2 = data[max_dist_index2]            #取距离point1最远的点,至此,为寻找下一级的两个子球的准备工作搞定dists_with_point2 = np.array([self.dist(point2[:-1], point) for point in data[:, :-1]])assign_point1 = dists_with_point1 < dists_with_point2root.left = self.build_BallTree_core(data[assign_point1])root.right = self.build_BallTree_core(data[~assign_point1])return root    #是一个Balldef search_KNN(self,target,K):if self.root is None:raise Exception('KD-Tree Must Be Not empty.')if K > len(self.values):raise ValueError("K in KNN Must Be Greater Than Lenght of data")if len(target) !=len(self.root.center):raise ValueError("Target Must Has Same Dimension With Data")self.KNN_result = [(None,self.KNN_max_now_dist)]self.nums = 0self.search_KNN_core(self.root,target,K)return self.nums# print("calu_dist_nums:",self.nums)def insert(self,root_ball,target,K):for node in root_ball.points:self.nums += 1is_duplicate = [self.dist(node[:-1], item[0][:-1]) < 1e-4 andabs(node[-1] - item[0][-1]) < 1e-4 for item in self.KNN_result if item[0] is not None]if np.array(is_duplicate, np.bool).any() and not allow_duplicate:continuedistance = self.dist(target,node[:-1])if(len(self.KNN_result)<K):self.KNN_result.append((node,distance))elif distance < self.KNN_result[0][1]:self.KNN_result = self.KNN_result[1:] + [(node, distance)]self.KNN_result = sorted(self.KNN_result, key=lambda x: -x[1])#root是一个Balldef search_KNN_core(self,root_ball, target, K):if root_ball is None:return#在合格的超体空间(必须是最后一层的子空间)内查找更近的数据点if root_ball.left is None or root_ball.right is None:self.insert(root_ball, target, K)if abs(self.dist(root_ball.center,target)) <= root_ball.radius + self.KNN_result[0][1] : #or len(self.KNN_result) < Kself.search_KNN_core(root_ball.left,target,K)self.search_KNN_core(root_ball.right,target,K)if __name__ == '__main__':csv_path = "winequality-white.csv"data,lables,dim_label = load_data(csv_path)split_rate = 0.8K=5train_num = int(len(data)*split_rate)  # 80%训练数据print("train_num:",train_num)start1 = time.time()ball_tree = BallTree(data[:train_num], lables[:train_num])end1 = time.time()diff_all=0accuracy = 0search_all_time = 0calu_dist_nums = 0for index,target in enumerate(data[train_num:]):start2 = time.time()calu_dist_nums+=ball_tree.search_KNN(target, K)end2 = time.time()search_all_time += end2 - start2# for res in ball_tree.KNN_result:#     print("res:",res[0][:-1],res[0][-1],res[1])pred_label = Counter(node[0][-1] for node in ball_tree.KNN_result).most_common(1)[0][0]diff_all += abs(lables[index] - pred_label)if (lables[index] - pred_label) <= 0:accuracy += 1print("accuracy:", accuracy / (index + 1))print("Total:{},MSE:{:.3f}    {}--->{}".format(index + 1, (diff_all / (index + 1)), lables[index],pred_label))print("BallTree构建时间:", end1 - start1)print("程序运行时间:", search_all_time/len(data[train_num:]))print("平均计算次数:", calu_dist_nums / len(data[train_num:]))#暴力KNN验证# KNN_res=[]# for index2,curData in enumerate(data[:train_num]):#     is_duplicate = [ball_tree.dist(curData,v[0])<1e-4 for v in KNN_res]#     if np.array(is_duplicate,np.bool).any() and not allow_duplicate:#         continue#     cur_dist = ball_tree.dist(curData,target)#     if len(KNN_res) < K:#        KNN_res.append((curData,lables[index2],cur_dist))#     elif cur_dist<KNN_res[0][2]:#         KNN_res = KNN_res[1:]+[(curData,lables[index2],cur_dist)]#     KNN_res=sorted(KNN_res, key=lambda x: -x[2])# pred_label2 = Counter(node[1] for node in KNN_res).most_common(1)[0][0]# for my_res in KNN_res:#     print("res:",my_res[0],my_res[1],my_res[2])# print("--------------{}--->{} vs {}------------------".format(lables[index],pred_label,pred_label2))

9、KNN模型的应用

KNN算法是一个非常优秀的数据挖掘模型,既可以解决离散型因变量的分类问题,也可以处理连续性因变量的预测问题。而且该算法对数据的分布特征没有任何要求。

Python中的sklearn模块提供了有关KNN算法实现分类和预测的功能,该功能存在于子模块nerghbors中,对于分类问题,需要调用KNeighborsClassifer类,而对于预测问题,则需要调用KNeighborRegressor类。

首先针对这两个类的语法和参数含义做详细描述:

from sklearn import neighborsneighbors.KNeighborsClassifier(neighbors=5,weights='uniform',algorithm='auto',leaf_size=30,p=2,metric='minkowskl',metric_params=None,n_jobs=1)neighbors.KNeighborsRegressor(neighbors=5,weights='uniform',algorithm='auto',leaf_size=30,p=2,metric='minkowskl',metric_params=None,n_jobs=1)

上述类中的参数的具体作用如下:

  • n_neighbor:用于指定近邻样本个数K,默认为5;
  • weights:用于指定近邻样本的投票权重,默认为’uniform’,表示所有近邻样本的投票权重一样;如果为’distance’,则表示投票权重与距离成反比,即近邻样本与未知类别的样本距离越远,权重越小,反之权重越大;
  • algorithm:用于指定近邻样本的搜寻算法,如果为’ball_tree’,则表示使用球树搜寻法找近邻样本;如果为’kd_tree’,则表示使用KD树搜寻法寻找近邻样本;如果为’brute’,则表示使用暴力搜寻法寻找近邻样本。默认为’auto’,表示KNN算法会根据数据特征自动选择最佳的搜寻算法;
  • leaf_size:用于指定球树或KD树叶子结点所包含的最小样本量,它用于控制树的生长条件,会影响树的查询速度,默认为30;
  • metric:用于指定距离的度量指标,默认为闵可夫斯基距离;
  • p:当参数metric为闵可夫斯基距离时,p=1,表示计算点之间的曼哈顿距离;p=2表示计算点之间的欧式距离,参数P的默认值为2;
  • metric_params:为metric参数所对应的距离指标添加关键字参数;
  • n_jobs:用于设置KNN算法并行计算所需的CPU数量,默认为1表示仅适用1个CPU运行算法,即不使用并行运算功能;

10、参考资料:

  1. kd树代码:https://baike.baidu.com/item/kd-tree/2302515?
  2. 球树搜寻法代码:https://github.com/qzq2514/KDTree_BallTree
  3. 《从零开始学Python —— 数据分析与挖掘》—— 刘顺祥;
  4. 《统计学习方法》—— 李航;

有监督学习 —— KNN算法相关推荐

  1. 机器学习-有监督学习-分类算法:k-近邻(KNN)算法【多分类】

    一.K-近邻算法简介 1.K-近邻算法(KNN)概念 k-近邻算法:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别. 相似的样本,同一 ...

  2. 教你用OpenCV实现机器学习最简单的k-NN算法

    前言:OpenCV 的构建是为了提供计算机视觉的通用基础接口,现在已经成为经典和最优秀的计算机视觉和机器学习的综合算法工具集.作为一个开源项目,研究者.商业用户和政府部门都可以轻松利用和修改现成的代码 ...

  3. KNN算法的机器学习基础

    KNN算法的机器学习基础 https://mp.weixin.qq.com/s/985Ym3LjFLdkmqbytIqpJQ 本文原标题 : Machine Learning Basics with ...

  4. 通过KNN算法,确定球星的风格(很水)

    KNN算法,故名思议,K个最邻近值的分类算法.监督学习中的一种,典型的懒人算法,通过计算所有的预测样本到学习样本的距离,选取其中K个最小值加入样本组中,样本组中的样本隶属于那个分类的个数最多,那么我们 ...

  5. 机器学习之kNN算法(纯python实现)

    前面文章分别简单介绍了线性回归,逻辑回归,贝叶斯分类,并且用python简单实现.这篇文章介绍更简单的 knn, k-近邻算法(kNN,k-NearestNeighbor). k-近邻算法(kNN,k ...

  6. 【机器学习入门】图解超经典的KNN算法

    出品:Python数据之道(ID:PyDataLab) 作者:Peter,来自读者投稿 编辑:Lemon 图解超经典的KNN算法 本文中介绍的机器学习算法中的一种监督学习的算法:KNN 算法,全称是 ...

  7. knn算法python代码_K-最近邻分类算法(KNN)及python实现

    一.引入 问题:确定绿色圆是属于红色三角形.还是蓝色正方形? KNN的思想: 从上图中我们可以看到,图中的数据集是良好的数据,即都打好了label,一类是蓝色的正方形,一类是红色的三角形,那个绿色的圆 ...

  8. 机器学习的练功方式(四)——KNN算法

    文章目录 致谢 致歉 4 KNN算法 4.1 sklearn转换器和估计器 4.1.1 转换器 4.1.2 估计器 4.2 KNN算法 4.2.1 概述 4.2.2 电影类型分析 4.2.3 算法实现 ...

  9. svm多分类代码_监督学习——分类算法I

    本文是监督学习分类算法的第一部分,简单介绍对样本进行分类的算法,包括 判别分析(DA) 支持向量机(SVM) 随机梯度下降分类(SGD) K近邻分类(KNN) 朴素贝叶斯分类(NaiveBayes) ...

最新文章

  1. 常用的Mybatis-Plus方法,让你的数据库增删改查(CRUD)一键实现
  2. 用python画简单的动物-使用Python的turtle画小绵羊
  3. 【华为】华为模拟器模拟静态、动态NAT、PAT技术
  4. redis 主从配置_Laravel 使用Redis缓存集群,主从集群配置就这么简单?
  5. 【Linux入门基础知识】Linux 脚本编写基础
  6. HDU-2072 单词数 水题一枚
  7. 平安性格测试题及答案_性格趣味小测试题 有趣的心理测试题大全及答案
  8. C++ 操作sqlite
  9. AI学习笔记(三)特征选择与提取、边缘提取
  10. 一些自成系统、完备的教程(链接、博客、github等)
  11. UCI机器学习数据库
  12. DHT11温湿度传感器(基于树莓派)
  13. JAVA如何利用迅雷接口解析磁力,关于神秘代码(磁力链接)如何正确使用
  14. MessageBox提示框自动关闭
  15. 【微服务】链路追踪 jaeger
  16. DevOps is dirty work - CI drives you crazy
  17. Android源码在线查看网站
  18. 计算机网络DV和LS
  19. 计算机国际会议排名,计算机学科国际会议排名
  20. Erika企业版实时操作系统

热门文章

  1. lvs+keepalived实现lvs nat模式热备配置
  2. 面向对象软件设计原则(一) —— 引子
  3. PPT(十)-动画基础知识学习
  4. TBXML常用API
  5. 无盘XP系统全套安装说明
  6. 面试官系统精讲Java源码及大厂真题 - 21 DelayQueue 源码解析
  7. 面试官系统精讲Java源码及大厂真题 - 07 List 源码会问哪些面试题
  8. Spring安全示例教程
  9. JSP+Servlet+C3P0+Mysql实现的azhuo商城
  10. CCF 201412-1 门禁系统