K近邻算法
k近邻算法:预测样本根据附近k个已知标签样本的投票情况,给出预测样本的结果。
k近邻算法是非常特殊的,可以被认为是没有模型的算法,仅统计当前样本中前k个样本的票数。
这样做仅仅是考虑样本个数问题,谁多我就挺谁?这样不一定是最好的,假设k=5,其中有两种类别的样本都是2票,此时产生平票的问题,当然我们可以随便选一个返回,这样做就过于简单直接了。
缓解上面平票情况,可以考虑距离的因素,离得近他的票就应该占比大,实际上这也是合理的,这种考虑距离的因素就是为前k个样本赋予了权重。
如图 k=5
1)不考虑距离因素,绿色样本应该被标记为蓝色类别。
2)考虑距离因素,绿色样本获得蓝色样本投票 1 3 + 1 4 = 0.5833 \frac{1}{3} + \frac{1}{4} =0.5833 31+41=0.5833,获得黄色样本投票 1 1 1,所以绿色样本应该被标记为黄色类别,其他类别的样本投票分别为 1 5 \frac{1}{5} 51和 1 6 \frac{1}{6} 61。
knn中两个重要超参数
- k
- 距离的参数
超参数:算法运行前需要的决定的参数(学习率、batchsize、dropout等)
模型参数:算法过程中学习的参数(各种权重和偏置等)
常见距离公式
1) 欧拉距离
∑
i
=
1
n
(
X
i
a
−
X
i
b
)
2
\sqrt{\sum^{n}_{i=1}(X_{i}^{a}-X_{i}^{b})^{2}}
∑i=1n(Xia−Xib)2
2)曼哈顿距离 ∑ i = 1 n ∣ X i a − X i b ∣ \sum^{n}_{i=1}|X^{a}_{i} - X^{b}_{i}| ∑i=1n∣Xia−Xib∣
3)明可夫斯基距离 ( ∑ i = 1 n ∣ X i a − X i b ∣ p ) 1 p (\sum^{n}_{i=1}|X^{a}_{i} - X^{b}_{i}|^{p})^{\frac{1}{p}} (∑i=1n∣Xia−Xib∣p)p1
如果想要使用距离作为权重,从公式中可以看出来,不同特征可能属于不同的数量级,很容易使结果倾向这些特征,因此需要对原始数据进行归一化,消除量纲和样本取值范围的影响。
实际使用knn时,往往需要根据业务场景对参数进行网格搜索,以便获取一组最佳的参数组合,本文只是knn算法学习理解,暂不涉及sklearn中网格搜索的内容。
数据归一化
目的是将所有数据映射到同一尺度。
常见的数据归一化
最值归一化 normalization:把所有数据映射到0-1之间,适用于有明显分界的数据。
x
s
c
a
l
e
=
x
−
x
m
i
n
x
m
a
x
−
x
m
i
n
x_{scale} = \frac{x-x_{min}}{x_{max}-x_{min}}
xscale=xmax−xminx−xmin
均值方差归一化 standardization:把所有数据归一化到均值为0方差为1的分布中。
x
s
c
a
l
e
=
x
−
x
m
e
a
n
s
,
s
是方差
x_{scale}=\frac{x-x_{mean}}{s}, \quad s是方差
xscale=sx−xmean,s是方差
代码实现
import numpy as np
from math import sqrt
from collections import Counter
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
class KNNClassifier:
def __init__(self, k, weights=None):
"""初始化"""
assert k >= 1
self.k = k
self._X_train = None
self._y_train = None
self.weights = weights
def fit(self, X_train, y_train):
"""根据训练数据集X_train和y_train训练kNN分类器"""
assert X_train.shape[0] == y_train.shape[0]
assert self.k <= X_train.shape[0]
# 仅仅保存传入的数据
self._X_train = X_train
self._y_train = y_train
return self
def predict(self, X_predict):
"""给定待预测数据集X_predict,返回表示X_predict的结果向量"""
assert self._X_train is not None and self._y_train is not None
assert X_predict.shape[1] == self._X_train.shape[1]
if self.weights is None:
y_predict = [self._predict(x) for x in X_predict]
elif self.weights == "distance":
y_predict = [self._predict_with_distance(x) for x in X_predict]
else:
pass
return np.array(y_predict)
def _predict(self, x):
""" 核心接口
计算欧式距离
升序后返回前k个中占比较多的样本
"""
# 断言预测样本的特征维度必须和训练样本的特征维度相同
assert x.shape[0] == self._X_train.shape[1]
# 计算每一个样本和预测样本的欧氏距离
distances = [sqrt(np.sum((x_train - x) ** 2))
for x_train in self._X_train]
# 升序后的索引,即离预测样本x距离从小到大的排列。
nearest = np.argsort(distances)
# 取前k个样本的标签
topK_y = [self._y_train[i] for i in nearest[:self.k]]
# 投票
votes = Counter(topK_y)
# 取票数最多的
# ! votes.most_common(1) 类型为list, 值的类型为tuple(元素, 个数) 如 [(0, 2)]
return votes.most_common(1)[0][0]
def _predict_with_distance(self, x):
""" 核心接口
预测样本到前k个训练样本距离的倒数作为权重
"""
# 断言预测样本的特征维度必须和训练样本的特征维度相同
assert x.shape[0] == self._X_train.shape[1]
# 计算每一个样本和预测样本的欧氏距离
distances = [sqrt(np.sum((x_train - x) ** 2))
for x_train in self._X_train]
nearest = np.argsort(distances)
nearest_k = nearest[:self.k]
# topK_y是前k个样本所对应的标签
topK_y = [self._y_train[i] for i in nearest_k]
result = {}
for y, k in zip(topK_y, nearest_k):
if y in result:
dist = result.get(y)
dist += 1 / distances[k]
result[y] = dist
else:
dist = 1 / distances[k]
result[y] = dist
# 找出最好的结果best_score是最结果的权重值
best_score = -1
best_label = None
for key, value in result.items():
if value > best_score:
best_score = value
best_label = key
return best_label
def score(self, X_test, y_test):
"""根据测试数据集 X_test 和 y_test 确定当前模型的准确度"""
y_predict = self.predict(X_test)
return accuracy_score(y_test, y_predict)
if __name__ == "__main__":
data = load_iris()
X_train = data.data
y_train = data.target
X_train, X_test, y_train, y_test = train_test_split(X_train, y_train, random_state=1)
# 图片中测试数据
# X_train = np.array([[3], [4], [5], [6], [1]])
# y_train = np.array([0, 0, 2, 3, 1])
# X_test = np.array([[0]])
# 前k近个样本进行投票判断新样本标签
# knn = KNNClassifier(k=5)
# 考虑欧式距离作为权重
knn = KNNClassifier(k=5, weights="distance")
knn.fit(X_train, y_train)
y_pred1 = knn.predict(X_test)
print(accuracy_score(y_test, y_pred1)) # 1
# p=2指定使用欧式距离
knn2 = KNeighborsClassifier(n_neighbors=5, weights="distance", p=2)
knn2.fit(X_train, y_train)
y_pred2 = knn2.predict(X_test)
print(accuracy_score(y_test, y_pred2)) # 1
代码和sklearn中封装的knn结果相同,可以证明代码暂时无问题,sklearn中实现的knn稍复杂一点。实验代码中没有加入数据归一化的操作,原因是load_iris()数据集合比较简单,没有明显的特征倾斜。
knn优缺点
优点
1)解决分类问题和多分类问题
2)思想简单,效果强大
3)可以解决回归问题
缺点
1)效率低,假设一个训练集有
m
m
m个样本
n
n
n个特征,则预测每一个新数据需要
O
(
m
∗
n
)
O(m*n)
O(m∗n)
2)高度数据相关
3)预测结果不具有可解释性
4)维度的灾难
随着维度的增加,看似比较近的点,其两者之间的距离也会越来越大。
维度 | 点坐标 | 欧式距离 |
---|---|---|
1 | 0到1的距离 | 1 |
2 | (0,0)到(1,1) | 1.414 |
3 | (0,0,0)到(1,1,1) | 1.73 |
64 | (0,0,…,0)到(1,1,…,1) | 8 |