K-近邻算法的核心思想是未标记样本的类别,由距离其最近的K个邻居投票来决定。
假设,我们有一个已经标记的数据集,即已经知道了数据集中每个样本所属的类别。此时,有一个未标记的数据样本,我们的任务是预测出这个数据样本所属的类别。K-近邻算法的原理是,计算待标记的数据样本和数据集中每个样本的距离,取距离最近的K个样本。待标记的数据样本所属的类别,就由这K个距离最近的样本投票产生。
假设X_test为待标记的数据样本,X_train为已标记的数据集,算法原理的伪代码如下:
优点:准确度高,对异常值和噪声有较高的容忍度。
缺点:计算量较大,对内存的需求也较大。从算法原理可以看出来,每次对一个未标记样本进行分类时,都需要全部计算一遍距离。
其算法参数是K,参数选择需要根据数据来决定。K值越大,模型的偏差越大,对噪声数据越不敏感,当K值很大时,可能造成模型欠拟合;K值越小,模型的方差就会越大,当K值太小,就会造成模型过拟合。
在scikit-learn里,使用K-近邻算法进行分类处理的是sklearn.neightbors.KNeightborsClassifier类。
from sklearn.datasets 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)
我们使用sklearn.datasets.samples_generator包下的make_blobs()函数来生成数据集,这里生成60个训练样本,这些样本分布在centers参数指定的中心点的周围。cluster_std是标准差,用来指明生成的点分布的松散程度。生成的训练数据集放在变量X里面,数据集的类别标记放在y里面。
我们可以把X和y的值打印出来查看,一个更直观的方法是使用matplotlib库,它可以很容易地把生成的点画出来:
import matplotlib.pyplot as plt
import numpy as np
plt.figure(figsize=(6,4),dpi=144)
c = np.array(centers)
plt.scatter(X[:,0],X[:,1],c=y,s=100,cmap='cool')
# cmap获取图谱 c是色彩或颜色序列,y是传入的数据标签。
plt.scatter(c[:,0],c[:,1],s=100,marker='^',c='orange')
plt.show()
这些点的分布情况在坐标轴上一目了然,其中三角形的点即各个类别的中心点。
使用KNeighborsClassifier来对算法进行训练,我们选择的参数是K=5。
KNeighborsClassifier(algorithm=‘auto’, leaf_size=30, metric=‘minkowski’,metric_params=None, n_jobs=1, n_neighbors=5, p=2,weights=‘uniform’)
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 = [0,2]
X_sample = np.array(X_sample).reshape(1, -1)
y_sample = clf.predict(X_sample)
neighbors = clf.kneighbors(X_sample,return_distance=False)
我们要预测的样本是[0,2],使用kneighbors()方法,把这个样本周围距离最近的5个点取出来。取出来的点是训练样本X里的索引,从0开始计算。
注意:kneighbors()接收一个二维数组作为参数,所以X_sample需要变成二维。
把待预测的样本以及和其最近的5个点标记出来。
plt.figure(figsize=(6,4),dpi=144)
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') #待预测的点
#预测点与距离最近的5个样本的连线
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)
plt.show()
本节使用KNN算法及其变种,对Pima印第安人的糖尿病进行预测。数据来源kaggle.com,网址为:https://www.kaggle.com/uciml/pima-indians-diabetes-database
大家可以自己去下载。
使用Pandas加载数据:
import pandas as pd
data = pd.read_csv('data\diabetes.csv')
print('dataset shape {}'.format(data.shape))
data.head()
输出:dataset shape (768, 9)
从打印出的信息可以看到,这个数据集一共有768个样本、8个特征、1个标签(Outcome:0表示没有糖尿病,1表示有糖尿病)。8个特征分别如下:
Pregnancies:怀孕的次数
Glucose:血浆葡萄糖浓度,采用2小时口服葡萄糖耐量试验测得
BloodPressure:舒张压(毫米汞柱)
SkinThickness:肱三头肌皮肤褶皱厚度(毫米)
Insulin:两个小时血清胰岛素(μU/毫升)
BMI:身体质量指数,体重除以身高的平方
DiabetesPedigreeFunction:糖尿病血统指数,糖尿病和家庭遗传相关
Age:年龄
我们可以进一步观察数据集里的阳性和阴性样本的个数:
data.groupby('Outcome').size()
输出:
Outcome
0 500
1 268
dtype: int64
其中,阴性样本500例,阳性样本268例。
接着需要对数据集进行简单处理,把8个特征值分离出来,作为训练数据集,把Outcome列分离出来作为目标值。然后,把数据集划分为训练数据集和测试数据集。
X = data.iloc[:,: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,)
分别使用普通的KNN算法、带权重的KNN算法和指定半径的KNN算法对数据集进行拟合并计算评分:
from sklearn.neighbors import KNeighborsClassifier, RadiusNeighborsClassifier
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)))
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.6948051948051948
name: KNN with weights; score: 0.5974025974025974
name: Radius Neighbors; score: 0.6428571428571429
带权重的KNN算法,我们选择了距离越近、权重越高。指定半径的KNN算法的半径选择了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算法性能更优一些。接下来,我们就使用普通的KNN算法模型对数据集进行训练,并查看对训练样本的拟合情况以及对测试样本的预测准确性情况:
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()))
输出:
train score: 0.8289902280130294; test score: 0.7272727272727273
从这个输出中可以看到两个问题。一是对训练样本的拟合情况不佳,评分才0.82多一些,这说明算法模型太简单了,无法很好地拟合训练样本。二是模型的准确性欠佳,不到69%的预测准确性。我们可以进一步画出学习曲线,证实结论。
import numpy as np
from matplotlib import pyplot as plt
from sklearn.model_selection import ShuffleSplit
from sklearn.model_selection import learning_curve
def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None,
n_jobs=1, train_sizes=np.linspace(.1, 1.0, 5)):
plt.figure()
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
knn = KNeighborsClassifier(n_neighbors=2)
cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0)
plt.figure(figsize=(10, 6))
plot_learning_curve(knn, "Learn Curve for KNN Diabetes", X, Y, ylim=(0.7, 1.01), cv=cv)