k-近邻算法梳理(从原理到示例)

k-近邻算法是一个有监督的机器学习算法,k-近邻算法也被称为knn算法,可以解决分类问题。也可以解决回归问题。本文主要内容整理为如下:

  • knn算法的原理、优缺点及参数k取值对算法性能的影响;
  • 使用knn算法处理分类问题的示例;
  • 使用knn算法解决回归问题的示例;
  • 使用knn算法进行糖尿病检测的示例;

1 算法原理

knn算法的核心思想是未标记样本的类别,由距离其最近的k个邻居投票来决定。
具体的,假设我们有一个已标记好的数据集。此时有一个未标记的数据样本,我们的任务是预测出这个数据样本所属的类别。knn的原理是,计算待标记样本和数据集中每个样本的距离,取距离最近的k个样本。待标记的样本所属类别就由这k个距离最近的样本投票产生。
假设X_test为待标记的样本,X_train为已标记的数据集,算法原理的伪代码如下:

  • 遍历X_train中的所有样本,计算每个样本与X_test的距离,并把距离保存在Distance数组中。
  • 对Distance数组进行排序,取距离最近的k个点,记为X_knn。
  • 在X_knn中统计每个类别的个数,即class0在X_knn中有几个样本,class1在X_knn中有几个样本等。
  • 待标记样本的类别,就是在X_knn中样本个数最多的那个类别。

1.1 算法优缺点

  • 优点:准确性高,对异常值和噪声有较高的容忍度。
  • 缺点:计算量较大,对内存的需求也较大。

1.2 算法参数

其算法参数是k,参数选择需要根据数据来决定。

  • k值越大,模型的偏差越大,对噪声数据越不敏感,当k值很大时,可能造成欠拟合;
  • k值越小,模型的方差就会越大,当k值太小,就会造成过拟合。

1.3 变种

knn算法有一些变种,其中之一是可以增加邻居的权重。默认情况下,在计算距离时,都是使用相同权重。实际上,可以针对不同的邻居指定不同的距离权重,如距离越近权重越高。这个可以通过指定算法的weights参数来实现。
另一个变种是,使用一定半径内的点取代距离最近的k个点。当数据采样不均匀时,可以有更好的性能。在scikit-learn里, R a d i u s N e i g h b o r s C l a s s i f i e r {RadiusNeighborsClassifier} RadiusNeighborsClassifier类实现了这个算法变种。

2 示例:使用k-近邻算法进行分类

在scikit-learn里,使用k-近邻算法进行分类的是 s k l e a r n . n e i g h b o r s . K N e i g h b o r s C l a s s i f i e r {sklearn.neighbors.KNeighborsClassifier} sklearn.neighbors.KNeighborsClassifier类。

  • 生成已标记的数据集:
from sklearn.datasets.samples_generator import make_blobs
# 生成数据
centers = [[-2,2], [2,2], [0,4]]
X, y = make_blobs(n_samples=60, centers=centers,
                 random_state=0, cluster_std=0.60)

我们使用 s k l e a r n . d a t a s e t s . s a m p l e s g e n e r a t o r {sklearn.datasets.samples_generator} sklearn.datasets.samplesgenerator包下的 m a k e b l o b s {make_blobs} makeblobs函数来生成数据集,上面代码中,生成60个训练样本,这60个样本分布在以centers参数指定中心点周围。cluster_std是标准差,用来指明生成的点分布的松散程度。生成的训练数据集放在变量X里面,数据集的类别标记放在y里面。
我们使用 m a t p l o t l i b {matplotlib} matplotlib库把生成的点画出来。

# 画出数据
import matplotlib.pyplot as plt
import numpy as np
plt.figure(figsize=(16,10), dpi=144)
c = np.array(centers)
# 画出样本
plt.scatter(X[:,0], X[:,1], c=y, s=100, cmap='cool')
# 画出中心点
plt.scatter(c[:,0], c[:,1], s=100, marker='^',c='orange')
plt.savefig('knn_centers.png')
plt.show()

这些点的分布情况在坐标轴上一目了然,其中三角形的点即各个类别的中心节点。
knn-centers

  • 使用KNeighborsClassifier对算法进行训练,我们选择参数k=5
from sklearn.neighbors import KNeighborsClassifier
# 模型训练
k = 5
clf = KNeighborsClassifier(n_neighbors = k)
clf.fit(X, y)

"""
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
           metric_params=None, n_jobs=None, n_neighbors=5, p=2,
           weights='uniform')
"""
  • 对一个新样本进行预测:
# 进行预测
X_sample = np.array([[0, 2]])
y_sample = clf.predict(X_sample)
neighbors = clf.kneighbors(X_sample, return_distance=False)

我们要预测的样本是[0, 2],使用kneighbors()方法,把这个样本周围距离最近的5个点取出来。取出来的点是训练样本X里的索引,从0开始计算。

  • 把待预测的样本以及和其最近的5个样本在图上标记出来。
# 画出示意图
plt.figure(figsize=(16,10), dpi=144)
c = np.array(centers)
plt.scatter(X[:,0], X[:,1], c=y, s=100, cmap='cool') # 出样本
plt.scatter(c[:,0], c[:,1], s=100, marker='^',c='k') # 中心点
plt.scatter(X_sample[0][0], X_sample[0][1], marker="x",
           s=100, cmap='cool')      # 待预测的点
for i in neighbors[0]:
    plt.plot([X[i][0], X_sample[0][0]], [X[i][1], X_sample[0][1]],
            'k--', linewidth=0.6)  # 预测点与距离最近的5个样本的连线
plt.savefig('knn_predict.png')
plt.show()

从下图可以清楚地看到k-近邻算法的原理。
predict

3 示例:使用k-近邻算法进行回归拟合

分类问题的预测值是离散的,我们也可以用knn算法在连续区间内对数值进行预测,回归拟合。我们使用 s k l e a r n . n e i g h b o r s . K N e i g h b o r s R e g r e s s o r {sklearn.neighbors.KNeighborsRegressor} sklearn.neighbors.KNeighborsRegressor类。

  • 生成数据集,它在余弦曲线的基础上加入了噪声。
import numpy as np
n_dots = 40
X = 5 * np.random.rand(n_dots, 1)
y = np.cos(X).ravel()

# 添加一些噪声
y += 0.2 * np.random.rand(n_dots) - 0.1
  • 使用KNeighborsRegressor来训练模型。
# 训练模型
from sklearn.neighbors import KNeighborsRegressor
k = 5
knn = KNeighborsRegressor(k)
knn.fit(X, y)

"""
KNeighborsRegressor(algorithm='auto', leaf_size=30, metric='minkowski',
          metric_params=None, n_jobs=1, n_neighbors=5, p=2,
          weights='uniform')
"""

我们怎么进行回归拟合呢? 一个方法是,在X轴上的指定区间内生成足够多的点,针对这些足够密集的点,使用训练出来的模型去预测,得到预测值y_pred,然后在坐标轴上把所有的预测点连接起来,这样就画出了拟合曲线。如下:

# 生成足够密集的点并进行预测
T = np.linspace(0, 5, 500)[:, np.newaxis]
y_pred = knn.predict(T)
knn.score(X, y) # 计算拟合曲线对训练样本的拟合准确性。

"""
0.9804488058034699
"""
  • 把这些预测点连接起来,构成拟合曲线。
# 画出拟合曲线。
import matplotlib.pyplot as plt
plt.figure(figsize=(16,10), dpi=144)
plt.scatter(X, y, c='g', label='data', s=100) # 画出训练样本
plt.plot(T, y_pred, c='k', label='prediction', lw=4) # 画出拟合曲线
plt.axis('tight')
plt.title('KNeighborsRegressor (k=%i)' % k)
plt.legend()
plt.savefig('knn_regressor.png')
plt.show()

knn_regressor

4 实例:糖尿病预测

我们使用knn算法及其变种,对Pina印第安人的糖尿病进行预测。数据集可从下面下载。
链接:https://pan.baidu.com/s/1qjWByd5gZ3PBj782Kv3Mkg
提取码:orfr

4.1 加载数据

import pandas as pd
data = pd.read_csv('Corpus/pima-indians-diabetes/diabetes.csv')
print('dataset shape {}'.format(data.shape))
data.info()

info
从输出可以看到,总共有768个样本、8个特征,其中Outcome为标签,0表示没有糖尿病,1表示有糖尿病。这8个特征分别为:

  • Pregnancies:怀孕次数
  • Glucose:血浆葡萄糖浓度,采用2小时口服葡萄糖耐量实验测得
  • BloodPressure:舒张压(毫米汞柱)
  • SkinThickness:肱三头肌皮肤褶皱厚度(毫米)
  • Insulin:两个小时血清胰岛素( μ U {\mu U} μU/毫升)
  • BMI:身体质量指数,体重除以身高的平方
  • Diabetes Pedigree Function:糖尿病血统指数,糖尿病和家庭遗传相关
  • Age:年龄
data.head()

head
进一步观察数据集里阳性和阴性样本的个数:

data.groupby('Outcome').size()

"""
Outcome
0    500
1    268
dtype: int64
"""

其中阴性样本500例,阳性样本268例。接着,我们要把数据集分词训练集和测试集。

X = data.iloc[:, 0:8]
Y = data.iloc[:, 8]
print('shape of X {}, shape of Y {}'.format(X.shape, Y.shape))

from sklearn.model_selection import train_test_split
X_train, X_test, Y_train,Y_test = train_test_split(X, Y, test_size=0.2)

"""
shape of X (768, 8), shape of Y (768,)
"""

4.2 模型比较

使用普通的knn算法、带权重的knn以及指定半径的knn算法分别对数据集进行拟合并计算评分。

from sklearn.neighbors import KNeighborsClassifier, RadiusNeighborsClassifier

# 构建3个模型
models = []
models.append(('KNN', KNeighborsClassifier(n_neighbors=2)))
models.append(('KNN with weights', KNeighborsClassifier(n_neighbors=2, weights='distance')))
models.append(('Radius Neighbors', RadiusNeighborsClassifier(n_neighbors=2, radius=500.0)))

# 分别训练3个模型,并计算得分
results = []
for name, model in models:
    model.fit(X_train, Y_train)
    results.append((name, model.score(X_test, Y_test)))
for i in range(len(results)):
    print('name: {}; score: {}'.format(results[i][0], results[i][1]))
    
"""
name: KNN; score: 0.6623376623376623
name: KNN with weights; score: 0.6493506493506493
name: Radius Neighbors; score: 0.6038961038961039
"""

权重算法,我们选择了距离越近,权重越高。RadiusNeighborsClassifier模型的半径选择了500.从输出可以看出,普通的knn算法还是最好。 问题来了,这个判断准确吗? 答案是:不准确。因为我们的训练集和测试集是随机分配的,不同的训练样本和测试样本组合可能导致计算出来的算法准确性有差异。 那么该如何解决呢? 我们可以多次随机分配训练集和交叉验证集,然后求模型评分的平均值。scikit-learn提供了KFold和cross_val_score()函数来处理这种问题。

from sklearn.model_selection import KFold
from sklearn.model_selection import cross_val_score

results = []
for name, model in models:
    kfold = KFold(n_splits=10)
    cv_result = cross_val_score(model, X, Y, cv=kfold)
    results.append((name, cv_result))
    
for i in range(len(results)):
    print('name: {}; cross_val_score: {}'.format(results[i][0], results[i][1].mean()))
    
"""
name: KNN; cross_val_score: 0.7147641831852358
name: KNN with weights; cross_val_score: 0.6770505809979495
name: Radius Neighbors; cross_val_score: 0.6497265892002735
"""

上述代码,我们通过KFold把数据集分成10份,其中1份会作为交叉验证集来计算模型准确性,剩余9份作为训练集。cross_val_score()函数总共计算出10次不同训练集和交叉验证集组合得到的模型评分,最后求平均值。 看起来,还是普通的knn算法性能更优一些。

4.3 模型训练及分析

根据上面模型比较得到的结论,我们接下来使用普通的knn算法模型对数据集进行训练,并查看对训练样本的拟合情况以及对测试样本的预测准确性情况:

knn = KNeighborsClassifier(n_neighbors=2)
knn.fit(X_train, Y_train)
train_score = knn.score(X_train, Y_train)
test_score = knn.score(X_test, Y_test)
print('train score: {}; test score : {}'.format(train_score, test_score))

"""
train score: 0.8485342019543974; test score : 0.6623376623376623
"""

从这里可以看到两个问题。

  • 对训练样本的拟合情况不佳,评分才0.84多一些,说明算法模型太简单了,无法很好地拟合训练样本。
  • 模型准确性不好,0.66左右的预测准确性。
    我们画出学习曲线,查看一下。
from sklearn.model_selection import learning_curve
import numpy as np

def plot_learning_curve(plt, estimator, title, X, y, ylim=None, cv=None,
                        n_jobs=1, train_sizes=np.linspace(.1, 1.0, 5)):
    """
    Generate a simple plot of the test and training learning curve.

    Parameters
    ----------
    estimator : object type that implements the "fit" and "predict" methods
        An object of that type which is cloned for each validation.

    title : string
        Title for the chart.

    X : array-like, shape (n_samples, n_features)
        Training vector, where n_samples is the number of samples and
        n_features is the number of features.

    y : array-like, shape (n_samples) or (n_samples, n_features), optional
        Target relative to X for classification or regression;
        None for unsupervised learning.

    ylim : tuple, shape (ymin, ymax), optional
        Defines minimum and maximum yvalues plotted.

    cv : int, cross-validation generator or an iterable, optional
        Determines the cross-validation splitting strategy.
        Possible inputs for cv are:
          - None, to use the default 3-fold cross-validation,
          - integer, to specify the number of folds.
          - An object to be used as a cross-validation generator.
          - An iterable yielding train/test splits.

        For integer/None inputs, if ``y`` is binary or multiclass,
        :class:`StratifiedKFold` used. If the estimator is not a classifier
        or if ``y`` is neither binary nor multiclass, :class:`KFold` is used.

        Refer :ref:`User Guide <cross_validation>` for the various
        cross-validators that can be used here.

    n_jobs : integer, optional
        Number of jobs to run in parallel (default 1).
    """
    plt.title(title)
    if ylim is not None:
        plt.ylim(*ylim)
    plt.xlabel("Training examples")
    plt.ylabel("Score")
    train_sizes, train_scores, test_scores = learning_curve(
        estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes)
    train_scores_mean = np.mean(train_scores, axis=1)
    train_scores_std = np.std(train_scores, axis=1)
    test_scores_mean = np.mean(test_scores, axis=1)
    test_scores_std = np.std(test_scores, axis=1)
    plt.grid()

    plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
                     train_scores_mean + train_scores_std, alpha=0.1,
                     color="r")
    plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
                     test_scores_mean + test_scores_std, alpha=0.1, color="g")
    plt.plot(train_sizes, train_scores_mean, 'o--', color="r",
             label="Training score")
    plt.plot(train_sizes, test_scores_mean, 'o-', color="g",
             label="Cross-validation score")

    plt.legend(loc="best")
    return plt
from sklearn.model_selection import ShuffleSplit

knn = KNeighborsClassifier(n_neighbors=2)
cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0)
plt.figure(figsize=(10,6), dpi=200)
plot_learning_curve(plt, knn, 'Learn Curve for KNN Diabetes', X, Y, ylim=(0.0, 1.01), cv=cv)

learning_curve
从图中可以看出训练样本评分较低,且测试样本与训练样本距离较大,这是欠拟合现象。不过,knn算法没有更好的措施解决欠拟合问题,也许可以换用其它类型的算法。

  • 63
    点赞
  • 378
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值