2.3 手写数字识别之数据处理
第2.2节我们使用“横纵式”教学法中的纵向极简方案快速完成手写数字识别任务的建模,但模型测试效果并未达成预期。我们换个思路,从横向展开,如图1所示,逐个环节优化,以达到最优训练效果。本节主要介绍手写数字识别模型中,数据处理的优化方法。
图1:“横纵式”教学法 — 数据处理优化
第2.2节我们通过调用飞桨提供的paddle.vision.datasets.MNISTAPI加载MNIST数据集。但实践中,我们面临的任务和数据环境千差万别,通常需要自己编写适合当前任务的数据处理程序,一般涉及如下五个环节:
- 读入数据
- 划分数据集
- 生成批次数据
- 训练样本集乱序
- 校验数据有效性
前提条件
在数据读取与处理前,首先要加载飞桨平台和数据处理库,代码如下。
In [1]
# 数据处理部分之前的代码,加入部分数据处理的库 import paddle from paddle.nn import Linear import paddle.nn.functional as F import os import gzip import json import random import numpy as np
2.3.1 读入数据并划分数据集
在实际应用中,保存到本地的数据存储格式多种多样,如MNIST数据集以json格式存储在本地,其数据存储结构如图2所示。
图2:MNIST数据集的存储结构
data包含三个元素的列表:训练集train_set、验证集val_set、测试集test_set,分别为50 000条训练样本、10 000条验证样本和10 000条测试样本。每条样本数据都包含手写数字的图像和对应的标签。
- 训练集 :用于确定模型参数。
- 验证集 :用于调节模型超参数(如多个网络结构、正则化权重的最优选择)。
- 测试集 :用于估计应用效果(没有在模型中应用过的数据,更贴近模型在真实场景应用的效果)。
train_set包含两个元素的列表:train_images、train_labels。
- train_images :[50 000, 784]的二维列表,包含50 000张图片。每张图片用一个长度为784的向量表示,内容是 28 × 28 像素的灰度值(黑白图片)。
- train_labels :[50 000, ]的列表,表示这些图片对应的分类标签,即0~9之间的一个数字。
在本地./work/
目录下读取文件名称为mnist.json.gz
的MNIST数据,并拆分成训练集、验证集和测试集,代码实现如下。
In [2]
# 声明数据集文件位置 datafile = './work/mnist.json.gz' print('loading mnist dataset from {} ......'.format(datafile)) # 加载json数据文件 data = json.load(gzip.open(datafile)) print('mnist dataset load done') # 读取到的数据区分训练集,验证集,测试集 train_set, val_set, eval_set = data # 观察训练集数据 imgs, labels = train_set[0], train_set[1] print("训练数据集数量: ", len(imgs)) # 观察验证集数量 imgs, labels = val_set[0], val_set[1] print("验证数据集数量: ", len(imgs)) # 观察测试集数量 imgs, labels = val= eval_set[0], eval_set[1] print("测试数据集数量: ", len(imgs)) print(len(imgs[0]))
loading mnist dataset from ./work/mnist.json.gz ......
mnist dataset load done
训练数据集数量: 50000
验证数据集数量: 10000
测试数据集数量: 10000
784
扩展阅读:为什么学术界的模型总在不断精进呢?
通常某组织发布一个新任务的训练集和测试集数据后,全世界的科学家都针对该数据集进行创新研究,随后大量针对该数据集的论文会陆续发表。论文1的A模型声称在测试集的准确率70%,论文2的B模型声称在测试集的准确率提高到72%,论文NN的xx模型声称在测试集的准确率提高到90% ...
然而这些论文中的模型在测试集上准确率提升真实有效么?我们不妨大胆猜测一下。假设所有论文共产生1000个模型,这些模型使用的是测试数据集来评判模型效果,并最终选出效果最优的模型。这相当于把原始的测试集当作了验证集,使得测试集失去了真实评判模型效果的能力,正如机器学习领域非常流行的一句话:“拷问数据足够久,它终究会招供”。
图3:拷问数据足够久,它总会招供
那么当我们需要将学术界研发的模型复用于工业项目时,应该如何选择呢?给读者一个小建议:当几个模型的准确率在测试集上差距不大时,尽量选择网络结构相对简单的模型。往往越精巧设计的模型和方法,越不容易在不同的数据集之间迁移。
2.3.2 训练样本乱序、生成批次数据
(1)训练样本乱序:先将样本按顺序进行编号,建立ID集合index_list****。然后将index_list乱序,最后按乱序后的顺序读取数据。
说明:
通过大量实验发现,模型对最后出现的数据印象更加深刻。训练数据导入后,越接近模型训练结束,最后几个批次数据对模型参数的影响越大。为了避免模型记忆影响训练效果,需要进行样本乱序操作。
(2)生成批次数据:先设置合理的batch size,再将数据转变成符合模型输入要求的np.array格式****返回。同时,在返回数据时将Python生成器设置为yield
模式,以减少内存占用。
在执行如上两个操作之前,需要先将数据处理代码封装成load_data函数,方便后续调用。load_data有三种模型:train
、valid
、eval
,分为对应返回的数据是训练集、验证集、测试集。
In [3]
imgs, labels = train_set[0], train_set[1] print("训练数据集数量: ", len(imgs)) # 获得数据集长度 imgs_length = len(imgs) # 定义数据集每个数据的序号,根据序号读取数据 index_list = list(range(imgs_length)) # 读入数据时用到的批次大小 BATCHSIZE = 100 # 随机打乱训练数据的索引序号 random.shuffle(index_list) # 定义数据生成器,返回批次数据 def data_generator(): imgs_list = [] labels_list = [] for i in index_list: # 将数据处理成希望的类型 img = np.array(imgs[i]).astype('float32') label = np.array(labels[i]).astype('float32') imgs_list.append(img) labels_list.append(label) if len(imgs_list) == BATCHSIZE: # 获得一个batchsize的数据,并返回 yield np.array(imgs_list), np.array(labels_list) # 清空数据读取列表 imgs_list = [] labels_list = [] # 如果剩余数据的数目小于BATCHSIZE, # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch if len(imgs_list) > 0: yield np.array(imgs_list), np.array(labels_list) return data_generator
训练数据集数量: 50000
In [4]
# 声明数据读取函数,从训练集中读取数据 train_loader = data_generator # 以迭代的形式读取数据 for batch_id, data in enumerate(train_loader()): image_data, label_data = data if batch_id == 0: # 打印数据shape和类型 print("打印第一个batch数据的维度:") print("图像维度: {}, 标签维度: {}".format(image_data.shape, label_data.shape)) break
打印第一个batch数据的维度:
图像维度: (100, 784), 标签维度: (100,)