前言
Knn算法和K-means算法是经常混淆的两个算法,本文主要针对Knn算法和K-means原理和代码实现,从而进一步区分了两种算法,并且加深算法的理解.
1. k 近邻模型
目的: k k k近邻法是基本且简单的分类与回归方法。 k k k近邻法的基本做法是:对给定的训练实例点和输入实例点,首先确定输入实例点的 k k k个最近邻训练实例点,然后利用这 k k k个训练实例点的类的多数来预测输入实例点的类。(有监督算法)
1.1.1 算法
输入: T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , … , ( x N , y N ) } T=\{(x_1,y_1),(x_2,y_2),\dots,(x_N,y_N)\} T={(x1,y1),(x2,y2),…,(xN,yN)}, x_i\in \cal{X}\sube{\bf{R}^n}, y_i\in\cal Y = { c 1 , c 2 , … , c k } {Y}=\{c_1,c_2,\dots, c_k\} Y={c1,c2,…,ck}; 实例特征向量 x x x
输出: 实例所属的 y y y
步骤:
-
根据指定的距离度量,在 T T T中查找 x x x的最近邻的 k k k个点,覆盖这 k k k个点的 x x x的邻域定义为 N k ( x ) N_k(x) Nk(x)
-
在 N k ( x ) N_k(x) Nk(x)中应用分类决策规则决定 x x x的类别 y y y(多数表决)
y = arg max c j ∑ x i ∈ N k ( x ) I ( y i = c j ) , i = 1 , 2 , … , N , j = 1 , 2 , … , K y=\arg\max_{c_j}\sum_{x_i\in N_k(x)}I(y_i=c_j), i=1,2,\dots,N, j=1,2,\dots,K y=argcjmaxxi∈Nk(x)∑I(yi=cj),i=1,2,…,N,j=1,2,…,K
这里提到了 k k k近邻模型的三要素,如算法描述中黑体标注的部分, 注意这里的三要素和前面说的统计学习方法的三要素不是一个东西。
1.1.2 模型
k近邻法实际上利用训练数据集对特征向量空间的进行划分并作为其分类器的模型,下图描述了具有两个特征维度的数据的Knn模型的建立.其中,每一个样本具有一个细胞单元,每一个细胞单元具有自己的标签.
Knn模型
1.1.3 距离度量
特征空间中的两个实例点的距离是两个实例点***相似度程度***的反应,设特征空间 x x x是 n n n维实数向量空间 , x i , x j ∈ X x_{i}, x_{j} \in \mathcal{X} xi,xj∈X, x i = ( x i ( 1 ) , x i ( 2 ) , ⋯ , x i ( n ) ) T x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(n)}\right)^{\mathrm{T}} xi=(xi(1),xi(2),⋯,xi(n))T, x j = ( x j ( 1 ) , x j ( 2 ) , ⋯ , x j ( n ) ) T x_{j}=\left(x_{j}^{(1)}, x_{j}^{(2)}, \cdots, x_{j}^{(n)}\right)^{\mathrm{T}} xj=(xj(1),xj(2),⋯,xj(n))T,则: x i x_i xi, x j x_j xj的 L p L_p Lp距离定义为:
L p ( x i , x j ) = ( ∑ i = 1 n ∣ x i ( i ) − x j ( l ) ∣ p ) 1 p L_{p}\left(x_{i}, x_{j}\right)=\left(\sum_{i=1}^{n}\left|x_{i}^{(i)}-x_{j}^{(l)}\right|^{p}\right)^{\frac{1}{p}} Lp(xi,xj)=(∑i=1n∣∣∣xi(i)−xj(l)∣∣∣p)p1
-
p = 1 p= 1 p=1 曼哈顿距离
-
p = 2 p= 2 p=2 欧氏距离
-
p = i n f p= inf p=inf 闵式距离
-
例题3.1:
描述:已知二维空间的3个点 x 1 = ( 1 , 1 ) T , x 2 = ( 5 , 1 ) T , x 3 = ( 4 , 4 ) T x_1 = (1, 1)^T, x_2 = (5, 1)^T, x3 = (4, 4)^T x1=(1,1)T,x2=(5,1)T,x3=(4,4)T,试求在 p p p取得不同值时, L p L_p Lp距离下的 x 1 x_1 x1的最近邻点。
#1.计算欧式距离
import math
from itertools import combinations
def L(x, y, p=2):
if len(x) == len(y) and len(y) > 1:
sum = 0
for i in range(len(x)):
sum += math.pow(abs(x[i]-y[i]), p)
return math.pow(sum , 1/p)
else:
return 0
#2. 数据准备
x1 = [1, 1]
x2 = [5, 1]
x3 = [4, 4]
#3.输入数据
for i in range(1, 5):
r = {'1-{}'.format(c):L(x1, c, p = i) for c in [x2, x3]}#字典
print(min(zip(r.values(), r.keys())))
print(r,i)
print("-"*10)
- 输出结果
p = 1,x1的最近邻点: (4.0, '[5, 1]')
p = 2,x1的最近邻点: (4.0, '[5, 1]')
p = 3,x1的最近邻点: (3.7797631496846193, '[4, 4]')
p = 4,x1的最近邻点: (3.5676213450081633, '[4, 4]')
1.1.4 knn算法的构造
-
加载数据并显示化
import numpy as np from collections import Counter from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split import matplotlib.pyplot as plt # 使用鸢尾花数据集的前两个特征和前一百个样本,这样有利于数据的可视化 iris = load_iris() X = iris.data[:, :2][:100] Y = iris.target[:100] # 划分数据集 X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size = 0.3) plt.scatter(X[Y==0, 0], X[Y==0, 1], label = "0") plt.scatter(X[Y==1, 0], X[Y==1, 1], label = "1") plt.xlabel("sepal length (cm)") plt.ylabel('sepal width (cm)') plt.legend() plt.savefig("Imgs/data.png") plt.show()
显示:
-
Knn算法的python实现
class Knn: def __init__(self, X_train, Y_train, n_neighbors=3, p=2): self.n = n_neighbors self.p = p #距离衡量 self.X_train = X_train self.y_train = Y_train def predct(self, X): """单个样本的预测效果""" knn_list = [] #存储k个邻节点 # 先取出n个点 for i in range(self.n): dist = np.linalg.norm(X - self.X_train[i], ord=self.p) knn_list.append((dist, self.y_train[i])) # 求解最近的n个样本 for i in range(self.n, len(self.X_train)): # 取出最大距离的index max_index = knn_list.index(max(knn_list, key=lambda x: x[0])) dist = np.linalg.norm(X - self.X_train[i], ord=self.p) if knn_list[max_index][0] > dist: knn_list[max_index] = (dist, self.y_train[i]) # 统计标签值 knn = [k[-1] for k in knn_list] count = Counter(knn) return sorted(count.items(), key= lambda x:x[1])[-1][0] def score(self, X_test, Y_test): """得到评测分数""" right_count = 0 n = 10 for X, Y in zip(X_test, Y_test): label = self.predct(X) if label == Y: right_count += 1 return right_count /len(X_test)
-
测试
单个样本的测试
knn = Knn(X_train, Y_train, n_neighbors=2, p=2) predict_point = [5.0, 2.6] print("预测点:{}\n预测标签:{}".format(predict_point, knn.predct(predict_point))) plt.scatter(X[Y==0, 0], X[Y==0, 1], label = "0") plt.scatter(X[Y==1, 0], X[Y==1, 1], label = "1") plt.scatter(predict_point[0], predict_point[1], label = "pred_point", c="green") plt.xlabel("sepal length (cm)") plt.ylabel('sepal width (cm)') plt.legend() plt.savefig("./Imgs/pred_sample.png") plt.show()
预测点:[5.0, 2.6]
预测标签:1
测试集预测
print("score:{}".format(knn.score(X_test, Y_test)*100))
score:100
-
k值的选择
在Knn算法中k值的选择对于k近邻法的结果产生重大的影响,如果选择较小的k值,就相当于用较小的领域的训练实例进行预测,k值的减小就意味着这整体模型变得复杂,容易发生过拟合.如果选择较大的k值,就相当于用较大的领域的训练实例进行预测,k值越大意味着整体的模型变得简单.下面对上述算法中的k值进行选择,并且观测其准确率随k值的变化情况
# k值的影响 max_n_neighbors = 10 score = [] for i in range(1, max_n_neighbors): knn = Knn(X_train, Y_train, n_neighbors=i, p=2) score.append(round(knn.score(X_test, Y_test) ,2)) plt.plot(range(1, max_n_neighbors), score) plt.xlabel("k") plt.ylabel("accuracy") plt.title("The accuracy in different k value") plt.savefig("./Imgs/accuracy.png") plt.show()
从图中可以看出较小的k值的估计误差较大,而随着k值增大,模型的准确率也在上升,但是k值不能无限增大,k值越大意味着模型越简单.所以,在实际的应用中,一般使用交叉验证的方法进行k值的选择.
2.1 构建kd-tree
**描述:**kd树是一种对k维空间中的实例点进行存储以便对其进行快速检索的树形数据结构。
算法描述:
输入: k k k维空间数据集 T = { x 1 , x 2 , … , x N } T=\{x_1,x_2,…,x_N\} T={x1,x2,…,xN},
其中 x i = ( x i ( 1 ) , x i ( 2 ) , ⋯ , x i ( k ) ) T x_{i}=\left(x_{i}^{(1)}, x_{i}^{(2)}, \cdots, x_{i}^{(k)}\right)^{\mathrm{T}} xi=(xi(1),xi(2),⋯,xi(k))T , i = 1 , 2 , … , N i=1,2,…,N i=1,2,…,N;
输出:kd树。
(1)开始:构造根结点,根结点对应于包含 T T T的 k k 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 j j的结点,选择 x ( 1 ) x^{(1)} x(1)为切分的坐标轴, l = j ( m o d k ) + 1 l=j(modk)+1 l=j(modk)+1,以该结点的区域中所有实例的 x ( 1 ) x^{(1)} x(1)坐标的中位数为切分点,将该结点对应的超矩形区域切分为两个子区域。切分由通过切分点并与坐标轴 x ( 1 ) x^{(1)} x(1)垂直的超平面实现。
由该结点生成深度为 j + 1 j+1 j+1的左、右子结点:左子结点对应坐标 x ( 1 ) x^{(1)} x(1)小于切分点的子区域,右子结点对应坐标 x ( 1 ) x^{(1)} x(1)大于切分点的子区域。
将落在切分超平面上的实例点保存在该结点。
(3)直到两个子区域没有实例存在时停止。从而形成kd树的区域划分。
- 例3.2:
问题描述了:给定一个二维空间的数据集:
T = { ( 2 , 3 ) T , ( 5 , 4 ) T , ( 9 , 6 ) T , ( 4 , 7 ) T , ( 8 , 1 ) T , ( 7 , 2 ) T } T = \{(2, 3)^T, (5, 4)^T, (9, 6)^T, (4, 7)^T, (8, 1)^T, (7, 2)^T\} T={(2,3)T,(5,4)T,(9,6)T,(4,7)T,(8,1)T,(7,2)T}
构造一个平衡kd树。
代码:
# kd-tree 每个结点中主要包含的数据如下:
class KdNode(object):
def __init__(self, dom_elt, split, left, right):
self.dom_elt = dom_elt#结点的父结点
self.split = split#划分结点
self.left = left#做结点
self.right = right#右结点
class KdTree(object):
def __init__(self, data):
k = len(data[0])
def CreateNode(split, data_set):
if len(data_set)==0:#数据集为空
return None
data_set.sort(key=lambda x:x[split])#开始找切分平面的维度
split_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:]))#创建右结点
self.root = CreateNode(0, data)#创建根结点
#KDTree的前序遍历
def preorder(root):
if root is None:
print("^", end="")
return
print(" (" + str(root.dom_elt), end="")
if root.left:
preorder(root.left)
if root.right:
preorder(root.right)
print(") ", end="")
if __name__ == "__mian__":
data = [[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]]
kd=KdTree(data)
preorder(kd.root)
输出结果:
([7, 2] ([5, 4] ([2, 3]) ([4, 7]) ) ([9, 6] ([8, 1]) ) )
kd 树构建图
![]() | ![]() |
---|---|
特征空间划分 | kd树实例 |
3.1 搜索kd树
算法描述:
输入:已构造的kd 树;目标点x;
输出:x 的最近邻
(1)在kd树中找出包含目标点 x x x的叶结点:
(2)以此叶节点为’当前最近点’
(3)递归向上回退,在每个节点进行以下操作:
(a)如果该结点保存的实例点比当前最近点距离目标点更近,则以该实例点为’当前最近点’
(b) 检查另一子结点对应的区域是否以目标点为球心,以目标点与’当前最近点’间的距离为半径的超球体相交。
如果相交,则在另一个子结点对应的区域存在距离目标点更近的点,移动到另一个子结点。接着,递归的进行最近邻搜索。
若不相交,则向上回退
(4)当回退到根结点时,搜索结束,最后的‘当前最近点’即为x的最近邻点。
- 代码描述:
from math import sqrt
from collections import namedtuple
# 定义一个namedtuple,分别存放最近坐标点、最近距离和访问过的节点数
result = namedtuple("Result_tuple",
"nearest_point nearest_dist nodes_visited")
#搜索开始
def find_nearest(tree, point):
k = len(point)#数据维度
def travel(kd_node, target, max_dist):
if kd_node is None:
return result([0]*k, float("inf"), 0)#表示数据的无
nodes_visited = 1
s = kd_node.split #数据维度分隔
pivot = kd_node.dom_elt #切分根节点
if target[s] <= pivot[s]:
nearer_node = kd_node.left #下一个左结点为树根结点
further_node = kd_node.right #记录右节点
else: #右面更近
nearer_node = kd_node.right
further_node = kd_node.left
temp1 = travel(nearer_node, target, max_dist)
nearest = temp1.nearest_point# 得到叶子结点,此时为nearest
dist = temp1.nearest_dist #update distance
nodes_visited += temp1.nodes_visited
print("nodes_visited:", nodes_visited)
if dist < max_dist:
max_dist = dist
temp_dist = abs(pivot[s]-target[s])#计算球体与分隔超平面的距离
if max_dist < temp_dist:
return result(nearest, dist, nodes_visited)
# -------
#计算分隔点的欧式距离
temp_dist = sqrt(sum((p1-p2)**2 for p1, p2 in zip(pivot, target)))#计算目标点到邻近节点的Distance
if temp_dist < dist:
nearest = pivot #更新最近点
dist = temp_dist #更新最近距离
max_dist = dist #更新超球体的半径
print("输出数据:" , nearest, dist, max_dist)
# 检查另一个子结点对应的区域是否有更近的点
temp2 = travel(further_node, target, max_dist)
nodes_visited += temp2.nodes_visited
if temp2.nearest_dist < dist: # 如果另一个子结点内存在更近距离
nearest = temp2.nearest_point # 更新最近点
dist = temp2.nearest_dist # 更新最近距离
return result(nearest, dist, nodes_visited)
return travel(tree.root, point, float("inf")) # 从根节点开始递归
if __name__ == "__main__":
data = [[2,3],[5,4],[9,6],[4,7],[8,1],[7,2]]
kd = KdTree(data)
preorder(kd.root)
ret = find_nearest(kd, [3, 4.5])
print(ret)
- 输出结果
Result_tuple(nearest_point=[2, 3], nearest_dist=1.8027756377319946, nodes_visited=4)
- 例3.3
描述:如图所示的kd树,根结点为A, 其子节点为B,C等,树上一个存储7个实例点;另有一个输入目标实例点S,求S的最近邻。
2. KMeans算法
在无监督学习中,训练样本的标记信息是未知的,目标是通过对无标记的训练样本的学习来揭示数据的内在性质及规律,为进一步的数据分析提供基础,此类学习任务中研究最多,应用最广的是"聚类".聚类试图将数据集中的样本划分成为若干个通常不想交的子集,每个子集称为一个"簇",下面介绍KMeans算法.
2.1 算法原理
KMeans核心的任务就是根据我们设定好的k,找出k个最优的质心,并将离这些质心最近的数据分别分配到这些质心代表的簇中去,最终被分到同一个簇中的数据是有相似性的,而不同簇中的数据是不同的,具体算法如下:
- 随机抽取k(最终要聚合的簇类个数,根据实际情况,人为设定)个样本作为最初的质心
- 将每一个样本分配到离它们最近的质心,生成k个簇
- 对于每一个簇,计算所有被分到该簇的样本点的平均值作为新的质心
- 重复执行2,3,当质心的位置不在发生改变,迭代停止,聚类完成
2.2 KMeans聚类的效果
处理原始数据
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from matplotlib import cm
# 制造数据集
X, y = make_blobs(n_samples=500, n_features=2, centers=4, random_state=1)
# 两个维度数据可视化
fig, ax = plt.subplots(1, figsize=(12, 6))
ax.scatter(X[:, 0]
, X[:,1]
, marker = "o"
, s = 8
)
plt.show()
上图是随机构造的一个二维数据分布图,从图中可以大概看出原始数据呈现聚堆的现象,从宏观上看数据可以分为两个大的类别或者三个类别.
k值的选择
下面是使用KMeans算法探索数据的内在关联的性质.
# 两类数据特征的分布
n_clusters = 4
cluster = KMeans(n_clusters=n_clusters, random_state=0).fit(X)
center = cluster.cluster_centers_
colors = ["red", "pink", "blue", "yellow"]
fig, ax = plt.subplots(1, figsize = (12, 6))
# 画出数据分布
for i in range(n_clusters):
ax.scatter(X[cluster.labels_ == i, 0], X[cluster.labels_ == i, 1]
, c=colors[i]
, marker="o"
, s = 8
)
# 画出簇的质心的位置
ax.scatter(center[:, 0], center[:, 1]
, marker="x"
, s = 18
, c="black"
)
plt.xlabel("feature1")
plt.ylabel("feature2")
plt.title("the cluster in different n_clusters")
plt.show()
下图中对应不同的k值,对数据进行了了不同类别聚合,其中每一个簇的质心由黑色星点表示,我们可以根据不同的需求把数据分成合适的簇类来研究各个簇类之间的关系.
![]() | ![]() |
---|---|
n_clusters = 1 | n_clusters = 2 |
![]() | ![]() |
n_clusters = 3 | n_clusters = 4 |
衡量模型的好坏
如果我们在使用KMeans算法时,事先不知道簇的个数,希望通过算法能够给我们输出一个较好聚类效果,这就需要我们使用一个模型评判的指标,在这里使用轮廓系数作为我们的评判标准(在KMeans中的实际可以使用inertia来作为衡量,但是inertia是一个没有边界的值),轮廓系数的值域为(-1, 1),轮廓系数越接近1模型越好.
下图左图是基于inertia做为模型的指标,从图中可以观察可以看出,随着k值的增大,inertia值不断减少,但是k值越大并不意味着模型的效果就会变好.而在右图中,基于silhouette_score的模型在k值为4时,出现了一个峰值,其值接近与1,而后随k值的增大,轮廓系数的值不断较小,模型的聚合效果差.所以,数据可以聚合成4簇有较好的效果,分类效果如上图n_clusters = 4.
![]() | ![]() |
---|---|
inertia | silhouette_score |
2.3 KMeans的应用
目的:非结构化数据往往占用比较 多的储存空间,文件本身也会比较大,运算非常缓慢,我们希望能够在保证数据质量的前提下,尽量地缩小非结构 化数据的大小,或者简化非结构化数据的结构
-
加载图像
import numpy as np import matplotlib.pyplot as plt import pandas as pd from sklearn.cluster import KMeans from sklearn.datasets import load_sample_image from sklearn.utils import shuffle %matplotlib inline china = load_sample_image("china.jpg") plt.figure(figsize=(12, 6)) plt.axis("off") plt.imshow(china) plt.show()
-
矢量量化
n_cluster = 64 china = np.array(china, dtype=np.float64)/china.max() w, h, d = oeiginal_shape = tuple(china.shape) assert d == 3 image_array = np.reshape(china, (w*h, d)) # 数据量化, 当数据量过大时,可以使用部分数据 # 进行训练得到各个簇的质心,而后用全部数据进行 # 的预于每一个样本属于哪一个簇 image_array_sample = shuffle(image_array, random_state=0)[:1000] kmeans = KMeans(n_clusters=n_cluster, random_state=0).fit(image_array_sample) label = kmeans.predict(image_array) # 质心的映射 image_kmeans = image_array.copy() for i in range(w*h): # 将每一个像素值替换为所在簇的质心 image_kmeans[i] = kmeans.cluster_centers_[label[i]] image_kmeans = image_kmeans.reshape(w, h, d) plt.figure(figsize=(12, 6)) plt.axis("off") plt.imshow(image_kmeans) plt.show()
下图KMeans矢量量化得到压缩图片,可以看出图片的失真率较低,图片色彩也没有丢失太多.