KNN分类算法原理及实现及sklearn中的使用方法

KNN分类算法原理及实现,sklearn中的使用方法

简介

右下图中,绿色圆要被决定赋予哪个类,是红色三角形还是蓝色四方形?如果K=3,由于红色三角形所占比例为2/3,绿色圆将被赋予红色三角形那个类,如果K=5,由于蓝色四方形比例为3/5,因此绿色圆被赋予蓝色四方形类。
KNN分类算法原理及实现及sklearn中的使用方法_第1张图片
K最近邻(k-Nearest Neighbor,KNN)分类算法,是一个理论上比较成熟的方法,也是最简单的机器学习算法之一。该方法的思路是:如果一个样本在特征空间中的k个最相似(即特征空间中最邻近)的样本中的大多数属于某一个类别,则该样本也属于这个类别。

实现及使用方法

下面的KNNClassifier是模仿sklearn中的api实现的一个简单类

  • init构造器传入的是一个k值,即要统一分析的最近的k个点
  • fit函数需要在对象创建后使用,传入训练数据集,X_train,y_train
  • predict方法传入的是一个测试数据集,返回的是一个测试结果集(这里统一返回np.array)
  • (在sklearn该方法在metrics模块中所以设置为私有方法)_accuracy_score是用来计算准确率的,传入的连个参数分别为 真实结果集 和 测试结果集
  • score方法用来计算该模型的准确率,传入的是 测试数据集 和 测试数据的真是结果集,返回该模型的准确度
import numpy as np 
from math import sqrt
from collections import Counter

# k近邻算法可以认为是一个没有训练过程的算法
# 对于knn来说,训练集就是模型
# 参数k,X_train训练集, y_train分类标签集, x新的点
class KNNClassifier:

    def __init__(self, k):
        """初始化kNN分类器"""
        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):
        """根据训练数据集X_train和y_train训练kNN分类器"""
        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):  
        """给定待测预测数据集X_predict,返回表示X_predict的结果向量集""" 
        assert self._X_train is not None and self._y_train is not None, \
            "must fit before predict!"
        assert X_predict.shape[1] == self._X_train.shape[1], \
            "the feature number of X_predict must be similar 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)

        # 前k个距离最小的标签的点集
        topK_y = [self._y_train[i] for i in nearest[:self.k]]
        # 投票统计
        votes = Counter(topK_y)

        # 返回票数最多的标签
        return votes.most_common(1)[0][0]

    def _accuracy_score(self, y_true, y_predict):
        """计算 y_true 和 y_predict 之间的准确率"""

        assert y_true.shape[0] == y_predict.shape[0], \
            "the size of y_true must be equal to the size of y_predict"

        return sum(y_true == y_predict) / len(y_true)

    def score(self, X_test, y_test):
        """根据测算数据集 X_test 和 y_test 确定当前模型的准确度"""
        y_predict = self.predict(X_test)
        return self._accuracy_score(y_test, y_predict)

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

model_selection模块下面train_test_split方法

  • 该方法的功能是实现把数据分为训练数据集和测试数据集两部分
  • 该方法传入一个特征集X和一个分类集y,test_ratio是测试数据集所占比例,seed是随机种子,默认为空
import numpy as np 

def train_test_split(X, y, test_ratio=0.2, seed=None):
    """将数据 X 和 y 按照test_ration分割成X_train, X_test, y_train, y_test"""

    assert X.shape[0] == y.shape[0], \
        "the size of X must be equal to the size of y"   

    assert 0.0 <= test_ratio <= 1.0, \
        "test_ration must be valid"

    if seed:
        np.random.seed(seed)

    # 随机打乱X
    shuffled_indexes = np.random.permutation(len(X))

    # 根据test_ration对打乱的索引进行切分
    test_size = int(len(X) * test_ratio)
    test_indexes = shuffled_indexes[:test_size]
    train_indexes = shuffled_indexes[test_size:]

    # funcyIndexing挑选数据
    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

简单使用一下自己实现的类,对鸢尾花数据集进行预测分类

  • 使用环境:Anaconda3,Jupyter

导入库

import matplotlib as mpl
import matplotlib.pyplot as plt
from sklearn import datasets

使用鸢尾花数据集

iris = datasets.load_iris()
iris.keys()

# 结果:data是数据特征集,target是分类集
dict_keys(['data', 'target', 'target_names', 'DESCR', 'feature_names'])

iris.data.shape
(150, 4)

# 使用两个特征
X = iris.data[:, :2]
X.shape
(150, 2)

y = iris.target
# 绘制散点图
plt.scatter(X[y==0, 0], X[y==0, 1], color="red", marker='o')
plt.scatter(X[y==1, 0], X[y==1, 1], color="blue", marker='x')
plt.scatter(X[y==2, 0], X[y==2, 1], color="green", marker='+')

KNN分类算法原理及实现及sklearn中的使用方法_第2张图片
可以看到该数据集具有明显的按照特征集群分布的特点,故可以使用KNN算法

#使用自己实现的模块
%run D:\机械学习\MySkl\model_selection.py
# 分类
X_train, X_test, y_train, y_test = train_test_split(x,y)

# 打印结果
print(X_train.shape)
print(y_train.shape)

print(X_test.shape)
print(y_test.shape)

(120, 4)
(120,)
(30, 4)
(30,)

%run D:\机械学习\MySki\KNN_classify.py
# 使用k值为3,用最近的3个点进行分析
knn_clf = KNNClassifier(k=3)
KNN(k=3)
# 传入训练数据
my_knn_clf.fit(X_train, y_train)
# 进行预测
y_predict = my_knn_clf.predict(X_test)
y_predict
# 预测结果:
array([0, 0, 0, 2, 2, 2, 1, 1, 0, 1, 0, 0, 2, 1, 2, 1, 2, 0, 2, 1, 0, 0,
       0, 0, 1, 1, 0, 1, 0, 2])
# 真实结果
y_test
array([0, 0, 0, 2, 2, 2, 1, 1, 0, 1, 0, 0, 2, 1, 2, 1, 2, 0, 2, 1, 0, 0,
       0, 0, 1, 1, 0, 1, 0, 2])
# 这次的结果正确率为100%(每次预测的结果可能不一样,受到随机分类的随机种子的影响)
sum(y_predict == y_test)/len(y_test)
1.0

# 数据一样,结果一样
my_knn_clf.score(X_test, y_test)
1.0

使用scikit-learn,对手写数字进行预测分类

  • 自己实现的类只是用来更好的理解sklearn,真实使用的画一般还是使用sklearn封装的api
# 导入相应的库
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from sklearn import datasets
# 加载手写数字的数据集
digits = datasets.load_digits()
# 查看内容
digits.keys()
dict_keys(['data', 'target', 'target_names', 'images', 'DESCR'])

# 显示部分结果,这是一个8*8的图片
print(digits['DESCR'])
Notes
-----
Data Set Characteristics:
    :Number of Instances: 5620
    :Number of Attributes: 64
    :Attribute Information: 8x8 image of integer pixels in the range 0..16.
    :Missing Attribute Values: None
    :Creator: E. Alpaydin (alpaydin '@' boun.edu.tr)
    :Date: July; 1998


# 该数据集有部分数据
# 特征集
x = digits.data
x.shape
(1797, 64)
# 特征集对应分类集
y = digits.target
y.shape
(1797,)
# 分类标签
digits.target_names
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# 查看显示第666个数据,为0
some_digit = x[666]
y[666]
0

# 绘制该数字
some_digit_image = some_digit.reshape(8,8)
plt.imshow(some_digit_image, cmap = matplotlib.cm.binary)
plt.show()

KNN分类算法原理及实现及sklearn中的使用方法_第3张图片

# 导入分类模块
from sklearn.model_selection import train_test_split
# 进行训练数据和测试数据的分类,随机种子为666可以复现结果
X_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=666)
# 导入knn的模块KNeighborsClassifier模块
from sklearn.neighbors import KNeighborsClassifier
# n_neighbors参数即k值
knn_clf = KNeighborsClassifier(n_neighbors=3)
# 模型拟合,传入训练数据
knn_clf.fit(X_train, y_train)
# 返回结果
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
           metric_params=None, n_jobs=1, n_neighbors=3, p=2,
           weights='uniform')
# 对测试数据进行预测,返回预测结果集
y_predict = knn_clf.predict(x_test)

# 导入计算正确率的模块
from sklearn.metrics import accuracy_score
# 传入真实结果和测试结果,返回正确率
accuracy_score(y_test, y_predict)
0.9888888888888889
# 直接传入测试数据进行模型评估
knn_clf.score(x_test, y_test)
0.9888888888888889

# 没有使用随机种子的完整代码,算正确率
import numpy as np
from sklearn import datasets

digits = datasets.load_digits()
x = digits.data
y = digits.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)

from sklearn.neighbors import KNeighborsClassifier
knn_clf = KNeighborsClassifier(n_neighbors=3)
knn_clf.fit(X_train, y_train)
knn_clf.score(X_test, y_test)
# 正确率
0.9916666666666667

机器学习库使用过程中要注意的一些地方

超参数和模型参数

  • 超参数: 在算法运行前需要决定的参数
  • 模型参数:算法中学习的参数
  • kNN算法没有模型参数
  • kNN算法中的k是典型的超参数

简单使用循环寻找最好的k值

import numpy as np
from sklearn import datasets

digits = datasets.load_digits()
x = digits.data
y = digits.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)
from sklearn.neighbors import KNeighborsClassifier


# 使用循环寻找最好的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 =  5
best_score =  0.9944444444444445

考虑距离? 不考虑距离?

我们默认使用的是欧拉距离
当一个点跟其他分类之间出现平票的时候,如果不考虑距离的话是会进行随机分类的
考虑距离的话则会选择与点之间的距离最小的那一个分类
距离计算:取每个特征距离的大小的倒数和 sum( 1/n )

# weights参数默认值'uniform'不考虑距离,'distance'为考虑距离

best_method = ""
best_score = 0.0
best_k = -1
for method in ['uniform', 'distance']:
    for k in range(1,11):
        knn_clf = KNeighborsClassifier(n_neighbors=k, weights=method)
        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_method = method

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

best_k =  5
best_score =  0.9944444444444445
best_method =  uniform

使用哪种距离?

  • 欧拉距离 : 两点距离
  • 曼哈顿距离 :维度距离
  • 明可夫斯基距离 p=1是曼哈顿,p=2是欧拉距离, ++p…
  • 默认使用明科夫斯基距离,p=2
    KNN分类算法原理及实现及sklearn中的使用方法_第4张图片
# 寻找最好的距离参数p
%%time
best_p = -1
best_score = 0.0
best_k = -1
for k in range(1,11):
    for p in range (1,6):
        knn_clf = KNeighborsClassifier(n_neighbors=k,weights='distance', p=p,metric='minkowski')
        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_k = ", best_k)
print("best_score = ", best_score)
print("best_p = ", best_p)

best_k =  3
best_score =  0.9972222222222222
best_p =  5
Wall time: 22.3 s

网格搜索

  • 网格搜索与k近邻算法中更多超参数
# 该参数是一个列表,里面的字典,是要搜索的内容,weights为 'uniform'时不使用距离p参数
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)]
    }
]

knn_clf = KNeighborsClassifier()
# 导入网格包搜索的模块GridSearchCV
from sklearn.model_selection import GridSearchCV
# 传入训练模型,参数集
grid_search = GridSearchCV(knn_clf, param_grid)
# 执行搜索
%%time
grid_search.fit(X_train, y_train)
# 搜索时间比较长
Wall time: 3min 2s
Out[18]:
GridSearchCV(cv=None, error_score='raise',
       estimator=KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
           metric_params=None, n_jobs=1, n_neighbors=5, p=2,
           weights='uniform'),
       fit_params=None, iid=True, n_jobs=1,
       param_grid=[{'weights': ['uniform'], 'n_neighbors': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}, {'weights': ['distance'], 'n_neighbors': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'p': [1, 2, 3, 4, 5]}],
       pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
       scoring=None, verbose=0)

# 结果集
# 最优参数
grid_search.best_estimator_
KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
           metric_params=None, n_jobs=1, n_neighbors=5, p=2,
           weights='distance')

# 最优成绩
grid_search.best_score_ 
0.988169798190675

# 最优搜索参数
grid_search.best_params_
{'n_neighbors': 5, 'p': 2, 'weights': 'distance'}

# 获得最好的评估器
knn_clf = grid_search.best_estimator_

# 对测试数据进行预测的成绩
knn_clf.score(X_test, y_test)
0.9944444444444445
  • 并发搜索
%%time
# n_jobs为使用核数,-1使用所有核,verbose边搜索边输出,使用一个整数,数越大输出越详细
grid_search = GridSearchCV(knn_clf, param_grid, n_jobs=-1, verbose=3)
grid_search.fit(X_train, y_train)

# 这次搜索使用了1.2分钟,大大缩短了时间
Fitting 3 folds for each of 60 candidates, totalling 180 fits
[Parallel(n_jobs=-1)]: Done  24 tasks      | elapsed:    2.5s
[Parallel(n_jobs=-1)]: Done 120 tasks      | elapsed:   39.9s
Wall time: 1min 13s
[Parallel(n_jobs=-1)]: Done 180 out of 180 | elapsed:  1.2min finished

数据归一化

将所有的数据映射到同一尺寸

  • 最值归一化 normalization
  • 把所有数据映射到0-1之间 Xscale = (X - Xmin)/ (Xmax - Xmin)
  • 适用于分布有明显边界的情况,受outlier影响较大
    比如有大部分数据都比较小,然后出现了一个特别大的数据,那么Xmax就很大,对Xscale影响很大,导致预测准确率下降

  • 均值方差归一化standardization

  • 数据分布没有明显的边界,有可能存在极端数据值
  • 均值方差归一化:把所有数据归一到均值为0方差为1的分布中
  • Xscale = (X - Xmean) / std
  • 一般使用均值方差归一化

实现StandarScalar类,用于理解原理

  • 功能:将数据归一化
  • fit方法传入训练数据,作为均值方差归一化的指标
  • transform方法传入要转换的数据,返回归一化的数据
  • 这里均使用二维数组
import numpy as np 


class StandardScalar:

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

    def fit(self, X):
        """根据训练数据集X获得数据的均值和方差"""

        assert X.ndim == 2, "The dimension of X must be 2"

        # 根据传入的标准数据计算 均值mean_ 和 方差scale_
        self.mean_ = np.array([np.mean(X[:, i]) for i in range(X.shape[1])])
        self.scale_ = np.array([np.std(X[:, i]) for i in range(X.shape[1])])

        return self

    def transform(self, X):
        """将X根据这个StandarScaler进行均值方差归一化处理"""
        assert X.ndim == 2, "The dimension of X must be 2"
        assert self.mean_ is not None and self.scale_ is not None, \
            "must fit before transform!"
        assert X.shape[1] == len(self.mean_), \
            "The feature number of X must be equal to mean_ and std_"

        # 先构建一个空的数组
        resX = np.empty(shape=X.shape, dtype=float)
        # 把根据公式归一化的数据放入数组中
        for col in range(X.shape[1]):
            resX[:,col] = (X[:,col] - self.mean_[col]) / self.scale_[col]

        return resX

scikit-learn中的StandardScaler

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(iris.data, iris.target)

# 导入归一化模块
from sklearn.preprocessing import StandardScaler
# 使用该类
standarScaler = StandardScaler()

# 传入训练数据作为标准
standarScaler.fit(X_train)
StandardScaler(copy=True, with_mean=True, with_std=True)

# 获取每个特征的均值
standarScaler.mean_
array([5.80178571, 3.11517857, 3.64553571, 1.15446429])

# 获取每个特征的方差(数据分布范围)
array([0.82721371, 0.43838376, 1.78829969, 0.77100218])

# 获得归一化的训练数据
X_train = standarScaler.transform(X_train)
# 获得归一化的测试数据
X_test_standar = standarScaler.transform(X_test)

# 查看获得的数据的均值和方差的情况
np.mean(X_train)
1.2965818894729507e-15 # 负15次方非常接近0
np.std(X_train)
1.0

# 进行预测
from sklearn.neighbors import KNeighborsClassifier
knn_clf = KNeighborsClassifier(n_neighbors=3)
knn_clf.fit(X_train, y_train)
knn_clf.score(X_test_standar, y_test)
# 成绩
0.9736842105263158

KNN的缺点

缺点1:效率低下,每个数据都需要O(n*m)
缺点2:高度数据相关
缺点3:预测结果不具有可解释性
缺点4: 维数灾难,随着维度的增加,“看似相近”的两个点之间的距离越来越大,解决方法:降维

KNN的优点

1.简单,易于理解,易于实现,无需估计参数,无需训练;
2. 适合对稀有事件进行分类;
3.特别适合于多分类问题(multi-modal,对象具有多个类别标签), kNN比SVM的表现要好。

机器学习算法的一般使用步骤

  • 对数据进行分类为训练数据和测试数据
  • 再使用Scaler进行数据归一化(可能存在数据尺度不一)
  • 对数据进行训练得到模型
  • 测试数据同样需要使用归一化
  • 然后送进模型来看分类准确度:accuracy来看模型的好坏
  • 使用网格搜索寻找最好的超参数

你可能感兴趣的:(机械学习)