KNN算法

KNN(K-Nearest Neighbors,K 近邻算法)是一种简单、直观、非参数的监督学习算法,常用于分类和回归问题。

KNN 的核心思想

物以类聚,人以群分(近朱者赤,近墨者黑)

当来了一个新样本时:

  • 找到距离它最近的 K 个已知样本

  • 看这 K 个样本大多数属于哪一类

  • 新样本就被分到那一类(分类)

  • 或者取这 K 个样本的平均值(回归)

如何选择K值

  • K太小导致“过拟合”(overfit)
  • 容易把噪声学进来
  • K太大导致”欠拟合“(underfit)
  • 决策效率低

距离计算方式

  • 明氏距离

D(X, Y) = \left( \sum_{i=1}^{n} \lvert x_i - y_i \rvert^{p} \right)^{\frac{1}{p}}

  • 曼哈顿距离(p=1)

D(X,Y)=\sum_{i=1}^{n}\left | x_i - y_i \right |

  • 欧氏距离(p=2)

D(X,Y)=\sqrt{\sum_{i=1}^{n}(x_{i}-y_i)^2}

特征归一化(normalization)

特征归一化 = 把不同“量纲、尺度”的特征,统一拉到一个可比的范围里

举例:年龄(age:0 ~ 100岁),年收入(income:0 ~ 100000)

当使用欧氏距离计算时,收入的数值过大,而年龄的数值过小导致年龄几乎不起作用,算法其实只在“看收入”,这样很不公平

KNN算法代码实现(均为Jupyter)

import numpy as np
import matplotlib.pyplot as plt
# 定义样本特征
data_X = [
    [1.3,6],
    [3.5,5],
    [4.2,2],
    [5,3.3],
    [2,9],
    [5,7.5],
    [7.2,4],
    [8.1,8],
    [9,2.5]
]
# 样本标记
data_y = [0,0,0,0,1,1,1,1,1]
# 训练集  转换为numpy的数组
X_train = np.array(data_X)
y_train = np.array(data_y)
# 新的样本点
data_new = np.array([4,5])
# 绘制标签为0的点,marker为散点的样式
plt.scatter(X_train[y_train==0,0],X_train[y_train==0,1],color='red',marker='x')
# 绘制标签为1的点
plt.scatter(X_train[y_train==1,0],X_train[y_train==1,1],color='black',marker='o')
plt.scatter(data_new[0],data_new[1],color='green',marker='^')
plt.show()

# 1.计算新样本点与已知样本点的距离
for data in X_train:
    print(np.sqrt(np.sum((data-data_new)**2)))

distances = [np.sqrt(np.sum((data-data_new)**2)) for data in X_train]
# 2.按距离进行排序
# 使用argsort获取排序后的索引值,训练集中的每个样本的索引
sort_index = np.argsort(distances)
# 3.确定k值
k=5
# 4.距离最近的k个点投票
# 前k个值的类别
# 训练集中第 1、3、5、0、2 号样本,离新样本最近
# 找这 k 个样本的标签
# 简单易懂:把距离最近的 k 个训练样本的类别拿出来,准备投票
first_k = [y_train[i] for i in sort_index[:k]]
# 投票表决,即给得到的标签类别计数,统计每个元素出现了多少次
# 结果:类别为0的有4个,类别为1的有1个
from collections import Counter
Counter(first_k)
# 把 first_k 里所有类别的出现次数,按“从多到少”全部列出来
Counter(first_k).most_common()
# 统计最近 k 个邻居中出现次数最多的类别,并取出这个类别标签,案例中是“0”
predict_y = Counter(first_k).most_common(1)[0][0]
# 输出最终的预测值
predict_y

scikit-learn中的KNN算法

from sklearn.neighbors import KNeighborsClassifier
# 创建类的实例,n_neighbors就是KNN算法中的k值
knn_classifier = KNeighborsClassifier(n_neighbors=5)
# 用训练数据把 KNN 模型“训练好 / 建立起来”
knn_classifier.fit(X_train,y_train)
# 把一条一维数据,变成 sklearn 能接受的“1 个样本、多维特征”的二维数据
# -1 的意思是:这一维的长度让 NumPy 自动算
data_new.reshape(1,-1)
# 进行预测,要求传入的数据为二维数组,但是此时data_new为一维,数据转换👆
knn_classifier.predict(data_new.reshape(1,-1))

数据集的划分

import numpy as np
from matplotlib import pyplot as plt
from sklearn.datasets import make_blobs

# 生成一份“假的/模拟的”分类数据
# make_clobs:人为生成一些“成团”的点(blobs),每一团代表一个类别
# 生成一堆“成团的点”,X 是坐标,y 是每个点属于哪一团(类别/标签)
x,y = make_blobs()

# c=y:用 y 来决定每个点的颜色(即相同类别的统一颜色)
# s=15:点的大小
plt.scatter(x[:,0],x[:,1],c=y,s=15)
plt.show()

# 参数
# n_samples:样本数量,n_features:特征,即生成几维数据,centers:种类的数目(类别/标签)
# cluster_std:类内的标准差,center_box:取值的范围,random_state:随机种子
# return_centers:增加一个类别中心点坐标的返回值
x,y = make_blobs(
    n_samples=300,
    n_features=2,
    centers=3,
    cluster_std=1,
    center_box=(-10,10),
    random_state=233,
    return_centers=False
)

plt.scatter(x[:,0],x[:,1],c=y,s=15)
plt.show()

# 生成和x长度相同的打乱的数组
np.random.seed(233)
shuffle = np.random.permutation(len(x))


# 训练集的比例
train_size = 0.7
# 把训练和测试的序号截取出来
train_index = shuffle[:int(len(x)*train_size)]
test_index = shuffle[int(len(x)*train_size):]

my_train_test_split()

def my_train_test_split(x,y,train_size = 0.7,random_state = None):
    if random_state:
        np.random.seed(random_state)
    shuffle = np.random.permutation(len(x))
    train_index = shuffle[:int(len(x)*train_size)]
    test_index = shuffle[int(len(x)*train_size):]
    return x[train_index],x[test_index],y[train_index],y[test_index]

x_train,x_test,y_train,y_test = my_train_test_split(x,y,train_size=0.7,random_state=233)
plt.scatter(x_train[:,0],x_train[:,1],c=y_train,s=15)
plt.show()

plt.scatter(x_test[:,0],x_test[:,1],c=y_test,s=15)
plt.show()

sklearn划分数据集

from sklearn.model_selection import train_test_split
x_train,x_test,y_train,y_test = train_test_split(x,y,train_size=0.7,random_state=233)

类别的均匀划分

from collections import Counter
Counter(y_test)

# 考虑类别的均匀划分
x_train,x_test,y_train,y_test = train_test_split(x,y,train_size=0.7,random_state=233,stratify=y)

Counter(y_test)

模型评价

import numpy as np
from sklearn import datasets

iris = datasets.load_iris()
X = iris.data
y =iris.target

# 打乱数据
# 为了不打乱x,y,我们对索引进行操作
shuffle_index = np.random.permutation(len(y))

train_ration = 0.8

# 结果可能是小数,所以强制转换
train_size = int(len(y)*train_ration)
train_index = shuffle_index[:train_size]
test_index = shuffle_index[train_size:]

X_train = X[train_index]
y_train = y[train_index]

X_test = X[test_index]
y_test = y[test_index]

# 使用sklearn方法
from sklearn.model_selection import train_test_split
X_train,X_test,y_train,y_test = train_test_split(X,y,train_size=train_ration,random_state=666)

# 预测
from sklearn.neighbors import KNeighborsClassifier
knn_classifier = KNeighborsClassifier(n_neighbors=5)

knn_classifier.fit(X_train,y_train)

# 不关注预测结果,关注评估指标可以直接使用KNN算法自带的
# knn_classifier.score(X_test,y_test)

y_predict = knn_classifier.predict(X_test)
# 看预测和真实值中有多少相同的
np.sum(y_predict == y_test)
# 计算准确度
accuracy = np.sum(y_predict == y_test)/len(y_test)

# 封装的计算准确度的方法
from sklearn.metrics import accuracy_score
accuracy_score(y_test,y_predict)

超参数(Hyperparameter)

  • 人为设置的参数(例如:KNN算法中的k值)
  • 经验值(理论知识)
  • 参数搜索(机器选取)

KNN的超参数

  • k个最近的邻居
  • 分类表决与加权分类表决
  • 明氏距离中的p值
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn import datasets
iris = datasets.load_iris()
x = iris.data
y = iris.target
x_train,x_test,y_train,y_test = train_test_split(x,y,train_size=0.7,random_state=233,stratify=y)

# 超参数
from sklearn.neighbors import KNeighborsClassifier
# 参数:n_neighbors:k的值,weights:权重,distance(与距离相关,成反比),uniform(相同的权重)
# distance:距离越远,权重越小
# p:曼哈顿距离(p=1)还是欧氏距离(p=2),默认欧氏距离
neigh = KNeighborsClassifier(
    n_neighbors=3,
    weights='distance',#'uniform'
    p = 2
)
neigh.fit(x_train,y_train)
# 测试集上的准确率
neigh.score(x_test,y_test)

网格搜索

# 使用网格搜索的方式查找最优超参数
# 初始化参数
best_score = -1  
best_n = -1
best_weight = ''
best_p = -1
for n in range(1,20):
    for weight in ['uniform','distance']:
        for p in range(1,7):
            neigh = KNeighborsClassifier(
            n_neighbors=n,
            weights=weight,
            p = p
        )
        neigh.fit(x_train,y_train)
        score = neigh.score(x_test,y_test)
        if score>best_score:
            best_score = score
            best_n = n
            best_weight = weight
            best_p = p

print("n_neighbors:",best_n)
print("weight:",best_weight)
print("p:",best_p)
print("score:",best_score)

sklearn的超参数搜索

from sklearn.model_selection import GridSearchCV
# 定义一个参数字典
params ={
    'n_neighbors':[n for n in range(1,20)],
    'weights':['uniform','distance'],
    'p':[p for p in range(1,7)]
}
# 定义网格搜索
# 参数:estimator:算法,param_grid:要搜索的参数网格,n_jobs:并行计算使用的 CPU 核数
grid = GridSearchCV(
    estimator=KNeighborsClassifier(),
    param_grid=params,
    n_jobs=-1
)
grid.fit(x_train,y_train)
grid.best_params_
grid.best_score_
# 拿到最优模型
grid.best_estimator_
# 用 最优模型 对测试集 x_test 进行预测
grid.best_estimator_.predict(x_test)
# 使用最优模型:在测试集 (x_test, y_test) 上计算 评分
grid.best_estimator_.score(x_test,y_test)

特征归一化

最大最小归一化:把原始数据按比例线性映射到一个固定区间

x_{norm}=\frac{x-x_{min}}{x_{max}-x_{min}}

零均值归一化:映射到正态分布上

x_{norm}=\frac{x-\mu }{\sigma }

最大最小化

import numpy as np
from sklearn import datasets
iris = datasets.load_iris()
X = iris.data
y = iris.target

X[:,0]=(X[:,0]-np.min(X[:,0]))/(np.max(X[:,0])-np.min(X[:,0]))
X[:,1]=(X[:,1]-np.min(X[:,1]))/(np.max(X[:,1])-np.min(X[:,1]))
X[:,2]=(X[:,2]-np.min(X[:,2]))/(np.max(X[:,2])-np.min(X[:,2]))
X[:,3]=(X[:,3]-np.min(X[:,3]))/(np.max(X[:,3])-np.min(X[:,3]))

零均值归一化

# 重新加载数据
iris = datasets.load_iris()
X = iris.data
y = iris.target

# 求均值和标准差
np.mean(X[:,0]),np.std(X[:,0])

X[:,0]=(X[:,0]-np.mean(X[:,0]))/(np.std(X[:,0]))
X[:,1]=(X[:,1]-np.mean(X[:,1]))/(np.std(X[:,1]))
X[:,2]=(X[:,2]-np.mean(X[:,2]))/(np.std(X[:,2]))
X[:,3]=(X[:,3]-np.mean(X[:,3]))/(np.std(X[:,3]))

sklearn中的特征归一化

iris = datasets.load_iris()
X = iris.data
y = iris.target
from sklearn.preprocessing import StandardScaler

standard_scaler = StandardScaler()
standard_scaler.fit(X)
standard_scaler.mean_
standard_scaler.scale_
# X本身并不会改变,所以要进行赋值
X = standard_scaler.transform(X)

测试集如何归一化?

在进行测试集的归一化的时候不能使用测试集的均值和标准差,要使用训练集的均值和标准差

KNN回归任务代码实现

import numpy as np
from sklearn import datasets
import matplotlib.pyplot as plt
# 定义样本特征
data_X = [
    [1.3,6],
    [3.5,5],
    [4.2,2],
    [5,3.3],
    [2,9],
    [5,7.5],
    [7.2,4],
    [8.1,8],
    [9,2.5]
]
data_y = [0.1,0.3,0.5,0.7,0.9,1.1,1.3,1.5,1.7]
# 训练集  转换为numpy的数组
X_train = np.array(data_X)
y_train = np.array(data_y)
# 新的样本点
data_new = np.array([4,5])

# 绘制标签为0的点,marker为散点的样式
plt.scatter(X_train[:,0],X_train[:,1],color='black')
plt.scatter(data_new[0],data_new[1],color='b',marker='^')
for i in range(len(y_train)):
    plt.annotate(y_train[i],xy=X_train[i],xytext=(-15,-15),textcoords='offset points')

from sklearn.neighbors import KNeighborsRegressor
knn_reg = KNeighborsRegressor(n_neighbors=5)
knn_reg.fit(X_train,y_train)
predict_y = knn_reg.predict(data_new.reshape(1,-1))

Housing数据集上的回归任务

import numpy as np
from sklearn import datasets
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
housing = datasets.fetch_california_housing()
X = housing.data
y = housing.target
X_train,X_test,y_train,y_test = train_test_split(X,y,train_size=0.7,random_state=233)
from sklearn.neighbors import KNeighborsRegressor
knn_reg = KNeighborsRegressor(n_neighbors=5,weights='distance',p=2)
knn_reg.fit(X_train,y_train)
knn_reg.score(X_test,y_test)

结果不好:

添加特征归一化

from sklearn.preprocessing import StandardScaler
standard_scaler = StandardScaler()
standard_scaler.fit(X_train)
x_train = standard_scaler.transform(X_train)
x_test = standard_scaler.transform(X_test)
knn_reg.fit(x_train,y_train)
knn_reg.score(x_test,y_test)

结果:

进一步优化:

from sklearn.model_selection import GridSearchCV
params ={
    'n_neighbors':[n for n in range(1,20)], 
    'weights':['uniform','distance'],
    'p':[p for p in range(1,7)]
}
grid = GridSearchCV(
    estimator=KNeighborsRegressor(),
    param_grid=params,
    n_jobs=-1
)
grid.fit(x_train,y_train)
grid.best_params_
grid.best_score_
# 用 最优模型 对测试集进行预测
grid.best_estimator_.predict(x_test)
# 使用最优模型:在测试集上计算 评分
grid.best_estimator_.score(x_test,y_test)

结果:

KNN优缺点和适用条件

优点:

  • 思想简单
  • 理论成熟
  • 数据无假设

缺点:

  • 样本依赖性强,k值不好选
  • 计算量大
  • 内存要求高
  • 可解释性不强

适用条件:

  • 类域交叉重叠
  • 样本均衡度高

改进方法:

  • 速度优化
  • 样本浓缩
  • 快速搜索(交叉索引)
  • 样本优化
  • 距离公式
  • K值选择

更多推荐