机器学习 - k近邻

29

(本系列的 Jupyter Notebook 可以到我的码云下载。)

概述

k近邻(k-Nearest Neighbors, kNN)是最简单的机器学习算法,它既可以用于分类任务,也可以用于回归任务。所谓 k近邻,就是“k个最近的邻居”的意思,该算法的思想就是找到与测试样本特征最相近的 k 个已知样本,用这 k 个已知样本的标签(分类)或数值(回归)来确定该测试样本的标签或数值。比如我们把人类和阿凡达作为样本集来判断一个拥有蓝皮肤、长耳朵和黄眼睛的新生儿属于哪个物种,我们就从人类和阿凡达中找出几个(比如3个)与这个婴儿特征最相近的生物,结果我们找出的这 3 个生物都是阿凡达,那么我们可以判断这个新生儿属于阿凡达。再比如我要在闲鱼网上卖一本《数据结构与算法》(因为我有两本),为了进行合理的定价,我比较了书名相同、新旧程度也最相近的 3 本书之后,发现它们的价格分别为 $19.00, $21.00, $20.00, 于是我就把我的这本定价为它们的均价:$20.00

那么现在问题来了,如何找出与测试样本最近的邻居呢?要回答这个问题,首先要回答另一个问题:如何计算两个样本的相近程度?

相近程度的计算

在衡量两个样本的相近程度时,我们需要综合考虑两个样本所有特征的相近程度,比如上面阿凡达的例子,我们需要分别比较两个样本的肤色、眼睛颜色、耳朵长度,如果三个特征都相近,我们就认为这两个样本相近。

那么如何计算两个特征的相近程度呢?其实我们在幼儿时代就会做这样的计算。在幼儿时代,经常有大人问我们:“ 3 和 2 差多少?”,“ 5 和 3 差多少?”。我们会很容易地给出回答:“ 3 和 2 差 1 ”,“ 5 和 3 差 2”。这是怎么计算的呢?当然是用减法:$ \left| 3 - 2 \right| = 1 $, $\left| 5 - 3 \right| = 2 $,这里加上绝对值是因为距离没有负值。

曼哈顿距离

通过以上讲解,可以得出一个距离公式:如果我们用 $p$ 和 $q$ 表示两个特征值,那么这两个特征的相近程度 $d$ 就可以表示为:


$$ d = \left| p - q \right| \tag{1} $$

这只是计算一个特征的距离,而样本一般会有多个特征,这时我们就需要把所有特征的距离加起来,作为两个样本的距离。假设 $ p_1, p_2, \cdots, p_n $ 为第一个样本的所有特征值, $ q_1, q_2, \cdots, q_n $ 为第二个样本的所有特征值,那么两个样本的距离计算公式为:


$$ d = \left| p_1 - q_1 \right| + \left| p_2 - q_2 \right| + \cdots + \left| p_n - q_n \right| = \sum_{i=1}^n \left| p_i - q_i \right| \tag{2} $$

这个公式 $(2)$ 称为曼哈顿距离(Manhattan Distance)公式。

欧氏距离

还有另外一种计算距离的公式,称为欧氏距离(Euclidean Distance)公式,相比于曼哈顿距离,欧氏距离对每个加数项取平方,并将最终的结果开根号:


$$ d = \sqrt{ \left| p_1 - q_1 \right|^2 + \left| p_2 - q_2 \right|^2 + \cdots + \left| p_n - q_n \right| } = \sqrt{ \sum_{i=1}^n \left| p_i - q_i \right|^2 } \tag{3} $$

到这里我们已经知道如何计算两个样本的距离了。而两个样本的距离正是衡量两个样本的相似程度的指标。显然,两个样本的距离越小,两个样本就越相近。

现在可以回答一开始的问题了:“如何找出与测试样本最近的邻居呢?” 答案就是:只要计算出测试样本与所有已知样本的距离,将它们从小到大排成一个队列,取队列中的第一个就是最近的邻居了。而 kNN 就是要在这个队列中,找出前 k 个距离最小的邻居,并根据这 k 个邻居的标签或数值,最终确定测试样本的标签或数值。

下面我们通过一个实例来更进一步地了解 kNN 。

示例: 长颈鹿和梅花鹿

Python 实现

假设要区分长颈鹿和梅花鹿,并用脖子的长度作为区分的特征。现在有10个样本,其中有5只长颈鹿,脖子长度为:[2.45, 2.31, 2.38, 2.40, 2.38],有5只梅花鹿,脖子长度为 [0.25, 0.3, 0.24, 0.31, 0.29]。现有脖子长度分别为 2.33, 0.28 的两个测试样本,试预测这两个测试样本是长颈鹿还是梅花鹿。

分析:为了预测测试样本是长颈鹿还是梅花鹿,需要计算测试样本与每一个已知样本之间的距离,最后取 k 个最近的距离,也就是取最近的 k 个样本,如果这 k 个样本中,大多数是长颈鹿,那么就将测试样本预测为长颈鹿,如果大多数是梅花鹿,那么就将测试样本预测为梅花鹿。k 的值可以随意取,但一般不能太大($ \le 20 $),这里我们取 3 .

首先需要把数据整理一下:

1
2
3
4
data = np.array([[2.45], [2.31], [2.38], [2.40], [2.38], [0.25], [0.3], [0.24], [0.31], [0.29]])
target = np.array([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])

test = np.array([[2.33], [0.28]])

1 行,为了将每个样本的所有特征放在一起,我们将每个样本的所有特征放到一个数组里,并将每个样本也放到一个数组里,这样我们就得到了一个二维数组。
2 行,很显然我们的模型为有监督学习,因此我们定义一个目标数组,数组中的每个元素表示相应的样本是长颈鹿还是梅花鹿,这里用 $1$ 表示长颈鹿,用 $0$ 表示梅花鹿。
4 行,定义了我们的两个测试样本。

接下来就是要计算测试样本与已知样本集中所有样本的距离了,为此我们先定义一个计算欧氏距离的函数:

1
2
3
4
5
6
7
8
9
def distance_Euclidean(x):
""" 计算单一样本 x 与整个样本集的欧式距离 """

m = data.shape[0] # 获取整个样本集的样本数量
diff = np.tile(x, [m, 1]) - data # 为了计算方便,将测试样本 x 提高维度,与整个样本集相减
diff_squared = diff ** 2 # 计算平方
distance_squared = diff_squared.sum(axis=1) # 计算平方和
distance = distance_squared ** 0.5 # 计算开方
return distance

这个函数的注释已经很清楚的说明了要做的事情,唯一需要注意的是第 5 行我们在计算测试样本和所有已知样本的差的时候使用了 np.tile() 来提高测试样本的维度,以方便计算,这个函数做的事情可以用下图说明:


$$ \text{np.tile([2.33], [10, 1])} = \begin{bmatrix} \left[ 2.33 \right] \\ \left[ 2.33 \right] \\ \left[ 2.33 \right] \\ \left[ 2.33 \right] \\ \left[ 2.33 \right] \\ \left[ 2.33 \right] \\ \left[ 2.33 \right] \\ \left[ 2.33 \right] \\ \left[ 2.33 \right] \\ \left[ 2.33 \right] \end{bmatrix} \tag{4} $$

也就是说 np.tile() 的第一个参数是要扩展的数组([2.33]),第二个参数为要扩展成的形状((10 x 1))。

计算了测试样本与所有已知样本的距离之后,就可以找到距离最小的前 k 个已知样本了,我们将根据这 k 个样本来预测测试样本的分类。具体来说就是比较这 k 个样本,看看其中最多的分类是哪个分类,那么测试样本就属于哪个分类。为此我们定义如下函数来找到距离最小的前 k 个已知样本,并预测分类:

1
2
3
4
5
6
7
8
9
def knn(d, k):
""" 找出单一样本前 k 个最小距离,并预测分类 """

d_sorted = d.argsort() # 对索引进行排序
min_indices = np.array([0, 0]) # 索引为分类,值为分类数量
for i in range(k): # 统计前 k 个最小距离中,每个分类的数量
min_indices[target[d_sorted[i]]] += 1
min_indices_sort = min_indices.argsort() # 索引从小到大排序
return min_indices_sort[-1] # 返回最大的索引

上面的程序中,
4 行对索引进行了从小到大的排序。
5 行定义了一个数组来保存分类的结果,这里我们使用索引代表每一个分类(0表示梅花鹿,1表示长颈鹿),而索引所对应的值则表示该分类的数量。
6~7 行的 for 循环统计每一个分类的数量。
最后,第 8~9 行对统计的结果进行排序,并返回预测值,这里需要注意的是 argsort() 是根据值从小到大对索引排序,而我们需要找出值最大的索引,因此返回 min_indices_sort[-1]

有了以上两个函数,我们就可以很轻松地预测测试样本的分类了:

1
2
3
4
5
6
7
8
# 计算每个测试样本与整个样本集的欧式距离
ds = np.array([distance_Euclidean(test[i]) for i in range(len(test))])

# 找出前 k 个最小的距离
ds_knn = [knn(d, k=3) for d in ds]

# 打印分类结果
print(ds_knn)

输出为:

1
[1, 0]

可以看到预测结果为第一个样本(脖子长度为 2.33)是长颈鹿,第二个样本(脖子长度为 0.28)是梅花鹿。

完整的程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import numpy as np

# 整理数据
data = np.array([[2.45], [2.31], [2.38], [2.40], [2.38], [0.25], [0.3], [0.24], [0.31], [0.29]])
target = np.array([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])

test = np.array([[2.33], [0.28]])

def distance_Euclidean(x):
""" 计算单一样本 x 与整个样本集的欧式距离 """

m = data.shape[0] # 获取整体样本集的样本数量
diff = np.tile(x, [m, 1]) - data # 为了计算方便,将测试样本 x 提高维度,与整体样本集相减
diff_squared = diff ** 2 # 计算平方
distance_squared = diff_squared.sum(axis=1) # 计算平方和
distance = distance_squared ** 0.5 # 计算开方
return distance

def knn(d, k):
""" 找出单一样本前 k 个最小距离,并预测分类 """

d_sorted = d.argsort() # 对索引进行排序
min_indices = np.array([0, 0]) # 索引为分类,值为分类数量
for i in range(k): # 统计前 k 个最小距离中,每个分类的数量
min_indices[target[d_sorted[i]]] += 1
min_indices_sort = min_indices.argsort() # 索引从小到大排序
return min_indices_sort[-1] # 返回最大的索引


# 计算每个测试样本与整个样本集的欧式距离
ds = np.array([distance_Euclidean(test[i]) for i in range(len(test))])

# 找出前 k 个最小的距离
ds_knn = [knn(d, k=3) for d in ds]

# 打印分类结果
print(ds_knn)

Scikit-Learn 实现

上面我们从零开始实现了一个自己的 kNN 算法,并解决了长颈鹿和梅花鹿的问题。事实上我们从零开始实现 kNN 只是为了更好地理解它,一些机器学习库已经为我们封装好了该算法(以及其他机器学习算法),比如著名的 scikit-learn (简称 sklearn)。现在我们一起看下如何使用 sklearn 解决长颈鹿和梅花鹿的问题:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np
from sklearn.neighbors import KNeighborsClassifier

data = np.array([[2.45], [2.31], [2.38], [2.40], [2.38], [0.25], [0.3], [0.24], [0.31], [0.29]])
target = np.array([1, 1, 1, 1, 1, 0, 0, 0, 0, 0])

test = np.array([[2.33], [0.28]])

neigh = KNeighborsClassifier(n_neighbors=3)
neigh.fit(data, target)
print(neigh.predict(test))

可以看到实际上只用了 3 行代码就完成了同样的 kNN 算法,很简单吧?这里对上面的代码做一些简单的讲解,更详细的用法请参见官网
2 行我们引入了 sklearnkNN 实现类 sklearn.neighbors.KNeighborsClassifier
9 行创建了一个 kNN 对象,构造函数只有一个参数,也就是 k
10 行调用 fit() 方法将数据交给 KNeighborsClassifier 进行训练(实际上什么也不做,因为 kNN 不需要训练)。
11 行调用 predict() 进行预测并打印结果。

可以想象得到,输出结果为:

1
[1 0]

以上的示例仅仅是演示单特征样本的 kNN 算法,现在考虑多特征样本的 kNN 算法。这里引入 《Machine Learning in Action》( Peter Harrington 著 )中的一个示例,在该书第二章中,我们的主人公海伦为了寻找约会对象,整理出来了一份约会对象数据集,该数据集中每一个样本都包含了三个特征:玩视频游戏所耗时间百分比、每年获得的飞行常客里程数、每周消费的冰淇淋公升数。以下是部分数据:

玩视频游戏所耗时间百分比 每年获得的飞行常客里程数 每周消费的冰淇淋公升数
0.8 400 0.5
12 134000 0.9
0 20000 1.1
67 32000 0.1

现在我们利用欧氏距离公式计算一下样本1和样本2之间的距离:


$$ \sqrt{ (0.8 - 12)^2 + (400 - 134000)^2 + (0.5 - 0.9)^2 } \tag{5} $$

可以看到 每年获得的飞行常客里程数 的基数是非常大的,在计算两个样本的距离时,该特征占据非常大的一部分权重,而把其他两个特征的影响忽略掉了,但是这三个特征应该是同等重要的。如何处理这种情况呢?那就是归一化。

归一化

归一化的思想就是把样本的所有特征值进行等比例地缩小(或放大)到 $ [0, 1] $ 的范围,这样就可以达到在特征分布不变的情况下,特征权重保持一致的效果。

归一化公式如下:


$$ new\_value = \frac{old\_value - min}{max - min} \tag{6} $$

也就是样本的每一个特征值与全体样本中该特征的最小值的差,再除以全体样本中该特征的最大值与最小值的差,所得的商就是归一化的结果。

我们可以使用下面的函数对数据进行归一化:

1
2
3
4
5
6
7
8
9
10
def normalize(data):
""" 实现归一化

data: numpy array 表示的数据集
"""

min_val = data.min()
max_val = data.max()
interval = max_val - min_val
return (data - min_val) / interval

下图对比了 每年获得的飞行常客里程数玩视频游戏所耗时间百分比 的散点在图归一化前和归一化后的结果:

归一化前后对比

可以看到特征的分布没有改变,而特征值都缩小到了 $ [0, 1] $ 范围之内,这样两个特征的权重就一致了。

K 的取值对预测结果的影响

在 kNN 算法中,k 的取值对预测结果影响比较大,尤其是在测试样本恰好分布在分界线上的情况,考虑下面的散点图:

k值对预测结果的影响

可以看到测试样本(绿色圆圈)正好处在两种分类的交界处,在 k 为 3 时,算法的预测结果为红色三角,而当 k 为 5 时,算法的预测结果为蓝色方块。

示例:鸢尾花分类

下面我们通过预测鸢尾花的品种来更详细的说明 kNN 的实现步骤。鸢尾花(Iris)数据集是 1936 年由 R.A. Fisher 引入的经典多维数据集,该数据集包含鸢尾花的三个品种(Iris Setosa, Iris Virginica, Iris versicolor),每个品种各 50 个样本,每个样本有 4 个特征:萼片(sepals)长,萼片宽,花瓣(petals)长,花瓣宽。本例仅用花瓣长和花瓣宽作为特征进行预测。

准备数据

sklearn 为我们提供了专门获取鸢尾花数据集的方法 load_iris()

1
2
3
4
from sklearn.datasets import load_iris

iris = load_iris()
print(iris.DESCR)

load_iris() 的返回值是一个字典,其中包含了下面几个字段:

字段 含义
DESCR 对数据集的说明
data 训练数据集
target 训练目标集
feature_names 特征名称
target_names 分类名称

上面的程序打印了对数据集的说明,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
Iris Plants Database
====================

Notes
-----
Data Set Characteristics:
:Number of Instances: 150 (50 in each of three classes)
:Number of Attributes: 4 numeric, predictive attributes and the class
:Attribute Information:
- sepal length in cm
- sepal width in cm
- petal length in cm
- petal width in cm
- class:
- Iris-Setosa
- Iris-Versicolour
- Iris-Virginica
:Summary Statistics:

============== ==== ==== ======= ===== ====================
Min Max Mean SD Class Correlation
============== ==== ==== ======= ===== ====================
sepal length: 4.3 7.9 5.84 0.83 0.7826
sepal width: 2.0 4.4 3.05 0.43 -0.4194
petal length: 1.0 6.9 3.76 1.76 0.9490 (high!)
petal width: 0.1 2.5 1.20 0.76 0.9565 (high!)
============== ==== ==== ======= ===== ====================

:Missing Attribute Values: None
:Class Distribution: 33.3% for each of 3 classes.
:Creator: R.A. Fisher
:Donor: Michael Marshall (MARSHALL%PLU@io.arc.nasa.gov)
:Date: July, 1988

This is a copy of UCI ML iris datasets.
http://archive.ics.uci.edu/ml/datasets/Iris

The famous Iris database, first used by Sir R.A Fisher

This is perhaps the best known database to be found in the
pattern recognition literature. Fisher's paper is a classic in the field and
is referenced frequently to this day. (See Duda & Hart, for example.) The
data set contains 3 classes of 50 instances each, where each class refers to a
type of iris plant. One class is linearly separable from the other 2; the
latter are NOT linearly separable from each other.

References
----------
- Fisher,R.A. "The use of multiple measurements in taxonomic problems"
Annual Eugenics, 7, Part II, 179-188 (1936); also in "Contributions to
Mathematical Statistics" (John Wiley, NY, 1950).
- Duda,R.O., & Hart,P.E. (1973) Pattern Classification and Scene Analysis.
(Q327.D83) John Wiley & Sons. ISBN 0-471-22361-1. See page 218.
- Dasarathy, B.V. (1980) "Nosing Around the Neighborhood: A New System
Structure and Classification Rule for Recognition in Partially Exposed
Environments". IEEE Transactions on Pattern Analysis and Machine
Intelligence, Vol. PAMI-2, No. 1, 67-71.
- Gates, G.W. (1972) "The Reduced Nearest Neighbor Rule". IEEE Transactions
on Information Theory, May 1972, 431-433.
- See also: 1988 MLC Proceedings, 54-64. Cheeseman et al"s AUTOCLASS II
conceptual clustering system finds 3 classes in the data.
- Many, many more ...

审查数据

数据审查的目的在于对数据有个整体的把握,这里可以通过画图对数据进行分析,下面的程序提取出鸢尾花的花瓣长和宽,并使用 matplotlib 展示出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import matplotlib.pyplot as plt
import numpy as np

def plot_iris(iris_data):
plt.figure(figsize=(8, 6))
markers = ['x', '+', '^']
labels = iris.target_names
for i in range(3):
data = iris_data[iris.target == i]
# 第 2、3 列分别表示花瓣长、宽
plt.scatter(data[:,2], data[:,3], marker=markers[i], s=60, label=labels[i])
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
plt.legend(loc='best', fontsize=18)
plt.show()

plot_iris(iris.data)

鸢尾花-审查数据

整理数据

对数据进行了审查之后,我们需要整理数据,该步骤一般包括数据清理、特征提取、特征工程、特征缩放等,由于本例的数据集比较简单,因此不需要对数据做过多的额外处理。在实际训练模型时,如果遇到了非数值类型的数据,为了计算距离,则必须将非数值类型的数据转化为数值型的数据。本例仅对数据进行归一化:

1
2
3
4
5
6
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
iris_data = scaler.fit_transform(iris.data)

plot_iris(iris_data)

上面的程序使用了专门用来做归一化的类 sklearn.preprocessing.MinMaxScaler 来对数据进行归一化,并绘制了归一化后的数据散点图:

鸢尾花-整理数据

准备数据集

该步骤需要将整个数据集分为训练集和测试集,这里用 $80\%$ 的数据作为训练集,$20\%$ 的数据集作为测试集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dataset_size = iris.data.shape[0]

np.random.seed(42)

# shuffle dataset
indices = np.random.permutation(dataset_size)
suffled_data = iris.data[indices]
suffled_target = iris.target[indices]

# prepare datasets
ratio = 0.8
train_data_size = int(dataset_size * 0.8)
train_data = suffled_data[:train_data_size]
train_target = suffled_target[:train_data_size]
test_data = suffled_data[train_data_size:]
test_target = suffled_target[train_data_size:]

训练模型

训练集准备好后,就可以开始训练模型了,下面我们分别使用 Python 和 Sklearn 实现 kNN。

Python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import operator

class KNNClassifier:

def __init__(self, k):
self.k = k

def fit(self, train_data, train_target):
self.train_data = train_data
self.train_target = train_target

def distance(self, test_data):
""" 计算测试集 test_data 与 训练集的欧式距离 """
train_size = self.train_data.shape[0] # 获取训练样本集的样本数量
ds = [] # 保存每个测试样本与训练样本的距离
for data in test_data:
diff = np.tile(data, [train_size, 1]) - self.train_data # 与训练样本集相减
diff_squared = diff ** 2 # 计算平方
distance_squared = diff_squared.sum(axis=1) # 计算平方和
distance = distance_squared ** 0.5 # 计算开方
ds.append(distance)
return np.array(ds)

def predict(self, test_data):
""" 预测分类结果 """

distances = self.distance(test_data) # 计算距离
distances_sorted = distances.argsort() # 对索引进行排序
predicts = [] # 保存预测结果
for distance in distances_sorted: # 遍历每一个样本的距离,找出最小的 k 个
predict_dict = {} # 保存预测结果
for i in range(self.k): # 统计前 k 个最小距离中,每个分类的数量
key = self.train_target[distance[i]]
predict_dict[key] = predict_dict.get(key, 0) + 1
items = predict_dict.items()
sorted_predict_dict = sorted(predict_dict.items(), key=operator.itemgetter(1), reverse=True)
predict = sorted_predict_dict[0][0]
predicts.append(predict)
return np.array(predicts)

kNN = KNNClassifier(k=3)
kNN.fit(train_data, train_target)
predicts = kNN.predict(test_data)
print(predicts)

输出的结果为:

1
[1 0 1 1 0 1 2 2 0 1 2 2 0 2 0 1 2 2 1 2 1 1 2 2 0 1 1 0 1 2]

Sklearn 实现

1
2
3
4
5
6
from sklearn.neighbors import KNeighborsClassifier

kNN = KNeighborsClassifier(n_neighbors=3)
kNN.fit(train_data, train_target)
predicts = kNN.predict(test_data)
print(predicts)

输出结果为:

1
[1 0 1 1 0 1 2 2 0 1 2 2 0 2 0 1 2 2 1 2 1 1 2 2 0 1 1 0 1 2]

评估模型

最后,我们使用正确率评估我们的模型,还是分为 Python 版本和 Sklearn 版本。

Python 实现

1
2
3
correct = (predicts == test_target).astype(float)
accuracy = np.mean(correct)
print("Test accuracy: ", accuracy)

输出结果为:

1
Test accuracy:  0.9666666666666667

Sklearn 实现

1
2
accuracy = kNN.score(test_data, test_target)
print("Test accuracy: ", accuracy)

输出结果为:

1
Test accuracy:  0.9666666666666667

总结

到这里 kNN 就基本介绍完了,下面做一些总结。

kNN 适用范围

  • 分类任务
  • 回归任务

kNN 的一般流程

  • 收集数据:可以使用任何方法
  • 准备数据:构造计算距离时所需要的数值型数据
  • 分析数据:可以使用任何方法
  • 训练算法:kNN 没有训练步骤
  • 测试算法:计算正确率或错误率
  • 使用算法:将输入数据整理为适用于模型的输入数据,应用 kNN 预测结果

kNN优缺点

  • 优点:精度高、对异常值不敏感、无数据输入假定。
  • 缺点:计算复杂度高、空间复杂度高。

感谢阅读!祝你度过愉快的一天!如果有任何疑问或建议,请在评论区留言!

版权声明:本文为原创文章,转载请注明出处。http://cynhard.com/2018/06/10/ML-kNN/

推荐文章