引言
本文介绍本系列的第一个机器学习算法——K近邻算法(K-Nearest Neighbors,knn)。
它的思想很简单,用到的数学知识也比较少(只用到了求距离公式),效果好(缺点?)。
本文还会涉及到和应用机器学习相关的问题的处理方式。
k近邻算法
下面解释下这个算法的思想。我们以一个例子来阐述。
以肿瘤大小和时间作为特征,以良性和恶性作为标签,我们画出下面的图:
这里用红色表示良性肿瘤,蓝色表示恶性肿瘤。这些作为一个初始信息,假设此时来了一个肿瘤患者,将其映射到上图中得到了下面绿色的点。
此时我们如何判断新来的患者是良性还是恶性(肿瘤)。
如果用k近邻算法来求解的话,我们需要先取一个 值,这里假设为 。
对于每个新的数据点,该算法做的事情是,寻找离新的数据点最近的3个点。
然后最近的点以它们自己的标签进行投票,这里将
设成奇数也是有道理的。
这里最近的3个点都是恶性肿瘤的点,因此k近邻算法就说这个新的数据点很可能是属于恶性肿瘤标签。
k近邻算法认为两个样本足够相似的话,它们就有更高的概率属于同一个类别。
这里用特征空间中的距离来描述相似性。
假设再来了一个新的样本点,下图绿点:
这时,最邻近的点进行投票,其中良性投票:恶性投票为 2 : 1,因此k近邻认为这个样本点更可能属于良性的。
k近邻算法主要解决监督学习中的分类问题。
下面我们通过代码来实现k近邻的思想。
import numpy as np
import matplotlib.pyplot as plt
# 这里先用假的数据集
raw_data_X = [[ 3.3935,2.3312],
[3.1101,1.7815],
[1.3438,3.3684],
[3.5823,4.6792],
[2.2804,2.8670],
[7.4234,4.6965],
[5.7451,3.5340],
[9.1721,2.5110],
[7.7928,3.4241],
[7.9398,0.7917]]
raw_data_y = [0,0,0,0,0,1,1,1,1,1] # 0良性肿瘤,1恶性肿瘤
其中大写的X
表示矩阵,小写的y
表示向量。
我们先绘制散点图,来看数据的分布是怎样的
X_train = np.array(raw_data_X)
y_train = np.array(raw_data_y)
# 分别用不同颜色来绘制不同类别的点
plt.scatter(X_train[y_train == 0,0] ,X_train[y_train==0,1],color='g')
plt.scatter(X_train[y_train == 1,0] ,X_train[y_train==1,1],color='r')
plt.show()
假设此时来了一个新的样本点x = np.array([8.0934,3.3657])
,我们如何通过knn来判断其类别。
我们在上面散点图的基础上,增加到新样本的绘制:
x = np.array([8.0934,3.3657]) #新的样本点
plt.scatter(X_train[y_train == 0,0] ,X_train[y_train==0,1],color='g')
plt.scatter(X_train[y_train == 1,0] ,X_train[y_train==1,1],color='r')
plt.scatter(x[0],x[1],color='b')
plt.show()
新样本点是上图蓝点,根据knn的思想,我们可以猜到它属于红色样本点类别。
接下来看如何用代码来实现knn的思想。
首先要计算新的样本点与所有原来的点之间的距离。那么怎么计算距离呢,我们用欧几里得距离公式来计算。
$$
在二维平面中,就是
代码实现也很简单(不了解numpy的可以参阅机器学习入门——numpy与matplotlib的使用简介):
distances = []#保存新样本点与原来点的距离
from math import sqrt
for x_train in X_train:
d = sqrt(np.sum((x_train - x)**2)) # x_train - x 两个向量对应元素相减,得到一个新的向量,每个元素再求平方,再通过聚合函数得到一个数,最后进行开方
distances.append(d)
这样我们就得到了训练数据中的每个点与新样本点之间的距离,接下来找到距离最小的 个点即可。
其实上面的for
循环可以用列表推导式简化:
distances = [sqrt(np.sum((x_train - x)**2)) for x_train in X_train]
接下来就是按照距离排序,但是我们知道最小的几个距离是没用的,我们还要知道哪些点与新样本是距离最小的。
此时argsort
就可以应用的,它返回的就是索引。
np.argsort(distances)
#array([8, 7, 5, 6, 9, 3, 0, 1, 4, 2], dtype=int64)
从上面可以看到,最近的是索引为8的那个点,其次是7,依此类推。
nearset = np.argsort(distances)
k = 6
topK_y = [y_train[i] for i in nearset[:k]] #i在nearset数组中前k个元素
接下来就计算一下这k个点里面属于哪个类别中的点最多,这里如果用偶数的话,55开怎么办。所以建议取奇数(其实还要考虑类别的个数,如果是3个类别,取奇数9,也有3:3:3的风险,因此k的选择很重要)。
先抛出奇数还是偶数的问题,我们可以用Counter
这个类很方便的计算属于哪个类别的多。
from collections import Counter
Counter(topK_y)
# Counter({1: 5, 0: 1})
可以看到,5个点投票类别1,1个点投票类别0。
我们可以调用most_common
方法得到投票数最多的类别,它返回的是一个元组列表
最终的代码就是:
from collections import Counter
votes = Counter(topK_y)
predict_y = votes.most_common(1)[0][0]
print(predict_y) # 1
得到它的类别为1。我们回过头来看下类别1使用红色点绘制的,和我们的猜想一致。
以上就是简单的用代码实现knn的思想。
我们汇总一下以上的代码,形成一个方法:
def kNN_classify(k,X_train,y_train,x):
distances = [sqrt(np.sum((x_train - x)**2)) for x_train in X_train]
nearset = np.argsort(distances)
topK_y = [y_train[i] for i in nearset[:k]]
votes = Counter(topK_y)
return votes.most_common(1)[0][0]
接下来运行一下
predict_y = kNN_classify(6,X_train,y_train,x)
print(predict_y) # 1
输出为1,没毛病。
我们来回顾下机器学习的流程。
在监督学习算法中,训练数据集通常包含训练数据和数据标签,通过训练算法得到模型的过程叫拟合(fit)。输入样例送给模型后,得到输出结果的过程叫预测(predict)。
我们把knn算法搬到这个流程里面会发现,knn算法并没有训练得到模型,确实是。可以说knn是一个不需要训练过程的算法。
这样在进行算法抽象,抽象出公共方法的时候就很不爽了。为了和其他算法统一,可以认为训练数据集本身就是knn算法的模型。
我们先来看看sklearn是如何调用knn算分进行预测的。
sklearn中的knn
sklearn采用面向对象的思想,每个算法都是一个类。
from sklearn.neighbors import KNeighborsClassifier
kNN_classifier = KNeighborsClassifier(n_neighbors=6)#这个参数就是k
kNN_classifier.fit(X_train,y_train) #对sklearn中的每个学习算法都需要fit
x = np.array([8.0934,3.3657]) #新的样本点
kNN_classifier.predict(x)
这段代码在新版的sklearn中会报错,在新版的sklearn中,所有的数据都应该是二维矩阵,哪怕它只是单独一行或一列。
ValueError: Expected 2D array, got 1D array instead
array=[8.0934 3.3657].
Reshape your data either using array.reshape(-1, 1) if your data has a single feature or array.reshape(1, -1) if it contains a single sample.
并且上面有解决方法,我们按照它提示的来处理一下。
x = np.array([8.0934,3.3657]).reshape(1, -1)#shape变成了(1, 2)
kNN_classifier.predict(x)
返回的结果是
array([1])
注意它返回的是一个数组,可以同时预测多个样本,这里我们只传入了一个样本(一个一行的矩阵,多行矩阵就是多个样本,用心良苦啊),因此数组中元素个数为1。
可以看到,上面的过程是符合这个图的
我们也把之前写的代码整理成这种模式。
这里我们继承了BaseEstimator
,这意味着它可以在任何使用scikit-learn estimator 的地方使用
import numpy as np
from math import sqrt
from collections import Counter
from sklearn.base import BaseEstimator
class KNNClassifier(BaseEstimator):
def __init__(self,k):
assert k >= 1, "k must be valid"
self.k = k
self._X_train = None
self._y_train = None
def fit(self,X_train,y_train):
assert X_train.shape[0] == y_train.shape[0], \
"the size of X_train must be equal to the size of y_train"
assert self.k <= X_train.shape[0], \
"the size of X_train must be at least k."
self._X_train = X_train
self._y_train = y_train
return self
def predict(self,X_predict):
assert self._X_train is not None and self._y_train is not None, \
"must fit before predict!"
# X_predict矩阵的行数无所谓,但是列数必须和训练集中的一样
assert X_predict.shape[1] == self._X_train.shape[1], \
"the feature number of X_predict must be equal to X_train"
y_predict = [self._predict(x) for x in X_predict]
return np.array(y_predict)
def _predict(self,x):
"""给定单个待预测数据x,返回x的预测结果值"""
assert x.shape[0] == self._X_train.shape[1], \
"the feature number of x must be equal to X_train"
distances = [sqrt(np.sum((x_train - x) ** 2))
for x_train in self._X_train]
nearest = np.argsort(distances)
topK_y = [self._y_train[i] for i in nearest[:self.k]]
votes = Counter(topK_y)
return votes.most_common(1)[0][0]
def __repr__(self):
return "KNN(k=%d)" % self.k #相当于java toString()
接下来应用一下我们刚才写的类:
knn_clf = KNNClassifier(k=6)
knn_clf.fit(X_train,y_train)
X_predict = x = np.array([8.0934,3.3657]).reshape(1, -1)
y_predict = knn_clf.predict(X_predict)
y_predict # array([1])
到此我们实现了knn算法,但是这个算法的表现如何,准确率高不高呢。接下来一起学习下如何评估算法的表现。
判断机器学习算法的性能
我们先来看下机器学习的过程,首先我们将原始数据都当成训练数据,训练出一个模型,在knn算法中是将新的数据与训练集中所有数据求距离,最后找出前k小的距离。也就是说,我们用全部数据得到的模型来预测新数据所属的类别。
我们得到模型的意义是想在真实环境中使用,现在我们这样做是有很大的问题的。
第一个非常严重的问题是,我们拿所有的训练数据去训练模型,我们只能将这个模型放到真实环境中去使用 了,如果模型很差怎么办?并且真实环境难以拿到真实的标签。
这样我们无法知道我们的模型是好还是坏。
改进这个问题最简单的方法是将训练和测试数据分离。
我们将原始数据的大部分作为训练数据,剩下的一部分作为测试数据。这样我们只用我们的训练数据训练出了模型,我们接下来就可以用没有参与到训练过程的测试数据来测试我们模型的好坏。
因此,我们可以通过测试数据直接判断模型好坏,这样可以在模型进入真实环境前改进模型。
这种方式叫 train test split,其实这种方式还是存在一些问题,但这里先不展开,后面的文章会介绍到。
我们先用这种方式来测试我们之前写好的knn算法。
此时我们用sklearn提供的iris数据集来进行训练:
import numpy as np
import matplotlib.pyplot as plt
from sklearn import datasets
iris = datasets.load_iris()# 使用iris数据集
X = iris.data # (150, 4)
y = iris.target #(150,)
得到了数据集后,我们就进行训练测试数据分离。
在拆分前一般需要先进洗牌操作,为什么呢,我们可以看下y
这个类别向量
可以看到,它是有序的。如果我们直接拿这个数据进行拆分的话,我们得到的训练数据样本分布就很不均匀,这样会导致算法的表现不好。
要注意的是,我们不能单独的对X
或y
进行洗牌,因为它们是有一一对应的关系的。我们可以对它们的索引进行洗牌:
shuffle_indexs = np.random.permutation(len(X))#对150个连续数进行随机排列
shuffle_indexs
接下来就可以开始拆分了,先指定下作为测试数据集的比例,这里假设20%的数据作为测试数据。
test_ratio = 0.2
test_size = int(len(X) * test_ratio)
test_indexes = shuffle_indexs[:test_size]#前20%是测试数据
train_indexes = shuffle_indexs[test_size:]#后80%是训练数据
得到了这些索引后,我们可以使用花式索引的方式来获取训练数据和测试数据:
X_train = X[train_indexes]
y_train = y[train_indexes]
X_test = X[test_indexes]
y_test = y[test_indexes]
拆分好后,我们来看下训练数据的shape:
将上述过程封装成一个函数,以便后面多次调用:
def train_test_split(X,y,test_ratio=0.2,seed=None):
if seed:
np.random.seed(seed) #支持指定随机种子
shuffle_indexs = np.random.permutation(len(X))
test_size = int(len(X) * test_ratio)
test_indexes = shuffle_indexs[:test_size]
train_indexes = shuffle_indexs[test_size:]
X_train = X[train_indexes]
y_train = y[train_indexes]
X_test = X[test_indexes]
y_test = y[test_indexes]
return X_train,X_test,y_train,y_test
接下来我们使用这个方法来对数据集进行拆分,并应用到我们自己写的knn分类算法中:
X_train,X_test,y_train,y_test = train_test_split(X,y) #其他两个参数取默认值
print(X_train.shape)
print(X_test.shape)
print(y_train.shape)
print(y_test.shape)
knn_clf = KNNClassifier(k=3)
knn_clf.fit(X_train,y_train)
y_predict = knn_clf.predict(X_test)#对所有的test数据进行预测
y_predict
我们将预测的结果与真实标签进行比较,发现只有一个样本判断错误,其他都是判断正确的。
那么如何量化上面这句话呢,就是通过准确率。
sum(y_predict == y_test)/len(y_test)
得出准确率:0.9666666666666667
从这里我们可以看到,虽然knn的思想简单,但是它的准确率还是很高的。
最后我们介绍下skleran中封装的train_test_splilt
:
from sklearn.model_selection import train_test_split
X_train,X_test,y_train,y_test = train_test_split(X,y)
可以看到使用起来和我们自己写的方法是一样的,因为在设计我们自己的方法的时候其实是参考了sklearn的方法的。
但是test_size
这个参数的名称不一样,这里要注意一下: