%matplotlib inline import matplotlib.pyplot as plt import numpy as np
KNN算法中,其算法参数是K,参数选择需要根据数据来决定。K值越大,模型的偏差越大,对噪声数据越不敏感,当K值很大时,可能造成模型欠拟合;K值越小,模型的方差就会越大,当K值太小,就会造成模型过拟合。
K-近邻算法有一些变种,其中之一就是可以增加邻居的权重。默认情况下,在计算距离时,都是使用相同的权重。实际上,我们可以针对不同的邻居指定不同的权重,如距离越近权重越高。这个可以通过指定算法的weights参数来实现。
另外一个变种是,使用一定半径内的点取代距离最近的K个点。在scikit-learn里,RadiusNeighborsClassifier类实现了这个算法的变种。当数据采样不均匀时,该算法变种可以取得更好的性能。
1. KNN实现分类
# from sklearn.datasets.samples_generator import make_blobs from sklearn.datasets import make_blobs # 根据中心点随机生成数据 centers = [[-2, 2], [2, 2], [0, 4]] # centers = [[-2, 0], [2, 0], [0, 4], [0,2]] X, y = make_blobs(n_samples=100, centers=centers, random_state=0, cluster_std=0.40) # 画出数据 plt.figure(figsize=(12,8)) 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'); # 画出中心点
使用KNeighborsClassifier来对算法进行训练,下面进行单样本预测
from sklearn.neighbors import KNeighborsClassifier # 模型训练 k = 5 clf = KNeighborsClassifier(n_neighbors=k) clf.fit(X, y) # 进行预测 X_sample = [0, 2] X_sample = np.array(X_sample).reshape(1, -1) # 根据两个x坐标预测一个y坐标 y_sample = clf.predict(X_sample) # 使用kneighbors()方法,把这个样本周围距离最近的5个点取出来。取出来的点是训练样本X里的索引,从0开始计算。 neighbors = clf.kneighbors(X_sample, return_distance=False) # 其中,这里输出的1表示一个样本,如果是多个样本,则为n,neighbors里面就会存储n个样本距离最近的前k个点 X_sample.shape, X.shape, y.shape, y_sample.shape, neighbors.shape
((1, 2), (100, 2), (100,), (1,), (1, 5))
# 画出示意图 plt.figure(figsize=(12, 8)) 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]: # 预测点与距离最近的 5 个样本的连线。其中第一个列表是存放x坐标,第二个列表存放y的坐标 plt.plot([X[i][0], X_sample[0][0]], [X[i][1], X_sample[0][1]], 'k--', linewidth=0.6)
多样本预测,上诉图中只使用了一个样本进行可视化,但是一般来说需要处理多个样本,这里使用同样的方法来可视化结果
# 这里的k设置为5 X_sample = np.random.randint(-2,4,[5, 2]) neighbors = clf.kneighbors(X_sample, return_distance=False) X_sample.shape, neighbors.shape
((5, 2), (5, 5))
# 画出示意图 plt.figure(figsize=(12, 8)) 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], X_sample[:,1], marker="x", s=100, cmap='cool') # 待预测的点 for i,neighbors_data in enumerate(neighbors): # 预测点与距离最近的 5 个样本的连线。其中第一个列表是存放x坐标,第二个列表存放y的坐标 for t in neighbors_data: plt.plot([X[t][0], X_sample[i][0]], [X[t][1], X_sample[i][1]], 'k--', linewidth=0.6)
如上图所示,直接根据最近的k个值来决定样本的类别
2. KNN实现回归
# 生成训练样本 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 # 显示图像 plt.scatter(X,y,s=20)
<matplotlib.collections.PathCollection at 0x1db687e7888>
# 训练模型 from sklearn.neighbors import KNeighborsRegressor k = 5 # 回归训练 knn = KNeighborsRegressor(k) knn.fit(X, y) # 生成足够密集的点并进行预测 T = np.linspace(0, 5, 500).reshape(-1,1) # 得到密集区间内的预测点 y_pred = knn.predict(T) # 使用score()方法计算拟合曲线对训练样本的拟合准确性 # knn.score(X, y) # out: 0.9790114894878551 # 再把这些预测点连起来,构成一条拟合曲线 plt.plot(T,y_pred,c='k') # 显示原散点图图像 plt.scatter(X,y,s=20,c='r')
<matplotlib.collections.PathCollection at 0x1db6894cdc8>
3. 示例:KNN实现糖尿病预测
加载数据集
import pandas as pd data = pd.read_csv('E:/学习/机器学习/b站资料/scikit-learn机器学习/code/datasets/pima-indians-diabetes/diabetes.csv') data.head(6)
Pregnancies Glucose BloodPressure SkinThickness Insulin BMI DiabetesPedigreeFunction Age Outcome 0 6 148 72 35 0 33.6 0.627 50 1 1 1 85 66 29 0 26.6 0.351 31 0 2 8 183 64 0 0 23.3 0.672 32 1 3 1 89 66 23 94 28.1 0.167 21 0 4 0 137 40 35 168 43.1 2.288 33 1 5 5 116 74 0 0 25.6 0.201 30 0 # 可以进一步观察数据集里的阳性和阴性样本的个数: data.groupby("Outcome").size()
Outcome 0 500 1 268 dtype: int64
把8个特征值分离出来,作为训练数据集,把Outcome列分离出来作为目标值。然后,把数据集划分为训练数据集和测试数据集。
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) X_train.shape, X_test.shape, Y_train.shape, Y_test.shape
shape of X (768, 8); shape of Y (768,) ((614, 8), (154, 8), (614,), (154,))
分别使用普通的KNN算法、带权重的KNN算法和指定半径的KNN算法对数据集进行拟合并计算评分:
from sklearn.neighbors import KNeighborsClassifier, RadiusNeighborsClassifier # 三种模型的构建:k值均为2 k = 5 models = [] models.append(("KNN", KNeighborsClassifier(n_neighbors=k))) models.append(("KNN with weights", KNeighborsClassifier(n_neighbors=k, weights="distance"))) models.append(("Radius Neighbors", RadiusNeighborsClassifier(n_neighbors=k, 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.7077922077922078 name: KNN with weights; score: 0.7142857142857143 name: Radius Neighbors; score: 0.6493506493506493
怎么样更准确地对比算法准确性呢?一个方法是,多次随机分配训练数据集和交叉验证数据集,然后求模型准确性评分的平均值。scikit-learn提供了KFold和cross_val_score()函数来处理这种问题:
- KFold把数据集分成10份,其中1份会作为交叉验证数据集来计算模型准确性,剩余的9份作为训练数据集。
- cross_val_score()函数总共计算出10次不同训练数据集和交叉验证数据集组合得到的模型准确性评分,最后求平均值。
from sklearn.model_selection import KFold from sklearn.model_selection import cross_val_score results = [] for name, model in models: # 把数据集分成10份,其中1份会作为交叉验证数据集来计算模型准确性,剩余的9份作为训练数据集。 kfold = KFold(n_splits=10) # 函数总共计算出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.7265550239234451 name: KNN with weights; cross val score: 0.7265550239234451 name: Radius Neighbors; cross val score: 0.6497265892002735
可以查看一下result的结果:
results
[('KNN', array([0.63636364, 0.83116883, 0.7012987 , 0.63636364, 0.71428571, 0.75324675, 0.74025974, 0.80519481, 0.68421053, 0.76315789])), ('KNN with weights', array([0.64935065, 0.83116883, 0.68831169, 0.63636364, 0.71428571, 0.75324675, 0.75324675, 0.79220779, 0.68421053, 0.76315789])), ('Radius Neighbors', array([0.5974026 , 0.71428571, 0.54545455, 0.5974026 , 0.64935065, 0.61038961, 0.81818182, 0.67532468, 0.68421053, 0.60526316]))]
可以查看一下models的结果:
models
[('KNN', KNeighborsClassifier()), ('KNN with weights', KNeighborsClassifier(weights='distance')), ('Radius Neighbors', RadiusNeighborsClassifier(radius=500.0))]
对比了不同的模型效果之后,可以进行训练与结果分析
knn = KNeighborsClassifier(n_neighbors=4) # 训练 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.8110749185667753; test score: 0.7142857142857143
从这个输出中可以看到两个问题:
- 一是对训练样本的拟合情况不佳,这说明算法模型太简单了,无法很好地拟合训练样本
- 二是模型的准确性欠佳,不到72%的预测准确性
画出学习曲线证实结论
from sklearn.model_selection import ShuffleSplit from utils import plot_learning_curve knn = KNeighborsClassifier(n_neighbors=2) # 多次计算使其平滑 cv = ShuffleSplit(n_splits=10, test_size=0.2, random_state=0) plt.figure(figsize=(10, 6)) # 这里划分了5次,所以对应五个点 plot_learning_curve(plt, knn, "Learn Curve for KNN Diabetes", X, Y, ylim=(0.0, 1.01), cv=cv);
从图中可以看出来,训练样本评分较低,且测试样本与训练样本距离较大,这是典型的欠拟合现象。KNN算法没有更好的措施来解决欠拟合问题,什么KNN算法不是针对这一问题的好模型?下面进行特征选择与可视化出来
特征选择及数据可视化
8个特征,无法在这么高的维度里画出数据,并直观地观察。一个解决办法是特征选择,即只选择2个与输出值相关性最大的特征,这样就可以在二维平面上画出输入特征值与输出值的关系了。scikit-learn在sklearn.feature_selection包里提供了丰富的特征选择方法。可以使用SelectKBest来选择相关性最大的两个特征:
from sklearn.feature_selection import SelectKBest selector = SelectKBest(k=2) X_new = selector.fit_transform(X, Y) X_new[:5]
array([[148. , 33.6], [ 85. , 26.6], [183. , 23.3], [ 89. , 28.1], [137. , 43.1]])
只使用这2个相关性最高的特征的话,对比不同的KNN模型
results = [] for name, model in models: kfold = KFold(n_splits=10) # 这里使用了X_new来进行训练 cv_result = cross_val_score(model, X_new, 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.7369104579630894 name: KNN with weights; cross val score: 0.7199419002050581 name: Radius Neighbors; cross val score: 0.6510252904989747
还是普通的KNN模型准确性较高, 实验的结果比使用8个特征还要好
现在我们只有2个特征,可以很方便地在二维坐标上画出所有的训练样本,观察这些数据的分布情况:
plt.figure(figsize=(14, 10)) plt.ylabel("BMI") plt.xlabel("Glucose") plt.scatter(X_new[Y==0][:,0],X_new[Y==0][:,1],s=20,c='r',marker='x') plt.scatter(X_new[Y==1][:,0],X_new[Y==1][:,1],s=20,c='b',marker='x') plt.title("Data distribution")
Text(0.5, 1.0, 'Data distribution')
从图中可以看出,在中间数据集密集的区域,阳性样本和阴性样本几乎重叠在一起了,所以比较难以检测