1.KNN算法原理
k k k近邻算法在1968年由Cover和Hart提出,它非常简单、直观:给定一个训练数据集,对新的输入实例,在训练数据集中找到与该输入实例最邻近的 k k k个实例,这 k k k个实例的多数属于某个类别,就把输入实例分为这个类别。如下图所示:
以下是来自李航《统计学习方法》对于
k
k
k近邻算法的论述:
输入:实例 x x x特征向量;训练数据集 T = { ( x 1 , y 1 ) , ( x 2 , y 2 ) , ( x 3 , y 3 ) , . . . , ( x N , y N ) } T = \left \{ (x_1,y_1),(x_2,y_2),(x_3,y_3),...,(x_N,y_N) \right \} T={(x1,y1),(x2,y2),(x3,y3),...,(xN,yN)}
其中 x i ∈ X ⊆ R n x_i∈X\subseteq R^n xi∈X⊆Rn 为实例的特征向量, y i ∈ Y = { c 1 , c 2 , c 3 , . . . , c K } y_i∈Y= \left \{ c_1,c_2,c_3,...,c_K \right \} yi∈Y={c1,c2,c3,...,cK}为实例类别。
输出:实例 x x x所属分类 y y y。
(1)根据给定的距离度量,在训练集 T T T中找到与实例 x x x最近邻的 k k k个点,将涵盖这 k k k个点的邻域记作 N k ( x ) N_k(x) Nk(x);
(2)在 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 ) y = \mathop{\arg\max}_{c_j} \sum_{x_i\in N_k(x)}^{} I(y_i=c_j) y=argmaxcjxi∈Nk(x)∑I(yi=cj)
其中 i = 1 , 2 , 3 , . . . , N ; j = 1 , 2 , 3 , . . . , K 。 I i=1,2,3,...,N; j=1,2,3,...,K。I i=1,2,3,...,N;j=1,2,3,...,K。I 为指示函数,即 y i = c j y_i=c_j yi=cj时 I = 1 I=1 I=1,否则 I = 0 I=0 I=0。
特殊的,当 k = 1 k=1 k=1时,称为最近邻算法。
k k k近邻算法的模型是基于样本集对特征空间的一个划分,没有显式的学习过程。 k k k近邻模型由 距离度量、 k k k值选择 和 分类决策规则 决定:
(1)距离度量:特征空间中两个特征向量 x i = ( x i 1 , x i 2 , . . . , x i n ) T , x j = ( x j 1 , x j 2 , . . . , x j n ) T x_{i}=(x_{i}^{1},x_{i}^{2},...,x_{i}^{n})^{T},x_{j}=(x_{j}^{1},x_{j}^{2},...,x_{j}^{n})^{T} xi=(xi1,xi2,...,xin)T,xj=(xj1,xj2,...,xjn)T 的p范数定义为
L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i l − x j l ∣ p ) 1 p L_{p}\left ( x_{i},x_{j} \right ) = \left ( \sum_{l=1}^{n}\left | x_{i}^{l} - x_{j}^{l} \right |^{p} \right ) ^{\frac{1}{p} } Lp(xi,xj)=(l=1∑n xil−xjl p)p1
由不同的度量方式所确定的最近邻点可能不同。
- 当
p
=
1
p=1
p=1时,称为曼哈顿距离
L p ( x i , x j ) = ∑ l = 1 n ∣ x i l − x j l ∣ L_{p}\left ( x_{i},x_{j} \right ) = \sum_{l=1}^{n}\left | x_{i}^{l} - x_{j}^{l} \right | Lp(xi,xj)=l=1∑n xil−xjl - 当
p
=
2
p=2
p=2时,称为欧式距离
L p ( x i , x j ) = ( ∑ l = 1 n ∣ x i l − x j l ∣ 2 ) 1 2 L_{p}\left ( x_{i},x_{j} \right ) = \left ( \sum_{l=1}^{n}\left | x_{i}^{l} - x_{j}^{l} \right |^{2} \right ) ^{\frac{1}{2} } Lp(xi,xj)=(l=1∑n xil−xjl 2)21 - 当
p
=
∞
p=∞
p=∞时,为各个坐标的距离最大值
L p ( x i , x j ) = max l ∣ x i l − x j l ∣ L_{p}\left ( x_{i},x_{j} \right ) = \mathop{\max}_{l} \left | x_{i}^{l} - x_{j}^{l} \right | Lp(xi,xj)=maxl xil−xjl
其它的距离度量可以参考这篇文章: 《从K近邻算法、距离度量谈到KD树、SIFT+BBF算法》。
(2) k k k值选择: k k k的选择反映了训练误差(近似误差)与测试误差(估计误差)的权衡,即:
- 若 k k k取较小值(模型复杂),预测实例较依赖于近邻样本,样本整体利用率低,模型对噪声数据敏感,且可能出现训练误差小(过拟合)、测试误差大的情况;
- 若 k k k值较大(模型简单),预测实例可利用较多的样本信息,模型抗干扰性强,但计算复杂,且可能出现训练误差大(欠拟合)、测试误差小的情况。
- 实际运用中,一般通过 交叉验证 选取较小的最优 k k k值。
(3)分类决策规则: k k k近邻算法中的分类决策规则往往是 多数表决规则(majority voting rule),即由输入实例 x x x的邻域 N k ( x ) N_k(x) Nk(x)中的多数类决定输入实例的分类。
如果分类的损失函数为0-1损失函数,即
f : R n ⟶ { c 1 , c 2 , . . . , c K } f:R^{n}\longrightarrow \left \{ c_{1},c_{2},...,c_{K} \right \} f:Rn⟶{c1,c2,...,cK}
那么误分概率为
P ( Y ≠ f ( X ) ) = 1 − P ( Y = f ( X ) ) P\left ( Y\ne f\left ( X \right ) \right ) = 1 - P\left ( Y = f\left ( X \right ) \right ) P(Y=f(X))=1−P(Y=f(X))
如果涵盖 N k ( x ) N_k(x) Nk(x)的类别是 c j c_{j} cj,那么误分概率为:
1 k ∑ x i ∈ N k ( x ) I ( y i ≠ c j ) = 1 − 1 k ∑ x i ∈ N k ( x ) I ( y i = c j ) \frac{1}{k} \sum_{x_{i}\in N_k(x) } I\left ( y_{i}\ne c_{j} \right ) = 1 - \frac{1}{k} \sum_{x_{i}\in N_k(x) } I\left ( y_{i} = c_{j} \right ) k1xi∈Nk(x)∑I(yi=cj)=1−k1xi∈Nk(x)∑I(yi=cj)
以上推导结果说明,要最小化误分概率,就要使 ∑ x i ∈ N k ( x ) I ( y i = c j ) \sum_{x_{i}\in N_k(x) } I\left ( y_{i} = c_{j} \right ) ∑xi∈Nk(x)I(yi=cj) 最大,所以多数表决规则等价于经验风险最小化。
2.Python基于K-D树实现KNN算法
KNN算法可以简单的基于线性搜索来实现,但是这样的方法是低效率的,没有实际应用意义。一般KNN算法可以基于K-D树来实现。K-D树的原理和实现请阅读我之前整理的文章《K-D树算法原理以及python实现》。以下是基于K-D树实现的KNN算法完整代码:
import random
from copy import deepcopy
from time import time
import numpy as np
from numpy.linalg import norm
def partition_sort(arr, k, key=lambda x: x):
"""
以枢纽(位置k)为中心将数组划分为两部分,
枢纽左侧的元素不大于枢纽右侧的元素。
:param arr: 待划分数组
:param k: 枢纽前部元素个数
:param key: 比较方式
:return: None
"""
start, end = 0, len(arr) - 1
assert 0 <= k <= end
while True:
i, j, pivot = start, end, deepcopy(arr[start])
while i < j:
# 从右向左查找较小元素
while i < j and key(pivot) <= key(arr[j]):
j -= 1
if i == j:
break
arr[i] = arr[j]
i += 1
# 从左向右查找较大元素
while i < j and key(arr[i]) <= key(pivot):
i += 1
if i == j:
break
arr[j] = arr[i]
j -= 1
arr[i] = pivot
if i == k:
return
elif i < k:
start = i + 1
else:
end = i - 1
def max_heap_replace(heap, new_node, key=lambda x: x[1]):
"""
大根堆替换堆顶元素
:param heap: 大根堆/列表
:param new_node: 新节点
:return: None
"""
heap[0] = new_node
root, child = 0, 1
end = len(heap) - 1
while child <= end:
if child < end and key(heap[child]) < key(heap[child + 1]):
child += 1
if key(heap[child]) <= key(new_node):
break
heap[root] = heap[child]
root, child = child, 2 * child + 1
heap[root] = new_node
def max_heap_push(heap, new_node, key=lambda x: x[1]):
"""
大根堆插入元素
:param heap: 大根堆/列表
:param new_node: 新节点
:return: None
"""
heap.append(new_node)
pos = len(heap) - 1
while 0 < pos:
parent_pos = pos - 1 >> 1 # 右移1位,相当于除以2,也就是取一半的值
if key(new_node) <= key(heap[parent_pos]):
break
heap[pos] = heap[parent_pos]
pos = parent_pos
heap[pos] = new_node
class KDNode(object):
"""K-D树节点"""
def __init__(self, data=None, label=None, left=None, right=None, axis=None, parent=None):
"""
构造函数
:param data: 数据
:param label: 数据标签
:param left: 左孩子节点
:param right: 右孩子节点
:param axis: 分割轴
:param parent: 父节点
"""
self.data = data
self.label = label
self.left = left
self.right = right
self.axis = axis
self.parent = parent
class KDTree(object):
"""K-D树"""
def __init__(self, X, y=None):
"""
构造函数
:param X: 输入特征集, n_samples*n_features
:param y: 输入标签集, 1*n_samples
"""
self.root = None
self.y_valid = False if y is None else True
self.create(X, y)
def create(self, X, y=None):
"""
构建K-D树
:param X: 输入特征集, n_samples*n_features
:param y: 输入标签集, 1*n_samples
:return: KDNode
"""
def create_(X, axis, parent=None):
"""
递归生成K-D树
:param X: 合并标签后输入集
:param axis: 切分轴
:param parent: 父节点
:return: KDNode
"""
n_samples = np.shape(X)[0]
if n_samples == 0:
return None
mid = n_samples >> 1 # 右移1位,相当于除以2,也就是取一半的值
partition_sort(X, mid, key=lambda x: x[axis])
if self.y_valid:
kd_node = KDNode(X[mid][:-1], X[mid][-1], axis=axis, parent=parent)
else:
kd_node = KDNode(X[mid], axis=axis, parent=parent)
next_axis = (axis + 1) % k_dimensions
kd_node.left = create_(X[:mid], next_axis, kd_node)
kd_node.right = create_(X[mid + 1:], next_axis, kd_node)
return kd_node
print('building kd-tree...')
k_dimensions = np.shape(X)[1]
if y is not None:
X = np.hstack((np.array(X), np.array([y]).T)).tolist()
self.root = create_(X, 0)
def search_knn(self, point, k, dist=None):
"""
K-D树中搜索k个最近邻样本
:param point: 样本点
:param k: 近邻数
:param dist: 度量方式
:return:
"""
def search_knn_(kd_node):
"""
搜索k近邻节点
:param kd_node: KDNode
:return: None
"""
if kd_node is None:
return
data = kd_node.data
distance = p_dist(data)
if len(heap) < k:
# 向大根堆中插入新元素
max_heap_push(heap, (kd_node, distance))
elif distance < heap[0][1]:
# 替换大根堆堆顶元素
max_heap_replace(heap, (kd_node, distance))
axis = kd_node.axis
if abs(point[axis] - data[axis]) < heap[0][1] or len(heap) < k:
# 当前最小超球体与分割超平面相交或堆中元素少于k个
search_knn_(kd_node.left)
search_knn_(kd_node.right)
elif point[axis] < data[axis]:
search_knn_(kd_node.left)
else:
search_knn_(kd_node.right)
if self.root is None:
raise Exception('kd-tree must be not null.')
if k < 1:
raise ValueError("k must be greater than 0.")
# 默认使用2范数度量距离
if dist is None:
p_dist = lambda x: norm(np.array(x) - np.array(point))
else:
p_dist = lambda x: dist(x, point)
heap = []
search_knn_(self.root)
return sorted(heap, key=lambda x: x[1])
def search_nn(self, point, dist=None):
"""
搜索point在样本集中的最近邻
:param point:
:param dist:
:return:
"""
return self.search_knn(point, 1, dist)[0]
def pre_order(self, root=KDNode()):
"""先序遍历"""
if root is None:
return
elif root.data is None:
root = self.root
yield root
for x in self.pre_order(root.left):
yield x
for x in self.pre_order(root.right):
yield x
def lev_order(self, root=KDNode(), queue=None):
"""层次遍历"""
if root is None:
return
elif root.data is None:
root = self.root
if queue is None:
queue = []
yield root
if root.left:
queue.append(root.left)
if root.right:
queue.append(root.right)
if queue:
for x in self.lev_order(queue.pop(0), queue):
yield x
@classmethod
def height(cls, root):
"""kd-tree深度"""
if root is None:
return 0
else:
return max(cls.height(root.left), cls.height(root.right)) + 1
from collections import Counter
# 用于计算出现频次最高的元素
# Counter([0, 1, 1, 2, 2, 3, 3, 4, 3, 3]).most_common(1)
class KNeighborsClassifier(object):
"""K近邻分类器"""
def __init__(self, k, dist=None):
"""构造函数"""
self.k = k
self.dist = dist
self.kd_tree = None
def fit(self, X, y):
"""建立K-D树"""
print('fitting...')
X = self._data_processing(X)
self.kd_tree = KDTree(X, y)
def predict(self, X):
"""预测类别"""
if self.kd_tree is None:
raise TypeError('Classifier must be fitted before predict!')
search_knn = lambda x: self.kd_tree.search_knn(point=x, k=self.k, dist=self.dist)
y_ptd = []
X = (X - self.x_min) / (self.x_max - self.x_min)
for x in X:
y = int(Counter(r[0].label for r in search_knn(x)).most_common(1)[0][0])
y_ptd.append(y)
return y_ptd
def score(self, X, y):
"""预测正确率"""
y_ptd = self.predict(X)
correct_nums = len(np.where(np.array(y_ptd) == np.array(y))[0])
return correct_nums / len(y)
def _data_processing(self, X):
"""数据归一化"""
X = np.array(X)
self.x_min = np.min(X, axis=0)
self.x_max = np.max(X, axis=0)
X = (X - self.x_min) / (self.x_max - self.x_min)
return X
3.基于KNN算法的约会网站数据集分类实战
现在我们利用以上实现的KNN算法,对 约会网站数据集进行分类。
# 读取约会网站数据集
import pandas as pd
df = pd.read_csv("datingTestSet2.txt", header=None, sep="\t",
names=['f1', 'f2', 'f3', 'label'])
df.tail()
# 数据集有3列特征值,1列标签值。数据没有异常信息。
# 可视化
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(14, 4))
ax = fig.add_subplot(131)
ax.scatter(df['f2'], df['f3'], 1.5*df['label'], 1.5*df['label'])
ax = fig.add_subplot(132)
ax.scatter(df['f1'], df['f3'], 1.5*df['label'], 1.5*df['label'])
ax = fig.add_subplot(133)
ax.scatter(df['f1'], df['f2'], 1.5*df['label'], 1.5*df['label'])
plt.show()
# 通过可视化看出,采用特征f1和f2可以得到较好的分类效果。
# 切分数据集
from sklearn.model_selection import train_test_split
df_train, df_test = train_test_split(df, test_size=0.1, random_state=1234)
# 训练knn模型
knn = KNeighborsClassifier(k=3)
knn.fit(df_train[['f1','f2','f3']].values, df_train['label'].values)
# 预测
prd = knn.predict(df_test[['f1','f2','f3']].values)
print(prd[:10])
print(df_test['label'].values.tolist()[:10])
# 预测分数
score = knn.score(df_test[['f1','f2','f3']].values, df_test['label'].values)
print(score)
4.参考文章
1.《统计学习方法》第2版:第3章k近邻法。作者:李航
2.K近邻(KNN)算法、KD树及其python实现
3.从K近邻算法、距离度量谈到KD树、SIFT+BBF算法
4.《机器学习实战》:第2章k近邻算法。作者:Peter Harrington