机器学习笔记四之kNN算法、超参数、数据归一化

上一篇笔记主要介绍了NumPy,Matplotlib和Scikit Learn中Datasets三个库的用法,以及基于欧拉定理的kNN算法的基本实现。这一篇笔记的主要内容是通过PyCharm封装kNN算法并且在Jupyter Notebook中调用,以及计算器算法的封装规范,kNN的k值如何计算,如何使用Scikit Learn中的kNN算法,还有机器学习算法中的一些主要概念,比如训练数据集、测试数据集,分类准确度,超参数,数据归一化。另外会具体用代码实现第一篇笔记中介绍过的线性回归算法。

封装kNN算法

上一篇笔记中我们对kNN算法在Jupyter Notebook中进行了实现,但是想要复用这个算法就很不方便,所以我们来看看如何在PyCharm中封装算法,并且在Jupyter Notebook中进行调用。

PyCharm的配置这里我就不再累赘,如图所示,我们创建了一个Python文件kNN.py,然后定义了kNNClassify方法,该方法有4个参数,分别是kNN算法的k值,训练样本特征数据集XTrain,训练样本类别数据集yTrain,预测特征数据集x。该方法中的实现和在Jupyter Notebook中实现的一模一样,只不过加了三个断言,让方法的健壮性更好一点。我们给出N维欧拉定理

$$ \sqrt {\sum_{i=1}^n(x_i^{(a)}-x_i^{(b)})^2} $$

# kNN.py
import numpy as np
from math import sqrt
from collections import Counter

def kNNClassify(k, XTrain, yTrain, x):

assert 1 <= k <= XTrain.shape[0], "k 的取值范围不正确"
assert XTrain.shape[0] == yTrain.shape[0], "训练样本数据行数应该与训练结果集行数相同"
assert XTrain.shape[1] == x.shape[0], "训练样本数据特性个数应该与被预测数据特性个数相同"

distances = [sqrt(np.sum((xTrain - x) ** 2)) for xTrain in XTrain]
nearest = np.argsort(distances)

topKy = [yTrain[i] for i in nearest[:k]]
votes = Counter(topKy)

return votes.most_common(1)[0][0]

这样我们就在PyCharm中封装好了kNN算法的方法,我们再来看看如何在Jupyter Notebook中调用封装好的方法呢,这就需要使用%run这个命令:

import numpy as np

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]

XTrain = np.array(raw_data_X)
yTrain = np.array(raw_data_y)

x = np.array([8.093607318, 3.365731514])

# 使用%run命令可以引入Python文件,并可使用该Python文件中定义的属性和方法
%run ../pycharm/kNN.py
predicty = kNNClassify(6, XTrain, yTrain, x)
predicty
# 结果
1

机器学习流程

这一小节我们来看看机器学习的大概流程是怎样的,如下图所示:

监督学习算法首先需要的是训练数据集,然后通过一个机器学习算法生成一个模型,最后就可以用这个模型来预测新的数据得到结果。通常,我们将使用机器学习生成模型的过程用fit来表示,使用模型预测新的数据的过程用predict来表示。这就是机器学习最基本的一个流程。

在第一篇笔记中,介绍了线性回归的概念,我们最后得到了一个二元线性回归的公式:$ F(a,b) = \sum_{i=1}^n(y_i-(ax_i + b))^2 $。这个公式其实就是通过线性回归算法得到的模型,通过fit过程,训练模型得到ab,然后通过predict过程预测新的样例数据得到结果。

但是我们发现kNN算法不存在训练模型的过程,因为新的样例数据其实是需要通过训练数据集来进行预测的,所以换个角度来看,kNN算法的模型就是它的训练数据集,在上图中模型阶段其实就是把训练数据集复制了一份作为模型来使用,那么对于fit和predict过程而言,kNN算法的predict过程其实是核心,而fit过程非常简单。

使用Scikit Learn中的kNN算法

这一节我们来看看如何使用Scikit Learn中封装的kNN算法:

# 导入Scikit Learn中的kNN算法的类库
from sklearn.neighbors import KNeighborsClassifier
# 初始化kNN算法分类器的实例,参数n_neighbors就是k值
kNNClassifier = KNeighborsClassifier(n_neighbors=6)
# 训练,拟合模型
kNNClassifier.fit(XTrain, yTrain)
# 预测新的样例数据,该方法接受的参数类型为二维数组,如果只有一行也需要转换为一行的二维数组
kNNClassifier.predict(x.reshape(1, -1))
# 结果
array([1])

从示例代码中可以看出,Scikit Learn中封装的kNN算法严格遵从了上一节介绍的机器学习的基本流程,其实不止是kNN算法,Scikit Learn中的所有机器学习算法都遵从这个基本流程。

重新封装kNN算法

所以我们可以优化一下我们之前封装的kNN算法的方法,将其封装为类似Scikit Learn中的方式:

class KNNClassifier:

# 初始化kNN分类器
def __init__(self, k):
assert k >= 1, "k 值不能小于1"

self.k = k
self._XTrain = None
self._yTrain = None

# 根据训练数据集XTrain和yTrain训练kNN分类器,在kNN中这一步就是复制训练数据集
def fit(self, XTrain, yTrain):
assert XTrain.shape[0] == yTrain.shape[0], \
"训练样本特征数据集的行数要与训练样本分类结果数据集的行数相同"
assert XTrain.shape[0] >= self.k, \
"训练样本特征数据集的行数,既样本点的数量要大于等于k值"

self._XTrain = XTrain
self._yTrain = yTrain
return self

# 输入样本数据,根据模型进行预测
def predict(self, XPredict):
assert self._XTrain is not None and self._yTrain is not None, \
"在执行predict方法前必须先执行fit方法"
assert XPredict.shape[1] == self._XTrain.shape[1], \
"被预测数据集的特征数,既列数必须与模型数据集中的特征数相同"

ypredict = [self._predict(x) for x in XPredict]
return np.array(ypredict)

# 实现私有的预测方法,kNN算法的核心代码
def _predict(self, x):
assert x.shape[0] == self._XTrain.shape[1], \
"输入的样本数据的特征数量必须等于模型数据,既训练样本数据的特征数量"

distance = [sqrt(np.sum((xTrain - x) ** 2)) for xTrain in self._XTrain]
nearest = np.argsort(distance)
topK = [self._yTrain[i] for i in nearest[:self.k]]
votes = Counter(topK)

return votes.most_common(1)[0][0]

def __repr__(self):
return "kNN(k=%d)" % self.k

上面的代码清晰的定义了fitpredict方法,至于_predict这个私有方法可以随意,可以将逻辑直接写在predict方法里,也可以拆分出来。然后我们在Jupyter Notebook中再来使用一下我们封装的kNN算法:

%run ../pycharm/kNN/kNN.py
myKNNClassifier = KNNClassifier(6)
myKNNClassifier.fit(XTrain, yTrain)
# 结果
kNN(k=6)
xTrain = x.reshape(1, -1)
myKNNClassifier.predict(xTrain)
# 结果
array([1])

判断机器学习算法的性能

现在大家应该知道机器算法的目的主要是训练出模型,然后输入样本,通过模型来预测结果,可见这个模型是非常关键的,模型的好坏直接影响预测结果的准确性,继而对实际运用会产生巨大的影响。模型的训练除了机器学习算法以外,对它影响比较大的还有训练样本数据,我们在实现kNN算法时,是将所有的样本数据用于训练模型,那么模型训练出来后就已经没有数据供我们验证模型的好坏了,只能直接投入真实环境使用,这样的风险是很大的。

所以为了避免上述这种情况,最简单的做法是将所有训练样本数据进行切分,将大部分数据用于训练模型,而另外一小部分数据用来测试训练出的模型,这样如果我们用测试数据发现这个模型不够好,那么我们就有机会在将模型投入真实环境使用之前改进算法,训练出更好的模型。

我们来看看如何封装拆分训练数据的方法:

import numpy as np

# 训练样本数据 X 和 y 按照 test_radio 分割成 X_train, y_train, X_test, y_test
def train_test_split(X, y, test_radio = 0.2, seed = None):
assert X.shape[0] == y.shape[0], \
"训练样本特征数据集的行数要与训练样本分类结果数据集的行数相同"
assert 0.0 <= test_radio <= 1.0, \
"test_radio 的值必须在 0 到 1 之间"

# 如果 seed 有值,将其设置进numpy的随机函数中
if seed:
np.random.seed(seed)

shuffled_indexes = np.random.permutation(len(X))
test_size = int(len(X) * test_radio)
test_indexes = shuffled_indexes[:test_size]
train_indexes = shuffled_indexes[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, y_train, X_test, y_test

我们来解读一下上面的代码:

  • 首先train_test_split函数有四个参数,两个必填参数,两个非必填有默认值的参数。X是训练样本特征数据集,y是训练样本分类结果数据集,test_radio是设置训练数据和测试数据的比例,seed就很好理解了,就是NumPy的随机函数提供的随机种子机制。
  • 上面代码中有一个方法大家之前应该没见过,那就是permutation(x),该方法表示返回一个乱序的一维向量,元素从0到x,所以shuffled_indexes是一个乱序的一维向量数组,它的元素总数为训练样本数据的总数,既训练样本数据矩阵的行数,元素的范围从0到训练样本数据的总数。
  • 根据test_radio计算出需要分割出的测试数据数量test_size
  • 根据test_sizeshuffled_indexes中取出test_indexestrain_indexes,这两个数组中存的元素就是作为索引来用的。
  • 根据test_indexestrain_indexesXy中得到X_trainy_trainX_testy_test

之前在Jupyter Notebook中我们使用%run命令使用我们封装的代码 ,这一节我们来看看如何使用import的方式使用我们自己封装的代码。其实这和Jupyter Notebook没多大关系,我们需要做的只是给Python设置一个搜索包的路径而已,这里这会对MacOS,以及安装了Anaconda的环境作以说明,Windows系统大同小异。

首先找到路径/anaconda3/lib/python3.6/site-packages,在该路径下创建一个文件XXX.pth,该文件的扩展名必须为pth,文件名称可以随意。然后在该文件中输入你希望Python搜索包的绝对路径即可。

设置完搜索路径后,我们需要修改一下PyCharm中的目录结构:

我新建了一个目录名为myMLkNN.py是我们之前封装的kNN算法相关的方法,modelSelection.py里就是我们刚才封装好的拆分训练和测试数据的方法,另外还增加了一个__init__.py的文件,因为有了这个文件,myML就变为了一个包。__init__.py的作用这里不做过多解释。

这样我们就可以在Jupyter Notebook中用import的方式导入我们封装的模块了:

from myML.modelSelection import train_test_split
X_train, y_train, X_test, y_test = train_test_split(X, y)
X_train.shape
# 结果
(120, 4)
y_train.shape
# 结果
(120,)
X_test.shape
# 结果
(30, 4)
y_test.shape
# 结果
(30,)

这样就可以很方便的使用我们封装的模块了,下面我们来看看怎么判断我们封装的kNN算法的好坏程度:

# 先用训练数据训练模型,然后输入测试样本特征数据,得到预测结果
from myML.kNN import KNNClassifier
my_knn_classifier = KNNClassifier(6)
my_knn_classifier.fit(X_train, y_train)
my_y_test = my_knn_classifier.predict(X_test)
my_y_test
# 结果
array([1, 0, 1, 1, 0, 1, 2, 2, 0, 1, 0, 1, 2, 1, 2, 1, 0, 1, 2, 2, 1, 1, 1,
1, 0, 1, 2, 1, 1, 2])
# 用预测出的结果和测试样本分类结果数据做对比,得出准确率
y_test
# 结果
array([1, 0, 1, 1, 0, 1, 2, 2, 0, 1, 0, 1, 2, 1, 2, 1, 0, 1, 2, 2, 1, 1, 1,
1, 0, 1, 1, 1, 1, 2])

sum(my_y_test == y_test) / len(y_test)
# 结果,准确率为96.67%
0.96666666666666667

这样我们就得出了一个算法的好坏程度。

超参数

目前我们在使用kNN算法时,k的值都是我们给定的值,这个作为算法的参数值称为超参数,也就是在运行机器学习算法之前需要指定的参数。还有一类参数称为模型参数,既在算法过程中学习的参数,但是大家已经知道kNN算法实际是没有模型的,所以也不存在模型参数,但是k值是一个典型的超参数。

寻找最好的k值

Scikit Learn中kNN算法的k值默认是5,有时候这个值并不是最优的值,那么我们可以通过一个简单的方式来寻找到最优的k值,那就是给定一个k值的范围,然后循环传入算法求训练分数最好的那个k值:

# 首先我们使用scikit learn中的手写数字数据集,并将其拆分为训练数据集和测试数据集
import numpy as np
from sklearn import datasets
from sklearn.model_selection import train_test_split
digits = datasets.load_digits()
X = digits.data
y = digits.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 666)

# 然后通过循环的方式寻找最好的k值
best_score = 0.0
best_k = -1
for k in range(1, 11):
knn_clf = KNeighborsClassifier(n_neighbors = k)
knn_clf.fit(X_train, y_train)
score = knn_clf.score(X_test, y_test)
if score > best_score:
best_k = k
best_score = score

print("best_k = ", best_k)
print("best_score = ", best_score)

# 结果
best_k = 4
best_score = 0.991666666667

从上面的代码示例中可以看到,在1到10这个范围的k值中,4是训练分数最高的k值。不过这里需要注意的是,如果求出k为10,那么我们需要再扩大范围进行寻找,因为有可能10并不是最优的k值,只因为我们给定的范围最大到10,所以这种情况下,我们需要根据实际情况对8至20的k值范围再进行计算,如果结果仍然为10,那么才认定10为最优k值。

距离的权重

上面这张图如果用之前我们了解过的kNN算法来分析的话,绿色的点肯定是属于蓝色点分类的,但是我们之前都一直忽略了一个问题,那就是当找到k个相邻的点后,在投票时是没有再考虑未知分类点与相邻点之间的距离的。就比如上图,如果考虑了3个最近相邻点与绿色点之间的距离的话,那么绿色点的分类就会属于红色点的分类,因为在计算距离权重时是取距离的倒数,所以绿色点与红色点的距离权重为1,绿色点与两个蓝色点的距离权重为1/3 + 1/4 = 7/12。

上图的情况如果不考虑距离权重的话,就会出现平票的情况,那么只能随机在三个分类中选一个作为绿色点的分类,如果加上距离权重,就能确定得出绿色点的分类了。

所以与相邻点的距离权重是kNN算法的另一个重要的超参数,大家可以看一下Scikit Learn的kNN官网,KNeighborsClassifier的构造函数中有一个参数weights,这就是距离权重参数,默认值为uniform,既不考虑距离权重,如果要考虑距离权重的话,需要设置值为distance

knn_clf = KNeighborsClassifier(n_neighbors = 4, weights = 'distance')

距离的类型

到目前为止,我们自己封装的kNN算法使用的距离公式是欧拉距离,其实还有其他的距离公式,比如曼哈顿距离:

$$\sum_{i=1}^n |X_i^{(a)}-X_i^{(b)}|$$

其实曼哈顿距离和欧拉距离在数学公式表现形式上是有一定相似性的,我们可以将欧拉距离做以转换:

$$ \sqrt {\sum_{i=1}^n(x_i^{(a)}-x_i^{(b)})^2} = \sqrt {\sum_{i=1}^n|x_i^{(a)}-x_i^{(b)}|^2} = (\sum_{i=1}^n|x_i^{(a)}-x_i^{(b)}|^2)^\frac 1 2 $$

对曼哈顿距离也做以转换:

$$ \sum_{i=1}^n |X_i^{(a)}-X_i^{(b)}| = (\sum_{i=1}^n |X_i^{(a)}-X_i^{(b)}|^1)^\frac 1 1 $$

通过上面两个公式可以得到一个共性的公式:

$$ (\sum_{i=1}^n |X_i^{(a)}-X_i^{(b)}|^p)^\frac 1 p $$

这个公式就称之为明可夫斯基距离(Minkowski Distance)

既当p为1时为曼哈顿距离,当p为2时为欧拉距离,当p大于2时表示其他距离,所以p又是一个kNN算法的超参数,在KNeighborsClassifier的构造函数中同样有一个参数p就是表示使用的距离类型,默认为2,既默认为欧拉距离。

best_score = 0.0
best_k = -1
best_p = -1
for k in range(1, 11):
for p in range(1, 6):
knn_clf = KNeighborsClassifier(n_neighbors = k, weights = "distance", p = p)
knn_clf.fit(X_train, y_train)
score = knn_clf.score(X_test, y_test)
if score > best_score:
best_k = k
best_score = score
best_p = p

print("best_p = ", best_p)
print("best_k = ", best_k)
print("best_score = ", best_score)

# 结果
best_p = 2
best_k = 3
best_score = 0.988888888889

从上面代码运行的结果来看,最优的p值为2,也就是欧拉距离,考虑了距离权重后,最优k值为3。而且一些超参数是组合使用的,比如当使用超参数p时,距离权重的超参数weights的取值就必须是distance。并且kp这两个超参数双重嵌套循环,就组成了一个类似网格的搜索方式,所幸Scikit Learn提供了封装好的网格搜索的方法供我们使用。

网格搜索超参数

在使用网格搜索前,我们需要先将各种超参数的组合定义出来:

param_grid = [
{
'weights': ['uniform'],
'n_neighbors': [i for i in range(1, 11)]
},
{
'weights': ['distance'],
'n_neighbors': [i for i in range(1, 11)],
'p': [i for i in range(1, 6)]
}
]

我们定义了一个param_grid数组,元素为字典,每个字典描述了一种超参数的组合,下面我们使用Scikit Learn提供的GridSearchCV来使用我们定义好的超参数组合:

from sklearn.model_selection import GridSearchCV
knn_clf = KNeighborsClassifier()
grid_search = GridSearchCV(knn_clf, param_grid)
grid_search.fit(X_train, y_train)
new_knn_clf = grid_search.best_estimator_
new_knn_clf
# 结果
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
metric_params=None, n_jobs=1, n_neighbors=3, p=3,
weights='distance')

上面的示例代码不难理解,我们使用构建出的kNN分类器knn_clf和超参数组合param_grid构造出了网格搜索对象grid_search,通过它进行fit操作,这个过程就是根据我们提供的超参数组合进行搜寻,找到最优的超参数组合。通过best_estimator_返回新的,已经设置了最优超参数组合的kNN分类器对象。从输出结果其实已经可以看到首先是选择了考虑距离权重的超参数组合,然后求出了k值,也就是n_neighbors为3,p值为3。

GridSearchCV也提供了几个属性,可以让我们方便的查看超参数和模型评分:

grid_search.best_params_
# 结果
{'n_neighbors': 3, 'p': 3, 'weights': 'distance'}

grid_search.best_score_
# 结果
0.98538622129436326

GridSearchCV的其他参数

在构造GridSearchCV对象时除了kNN分类器和超参数组合外,还有几个比较有用的参数:

  • n_jobs:该参数决定了在进行网格搜索时使用当前计算机的CPU核数,1就是使用1个核,2就是使用2个核,如果设置为-1,那么代表使用所有的核进行搜索。
  • verbose:该参数决定了在网格搜索时的日志输出级别。

数据归一化

大家先看看上面表格中的样本数据,两个样本的肿瘤大小相差有5倍,从医学角度来讲这个差距已经是非常大了,但从实际数值差距来讲并不是很大。再看看发现时间,两个样本之间相差100天,在数值上的差距远远大于肿瘤大小的差距。所以如果使用kNN算法,用欧拉距离计算的话,两个样本发现时间之差远远大于肿瘤大小之差,所以就会主导样本间的距离,这个显然是有问题的,对预测的结果是有偏差的。

所以我们就需要对样本数据进行数据归一化,将所有的数据映射到同一尺度。比较简便的方式就是最值归一化,既用下面的公式把所有数据映射到0-1之间:

$$ x_{scale} = \frac {x - x_{min}} {x_{max} - x_{min}} $$

最值归一化虽然简便,但是是有一定适用范围的,那就是适用于样本数据有明显分布边界的情况,比如学生的考试分数,从0到100分,或者像素值,从0到255等。假如像人的月收入这种没有边界的样本数据集,就不能使用最值归一化了,此时就需要用到另外一个数据归一化的方法均值方差归一化,该方法就是把所有数据归一到均值为0方差为1的分布中,公式如下:

$$ x_{scale} = \frac {x - x_{mean}} S $$

就是将每个值减去均值,然后除以方差,通过均值方差归一化后的数据不一定在0-1之间,但是他们的均值为0,方差为1。

下面我们来分别实现一下这两个数据归一化方法。先来看看最值归一化的实现:

import numpy as np
import matplotlib.pyplot as plt
# 生成从0到100,一共100个元素的数组
x = np.random.randint(0, 100, size = 100)
# 变更数组元素的类型
x = np.array(x, dtype = float)
x_scale = (x - np.min(x)) / (np.max(x) - np.min(x))

# 生成50行,2列的矩阵,元素在0到100之间
X = np.random.randint(0, 100, (50, 2))
# 对每一列数据进行最值归一化
X[:, 0] = (X[:, 0] - np.min(X[:, 0])) / (np.max(X[:, 0]) - np.min(X[:, 0]))
X[:, 1] = (X[:, 1] - np.min(X[:, 1])) / (np.max(X[:, 1]) - np.min(X[:, 1]))

# 用matplotlib将X展示出来
plt.scatter(X[:, 0], X[:, 1])
plt.show()

可以看到最值归一化后数据都在0到1之间。我们再来看看均值方差归一化的实现:

X2 = np.random.randint(0, 100, (50, 2))
X2 = np.array(X2, dtype = float)
X2[:, 0] = (X2[:, 0] - np.mean(X2[:, 0])) / np.std(X2[:, 0])
X2[:, 1] = (X2[:, 1] - np.mean(X2[:, 1])) / np.std(X2[:, 1])

# 均值接近0
np.mean(X2[:, 0])
# 结果
6.2172489379008772e-17

# 方差接近1
np.std(X2[:, 0])
# 结果
0.99999999999999989

plt.scatter(X2[:, 0], X2[:, 1])
plt.show()

如何对测试数据集进行归一化

之前我们说过会对样本数据进行拆分,拆分为训练数据和测试数据,对于训练数据我们可以直接使用最值归一化或均值方法归一化,但是对测试数据我们就不能直接使用归一化的方法了,因为测试数据其实充当了真实环境中需要预测的数据,很多时候需要预测的数据只有一组,这时候我们是没办法对一组数据进行归一化的,因为无法得到均值和方差,所以我们需要结合归一化后训练数据归一化测试数据:(x_test - mean_train) / std_train。那么我们就需要保存训练数据归一化后的数据,此时我们就可以用到Scikit Learn提供的数据归一化的对象Scalar

Scalar的使用流程和机器学习算法的使用流程很像,输入训练数据集,进行fit操作,这里的fit操作就不是训练模型了,而是进行数据归一化处理,然后是transform,既对需要预测的数据进行归一化。我们来看看如何使用:

# 使用鸢尾花数据集
import numpy as np
from sklearn import datasets

iris = datasets.load_iris()
X = iris.data
y = iris.target

# 分割出训练数据集和测试数据集
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, random_state = 666)

# 导入StandardScaler,也就是均值方差归一化的对象
from sklearn.preprocessing import StandardScaler
standardScaler = StandardScaler()
standardScaler.fit(X_train)

# 将特征训练数据集和特征测试数据集进行归一化处理
X_train_standard = standardScaler.transform(X_train)
X_test_standard = standardScaler.transform(X_test)

# 使用kNN
from sklearn.neighbors import KNeighborsClassifier
knn_clf = KNeighborsClassifier(n_neighbors = 3)
knn_clf.fit(X_train_standard, y_train)
knn_clf.score(X_test_standard, y_test)

封装自己的数据归一化方法

import numpy as np

class StandardScaler:

def __init__(self):
self.mean_ = None
self.scaler_ = None

# 获取训练数据集的平均值和方差
def fit(self, X):
assert X.ndim == 2, "X 的维度必须为2,既X是一个矩阵"

self.mean_ = np.array([np.mean(X[:, i]) for i in range(X.shape[1])])
self.scaler_ = np.array([np.std(X[:, i]) for i in range(X.shape[1])])

return self

# 进行均值方差归一化处理
def transform(self, X):
assert X.ndim == 2, "X 的维度必须为2,既X是一个矩阵"
assert self.mean_ is not None and self.scaler_ is not None, "均值和方差不能为空"
assert X.shape[1] == len(self.mean_), "训练数据集矩阵的列数必须等于均值数组的元素个数"
assert X.shape[1] == len(self.scaler_), "训练数据集矩阵的列数必须等于方差数组的元素个数"

X_transform = np.empty(shape=X.shape, dtype=float)
for col in range(X.shape[1]):
X_transform[:, col] = (X[:, col] - self.mean_[col]) / self.scaler_[col]

return X_transform

这样我们就封装好了自己的均值方差归一化的方法,另外,Scikit Learn也提供了最值归一化的对象MinMaxScaler,使用流程都是一样的,大家也可是试试看。

总结

这一篇笔记主要介绍了kNN算法实现逻辑以外的概念,但也是机器学习中非常重要的一些概念,以后也会经常看到它们的身影。通过两篇笔记的介绍,我们知道kNN算法是一个解决多分类问题的算法,而且算法实现相对比较简单,但效果很强大。下一篇我们来实现第一篇笔记中介绍过的线性回归法。

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

分享到: