机器学习笔记三之NumPy、Matplotlib、kNN算法

NumPy

NumPy是Python中的一个类库,它支持高阶维度数组(矩阵)的创建及各种操作、运算,是我们在机器学习中经常会使用的一个类库。这一节再看一些NumPy的矩阵用法。

numpy.random

NumPy也提供了生成随机数和随机元素数组的方法,我们来看一下:

# 生成从0到10之间的随机数
np.random.randint(0, 10)
# 结果
3

# 生成元素从0到10,一共4个随机元素的数组
np.random.randint(0, 10, size=4)
# 结果
array([4, 7, 8, 1])

# 生成元素随机从0到10,3行5列的矩阵
np.random.randint(0, 10, size=(3, 5))
# 结果
array([[6, 9, 7, 0, 9],
[7, 4, 8, 7, 8],
[4, 4, 9, 7, 2]])

如果我们希望每次使用随机方法生成的结果都是一样的,一般调试时候有这个需求,此时NumPy的random()方法也提供了方便简单的方式,既随机种子的概念:

# 生成随机矩阵前给定一个种子
np.random.seed(123)
# 然后生成随机矩阵
np.random.randint(0, 10, size=(4, 5))
# 结果
array([[2, 2, 6, 1, 3],
[9, 6, 1, 0, 1],
[9, 0, 0, 9, 3],
[4, 0, 0, 4, 1]])

# 再次生成随机矩阵时,只要传入相同的种子,就可以得到相同结果的矩阵
np.random.seed(123)
np.random.randint(0, 10, size=(4, 5))
# 结果
array([[2, 2, 6, 1, 3],
[9, 6, 1, 0, 1],
[9, 0, 0, 9, 3],
[4, 0, 0, 4, 1]])

# 默认范围是从0.0到1.0,返回值为float型
np.random.random()
# 结果
0.18249173045349998

# 传入的参数是数组的大小
np.random.random(10)
# 结果
array([ 0.17545176, 0.53155137, 0.53182759, 0.63440096, 0.84943179,
0.72445532, 0.61102351, 0.72244338, 0.32295891, 0.36178866])

# 创建4行5列,元素值的范围从0.0到1.0的矩阵
np.random.random((4, 5))
# 结果
array([[ 0.22826323, 0.29371405, 0.63097612, 0.09210494, 0.43370117],
[ 0.43086276, 0.4936851 , 0.42583029, 0.31226122, 0.42635131],
[ 0.89338916, 0.94416002, 0.50183668, 0.62395295, 0.1156184 ],
[ 0.31728548, 0.41482621, 0.86630916, 0.25045537, 0.48303426]])

指定均值和标准差生成随机数数组或矩阵

我们先来看看均值、方差、标准差的概念。均值很好理解,就是所有样本数据的平均值,描述了样本集合的中间点:

$$ \overline X=\frac{\sum_{i=1}^nX_i}n $$

方差是衡量样本点和样本期望值相差的度量值:

$$ S^2 = \frac{\sum_{i=1}^n(X_i-\overline X)^2} n $$

标准差描述的是样本集合的各个样本点到均值的距离之平均:

$$ S = \sqrt {\frac{\sum_{i=1}^n(X_i-\overline X)^2} n } $$

标准差也就是对方差开根号。举个例子,[0, 8, 12, 20][8, 9, 11, 12],两个集合的均值都是10,但显然两个集合的差别是很大的,计算两者的标准差,前者是8.3后者是1.8,显然后者较为集中,标准差描述的就是这种散布度或者叫做波动大小。综上,方差的意义在于描述随机变量稳定与波动、集中与分散的状况。标准差则体现随机变量取值与其期望值的偏差。

NumPy也提供了指定均值和标准差生成随机数的方法,我们来看一下:

# 第一个参数是均值,第二个参数是标准差
np.random.normal(10, 100)
# 结果
53.781947121910044

# 创建均值为10,方差为100的3行5列矩阵
np.random.normal(10, 100, size=(3, 5))
# 结果
array([[ 124.10915759, 27.14517732, -144.95788359, -87.40234817,
-94.91106048],
[ -36.4834381 , -39.05598871, 110.07456975, 224.85141913,
153.24092557],
[ -3.33533336, 10.57740526, -56.76208107, -84.06189149,
103.08098119]])

# 创建一个正态分布的3行5列矩阵,既均值为0,标准差为1
np.random.normal(0, 1, size=(3, 5))
# 结果
array([[-0.94574322, 2.0742057 , 0.34477911, 0.1375712 , 0.45385364],
[-2.07928914, 1.26474497, 1.56236822, -1.0032234 , -0.14807477],
[ 0.01992922, 0.3924738 , -0.11268871, 2.04509319, 0.01095378]])

查看数组维度

# 生成10个元素的一维数组和3行5列的矩阵
import numpy as np
x = np.arange(10)
X = np.arange(15).reshape(3, 5)
x
X
# 结果
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])

# 查看x的维度
x.ndim
# 结果
1

# 查看X的维度
X.ndim
# 结果
2

# 查看数组每个维度的具体信息
x.shape
# 结果
(10,)

X.shape
# 结果
(3, 5)

numpy.array的数据访问

# 一维数组访问第1个元素
x[0]
# 结果
0

# 一维数组访问最后一个元素
x[-1]
# 结果
9

# 二维数组访问第3行,第4列的元素
X[2, 3]
# 结果
13

切片

Python中,有一个获取数组片段非常方便的方法,叫做切片,numpy.array中同样支持切片,我们来看一下:

# 获取x数组中从第1个元素到第5个元素的片段
x[0:5]
# 结果
array([0, 1, 2, 3, 4])

# 如果冒号前不指定位置,那么默认从第一个元素开始
x[:3]
# 结果
array([0, 1, 2])

# 如果冒号后面不指定位置,那么默认取到最后一个元素
x[3:]
# 结果
array([3, 4, 5, 6, 7, 8, 9])

# 切片也支持步长
x[0:10:2]
# 结果
array([0, 2, 4, 6, 8])

x[::2]
# 结果
array([0, 2, 4, 6, 8])

# 取X矩阵的前2行,前3列
X[:2, :3]
# 结果
array([[0, 1, 2],
[5, 6, 7]])

# 对于每个维度都可以指定步长
X[:2, ::2]
# 结果
array([[0, 2, 4],
[5, 7, 9]])

一般将高维矩阵降为低维矩阵其实也是使用切片来处理:

# 取X矩阵所有行的第一列
X[:, 0]
# 结果
array([ 0, 5, 10])

另外需要注意的是通过切片获取NumPy的数组或者矩阵的子数组,子矩阵是通过引用方式的,而Python中的数组通过切片获取的子数组是拷贝方式的。NumPy主要是考虑到性能效率问题。我们来看一下:

# 取X矩阵的前2行,前3列作为子矩阵
subX = X[:2, :3]
subX
# 结果
array([[0, 1, 2],
[5, 6, 7]])

# 给subX矩阵的第1行,第1列的元素赋值
subX[0, 0] = 100
subX
# 结果
array([[100, 1, 2],
[ 5, 6, 7]])

# 再看看X矩阵
X
# 结果
array([[100, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[ 10, 11, 12, 13, 14]])

# 复制子矩阵
subX1 = X[:2, :3].copy()
subX1
# 结果
array([[100, 1, 2],
[ 5, 6, 7]])

subX1[0, 0] = 0
subX1
# 结果
array([[0, 1, 2],
[5, 6, 7]])

X
# 结果
array([[100, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[ 10, 11, 12, 13, 14]])

改变数组维度

NumPy也提供了修改数组维度的方法,我们来看看:

# x是一个一维数组
x
# 结果
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# 将x改为二维数组,既2行5列的矩阵
x.reshape(2, 5)
x
# 结果
array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])

# 如果想让NumPy自动计算某个维度的数,比如我只想将x转换为只有2列的矩阵,有多少行交给NumPy处理
x.reshape(-1, 2)
# 结果
array([[0, 1],
[2, 3],
[4, 5],
[6, 7],
[8, 9]])

数组合并操作

NumPy也提供两个数组合并的操作:

x = np.arange(5)
x
# 结果
array([0, 1, 2, 3, 4])

y = np.arange(5)
y
# 结果
array([0, 1, 2, 3, 4])

# 将x,y这两个一维数组合并
np.concatenate([x, y])
# 结果
array([0, 1, 2, 3, 4, 0, 1, 2, 3, 4])

z = np.array([6, 6, 6, 6, 6])
z
# 结果
array([6, 6, 6, 6, 6])

# 将x,y,z三个一维数组合并
np.concatenate([x, y, z])
# 结果
array([0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 6, 6, 6, 6, 6])

多维数组也支持合并:

# 2行3列的矩阵
V = np.random.randint(0, 10, size=(2, 3))
V
# 结果
array([[9, 2, 0],
[8, 0, 2]])

# 将两个V矩阵合并
np.concatenate([V, V])
# 结果
array([[9, 2, 0],
[8, 0, 2],
[9, 2, 0],
[8, 0, 2]])

# 合并多维数组时,可以设置按照哪个维度合并,axis参数为0时按照行合并,axis参数为1时按照列合并,默认axis为0
np.concatenate([V, V], axis=1)
# 结果
array([[9, 2, 0, 9, 2, 0],
[8, 0, 2, 8, 0, 2]])

上面的示例都是同维度的数组进行合并,那么不同维度的数组如何合并呢,我们来看一下:

# z为一个一维数组
z = np.array([1, 2, 3])
z
# 结果
array([1, 2, 3])

# V为一个2行3列的矩阵
V
# 结果
array([[9, 2, 0],
[8, 0, 2]])

# 直接将V和z合并会抛异常
np.concatenate([V, z])
# 结果
ValueError: all the input arrays must have same number of dimensions

# 在合并时将一维数组z转变为二维数组
np.concatenate([V, z.reshape(1, -1)])
# 结果
array([[8, 2, 9],
[7, 3, 8],
[1, 2, 3]])

其实NumPy提供了更智能的不同维度数组合并的方法,我们来看一下:

# 按垂直方向合并
np.vstack([V, z])
# 结果
array([[8, 2, 9],
[7, 3, 8],
[1, 2, 3]])

# 创建一个2行2列的矩阵
V1 = np.full((2, 2), 10)
V1
# 结果
array([[10, 10],
[10, 10]])

# 按水平方向合并
np.hstack([V, V1])
# 结果
array([[ 8, 2, 9, 10, 10],
[ 7, 3, 8, 10, 10]])

数组分割操作

有合并自然就会有分割,我们来看看NumPy提供的分割方法:

x = np.arange(10)
x
# 结果
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# 对x进行分割,然后传入分割点,如下有两个分割点,所以将会把x分割为3个数组
x1, x2, x3 = np.split(x, [3, 7])
x1
x2
x3
# 结果
array([0, 1, 2])
array([3, 4, 5])
array([6, 7, 8, 9])

# 对于多维数组也是一样
X = np.arange(16).reshape((4, 4))
X
# 结果
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])

# 传入一个分割点,既将X矩阵分割为两个矩阵
X1, X2 = np.split(X, [2])
X1
X2
# 结果
array([[0, 1, 2, 3],
[4, 5, 6, 7]])
array([[ 8, 9, 10, 11],
[12, 13, 14, 15]])

# 分割多维数组同样可以设定按照哪个维度分割,axis默认为0,既按行分割
# axis为1时按列分割
X3, X4 = np.split(X, [2], axis=1)
X3
X4
# 结果
array([[ 0, 1],
[ 4, 5],
[ 8, 9],
[12, 13]])
array([[ 2, 3],
[ 6, 7],
[10, 11],
[14, 15]])

和合并一样,分割也有更快接的方法:

# 按垂直方向分割,既按行分割
X5, X6 = np.vsplit(X, [2])
X5
X6
# 结果
array([[0, 1, 2, 3],
[4, 5, 6, 7]])
array([[ 8, 9, 10, 11],
[12, 13, 14, 15]])

# 按水平方向分割,既按列分割
X7, X8 = np.hsplit(X, [2])
X7
X8
# 结果
array([[ 0, 1],
[ 4, 5],
[ 8, 9],
[12, 13]])
array([[ 2, 3],
[ 6, 7],
[10, 11],
[14, 15]])

矩阵运算

NumPy中提供了完整的矩阵的运算,我们从加减法来看一下:

# A为一个2行5列的矩阵
A = np.arange(10).reshape(2, 5)
A
# 结果
array([[0, 1, 2, 3, 4],
[5, 6, 7, 8, 9]])

# B也是一个2行5列的矩阵
B = np.random.randint(0, 10, size=(2, 5))
B
# 结果
array([[4, 8, 3, 5, 7],
[9, 6, 6, 6, 6]])

# 矩阵加常数
A + 1
#结果
array([[ 1, 2, 3, 4, 5],
[ 6, 7, 8, 9, 10]])

# 矩阵减常数
A - 1
# 结果
array([[-1, 0, 1, 2, 3],
[ 4, 5, 6, 7, 8]])

# 矩阵加矩阵
A + B
# 结果
array([[ 4, 9, 5, 8, 11],
[14, 12, 13, 14, 15]])

# 矩阵减矩阵
A - B
# 结果
array([[-4, -7, -1, -2, -3],
[-4, 0, 1, 2, 3]])

下面我们再来看看数乘:

# 矩阵乘常数
2*A
# 结果
array([[ 0, 2, 4, 6, 8],
[10, 12, 14, 16, 18]])

# C为5行3列的矩阵
C = np.arange(15).reshape(5, 3)
C
# 结果
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9, 10, 11],
[12, 13, 14]])

# 矩阵乘矩阵,真正的矩阵相乘
A.dot(C)
# 结果
array([[ 90, 100, 110],
[240, 275, 310]])

# 矩阵中每个对应元素相乘
A*B
# 结果
array([[ 0, 8, 6, 15, 28],
[45, 36, 42, 48, 54]])

我们再来看看矩阵的转置:

A.T
# 结果
array([[0, 5],
[1, 6],
[2, 7],
[3, 8],
[4, 9]])

聚合操作

NumPy中有很多对数组的聚合操作方法,我们先来看看一维数组:

# 随机取10个元素的 一维数组
D = np.random.random(10)
D
# 结果
array([ 0.70908471, 0.29268356, 0.69885019, 0.28796429, 0.04189265,
0.36932107, 0.0641322 , 0.63989077, 0.02753356, 0.0605743 ])

# 求每个元素的和
np.sum(D)
# 结果
3.1919272951030706

# 求元素的最小值
np.min(D)
# 结果
0.027533561561906672

# 求元素最大值
np.max(D)
# 结果
0.70908470606410545

# 求元素的均值
np.mean(D)
# 结果
0.31919272951030708

# 求元素的标准差
np.std(D)
# 结果
0.26402525382852743

我们再来看看矩阵的聚合操作:

# X为2行3列的矩阵
X = np.arange(6).reshape(2, 3)
X
# 结果
array([[0, 1, 2],
[3, 4, 5]])

# 矩阵中所有元素的和
np.sum(X)
# 结果
15

# 矩阵中所有元素的乘积
np.prod(X)
# 结果
0

np.prod(X + 1)
# 结果
720

# 矩阵元素的均值
np.mean(X)
# 结果
2.5

# 矩阵元素的中位数,median可有效避免元素中出现极值,从而导致均值不准的问题
np.median(X)
# 结果
2.5

# 矩阵元素的方差
np.var(X)
# 结果
2.9166666666666665

# np.std(X)
# 结果
1.707825127659933

索引和排序的相关操作

NumPy提供了一系列对数组索引操作的方法,我们来看一下:

# 随机一维数组
x = np.random.random(10)
x
# 结果
array([ 0.17035458, 0.8968506 , 0.01007584, 0.45925501, 0.6838149 ,
0.32393039, 0.53746647, 0.68561243, 0.66195346, 0.32696068])

# x中元素最小值
np.min(x)
# 结果
0.010075835471876626

# x中最小值元素所在的索引位置
np.argmin(x)
# 结果
2

我们再来看看排序:

# 对x进行排序
np.sort(x)
# 结果
array([ 0.01007584, 0.17035458, 0.32393039, 0.32696068, 0.45925501,
0.53746647, 0.66195346, 0.6838149 , 0.68561243, 0.8968506 ])

# 获取排序后的索引,返回的数组中的元素是索引
np.argsort(x)
# 结果
array([2, 0, 5, 9, 3, 6, 8, 4, 7, 1])

# 对矩阵排序
X1 = np.random.randint(0, 15, size=(3, 5))
X1
# 结果
array([[ 8, 1, 8, 12, 4],
[ 8, 6, 6, 6, 13],
[13, 2, 5, 11, 4]])

np.sort(X1)
# 结果
array([[ 1, 4, 8, 8, 12],
[ 6, 6, 6, 8, 13],
[ 2, 4, 5, 11, 13]])

# 获取排序后的索引
np.argsort(X1)
# 结果
array([[1, 4, 0, 2, 3],
[1, 2, 3, 0, 4],
[1, 4, 2, 3, 0]])

NumPy的Fancy Indexing

一般情况下我们访问NumPy数组的数据,可以使用索引,甚至可以用步长来取:

x = np.arange(16)
x
# 结果
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])

# 取从0到6范围内,步长为2的元素
x[0:6:2]
# 结果
array([0, 2, 4])

但是有时候我们需要取数组中没有什么规律的元素,比如元素之间步长不等的,这就需要用到NumPy提供的Fancy Indexing机制来获取了:

# 将我们需要访问的索引生产一个数组,然后将索引数组传入x数组
ind = [3, 5, 8]
x[ind]
# 结果
array([3, 5, 8])

# 矩阵也是同样的,先生成索引矩阵
ind1 = np.array([[0, 1],
[2, 3]])
x[ind1]
# 结果
array([[0, 1],
[2, 3]])

# 将x转换为4行4列的矩阵X
X = x.reshape(4, -1)
X
# 结果
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])

# 生成希望查询的行和列的索引矩阵,然后传入矩阵X
row = np.array([0, 1, 2])
col = np.array([1, 2, 3])
X[row, col]
# 结果
array([ 1, 6, 11])

# 或者指定前两行
X[:2, col]
# 结果
array([[1, 2, 3],
[5, 6, 7]])

除了使用指定索引以外,我们还可以使用布尔数组或者矩阵来使用Fancy Indexing,我们来看一下:

X
# 结果
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])

# 生成一个布尔数组,True表示感兴趣的索引,False表示不感兴趣的索引
col = [True, False, True, True]
# 然后传入矩阵X,比如我们要获取前三行,第1列,第3列,第4列的元素
X[1:3, col]
# 结果
array([[ 4, 6, 7],
[ 8, 10, 11]])

x
# 结果
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15])

# x数组中的元素小于2的有几个
np.sum(x < 2)
# 结果
2

# x数组中的元素大于3小于10的有几个
np.sum((x > 3) & (x < 10))
# 结果
6

# 判断x数组中的所有元素是否满足一个条件,若有一个满足返回True,若都不满足返回False
np.any(x == 0)
# 结果
True

np.any(x < 0)
# 结果
False

# 判断x数组中的所有元素是否满足一个条件,若有所有元素都满足返回True,若有一个元素不满足返回False
np.all(X > 0)
# 结果
False

np.all(X >= 0)
# 结果
True

# 获取x数组中元素小于5的是哪几个元素
x[x < 5]
# 结果
array([0, 1, 2, 3, 4])

# 获取x数组中元素是偶数的是哪几个元素
x[x % 2 == 0]
# 结果
array([ 0, 2, 4, 6, 8, 10, 12, 14])

X
# 结果
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])

X[:, 3]
# 结果
array([ 3, 7, 11, 15])

X[:, 3] % 3 == 0
# 结果
array([ True, False, False, True], dtype=bool)

X[X[:, 3] % 3 == 0, :]
# 结果
array([[ 0, 1, 2, 3],
[12, 13, 14, 15]])

Matplotlib

在Python中,除了有NumPy这种对数组操作的类库,还有一个类一个在机器学习中使用比较广泛的类库是Matplotlib,这是一个绘制二维图像的类库,我们来看一下:

# 首先导入matplotlib的类库
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt

# 创建数组x,元素从0到10,一共100个元素
x = np.linspace(0, 10, 100)
# 对x数组求sin,获得siny
siny = np.sin(x)

# 使用matplotlib将x数组和y数组中的元素绘制出来
plt.plot(x, y)
plt.show()

# 可以同时绘制两条线
cosy = np.cos(x)
plt.plot(x, siny)
plt.plot(x, cosy)
plt.show()

# 可以指定某条线的颜色
plt.plot(x, siny)
plt.plot(x, cosy, color="red")
plt.show()

# 可以指定线的样式
plt.plot(x, siny)
plt.plot(x, cosy, color="red", linestyle="--")
plt.show()

# 可以指定x轴,y轴的区间
plt.plot(x, siny)
plt.plot(x, cosy, color="red", linestyle="--")
plt.xlim(-5, 15)
plt.ylim(0, 1.5)
plt.show()

# 另一种指定x轴,y轴区间的方法
plt.plot(x, siny)
plt.plot(x, cosy, color="red", linestyle="--")
plt.axis([-1, 11, -2, 2])
plt.show()

# 给x轴和y轴加说明
plt.plot(x, siny)
plt.plot(x, cosy, color="red", linestyle="--")
plt.axis([-1, 11, -2, 2])
plt.xlabel("x axis")
plt.ylabel("y value")
plt.show()

# 加图例
plt.plot(x, siny, label="sin(x)")
plt.plot(x, cosy, color="red", linestyle="--", label="cos(x)")
plt.axis([-1, 11, -2, 2])
plt.xlabel("x axis")
plt.ylabel("y value")
plt.legend()
plt.show()

# 加标题
plt.plot(x, siny, label="sin(x)")
plt.plot(x, cosy, color="red", linestyle="--", label="cos(x)")
plt.axis([-1, 11, -2, 2])
plt.xlabel("x axis")
plt.ylabel("y value")
plt.legend()
plt.title("Welcome to ML!")
plt.show()

以上都是利用matplotlib画折线图,下面来看看如何画散点图:

plt.scatter(x, siny)
plt.show()

plt.scatter(x, siny)
plt.scatter(x, cosy)
plt.show()

x = np.random.normal(0, 1, 100)
y = np.random.normal(0, 1, 100)
plt.scatter(x, y)
plt.show()

# 设置点的透明度
plt.scatter(x, y, alpha=0.5)
plt.show()

基于Scikit Learn的数据探索

Scikit-learn是Python语言中专门针对机器学习应用而发展起来的一款开源框架,其中有一个模块叫Datasets,它提供了机器学习的一些常用的数据集以及产生数据集的方法,比如波士顿房价数据集、乳腺癌数据集、糖尿病数据集、手写字体数据集、鸢尾花数据集等等。这一小节我们就通过Scikit Learn的Datasets来初步对机器学习的数据进行探索。

我们使用NumPy和Matplotlib对Scikit Learn Datasets中的鸢尾花这个数据集进行探索:

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
# 我们只导入Scikit Learn中的datasets模块
from sklearn import datasets

# 加载鸢尾花数据集,获取到的iris的数据结构是一个字典
iris = datasets.load_iris()
# 看看字典的key都有什么
iris.keys()
# 结果
dict_keys(['data', 'target', 'target_names', 'DESCR', 'feature_names'])

从上面的示例可以看到鸢尾花这个字典一共包含五种信息,我们逐一来看看这五种信息:

# 先看一下DESCR信息,该信息解释了鸢尾花这个数据集
print(iris.DESCR)
# 结果
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!)
============== ==== ==== ======= ===== ====================
.....
.....

DESCR详细的描述了鸢尾花这个数据集一共有150组数据,每组数据有4个特征,分别是萼片的长度和厚度、花瓣的长度和厚度,还有3种鸢尾花的类别以及这些数据的统计信息和详细的解释说明。

# 再来看看data
iris.data
# 结果
array([[ 5.1, 3.5, 1.4, 0.2],
[ 4.9, 3. , 1.4, 0.2],
[ 4.7, 3.2, 1.3, 0.2],
[ 4.6, 3.1, 1.5, 0.2],
[ 5. , 3.6, 1.4, 0.2],
[ 5.4, 3.9, 1.7, 0.4],
[ 4.6, 3.4, 1.4, 0.3],
[ 5. , 3.4, 1.5, 0.2],
[ 4.4, 2.9, 1.4, 0.2],
[ 4.9, 3.1, 1.5, 0.1],
[ 5.4, 3.7, 1.5, 0.2],
[ 4.8, 3.4, 1.6, 0.2],
...
...
[ 5.9, 3. , 5.1, 1.8]])

# 看看data这个数组的行列情况
iris.data.shape
# 结果
(150, 4)

可以看到data中的数据就是萼片长度、厚度,花瓣长度、厚度的值。是一个150行,4列的矩阵。

# feature_names的值就是4个特征的说明
iris.feature_names
# 结果
['sepal length (cm)',
'sepal width (cm)',
'petal length (cm)',
'petal width (cm)']

# target描述了每一行鸢尾花的数据是哪个类别的
iris.target
# 结果
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])

# target是一个一维数组
iris.target.shape
# 结果
(150,)

# target_names就是类别名称
iris.target_names
# 结果
array(['setosa', 'versicolor', 'virginica'],
dtype='<U10')

下面我们用Matplotlib,用图将鸢尾花的数据展示出来,这样就能更直观的来分析这些数据。

# 因为matplotlib只能绘制二维图像,所以我们先来看看鸢尾花萼片的数据,取data的所有行,前2列
X = iris.data[:, :2]
# 将鸢尾花萼片的长度和宽度用散点图绘制出来
plt.scatter(X[:, :1], X[:, 1:2])
plt.show()

# 我们再来看看这150组鸢尾花数据从萼片维度的类别分类情况
y = iris.target
plt.scatter(X[y == 0, 0], X[y == 0, 1], color="red")
plt.scatter(X[y == 1, 0], X[y == 1, 1], color="blue")
plt.scatter(X[y == 2, 0], X[y == 2, 1], color="green")
plt.show()

# 取data矩阵的所有行,后2列的数据,既鸢尾花的花瓣长度和宽度信息
X1 = iris.data[:, 2:]
plt.scatter(X1[:, :1], X1[:, 1:2])
plt.show()

# 我们再来看看这150组鸢尾花数据从花瓣维度的类别分类情况
plt.scatter(X1[y == 0, 0], X1[y == 0, 1], color="red")
plt.scatter(X1[y == 1, 0], X1[y == 1, 1], color="blue")
plt.scatter(X1[y == 2, 0], X1[y == 2, 1], color="green")
plt.show()

kNN算法

kNN算法又称k近邻算法,是k-Nearest Neighbors的简称,该算法是监督学习中解决分类问题的算法,也是需要数据知识最少的一个算法,但是效果往往不差,能较好的解释机器学习算法使用过程中的很多细节问题,并且能很好的刻画机器学习应用的流程。

kNN算法解释

上图描述了肿瘤大小和时间的二维关系图,圆点的颜色表示肿瘤的性质是良性还是恶性。

此时又有一个病人的数据采集到,那么我们如何判断这个病人的肿瘤是良性还是恶性呢?

首先我们必须取一个k值,至于这个k值是该如何取后续会讲,这里比如我们取k=3,这个k值的作用就是基于新来的这个点,找到离它最近的k个点,这里也就是找到离绿色点最近的三个点:

然后根据这三个代表的特征进行投票,票数最多的特征就是这个绿色点的特征,这个示例中离绿色点最近的三个点都是蓝色点,既恶性肿瘤,那么可判定绿色点代表的肿瘤性质有很高的概率也是恶性。

欧拉距离

kNN算法中唯一用到的数学知识就是如何求点与点之间的距离,在这里我们先使用最普遍的欧拉距离来进行计算,欧拉距离的公式如下:

二维:
$$ \sqrt {(x^{(a)}-x^{(b)})^2+(y^{(a)}-y^{(b)})^2} $$

三维:
$$ \sqrt {(x^{(a)}-x^{(b)})^2+(y^{(a)}-y^{(b)})^2+(z^{(a)}-z^{(b)})^2} $$

N维(N个特征):

$$ \sqrt {(x_1^{(a)}-x_1^{(b)})^2+(x_2^{(a)}-x_2^{(b)})^2+…+(x_n^{(a)}-x_n^{(b)})^2} =\sqrt {\sum_{i=1}^n(x_i^{(a)}-x_i^{(b)})^2} $$

用大白话解释就是两个点的所有相同维度之差求平方,然后全部相加再开方。有兴趣的话大家可以再深入研究一下点与点间距离的计算。

编码实现kNN算法

首先我们来准备一下数据:

import numpy as np
import matplotlib.pyplot as plt
raw_data_X = [[3.393533211, 2.331273381],
[3.110073483, 1.781539638],
[1.343808831, 3.368360954],
[3.582294042, 4.679179110],
[2.280362439, 2.866990263],
[7.423436942, 4.696522875],
[5.745051997, 3.533989803],
[9.172168622, 2.511101045],
[7.792783481, 3.424088941],
[7.939820817, 0.791637231]
]
raw_data_y = [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]

我们将这些样本数据绘制出来看看:

# 和绘制鸢尾花的方式一样
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="red")
plt.scatter(X_train[y_train == 1, 0], X_train[y_train == 1, 1], color="blue")
plt.show()

下面再创建一组数据,来模拟需要被分类的数据:

x = np.array([8.093607318, 3.365731514])
plt.scatter(X_train[y_train == 0, 0], X_train[y_train == 0, 1], color="red")
plt.scatter(X_train[y_train == 1, 0], X_train[y_train == 1, 1], color="blue")
plt.scatter(x[0], x[1], color="green")
plt.show()

我们现在就要通过kNN算法来分析这个绿点属于哪个类别,虽然从图上我们已经可以看得出。

我们先来通过欧拉距离公式求出所有点与绿点的距离:

# 导入开方的类库
from math import sqrt
distances = []
# 通过for循环求出每一个点与绿点的距离
for x_train in X_train:
d = sqrt(np.sum((x_train - x) ** 2))
distances.append(d)

distances
# 结果
[4.812566907609877,
5.229270827235305,
6.749798999160064,
4.6986266144110695,
5.83460014556857,
1.4900114024329525,
2.354574897431513,
1.3761132675144652,
0.3064319992975,
2.5786840957478887]


# 其实还可以更简洁的使用行内表达式
distances = [sqrt(np.sum((x_train - x) ** 2)) for x_train in X_train]
distances
# 结果是一样的
[4.812566907609877,
5.229270827235305,
6.749798999160064,
4.6986266144110695,
5.83460014556857,
1.4900114024329525,
2.354574897431513,
1.3761132675144652,
0.3064319992975,
2.5786840957478887]

现在我们就求出了所有点与绿点的距离,但是求出距离并不能表示出每个距离对应点的类别,所以还需要知道这每个距离对应的是哪个点:

# 此时上文中说过的argsort方法就派上用场了
# argsort方法可以将数组排序,但是返回数组中元素的索引
nearest = np.argsort(distances)
nearest
# 结果
array([8, 7, 5, 6, 9, 3, 0, 1, 4, 2])
In [ ]:

从上面的结果可以看到,距离绿点最近的点是X_train中的第8行样本。那么接下来我们看看如何通过k值获取这些点的类别:

# 首先定义k的值为6
k = 6
# 离绿点距离最近的前6个点的类别
topK_y = [y_train[i] for i in nearest[:k]]
topK_y
# 结果
[1, 1, 1, 1, 1, 0]

# 导入collections的Counter类库用于计数计算
from collections import Counter
votes = Counter(topK_y)
votes
# 结果
Counter({0: 1, 1: 5})

# 通过most_common方法获取最大的若干个结果,传入的参数为想要获取最大的几个结果,这里我们只需要最大的一个结果,既投票票数最多的那个结果
votes.most_common(1)
# 结果
[(1, 5)]

# 更精准的获取投票票数最多的结果
predict_y = votes.most_common(1)[0][0]
predict_y
# 结果
1

到目前位置,我们就判断出了绿点有很大概率类别属于1,这个过程就是kNN算法的核心过程。

总结

在下一篇笔记中,将会介绍Scikit Learn中是如何封装kNN算法的,以及我们会自己封装一个kNN算法,以及对分类准确度评定,超参数,数据归一化等知识点的讲解。

申明:本文为慕课网liuyubobobo老师《Python3入门机器学习 经典算法与应用》课程的学习笔记,未经允许不得转载。

分享到: