机器学习Python实战-第三章-分类问题-1.K近邻算法

news/2024/9/28 21:13:15 标签: 机器学习, python, sklearn, 近邻算法, 分类, 经验分享

K近邻算法简介

K近邻(K-nearest neighbor,KNN)算法是机器学习算法中一种基本的分类与回归方法,以输入为实例的特征向量,通过计算新数据与训练数据特征值之间的距离,然后选取kk≥1)个距离中最近的邻居进行分类判断(投票法)或者回归。如果k=1,那么新数据被简单的分配给其他近邻的类。

K近邻算法不包含显式的学习过程,而是直接进行预测。实际上它是利用训练数据集对特征向量空间进行划分,作为其分类的“模型”。

 分类问题:分类问题的输出为实例的类别分类时,对于新的实例,根据其k个最近邻的训练实例的类别,通过多数表决规则进行预测。

回归问题: 回归问题的输出为实例的值。回归时,对于新的实例,取其k个最近邻的训练实例的平均值为预测值。

3.1.1        原理简介

K近邻算法的三要素分别是k值选择距离度量、和分类决策规则

1.k值选择 

k=1时,K近邻算法又称为最近邻算法,此时会将训练集中与x最近的点的类别作为x分类。(k值的选择会对K近邻算法的结果产生重大影响)

(1)若k值选取的较小,相当于用较小的邻域中的训练实例进行预测。 

  •  只有与输入实例较近的(相似的)训练实例才会对预测结果起作用。
  • 学习的估计误差增大,预测结果会对近邻的实例点非常敏感。若近邻的训练实例点刚好是噪声,则预测会出错。即k值的减小意味着模型整体变复杂,易发生过拟合

(2)若k值选取的较大,相当于用较大的邻域中的训练实例进行预测。

  • 可以减少学习的预估误差
  • 学习的近似误差增大。这时与输入实例较远的(不相似的)训练实例也会对预测结果起作用,使预测产生错误,即k值的增大意味着模型整体变简单
  • 当k=N时,无论输入实例是什么,都将其预测为训练实例中最多的类(即预测结果是一个常量),此时模型过于简单,完全忽略了训练实例中大量有用的信息。

(3)应用情况 

k值一般选取一个较小的数,通常采用交叉验证法来选取最优的k值。即比较不同k值时的交叉验证平均误差率,选择误差率较最小的那个k值。例如,选择k=1,2,3…,对每个k=i做若干次交叉验证,计算出平均误差率,然后比较并选取最小的那个。

2.距离度量 

k近邻算法要求数据的所有特征都可以做可比较的量化。若在数据特征中存在非数值的类型,则必须采取手段将其量化为数值(例:若样本特征中存在颜色,因为颜色是没有距离可言的,可以将颜色转换为灰度值来计算距离)。另外,样本中有多个参数,每个参数都有自己的定义域取值范围,它们对距离计算的影响也就不一样,如取值较大的影响力会盖过取值较小的参数。为了公平,样本参数必须做一些归一化处理,最简单的方式就是所有特征的数值都进行归一化处理

特征空间中两个实例点的距离是两个实例点相似程度的反映,K近邻模型的特征空间一般是n维实数向量空间R^n。尝试用的距离是欧氏距离,但有时也可以是其他距离,如曼哈顿距离。一般的距离公式表示为L_p距离(L_pdistance)。

L_p(x_i,x_j)=(\sum_{l=1}^{n}\left | x_i^{(l)}-x_j^{(l)} \right |^p)^{\frac 1 p}, \begin{cases}x_i,x_j\in \chi \subseteq R^n\\ x_i=(x_i^{(1)}, x_i^{(2)}, ...,x_i^{(n)})^T\\ x_j=(x_i^{(1)}, x_i^{(2)}, ...,x_i^{(n)})^T\\ p\ge1 \end{cases} 

上式中,当p=1时,为哈曼顿距离(Manhattan distance),即

L_1(x_i,x_j)=\sum_{l=1}^{n}\left | x_i^{(l)}-x_j^{(l)} \right |

当p=2时,为欧式距离(Euclidean distance),即

L_2(x_i,x_j)=(\sum_{l=1}^{n}\left | x_i^{(l)}-x_j^{(l)} \right |^2)^{\frac 1 2}

当p=\infty时,为各维度距离中的最大值,即

L_\infty (x_i,x_j)=\underset{l}{max}\left | x_i^{(l)}-x_j^{(l)} \right |

不同距离度量所确定的最近邻点是不同的,需要根据具体应用场景确定使用哪一种度量方式。一般情况下,欧氏距离适用于连续变量曼哈顿距离适用于计算棋盘格局相似性机器人避障路径选择城市道路长度计算等问题。

3.分类决策规则

 K近邻算法分类决策通常采用多数表决规则,即由输入实例的k个邻近的训练实例中的多数类决定输入实例的类。也可以基于距离的远近进行加权投票,距离越近的样本实例权重越大。

多数表决规则等价于经验风险最小化,设分类的损失函数为0-1损失函数(指预测值与损失值不相等为1,否则为0),分类函数为f:R^n \longrightarrow \left \{ c_1,c_2,...,c_K \right \},分类概率为

P(Y\neq f(X))=1-P(Y=f(X))

给定输入实例x \in \chi,其最邻近的k个样本实例构成集合N_k(x)。设涵盖N_k(x)区域的类别为c_j(是一个待求的未知量,但它肯定是c_1,c_2,...,c_K之一),则误分类概率为

\frac 1 k \underset {x_i \in N_k(x)} {\sum} I(y_i \neq c_j) = 1 - \frac 1 k \underset {x_i \in N_k(x)} {\sum} I(y_i = c_j) , \begin{cases} i=1,2,\dots,N \\ j=1,2,\dots ,N \end{cases}

分类概率就是训练数据的经验风险,要使误分类概率最小,即经验风险最小,

就要使\underset {x_i \in N_k(x)} {\sum} I(y_i = c_j)最大。即

c_j=\underset{c_j}{argmax} \underset {x_i \in N_k(x)} {\sum} I(y_i = c_j), \begin{cases} i=1,2,\dots,N \\ j=1,2,\dots ,K \end{cases}

3.1.2        算法步骤 

K近邻算法步骤如下:

  1. 输入:训练数据集T=\left \{(x_1,y_1),(x_2,y_2),\dots (x_N,y_N)\right \},x_i \in \chi \subseteq R^n为样本实例,y_i= \gamma=\left\{c_1,c_2,...,c_K \right \}为样本实例的类别,i=1,2,\dots,N。给定输入实例x(x\in \chi)
  2. 输出:输入实例x的类别y。
  3. 算法步骤:
    1. 根据选取的距离度量,在T中寻找与输入实例x最近邻的k个样本实例点x_i。涵盖这k个样本实例点的邻域记作N_k(x)
    2. N_k(x)中,根据分类决策规则(如多数表决规则)决定输入实例x的类别y:

y=\underset{c_j}{argmax} \underset {x_i \in N_k(x)} {\sum} I(y_i = c_j), \begin{cases} i=1,2,\dots,N \\ j=1,2,\dots ,K \end{cases}

其中,I()为指示函数,I(true)=1,I(false)=0。上式中,对于y_i,i=1,2,\dots,N,只有x\in N_k^{(x)}中的样本点时才考虑。

3.1.3        实战

1.数据集

首先导入包

python">import numpy as np
import matplotlib.pyplot as plt
from sklearn import neighbors, datasets, model_selection

然后给出加载数据的函数

python">def load_classification_data():
    """
    加载分类模型使用的数据集
    :return: 一个元组,依次为训练样本集、测试样本集、训练样本的标记、测试样本的标记
    """
    # 使用scikit-learn自带的手写识别数据集
    digits = datasets.load_digits()
    X_train = digits.data
    y_train = digits.target
    # 进行分层采样拆分,测试集占1/4
    return model_selection.train_test_split(X_train, y_train, test_size=0.25, random_state=0, stratify=y_train)
    

2.sklearn实现

1.k近邻分类

首先使用KNeighborsClassifier,给出如下测试函数:
python">def test_KNeighborsClassifier(*data):
    """
    测试KNeighborsClassifier分类器的用法
    :param data: 可变参数。第一个参数为训练样本集,第二个参数为测试样本集,第三个参数为训练样本集对应的标记,第四个参数为测试样本集对应的标记
    :return: None
    """
    X_train, X_test, y_train, y_test = data
    # 创建KNN分类器实例
    clf = neighbors.KNeighborsClassifier()
    # 训练模型
    clf.fit(X_train, y_train)
    # 在训练集上做预测
    print("Training score: %f" % model.score(X_train, y_train))
    print("Testing score: %f" % model.score(X_test, y_test))


X_train, X_test, y_train, y_test = load_classification_data()
test_KNeighborsClassifier(X_train, X_test, y_train, y_test)

 代码结果:

声明(有效避免部分潜在的报错): 

有时,环境的配置可能导致问题。确保你在一个干净的虚拟环境中运行代码,可以考虑创建一个新的环境并重新安装依赖项。

 存在的问题以及解决办法:

如果你是在本地运行,并且代码如下,可能会出现以下两种报错

报错:E AttributeError: 'NoneType' object has no attribute 'split'

(这个报错我忘记截图了,但是这个错误可以通过升级相关库解决)

解决办法:在 pycharm 的 terminal 输入

python">pip install --upgrade scikit-learn threadpoolctl

升级之后再次运行,然后你就会发现出现下面这个错误(😋)

 解决办法:代码如下,然后直接运行即可得到上面的运行结果

考察k值以及投票策略对预测性能的影响,测试代码如下:
python">def test_KNeighborsClassifier_k_w(data):
    """
    测试KNeighborsClassifier分类器的n_neighbors和weights参数的影响
    :param data: 包含训练和测试样本及其对应标签的元组
    :return: None
    """
    X_train, X_test, y_train, y_test = data
    Ks = np.linspace(1, y_train.size, num=100, endpoint=False, dtype='int')
    weights = ['uniform', 'distance']

    flg = plt.figure()
    ax = flg.add_subplot(1, 1, 1)
    # 绘制不同weights下,不同K值对应的训练和测试准确率
    for weight in weights:
        training_scores = []
        testing_scores = []
        for K in Ks:
            clf = neighbors.KNeighborsClassifier(n_neighbors=K, weights=weight)
            clf.fit(X_train, y_train)
            training_scores.append(clf.score(X_train, y_train))
            testing_scores.append(clf.score(X_test, y_test))
        ax.plot(Ks, training_scores, label='Training score: %s' % weight)
        ax.plot(Ks, testing_scores, label='Testing score: %s' % weight)
    ax.legend(loc='best')
    ax.set_title('KNeighborsClassifier: k and weights')
    ax.set_xlabel('k')
    ax.set_ylabel('score')
    ax.set_ylim([0.0, 1.05])
    plt.show()

代码结果: 

从图中可以看到,

在使用uniform投票策略的情况下(即投票权重都相同),分类器随着k增长,预测性能稳定下降。这是因为当k增大时,距输入实例较远的训练实例也会对预测起作用,使预测发生错误。

在使用distance投票策略的情况下(即投票权重与距离成反比),分类器随着k的增长,对测试集的预测性能相对稳定。这是因为虽然k增大时,距输入实例较远的训练实例也会对预测起作用,但因为距离较远,其影响小得多(权重很小)。

然后考察p值(即距离函数的形式)对预测性能的影响,测试函数如下:
python">def test_KNeighborsClassifier_k_p(data):
    """
    测试KNeighborsClassifier分类器的p参数的影响
    :param data: 包含训练和测试样本及其对应标签的元组
    :return: None
    """
    X_train, X_test, y_train, y_test = data
    Ks = np.linspace(1, y_train.size, endpoint=False, dtype='int')
    Ps = [1, 2, 10]

    flg = plt.figure()
    ax = flg.add_subplot(1, 1, 1)
    # 绘制不同p值下,不同K值对应的训练和测试准确率
    for P in Ps:
        training_scores = []
        testing_scores = []
        for K in Ks:
            clf = neighbors.KNeighborsClassifier(n_neighbors=K, p=P)
            clf.fit(X_train, y_train)
            training_scores.append(clf.score(X_train, y_train))
            testing_scores.append(clf.score(X_test, y_test))
        ax.plot(Ks, training_scores, label='Training score:P=%d' % P)
        ax.plot(Ks, testing_scores, label='Testing score: P=%d' % P)
    ax.legend(loc='best')
    ax.set_title('KNeighborsClassifier: k and p')
    ax.set_xlabel('k')
    ax.set_ylabel('score')
    ax.set_ylim([0.0, 1.05])
    plt.show()

可以看到,参数p对分类器的预测性能没有太大的影响。由于有

L_p(x_i,x_j)=(\sum_{l=1}^{n}\left | x_i^{(l)}-x_j^{(l)} \right |^p)^\frac 1p

因此,当p=1时,x_ix_j的最近的点;当p为其他值时,该结论也成立。

2.k近邻回归

首先使用KNeighborsRegressor,给出如下测试函数:
python">def test_KNeighborsRegressor(data):
    """
    测试KNeighborsRegressor的用法
    :param data: 包含训练和测试样本及其对应标签的元组
    :return: None
    """
    X_train, X_test, y_train, y_test = data
    model = neighbors.KNeighborsRegressor()
    model.fit(X_train, y_train)
    print("\nTraining score: %f" % model.score(X_train, y_train))
    print("Testing score: %f" % model.score(X_test, y_test))

结果: 

然后考察k值以及投票策略对预测性能的影响,测试代码如下:
python">def test_KNeighborsRegressor_k_w(data):
    """
    测试KNeighborsRegressor的k和w参数
    :param data: 包含训练和测试样本及其对应标签的元组
    :return: None
    """
    X_train, X_test, y_train, y_test = data
    Ks = np.linspace(1, y_train.size, num=100, endpoint=False, dtype='int')
    weights = ['uniform', 'distance']

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)
    # 绘制不同weights下,预测得分随n_neighbors变化的曲线
    for weight in weights:
        training_scores = []
        testing_scores = []
        for K in Ks:
            model = neighbors.KNeighborsRegressor(n_neighbors=K, weights=weight)
            model.fit(X_train, y_train)
            training_scores.append(model.score(X_train, y_train))
            testing_scores.append(model.score(X_test, y_test))
        ax.plot(Ks, training_scores, label='Training score: %s' % weight)
        ax.plot(Ks, testing_scores, label='Testing score: %s' % weight)
    ax.legend(loc='best')
    ax.set_xlabel('n_neighbors')
    ax.set_ylabel('score')
    ax.set_ylim(0.0, 1.05)
    ax.set_title('KNeighborsRegressor: k and w')
    plt.show()

然后考察p值(即距离函数的形式)与预测性能的影响,测试函数如下:

python">def test_KNeighborsRegressor_k_p(data):
    """
    测试KNeighborsRegressor的k和p参数
    :param data: 包含训练和测试样本及其对应标签的元组
    :return: None
    """
    X_train, X_test, y_train, y_test = data
    Ks = np.linspace(1, y_train.size, endpoint=False, dtype='int')
    Ps = [1, 2, 10]

    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)
    # 绘制不同weights下,预测得分随n_neighbors变化的曲线
    for P in Ps:
        training_scores = []
        testing_scores = []
        for K in Ks:
            model = neighbors.KNeighborsRegressor(n_neighbors=K, p=P)
            model.fit(X_train, y_train)
            training_scores.append(model.score(X_train, y_train))
            testing_scores.append(model.score(X_test, y_test))
        ax.plot(Ks, training_scores, label='Training score: p=%d' % P)
        ax.plot(Ks, testing_scores, label='Testing score: p=%d' % P, linestyle='--')
    ax.legend(loc='best')
    ax.set_xlabel('n_neighbors')
    ax.set_ylabel('score')
    ax.set_ylim(0.0, 1.05)
    ax.set_title('KNeighborsRegressor: k and p')
    plt.show()

结果:

3.算法实现

数据选择经典的Iris鸢尾花数据集,下载代码如下:

(由于是自己在sklearn下载的数据集,部分范围与书上存在出入,如果是按照我的流程来编写的代码请以我的代码为准。

python">from sklearn.datasets import load_iris
import pandas as pd

# 下载数据集
iris = load_iris()

# 将数据转换为DataFrame
df = pd.DataFrame(data=iris.data, columns=iris.feature_names)
df['target'] = iris.target

# 保存为CSV文件
df.to_csv('iris_dataset.csv', index=False)

 提取setosa、versicolor、virginica这3个类别的数据,分别用0、1、2表示,选择sepal length和petal length两个特征,在二维平面作图,代码如下:

python">import pandas as pd
import matplotlib.pyplot as plt

data = pd.read_csv('iris_dataset.csv', header=0)  # 修改为 header=0
data.columns = ['sepal length', 'sepal width', 'petal length', 'petal width', 'species']

X = data.iloc[0:150, [0, 2]].values  # 获取 sepal length 和 petal length
y = data.iloc[0:150, 4].values
y[y == 'Iris-setosa'] = 0
y[y == 'Iris-versicolor'] = 1
y[y == 'Iris-virginica'] = 2

X_setosa, y_setosa = X[0:50], y[0:50]
X_versicolor, y_versicolor = X[50:100], y[50:100]
X_virginica, y_virginica = X[100:150], y[100:150]

plt.scatter(X_setosa[:, 0], X_setosa[:, 1], color='red', marker='o', label='setosa')
plt.scatter(X_versicolor[:, 0], X_versicolor[:, 1], color='blue', marker='^', label='versicolor')
plt.scatter(X_virginica[:, 0], X_virginica[:, 1], color='green', marker='s', label='virginica')
plt.xlabel('sepal length')
plt.ylabel('petal length')
plt.legend(loc='upper left')
plt.show()

 结果

接下来将每个类别的所有样本分成训练集(training set)、验证集(validation set)和测试集(test set),各占所有样本的比例分别为60%、20%、20%。代码如下:

(希望代码简洁,可以使用我这种写法,不过后面画图需要用到数据请自行声明)

python"># 希望代码简洁,可以使用我这种写法,不过后面画图需要用到数据请自行声明
# 训练集
X_train = np.vstack((X_setosa[0:30, :], X_versicolor[0:30, :], X_virginica[0:30, :]))
y_train = np.hstack((y_setosa[0:30], y_versicolor[0:30], y_virginica[0:30]))

# 验证集
X_val = np.vstack((X_setosa[30:40, :], X_versicolor[30:40, :], X_virginica[30:40, :]))
y_val = np.hstack((y_setosa[30:40], y_versicolor[30:40], y_virginica[30:40]))

# 测试集
X_test = np.vstack((X_setosa[40:50, :], X_versicolor[40:50, :], X_virginica[40:50, :]))
y_test = np.hstack((y_setosa[40:50], y_versicolor[40:50], y_virginica[40:50]))


class KNearestNeighbor(object):
    def __init__(self):
        self.X_train = None
        self.y_train = None

    # 训练函数
    def train(self, X, y):
        self.X_train = X
        self.y_train = y

    # 预测函数
    def predict(self, X, k=1):
        num_test = X.shape[0]       # 计算欧氏距离
        num_train = self.X_train.shape[0]
        np.zeros((num_test, num_train))
        d1 = -2 * np.dot(X, self.X_train.T)
        d2 = np.sum(np.square(X), axis=1, keepdims=True)
        d3 = np.sum(np.square(self.X_train), axis=1)
        dists = np.sqrt(d1 + d2 + d3)
        # 根据k值选择可能属于的类别
        y_pred = np.zeros(num_test)
        for i in range(num_test):
            dist_k_min = np.argsort(dists[i])[:k]
            y_kclose = self.y_train[dist_k_min]
            y_pred[i] = np.argmax(np.bincount(y_kclose.tolist()))
        return y_pred

 创建一个KnearestNeighbor实例对象,然后在验证集上进行k-hold交叉验证。通过多次实验,发现当3\leq k \leq 17时得到的结果最高,为0.97(该数据可能存在错误)。选择完k值后就可以对测试集进行预测分析了。代码如下:

python">KNN = KNearestNeighbor()
KNN.train(X_train, y_train)
y_pred = KNN.predict(X_test, k=3)
accuracy = np.mean(y_pred == y_test)
print('Accuracy: {:.2f}'.format(accuracy))

 这个报错说明数据中存在负数,只需要在predict函数中做如下修改即可:

python">    def predict(self, X, k=1):
        num_test = X.shape[0]
        num_train = self.X_train.shape[0]

        # 计算欧氏距离
        dists = np.zeros((num_test, num_train))

        dists = np.sum(X ** 2, axis=1, keepdims=True) + np.sum(self.X_train ** 2, axis=1) - 2 * np.dot(X,
                                                                                                       self.X_train.T)

        # 将负值置为0,以避免 sqrt 计算无效值
        dists[dists < 0] = 0

        dists = np.sqrt(dists)

        y_pred = np.zeros(num_test)
        for i in range(num_test):
            dist_k_min = np.argsort(dists[i])[:k]
            y_kclose = self.y_train[dist_k_min]
            y_pred[i] = np.argmax(np.bincount(y_kclose))

        return y_pred

 修改后:

 最后将预测结果绘图表示。只选择sepal length和petal length两个特征,代码如下:

python"># 训练集
X_setosa_train = X_setosa[0:30, :]
X_versicolor_train = X_versicolor[0:30, :]
X_virginica_train = X_virginica[0:30, :]
plt.scatter(X_setosa_train[:, 0], X_setosa_train[:, 1], color='red', marker='o', label='setosa_train')
plt.scatter(X_versicolor_train[:, 0], X_versicolor_train[:, 1], color='green', marker='^', label='versicolor_train')
plt.scatter(X_virginica_train[:, 0], X_virginica_train[:, 1], color='blue', marker='s', label='virginica_train')

# 测试集
X_setosa_test = X_setosa[40:50, :]
X_versicolor_test = X_versicolor[40:50, :]
X_virginica_test = X_virginica[40:50, :]
plt.scatter(X_setosa_test[:, 0], X_setosa_test[:, 1], color='y', marker='o', label='setosa_test')
plt.scatter(X_versicolor_test[:, 0], X_versicolor_test[:, 1], color='y', marker='^', label='versicolor_test')
plt.scatter(X_virginica_test[:, 0], X_virginica_test[:, 1], color='y', marker='s', label='virginica_test')
plt.legend(loc=4)
plt.xlabel('sepal length')
plt.ylabel('petal length')
plt.show()

实验结果

3.1.4        实验

1.实验目的

理解K近邻算法的原理,掌握k值选择、距离度量和分类决策的设置方法,并分别利用sk-learn的相关包、Python语言编程来实现该算法。

2实验数据

实验数据为sk-learn自带的手写识别数据集DigitDataset。该数据集由1797张样本图片组成。每张图片都是一个8×8像素大小的手写数字位图(如上图所示)。为了使用这样的8×8像素图形,必须首先将其转换为长度为64的特征向量。代码如下:

python">from sklearn import datasets
import matplotlib.pyplot as plt

# 导入手写数字数据集
digits = datasets.load_digits()

# 显示最后一个手写数字位图
plt.figure(1, figsize=(3, 3))
plt.imshow(digits.images[-1], cmap=plt.cm.gray_r, interpolation='nearest')
plt.show()

3.实验要求

利用python实现如下功能。

  1. 数据预处理:主要做好加载数据、交叉验证、归一化等功能,以实现数据预处理。
  2. 模型训练:利用python来实现训练算法。
  3. 模型验证:编程实现对算法的验证,以评估算法的有效性。

1.数据预处理  

对于交叉验证的方法,大家可以参考这个:sklearn中文社区-交叉验证 

对于归一化的方法,大家可以参考这篇博客:使用sklearn对数据进行标准化、归一化以及将数据还原

python"># 数据预处理
def load_and_preprocess_data():
    # 加载手写数字数据集
    digits = datasets.load_digits()

    # 数据特征:64维向量,每一行为一个样本
    X = digits.data
    # 标签:手写数字的真实分类
    y = digits.target

    # 将数据集划分为训练集和测试集
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

    # 对特征进行归一化处理
    scaler = StandardScaler()
    X_train = scaler.fit_transform(X_train)
    X_test = scaler.transform(X_test)

    return X_train, X_test, y_train, y_test, digits

可以从代码中看到,我并没有使用交叉验证,直接简单的加载数据然后进行了归一化处理,我们可以通过交叉验证和归一化处理来观察二者对模型精确度的影响。

2.模型训练

python"># 模型训练
def train_knn_model(X_train, y_train, k):
    knn = KNeighborsClassifier(n_neighbors=k)
    knn.fit(X_train, y_train)
    return knn

3.模型验证

python"># 模型验证
def validate_model(knn, X_test, y_test):
    y_pred = knn.predict(X_test)

    # 评估模型的准确性
    accuracy = accuracy_score(y_test, y_pred)
    print(f"模型的准确率: {accuracy * 100:.2f}%")

    # 混淆矩阵
    conf_matrix = confusion_matrix(y_test, y_pred)
    print("混淆矩阵:\n", conf_matrix)
python"># 主函数
if __name__ == "__main__":
    # 加载和预处理数据
    X_train, X_test, y_train, y_test, digits = load_and_preprocess_data()

    # 使用k=3训练模型
    knn_model = train_knn_model(X_train, y_train, k=3)

    # 验证模型
    validate_model(knn_model, X_test, y_test)

    # 可视化一个手写数字
    plt.figure(figsize=(3, 3))
    plt.imshow(digits.images[-1], cmap=plt.cm.gray_r, interpolation='nearest')
    plt.show()

可以看到最终结果:模型的准确率是96.85%,我们回到数据预处理,尝试不同的数据预处理对于模型准确率的影响

4.优化模型

上文中对于数据预处理,没有使用交叉验证,归一化处理使用的是StandardScaler函数。通过查询,在sklearn中提供了以下方法进行归一化处理:

  • MinMaxScaler:将数据缩放到指定范围(通常是0到1)。
  • StandardScaler:将数据标准化为均值为0,标准差为1的分布。
  • RobustScaler:使用中位数和四分位数进行缩放,对异常值更鲁棒。
  • Normalizer:按行缩放样本,使得每个样本的范数为1。

对于手写数字数据集,我们可以使用StandardScaler或者MinMaxScaler。在使用一些基于距离的算法(如K近邻算法)时,MinMaxScaler表现更好。

python"># 数据预处理
def load_and_preprocess_data():
    digits = datasets.load_digits()
    X, y = digits.data, digits.target

    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

    scaler = MinMaxScaler()
    X_train = scaler.fit_transform(X_train)
    X_test = scaler.transform(X_test)
    return X_train, X_test, y_train, y_test, digits

从结果可以明显看出使用 MinMaxScaler进行归一化处理模型准确度更高。

到目前为止,我们还没有使用交叉验证,通过查询,在sklearn中提供了以下方法进行交叉验证:

  • KFold:将数据集分成k个折叠,每次用一个折叠作为验证集,其余作为训练集。
  • StratifiedKFold:类似于KFold,但保持每个折叠中的类比例,以确保分类问题的代表性。
  • LeaveOneOut (LOO):每次只留一个样本作为验证集,适用于小数据集。
  • GroupKFold:用于分组数据的交叉验证,确保同一组的数据不会出现在训练和验证集中。
  • TimeSeriesSplit:专为时间序列数据设计,保留时间顺序。

对于手写数字数据集我们可以使用以下两种交叉验证的方法:

  • KFold适合普通的分割和评估。
  • StratifiedKFold更好,因为它可以保持每个数字类的比例,确保模型在每个类别上都得到良好的评估。

使用 StratifiedKFold 的代码如下,如果使用KFold,只需要修改相关代码为kf即可

python">kf = KFold(n_splits=5, shuffle=True, random_state=42)

得到结果在代码下方,可以看出StratifiedKFold 的效果更好

python">import numpy as np
from sklearn.datasets import load_digits
from sklearn.model_selection import StratifiedKFold
from sklearn.preprocessing import MinMaxScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score

# 加载数据集
digits = load_digits()
X, y = digits.data, digits.target

# 应用MinMaxScaler
scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)

# 设置StratifiedKFold
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

accuracies = []

# 进行交叉验证
for train_index, test_index in skf.split(X_scaled, y):
    X_train, X_test = X_scaled[train_index], X_scaled[test_index]
    y_train, y_test = y[train_index], y[test_index]

    knn = KNeighborsClassifier(n_neighbors=3)  # 选择k值
    knn.fit(X_train, y_train)
    y_pred = knn.predict(X_test)

    accuracy = accuracy_score(y_test, y_pred)
    accuracies.append(accuracy)

# 输出平均准确率
print(accuracies)
print(f'Average Accuracy: {np.mean(accuracies):.5f}')

StratifiedKFold

KFlod 

在最初的数据预处理中,我提到可以参考sklearn中文社区中的方法,使用cross_val_score函数进一步优化代码。仅用两行代码便可达到上文的效果,得到结果与上述一致。

python"># 进行交叉验证
knn = KNeighborsClassifier(n_neighbors=3)
scores = cross_val_score(knn, X_scaled, y, cv=skf, scoring='accuracy')

# 输出平均准确率
print(scores)
print(f'Average Accuracy: {np.mean(scores):.5f}')

现在模型已经优化的差不多了,我们可以先进行一个数据报告,并计算模型的准确率、精确率、召回率、F1值。

在validate_model函数中添加代码(得到结果不一样是因为我把测试集大小调为了0.5):

python">print("分类报告:\n", classification_report(y_test, y_pred))

到这里已经大功告成了。还剩下能优化的地方就是数据可视化了,这里我提供两个方向给大家:

  1. 绘制测试样本的图片,并且在标题输出样本的预测值与真实值;
  2. 绘制混淆矩阵的图片,提供每个分类的真实预测情况。

结果图如下,仅供参考:

测试样本:

混淆矩阵: 


http://www.niftyadmin.cn/n/5681857.html

相关文章

【原创】java+swing+mysql企业招聘管理系统设计与实现

个人主页&#xff1a;程序员杨工 个人简介&#xff1a;从事软件开发多年&#xff0c;前后端均有涉猎&#xff0c;具有丰富的开发经验 博客内容&#xff1a;全栈开发&#xff0c;分享Java、Python、Php、小程序、前后端、数据库经验和实战 文末有本人名片&#xff0c;希望和大家…

基于nodejs+vue的外卖管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏&#xff1a;Java精选实战项目…

[数据集][目标检测]猪数据集VOC-2856张

数据集格式&#xff1a;Pascal VOC格式(不包含分割的txt文件&#xff0c;仅仅包含jpg图片和对应的xml) 图片数量(jpg文件个数)&#xff1a;2856 标注数量(xml文件个数)&#xff1a;2856 标注类别数&#xff1a;1 标注类别名称:["pig"] 每个类别标注的框数&#xff1a…

numpy数组与矩阵运算

重点在于对数组和矩阵的处理。 一、数组 1.创建数组 方式多样 np.array(列表 元组 range对象) np.arange(n)&#xff1a;同range np.linspace()&#xff1a;等差 np.logspace()&#xff1a;等比 np.zeros((a,b)) np.ones((a,b)) np.identity(dim)&#xff1a;dim*dim的单位矩阵…

计算机网络自顶向下(1)---网络基础

目录 1.网络的分类 2.网络协议 3.网络分层结构 1.OSI七层模型 2.TCP/IP四层模型 3.网络与OS的关系 4.网络传输基本流程 1.协议报头 5.网络中的地址管理 1.IP地址 2.端口号 6.传输层协议 1.TCP协议 2.UDP协议 3.网络字节序 7.socket 1.网络的分类 局域网&…

BUG项目管理

最近只要改项目就有可能产生bug。 目前这项目&#xff0c;从一开始我就参与开发。 很长一段时间都是敏捷开发&#xff0c;有时候连UI图都是后出。 随着时间加长&#xff0c;需求复杂度增加&#xff0c;有时候动下代码就伤筋动骨&#xff0c;事故不断&#xff0c;主要是影响口…

WebGL动画与交互

目录 动画交互拖放触摸事件多点触控手势识别滑动手势缩放和平移键盘控制游戏控制

nodejs逐字读取文件示例

像chatpGPT显示文字一样.主要是服务器流式返回数据.前端用for await读取response.body <button id"fetchjson" onclick"FetchJSON()">fetch json看console与network</button> <button id"fetchstream" onclick"FetchStrea…