引言

这是我第一篇关于机器学习的文章。本篇文章将配合代码详细讲解关于KNN算法的重要内容。

简单介绍

首先我们必须要搞清楚KNN算法到底是解决什么问题的。我想我们的故事应该从机器学习的本质开始。我们机器学习就是给计算机一些样本,让计算机从这些数据中学习到一些经验,然后运用于之后地判断之中。我们计算机有很多种的学习方式,监督学习,无监督学习,半监督学习,强化学习。而其中,我们也必须掌握几个概念,一个是分类:标签值是不连续的;一个是回归:标签值是连续的。

然后我们需要介绍一下机器学习的流程,这个无论到哪里都不会有太大的变化。第一步就是获取数据,决定一个模型是不是强大的,上限就是数据的多少;第二步预处理数据,去除那一些有问题的数据;第三步特征工程,因为我们的数据可能包含很多种类,而我们的目标对于其中的某些特征并不需要,需要剔除,还有防止量纲的影响等;第四步是模型训练,第五步是模型评估。

而特征工程就是我们这篇文章的一个重点,其中在官网上已经包含了基于Python的 scikit-learn 库,所以我们使用的时候需要调包

而KNN算法其实就是帮助我们来训练模型的

KNN

话不多说,我们进入正题。KNN(K Nearest Neighbor)从名字里面我们也可以看出来,这个也叫K近邻算法。我们思路是把所有的数据用N维坐标的形式表示出来,假如我们需要测试一个数据,我们会求出这个数据与其它训练数据的欧式距离,选择最近的K个样本,看看这些标签最多的是哪个,那么这个训练数据的标签就是啥,这个就是我们的分类思想

我们代码里面fit这个就是在训练,当然是在懒惰训练,也就是说仅仅是把数据放到模型里面,具体涉及到计算的时候是在predict里面,所以我们也说KNN是懒惰算法,因为在训练的时候什么也没干。

当然要为后面的文章埋下一个伏笔,这个n_neighbors = 2并不是确定的,我们需要网格搜索和交叉验证才可以确定到底是多少这个才是最完美的,因为如果这个超参过多会过拟合,过少会欠拟合。

"""
KNN算法 (K Nearest Neighbors) K近邻算法
"""

# 导包
from sklearn.neighbors import KNeighborsClassifier

# 准备数据集
x_train = [[0], [1], [2], [3]] # 因为特征可以是多个特征,所以是一个二维数组
y_train = [0, 0, 1, 1]
x_test = [[5]]

# 创建(KNN分类模型)模型对象
estimator = KNeighborsClassifier(n_neighbors = 2)

# 模型训练
estimator.fit(x_train, y_train)

# 模型预测
y_pred = estimator.predict(x_test)

# 打印预测结果
print(f'预测值为 {y_pred}')

这个是KNN的回归算法,意思就是取最近的k个数据的标签取平均值,作为预测结果。这个和分类的本质区别就是标签要是连续的,比如要判断一个人的期末成绩就可以。但是不可以判断这个水果是不是苹果。

from sklearn.neighbors import KNeighborsRegressor
x_train = [[0, 0, 1], [1, 1, 0], [3, 10, 10], [4, 11, 12]]
y_train = [0.1, 0.2, 0.3, 0.4]
x_test = [[3, 11, 10]]

estimator = KNeighborsRegressor(n_neighbors = 2)

estimator.fit(x_train, y_train)

pred = estimator.predict(x_test)

print(f'预测值: {pred}')

特征工程

我们对于特征的处理首先就是要排除量纲的影响,所以我们有两个办法,一个是标准化,一个是归一化,我们一般都选择前者,因为后者受最大值最小值的影响太大了。

所以我们这里只给出标准化的方程,我们之后也只会用标准化的方法来解决量纲的问题。

大家可以看到代码里面fit_transform,这个是又训练又转化的意思,这里就不再是只存储数据了,这里还要计算数据的标准方差,平均值等等,所以我们之后可以访问其标准差等数据,然后转换这一步就是执行上面那个公式,让x_train_new 存储所有x‘的值

from sklearn.preprocessing import StandardScaler

x_train = [[90, 2, 10, 40], [60, 4, 15, 45], [75, 3, 13, 46]]
transfer = StandardScaler()

x_train_new = transfer.fit_transform(x_train)
print('标准化后的数据集为:\n')
print(x_train_new)

print(f'数据集的均值为:{transfer.mean_}')
print(f'数据集的方差:{transfer.var_}')
print(f'数据集的标准差:{transfer.scale_}')

鸢尾花的案例

from sklearn.preprocessing import StandardScaler
import seaborn as sns  # 数据可视化
import pandas as pd    # 带标签的数据库
import matplotlib.pyplot as plt  # 图形库
from sklearn.datasets import load_iris  # 加载鸢尾花
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split  # 分割训练集和测试集
from sklearn.metrics import accuracy_score  # 模型评估

第一步我们必须要加载数据,因为我们训练模型的第一步就是要拿到数据,没有数据怎么训练呢?

然后这个数据和字典差不多,但是不是字典(这个鸢尾花在python的机器学习库中存在,一共150个样本)

def dm01_loadiri():
    # 加载鸢尾花的数据集
    iris_data = load_iris()
    # 查看数据集
    # print(f'数据集:{iris_data}') # 字典形态 (不是字典但是和字典很像)
    # print(f'数据集的类型:{type(iris_data)}')
    # 查看数据集的键对应的值
    print(f'数据集的键:{iris_data.keys()}')
    # 查看数据集的键对应的值
    print(f'具体的数据:{iris_data.data}')
    print(f'具体的标签:{iris_data.target}')
    print(f'标签的名称:{iris_data.target_names}')
    print(f'特征对应的名称:{iris_data.feature_names}')
    # print(f'数据集的描述:{iris_data.DESCR}')
    # print(f'数据集的框架:{iris_data.frame}')   # None纯数据集,没有框架
    # print(f'数据集的文件名:{iris_data.filename}')
    # print(f'数据集的模型(在哪个包下):{iris_data.data_module}')

第二部是要绘制数据的散点图,那么我们就会用到matplotlib库来作为图形库, seaborn让数据可视化,pandas把数据变成excel表格类似的。

我们顺便提一下怎么使用这几个函数的。

pd.DataFrame()函数第一个参数是数据内容,第二个是表格每一列的名称。

sns.lmplot()函数注意一下里面x,y的名称一定要与表格的一样,因为python回到内存里面找一个叫sepal length (cm)的变量,如果没找到就会直接报错。

def dm02_show_iris():
    # 加载数据集
    iris_data = load_iris()
    # 把鸢尾花数据集封装成 DataFrame对象
    iris_df = pd.DataFrame(iris_data.data, columns = iris_data.feature_names)
    # 给df对象新增一列 --> 标签列
    iris_df['label'] = iris_data.target
    # print(iris_df)

    # 通过 Seaborn绘制散点图
    # 参数1:数据集 参数2:x轴 参数3:y轴 参数4:分组字段 参数5:是否显示拟合线
    sns.lmplot(data = iris_df, x = 'sepal length (cm)', y = 'sepal width (cm)', hue = 'label', fit_reg = True)
    # 设置标题,显式
    plt.title('iris data')
    plt.tight_layout()
    plt.show()

最后这个是一个整个的过程,我们还多加了一个评分的API,就是拿测试值和预测值做对比,计算平均值,看这个模型到底行不行。

def dm04_iris_evaluate_test():
    iris_data = load_iris()
    x_train, x_test, y_train, y_test = train_test_split(iris_data.data, iris_data.target, test_size = 0.2, random_state = 23)
    transfer = StandardScaler()
    x_train = transfer.fit_transform(x_train)
    x_test = transfer.transform(x_test)

    estimator = KNeighborsClassifier(n_neighbors = 3)
    estimator.fit(x_train, y_train)

    y_pred = estimator.predict(x_test)

    print(f'预测值:{y_pred}')

    my_data = [[7.8, 2.1, 3.9, 1.6]]
    my_data = transfer.transform(my_data)

    y_pred_new = estimator.predict(my_data)
    print(f'预测值:{y_pred_new}')

    y_pred_proba = estimator.predict_proba(my_data)
    print(f'(各分类)的概率:{y_pred_proba}')

    print(f'正确率:{accuracy_score(y_test, y_pred)}')

交叉验证和网格搜索

不知道大家还记不记得我们文章一开始提到得那个问题,我们的超参怎么确定得。首先评判一个模型好不好,我们选择得是交叉验证。而网格搜索是先确定一个几折验证,然后把自己选择的超参全部传递进去,一个一个地交叉验证,对每一个都打出一个分数,最后取最高分

from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.datasets import load_iris

data_iris = load_iris()

x_train, x_test, y_train, y_test = train_test_split(data_iris.data, data_iris.target, test_size=0.2, random_state=25)

transfer = StandardScaler()
x_train = transfer.fit_transform(x_train)
x_test = transfer.transform(x_test)

estimator = KNeighborsClassifier()

param_dict = {'n_neighbors' : [i for i in range(1, 11)]}
estimator = GridSearchCV(estimator, param_dict, cv = 5)

estimator.fit(x_train, y_train)


print(f'最优评分:{estimator.best_score_}')
print(f'最优超参组合:{estimator.best_params_}')
print(f'最优的估计器对象:{estimator.best_estimator_}')
print(f'具体的交叉验证结果:{estimator.cv_results_}')


y_pred = estimator.best_estimator_.predict(x_test)

print(f'准确率:{accuracy_score(y_test, y_pred)}')

手写字识别

首先我们要接受数据,然后把数据变成x,y两个部分,x代表的是特征值,y代表的是标签值,但是因为我们这个数据收到的时候是一个表格的形式,我们的字体是28*28的像素,那么我们需要把一横排的数据转化为28*28的样子,这个其实pandas库里面已经提供了reshape函数,然后利用matplotlib.pyplot库生成对应的图片

# 接受用户传进来的索引,展示该索引对应的图片
def show_digit(index):
    df = pd.read_csv('./data/手写数字识别.csv')

    if index < 0 or index >= len(df) - 1 :
        print('索引越界')
        return

    x = df.iloc[:, 1 :]
    y = df.iloc[:, 0]

    print(f'该图片对应的数字是:{y.iloc[index]}')
    print(f'查看所有标签的分布情况:{Counter(y)}')

    x = x.iloc[index].values.reshape(28, 28) # 变成28 * 28的像素点
    plt.imshow(x, cmap='gray')
    plt.axis('off')
    plt.show()

训练模型我们最重要的有两个步骤,一个是处理样本数据,训练我们的模型,还有一个是保存模型到一个地方,因为我们不可能每一次问一个问题我们就训练一次模型来解决问题,肯定是拿已经训练好的模型直接解决问题。首先我们需要处理归一化的问题,然后分离测试集和训练集, stratify = y这个参数的意思就是分层取样,保证每个样本的数据都有,不会出现偶然情况导致有些小概率特征被漏掉。

然后我们利用joblib.dump()把我们训练好的模型保存到另外一个文件夹里面

注意我们这个训练的模型不需要变成28*28的像素

def train_model() :
    df = pd.read_csv('./data/手写数字识别.csv')
    x = df.iloc[:, 1 :]
    y = df.iloc[:, 0]

    x = x / 255.0

    x_train, x_test, y_train, y_test = train_test_split(x, y, train_size = 0.2, random_state = 20, stratify = y)
    estimator = KNeighborsClassifier(n_neighbors = 4)
    estimator.fit(x_train, y_train)

    print(f'准确率:{accuracy_score(y_test, estimator.predict(x_test))}')

    # 保存模型
    joblib.dump(estimator, './model/手写数字识别.pkl') # pickle文件:python(pandas)独有的文件类型
    print('模型保存成功')

最后我们要使用模型,首先我们先导入一个图片,这个图片肯定是28*28像素的,所以用spt导入,然后把我们的模型加载到estimator里面,然后我们把这个28*28的像素展开成一横排,好让我们的模型进行预测。

这里有一个注意点就是可能大家因为我们训练模型的时候进行了归一化,这里也要除以255,但是这里千万不要。因为我们归一化是有误差的,我们读这个图片的时候一定要是准确的,最后我们拿这个模型进行预测的时候,如果这个图片不准确就很可能读错,并且归一化的本质就是按比例缩小,所以对近邻算法没有什么影响。

大家如果跑一下这个代码,如果做了归一化,那么结果预测的正好错误。

def use_model() :
    x = plt.imread('./data/demo.png')
    plt.imshow(x, cmap = 'gray')
    plt.axis('off')
    plt.show()

    # 加载模型
    estimator = joblib.load('./model/手写数字识别.pkl')

    x = x.reshape(1, -1) # 语法糖
    y_pred = estimator.predict(x)

    print(f'预测的结果:{y_pred}')

总结

本篇文章到这里就结束了!!!希望可以帮助大家理解机器学习,和大家一起进步~~~

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐