本文已参与「新人创作礼」活动,一起开启掘金创作之路。
%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')
从图中可以看出,在中间数据集密集的区域,阳性样本和阴性样本几乎重叠在一起了,所以比较难以检测