4.6 实例分析手写体识别#

经过前面几节的介绍,我们对模型的改善与泛化已经有了一定的认识。下面我们就通过一个实际的手写体分类任务进行示范,介绍一下常见的操作流程。同时顺便介绍一下sklearn和matplotlib中几种常见方法的使用,以下完整示例代码可参见AllBooKCode/Chapter04/C20_digits_classification 文件。

4.6.1数据预处理#

在4.1.3节中,我们详细介绍了为何需要对输入特征进行标准化操作,以及两种常见的标准化方法。接下来,就来详细介绍标准化在模型训练过程中的具体使用流程。

1. 载入并划分数据集

首先,载入模型训练时所需要的数据集。这里以sklearn中常见的手写体数据集load_digits为例,它一共包含1797个样本共10个类别,每个样本包含64个特征维度,载入示例代码如下:

1 def load_data():
2     data = load_digits()
3     x, y = data.data, data.target
4     x_train, x_test, y_train, y_test = \
5         train_test_split(x, y, test_size=0.3, random_state=20)

在上述代码中,第2~3行是载入原始数据的特征和标签;第4~5行是将训练集划分成两部分,其中test_size=0.3表示测试集的比例为30%,random_state=20表示设置一种状态值,它的作用是使每次划分的结果都一样但也可以设置其他值。在sklearn中对于包含随机操作的函数或者方法,一般都有这个参数,固定下来的目的是便于其他人复现结果。同时,如果需要将整个数据集划分成3份,只需要再次使用train_test_split()方法对x_trainy_train进行对应比例的划分即可。

2. 样本可视化

在完成数据集的载入后还可以选择部分样本对其进行可视化,示例代码如下:

 1 import matplotlib.pyplot as plt
 2 def visualization(x):
 3     images = x.reshape(-1, 8, 8)  # reshape成一张图片的形状
 4     fig, ax = plt.subplots(3, 5)
 5     for i, axi in enumerate(ax.flat):
 6         image = images[i]
 7         axi.imshow(image)
 8         axi.set(xticks=[], yticks=[])
 9     plt.tight_layout()
10     plt.show()

在上述代码中,第1行是导入matplotlib中的pyplot作图模块。第3行是将原始输入的形状从[n,64]变形为[n,8,8],即原始的每张图片为一个64维的向量,在可视化时需要将其变成一个像素矩阵。第4行是行定义一个包含有3行5列的子图画布。第5~8行是循环展示每一张图片,其中第8行是去掉每个子图对应的横纵坐标轴。第9行是自适应各子图间的距离。第10行是对最后的结果进行可视化,结果如图4-28所示。

图 4-28 手写体可视化

3. 数据集标准化

在完成数据集载入和划分以后,进一步需要对特征维度进行标准化并保存标准化过程中计算得到的相关参数。例如在以4.2.3节中介绍的去均值方法进行标准化时,就需要保存每个维度对应的均值和标准差。这里可以借助sklearn中的StandardScaler方法来完成,示例代码如下:

1 from sklearn.preprocessing import StandardScaler
2 def load_data():
3 	# 此处接 "1.载入并划分数据集"中的代码
4     ss = StandardScaler()
5     x_train = ss.fit_transform(x_train)
6     x_test = ss.transform(x_test)
7     return x_train, x_test, y_train, y_test

在上述代码中,第1行是导入取均值特征标准化模块。第4行用来定义4.2.3节中的去均值标准化方法实例对象。第5行是利用训练集来计算每个维度对应的均值和标准差,然后对训练集中每个特征维度进行标准化。第6行是利用在训练集上计算得到的均值和方差来对测试集中的每个维度进行标准化。如果这里再使用.fit_transform()方法进行标准化,则是根据测试集中的参数来对测试集进行标准化,而这将严重影响模型在未来新数据上的泛化能力。第7行则是返回最后各部分的结果。

注意: 无论用什么方法对数据集进行标准化,都必须遵循上述步骤。

4.6.2 模型选择#

正如4.5.3节中的介绍,不同的模型实际上是根据选择不同的超参数组合所形成,因此,选择模型的第一步就是确定好有哪些可供选择的超参数,以及每个超参数可能的取值。由于此处将采用逻辑回归算法对手写体图片进行分类,所以目前涉及的超参数仅有学习率和惩罚系数。下面根据4.5.3节中介绍的方法来一步步完成模型的选择和训练过程。

1. 列举超参数

根据分析,这里需要列出学习率和惩罚系数的可能取值,候选结果如下:

1 learning_rates = [0.001, 0.03, 0.01, 0.03, 0.1, 0.3, 1, 3]
2 penalties = [0, 0.01, 0.03, 0.1, 0.3, 1, 3]

在这里,取值方法一般来讲可以每次扩大3倍,但是也可以每次都增加相同的步长(例如0.002),只不过这样需要花费更多的时间来遍历所有可能的超参数组合,具体可以视情况而定。

2. 定义模型

在列举出所有参数的可能取值后,需要遍历所有的参数组合来形成不同的模型,示例代码如下:

1 def model_selection(X, y, k=5):
2     learning_rates = [0.001, 0.03, 0.01, 0.03, 0.1, 0.3, 1, 3]
3     penalties = [0, 0.01, 0.03, 0.1, 0.3, 1, 3]
4     all_models = []
5     for lr in learning_rates:
6         for p in penalties:
7             print(f"正在训练模型: learning_rate = {lr}, penalty = {p}")
8             model = SGDClassifier(loss='log_loss',penalty='l2', 
9                                   learning_rate='constant', eta0=lr, alpha=p)

在上述代码中,第8行使用的是sklearn中的SGDClassifier类来建立逻辑回归模型。它与第3章中所介绍的LogisticRegression的区别在于,LogisticRegression并没有通过梯度下降进行参数求解,而SGDClassifier使用的便是梯度下降算法进行求解。同时,在SGDClassifier中可以通过loss='log_loss'来指定为逻辑回归,penalty='l2'来指定为$\mathcal{l}_2$正则化,通过learning_rate='constant'来指定使用自定义的学习率,即根据eta0=lr来设定,因为默认SGDClassifier中的学习率都是根据训练过程动态适应的,通过alpha=p来指定相应的惩罚系数。

3. 交叉验证

在根据不同的超参数组合定义得到不同的模型后,需要对训练集进行划分以实现模型的交叉验证。在这里可以借助sklearn中的KFold()方法来对训练集进行划分,示例代码如下:

 1 def model_selection(X, y, k=5):
 2 	# 此处接“2.定义模型”中的代码
 3             kf = KFold(n_splits=k, shuffle=True, random_state=10)
 4             model_score = []
 5             for train_index, dev_index in kf.split(X):
 6                 X_train, X_dev = X[train_index], X[dev_index]
 7                 y_train, y_dev = y[train_index], y[dev_index]
 8                 model.fit(X_train, y_train)
 9                 s = model.score(X_dev, y_dev)
10                 model_score.append(s)
11             all_models.append([np.mean(model_score), lr, p])
12     print("最优模型: ", sorted(all_models, reverse=True,key=lambda x: x[0])[0])

在上述代码中,第3行用来生成交叉验证时样本对应的索引,其中n_splits=k表示使用k折交叉验证,shuffle=True表示在划分时对训练集进行随机打乱。第5~7行用来取每一次交叉验证时样本的索引,并根据这些索引获取每次对应的训练集和验证集。第8行是调用模型中的.fit()方法进行训练。第9~10行是通过验证集来评估每个模型的准确率,并将每个模型评价指标进行保存。第11行是计算每个模型的平均准确率,同时将两个对应超参数机保存。第12行是对所有的模型以二维列表中内层列表的第0个元素为比较对象进行降序排序,并输出平均准确率最大的模型对应的一组超参数。

当执行完上述代码后,便能够得到如下所示的运行结果:

正在训练模型: learning_rate = 0.001, penalty = 0
正在训练模型: learning_rate = 0.001, penalty = 0.01
……
正在训练模型: learning_rate = 3, penalty = 3
最优模型:  [0.9586163283374439, 0.03, 0]

经过交叉验证选择完模型后可以发现,当学习率为0.03,惩罚系数为0时对应的模型为最优模型。同时,由于备选的学习率有8个,备选的惩罚系数有7个,并且这里采用了5折交叉验证,因此一共就需要拟合280次模型。尽管此处我们自己编码实现了模型的K折交叉验证,但是sklearn还提供了更为简洁的接口,只需要通过4行代码便能够实现上述过程,这部分内容将在5.3节内容中进行介绍。

4.6.3 模型测试#

通过交叉验证选择完模型后,可以再用完整的训练集对该模型进行训练,然后在测试集上测试其泛化误差,示例代码如下:

1     model = SGDClassifier(loss='log_loss', penalty='l2', 
2     		learning_rate='constant', eta0=0.03, alpha=0.0)
3     model.fit(x_train, y_train)
4     y_pred = model.predict(x_test)
5     print(classification_report(y_test, y_pred))

在运行完上述代码后,便能够得到如下结果:

1        			  precision  recall  f1-score   support
2     accuracy                            0.96       540
3    macro avg       0.96      0.96       0.96       540
4 weighted avg       0.96      0.96       0.96       540

到此,对于一个模型从数据预处理到模型选择,再到模型测试的全部流程就介绍完了。不过上述过程在模型选择部分需要自己手动划分数据集以便进行交叉验证后选择模型,这样看起来稍微有点烦琐,不过好在sklearn已经将上述过程进行了封装,只需几行代码就能实现上述完整过程。这部分内容将在第5章中进行介绍。

4.6.4 小结#

在本节中,我们首先通过逻辑回归算法进行了手写体分类的示例,介绍了如何对数据集进行预处理及其对应的完整流程;接着介绍了如何对备选模型进行选择,包括列举超参数、定义模型及进行交叉验证等步骤;最后介绍了如何在测试集上来测试最优模型的泛化误差。通过本节内容的介绍,我们对于机器学习算法的整体建模流程算是有了比较完整的了解。

总结一下,在本章中我们首先介绍了什么是特征标准化、为什么需要特征标准化及一种常见的特征标准化方法,接着介绍了什么是过拟合与欠拟合、如何通过$\mathcal{l}_2$正则化方法来缓解模型的过拟合现象及$\mathcal{l}_2$正则化背后的原理,然后介绍了什么是模型的偏差与方差及模型如何对模型进行选择的方法,最后通过一个完整的手写体识别示例展示了从数据预处理(包括载入数据、划分数据和标准化数据)到模型选择(包括列举模型参数、定义模型和交叉验证),再到模型测试的完整流程。对于这部分内容的介绍就先告一段落。不过在后续章节的介绍中,我们也会继续补充相关提高模型性能的方法与技巧。