-
摘要
在后面两次的分享中,咱们次要探讨了 LeNet 卷积神经网络,剖析了卷积、池化、全连贯这些操作运算的特点和用法,以及 LeNet 中每一层的计算和作用。在理解过该网络的原理后,那么本次咱们将通过应用 MindSpore 工具实现 MNIST 数据集的分类。
- 模型的结构
对于一个残缺图片分类模型,通常有以下几个组成部分。
模型:假如一个样本图片信息是 X(i),输入标签为 Y,那么咱们须要建设基于输出 X(i)和输入标签 Y 的表达式,也就是模型(model)。模型输入的 Y 是对实在样本的预测或预计,咱们通常会容许它们之间有误差。
模型训练:通过数据来寻找特定的模型参数值,使模型在数据上的误差尽可能小。这个过程叫作模型训练(model training)。上面咱们介绍模型训练所波及的 3 个因素。
训练数据:咱们通常应用一系列的实在数据,例如多个图片的实在标签和它们蕴含的不同像素数组。咱们心愿在这个数据下面寻找模型参数来使模型的预测后果更靠近实在标签。在机器学习术语里,该数据集被称为训练数据集(training data set)或训练集(training set),一个图片被称为一个样本(sample),其实在类别叫作标签(label),用来预测标签的因素叫作特色(feature)。特色用来表征样本的特点。
损失函数:在模型训练中,咱们须要掂量预测类别与实在类别之间的误差。通常咱们会选取一个非正数作为误差,且数值越小示意误差越小。
优化算法:当模型和损失函数模式较为简单时,最优解能够间接用公式表达出来。这类解叫作解析解(analytical solution)。然而,大多数深度学习模型并没有解析解,只能通过优化算法无限次迭代模型参数来尽可能升高损失函数的值。这类解叫作数值解(numerical solution)。
模型预测:模型训练实现后,咱们将模型参数在优化算法进行时的值别离记录。留神,这里咱们失去的并不一定是最小化损失函数的最优解,而是对最优解的一个近似。而后,咱们就能够应用学出的图片分类模型来估算训练数据集以外任意一张图片所属的类别了。这里的估算也叫作模型预测、模型推断或模型测试。
- MindSpore 代码实现
咱们将在上面代码是应用 MindSpore 深度学习框架实现的,上面逐渐剖析咱们我的项目中所应用的数据、模型、损失函数、优化算法、模型验证。
3.1 数据集筹备
MNIST 数据集 (Mixed National Institute of Standards and Technology database) 是大型手写数字数据库, 蕴含 60000 个示例的训练集以及 10000 个示例的测试集,每个样本图像的宽高为 28*28 的灰度图。
上面提供了两种数据集的应用形式:
(1)数据集曾经在同级文件夹目录下时,可执行下段代码解压应用。
def unzipfile(gzip_path):
"""unzip dataset file
Args:
gzip_path: dataset file path
"""open_file = open(gzip_path.replace('.gz',''), 'wb')
gz_file = gzip.GzipFile(gzip_path)
open_file.write(gz_file.read())
gz_file.close()
(2)文件夹中还没有数据集的时候,须要下载应用。
def download_dataset():
"""Download the dataset from http://yann.lecun.com/exdb/mnist/."""
print("******Downloading the MNIST dataset******")
train_path = "./MNIST_Data/train/"
test_path = "./MNIST_Data/test/"
train_path_check = os.path.exists(train_path)
test_path_check = os.path.exists(test_path)
if train_path_check == False and test_path_check ==False:
os.makedirs(train_path)
os.makedirs(test_path)
train_url = {"http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz", "http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz"}
test_url = {"http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz", "http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz"}
for url in train_url:
url_parse = urlparse(url)
# split the file name from url
file_name = os.path.join(train_path,url_parse.path.split('/')[-1])
if not os.path.exists(file_name.replace('.gz','')):
file = urllib.request.urlretrieve(url, file_name)
unzipfile(file_name)
os.remove(file_name)
for url in test_url:
url_parse = urlparse(url)
# split the file name from url
file_name = os.path.join(test_path,url_parse.path.split('/')[-1])
if not os.path.exists(file_name.replace('.gz','')):
file = urllib.request.urlretrieve(url, file_name)
unzipfile(file_name)
os.remove(file_name)
3.2 数据加强
次要是对数据进行归一化和丰盛数据样本数量。常见的数据加强形式包含裁剪、翻转、偏移变动等等。MindSpore 通过调用 map 办法在图片上执行加强操作。
import mindspore.dataset as ds
import mindspore.dataset.transforms.c_transforms as C
import mindspore.dataset.transforms.vision.c_transforms as CV
from mindspore.dataset.transforms.vision
import Interfrom mindspore.common
import dtype as mstype
def create_dataset(data_path, batch_size=32, repeat_size=1,
num_parallel_workers=1):
""" create dataset for train or test
Args: data_path: Data path
batch_size: The number of data records in each group
repeat_size: The number of replicated data records
num_parallel_workers: The number of parallel workers
"""
# define dataset
mnist_ds = ds.MnistDataset(data_path)
# define operation parameters
resize_height, resize_width = 32, 32
rescale = 1.0 / 255.0
shift = 0.0
rescale_nml = 1 / 0.3081
shift_nml = -1 * 0.1307 / 0.3081
# define map operations
resize_op = CV.Resize((resize_height, resize_width), interpolation=Inter.LINEAR) # resize images to (32, 32)
rescale_nml_op = CV.Rescale(rescale_nml, shift_nml) # normalize images
rescale_op = CV.Rescale(rescale, shift) # rescale images
hwc2chw_op = CV.HWC2CHW() # change shape from (height, width, channel) to (channel, height, width) to fit network.
type_cast_op = C.TypeCast(mstype.int32) # change data type of label to int32 to fit network
# apply map operations on images
mnist_ds = mnist_ds.map(input_columns="label", operations=type_cast_op, num_parallel_workers=num_parallel_workers)
mnist_ds = mnist_ds.map(input_columns="image", operations=resize_op, num_parallel_workers=num_parallel_workers)
mnist_ds = mnist_ds.map(input_columns="image", operations=rescale_op, num_parallel_workers=num_parallel_workers)
mnist_ds = mnist_ds.map(input_columns="image", operations=rescale_nml_op, num_parallel_workers=num_parallel_workers)
mnist_ds = mnist_ds.map(input_columns="image", operations=hwc2chw_op, num_parallel_workers=num_parallel_workers)
# apply DatasetOps
buffer_size = 10000
mnist_ds = mnist_ds.shuffle(buffer_size=buffer_size) # 10000 as in LeNet train script
mnist_ds = mnist_ds.batch(batch_size, drop_remainder=True)
mnist_ds = mnist_ds.repeat(repeat_size)
return mnist_ds
3.3 模型构建
依据 MNIST 数据集的性质,这里咱们应用图像分类工作的规范算法卷积神经网络 -LeNet。卷积神经网络采纳分层的构造对图片进行特征提取,由一系列的网络层重叠而成,比方卷积层、池化层、激活层、全连贯层、输入层,LeNet 模型的具体构造可参考上篇 MindSpore 图片分类之 LeNet 网络池化和全连贯。
import mindspore.nn as nn
from mindspore.common.initializer import Normalclass LeNet5(nn.Cell):
"""Lenet network structure"""
#define the operator required
def __init__(self, num_class=10, num_channel=1):
super(LeNet5, self).__init__()
self.conv1 = nn.Conv2d(num_channel, 6, 5, pad_mode='valid')
self.conv2 = nn.Conv2d(6, 16, 5, pad_mode='valid')
self.fc1 = nn.Dense(16 * 5 * 5, 120, weight_init=Normal(0.02))
self.fc2 = nn.Dense(120, 84, weight_init=Normal(0.02))
self.fc3 = nn.Dense(84, num_class, weight_init=Normal(0.02))
self.relu = nn.ReLU()
self.max_pool2d = nn.MaxPool2d(kernel_size=2, stride=2)
self.flatten = nn.Flatten()
#use the preceding operators to construct networks
def construct(self, x):
x = self.max_pool2d(self.relu(self.conv1(x)))
x = self.max_pool2d(self.relu(self.conv2(x)))
x = self.flatten(x)
x = self.relu(self.fc1(x))
x = self.relu(self.fc2(x))
x = self.fc3(x)
return x
3.4 定义损失函数
模型在我的项目中的职责是拟合数据的规定个性,拟合的水平咱们引入损失函数示意,接下来须要定义损失函数(Loss)。损失函数是深度学习的训练指标,也叫指标函数,能够了解为神经网络的输入(Logits)和标签 (Labels) 之间的间隔,是一个标量数据。
本次模型输入是⼀个图像类别这样的离散值。对于这样的离散值预测问题,咱们能够使⽤诸如 softmax 回归在内的分类模型。和线性回归不同,softmax 回归的输入单元从⼀个变成了多个,且引⼊了 softmax 运算 使输入更适宜离散值的预测和训练。
image.png
图 1:softmax 全连贯运算图
o1 = x1w11 + x2w21 + x3w31 + x4w41 + b1,
o2 = x1w12 + x2w22 + x3w32 + x4w42 + b2,
o3 = x1w13 + x2w23 + x3w33 + x4w43 + b3.
既然分类问题须要失去离散的预测输入,⼀个简略的方法是将输入值 oi 当作预测类别是 i 的相信 度,并将值最⼤的输入所对应的类作为预测输入,即输入 argmaxioi。然而,间接使⽤输入层的输入有两个问题。⼀⽅⾯,因为输入层的输入值的范畴不确定,咱们难以直观上判断这些值的意义。另⼀⽅⾯,因为实在标签是离散值,这些离散值与不确定范畴的输入值之间的误差难以掂量。
softmax 运算符(softmax operator)解决了以上两个问题。它通过下式将输入值变换成值为正且 和为 1 的概率分布:
image.png
图 2:softmax 运算符计算图
容易看出 yˆ1 + ˆy2 + ˆy3 = 1 且 0 ≤ yˆ1, yˆ2, yˆ3 ≤ 1,因而 yˆ1, yˆ2, yˆ3 是⼀个非法的概率分布。这时候,如 果 yˆ2 = 0.8,不论 yˆ1 和 yˆ3 的值是多少,softmax 运算不扭转预测类别输入。
使⽤ softmax 运算后能够更⽅便地与离散标签计算误差。咱们曾经晓得,softmax 运算 将输入变换成⼀个非法的类别预测散布。
实际上,实在标签也能够⽤类别散布表白:对于样本 i,咱们结构向量 y (i) ∈ R q,使其第 y (i)(样本 i 类别的离散数值)个元素为 1,其余为 0。这样咱们的 训练⽬标能够设为使预测概率分布 yˆ(i)尽可能靠近实在的标签概率分布 y(i)。
咱们能够像线性回归那样使⽤平⽅损失函数∥yˆ(i) − y (i)∥ 2/2。然而,想要预测分类后果正确,咱们其实并不需要预测概率齐全等于标签概率。咱们只须要其中的一个预测值足够大,就足够咱们分类应用。例如咱们预测一个数字图片类别为“1”的预测值为 0.6,预测为“7”和“9”的值为 0.2,或者预测为“7”和的值为 0.35,预测为“9”的值为 0.05,两种状况下分类都是正确的,然而计算的损失值是不同的。改善上述问题的⼀个⽅法是使⽤更适宜掂量两个概率分布差别的测量函数。其中,穿插熵(cross entropy)是⼀个常⽤的掂量⽅法。图像分类利用通常采纳穿插熵损失(CrossEntropy)。
image.png
图 3:穿插熵损失函数表达式
其中带下标的 y(i)j 是向量 y(i)中⾮ 0 即 1 的元素,须要留神将它与样本 i 类别的离散数值,即不带下标的 y(i)辨别。在上式中,咱们晓得向量 y (i)中只有第 y(i)个元素 y(i) y(i)为 1,其余全为 0,于是 H(y (i) , yˆ (i) ) = − log yˆ(i) y(i)。也就是说,穿插熵只关⼼对正确类别的预测概率,因为只有其值⾜ 够⼤,就能够确保分类后果正确。当然,遇到⼀个样本有多个标签时,例如图像⾥含有不⽌⼀个物体时,咱们并不能做这⼀步简化。但即使对于这种状况,穿插熵同样只关⼼对图像中呈现的物体类别的预测概率。假如训练数据集的样本数为 n,穿插熵损失函数定义为
image.png
图 4:简化穿插熵损失表达式
从另⼀个⻆度来看,最小化穿插熵损失函数等价于最⼤化训练数据集所有标签类别的联结预测概率。
from mindspore.nn.loss import SoftmaxCrossEntropyWithLogits
if name == “__main__”:
...
#define the loss function
net_loss = SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')
...
3.5 定义优化器
损失函数的定义不便了样本的输入与标签之间误差的计算,也就是损失值,但仅仅是计算出了损失值是不够的,到这里模型还是一个“死”的,咱们须要通过减小损失值的模式优化模型参数,优化器用于神经网络求解(训练)。因为神经网络参数规模宏大,无奈间接求解,因此深度学习中采纳随机梯度降落算法(SGD)及其改良算法进行求解。MindSpore 封装了常见的优化器,如 SGD、ADAM、Momemtum 等等。本例采纳 Momentum 优化器,通常须要设定两个参数,动量(moment)和权重衰减项(weight decay)。
本次体验我的项目中应用的是动量法。指标函数无关自变量的梯度代表了指标函数在自变量以后地位降落最快方向。因而,梯度降落也叫最陡降落。每次迭代中,梯度降落依据自变量以后地位,沿着以后地位的梯度更新自变量。然而,如果自变量的迭代方向仅仅取决于自变量以后地位,这可能会带来一些问题。
例如咱们应用一个输出和输入别离为二维向量 x =[x1,x2]⊤和标量的⽬标函数。
image.png
图 5:二维指标函数
基于这个指标函数的梯度降落,演示应用学习率为 0.4 时自变量的迭代轨迹。
image.png
图 6:指标函数自变量迭代轨迹
能够看到,指标函数在竖直方向比在程度方向的斜率的绝对值更大。因而给定学习率,梯度降落迭代自变量时会使自变量在竖直方向比在程度方向挪动幅度更大。那么,咱们须要一个较小的学习率从而防止自变量在竖直方向上越过指标函数最优解。然而这样也会造成自变量在程度方向上朝最优解挪动变慢。
动量法的提出就是解决梯度降落中的上述问题的。设置工夫步 t 的自变量为 Xt,随机梯度为 gt,学习率为 ηt。动量法创立速度变量 v0,并将其元素初始化成 0。在 工夫步 t > 0,动量法对每次迭代的步骤做如下批改:
vt ← γvt−1 + ηtgt
xt ← xt−1 − vt
其中,动量超参数 γ 满⾜ 0≤γ<1。当 γ = 0 时,动量法等价于梯度降落。
if name == “__main__”:
...
#learning rate setting
lr = 0.01
momentum = 0.9
#create the network
network = LeNet5()
#define the optimizer
net_opt = nn.Momentum(network.trainable_params(), lr, momentum)
...
3.6 训练网络
配置模型保留:在上述几步机制设置实现后,便能够开始训练网络了。MindSpore 提供了 callback 机制,能够在训练过程中执行自定义逻辑,这里应用框架提供的 ModelCheckpoint 为例。ModelCheckpoint 能够保留网络模型和参数,以便进行后续的 fine-tuning(微调)操作。
from mindspore.train.callback import ModelCheckpoint, CheckpointConfig
if name == “__main__”:
...
# set parameters of check point
config_ck = CheckpointConfig(save_checkpoint_steps=1875, keep_checkpoint_max=10)
# apply parameters of check point
ckpoint_cb = ModelCheckpoint(prefix="checkpoint_lenet", config=config_ck)
...
配置训练网络
通过 MindSpore 提供的 model.train 接口能够不便地进行网络的训练。LossMoniter 能够监控训练过程中 loss 值的变动。这里把 epoch_size 设置为 1,对数据集进行 1 个迭代的训练。
from mindspore.nn.metrics
import Accuracyfrom mindspore.train.callback
import LossMonitorfrom mindspore.train import Model
…def train_net(args, model, epoch_size, mnist_path, repeat_size, ckpoint_cb, sink_mode):
"""define the training method"""
print("============== Starting Training ==============")
#load training dataset
ds_train = create_dataset(os.path.join(mnist_path, "train"), 32, repeat_size)
model.train(epoch_size, ds_train, callbacks=[ckpoint_cb, LossMonitor()], dataset_sink_mode=sink_mode)
…
if __name__ == "__main__":
...
epoch_size = 1
mnist_path = "./MNIST_Data"
repeat_size = 1
model = Model(network, net_loss, net_opt, metrics={"Accuracy": Accuracy()})
train_net(args, model, epoch_size, mnist_path, repeat_size, ckpoint_cb, dataset_sink_mode)
...
image.png………………….image.png
图 7:模型训练 loss 值变动图
如上图中所示,咱们在配置实现模型、损失函数、优化器后,启动模型训练后的 loss 值变动状况。通过咱们设定次数的迭代后,此时的 loss 值曾经十分小,示意教训误差很小,但并不示意模型的泛化误差会很小,所以须要保留模型参数,测验模型的泛化误差,也叫验证模型。
3.7 验证模型
在失去模型文件后,通过模型运行测试数据集失去的后果,验证模型的泛化能力。应用 model.eval 接口读入测试数据集。应用保留后的模型参数进行推理。应用准确率检测模型的泛化性能。
from mindspore.train.serialization import load_checkpoint, load_param_into_net
…def test_net(args,network,model,mnist_path):
"""define the evaluation method"""
print("============== Starting Testing ==============")
#load the saved model for evaluation
param_dict = load_checkpoint("checkpoint_lenet-1_1875.ckpt")
#load parameter to the network
load_param_into_net(network, param_dict)
#load testing dataset
ds_eval = create_dataset(os.path.join(mnist_path, "test"))
acc = model.eval(ds_eval, dataset_sink_mode=False)
print("============== Accuracy:{} ==============".format(acc))
if name == “__main__”:
...
test_net(args, network, model, mnist_path)
image.png
图 8:模型预测准确率
模型验证时,首先将训练模型保留的参数文件传入,预测数据集喂给模型预测应用。本次训练失去的准确率为 0.963…,示意泛化误差很小。证实模型并未有过拟合景象,模型的鲁棒性能很好。但每次训练失去的模型并不是完全一致的,所以验证模型时失去的准确率也不肯定雷同。超参数的的调整是否有成果,模型验证的后果是很直观的示意。
- 总结
本次分享内容首先演绎通常机器学习我的项目的组成部分,由数据集、模型、损失函数、优化器、模型预测这几局部组成。并且在每一个局部中,又有很多不同的办法。例如卷积模型有 LeNet 和 ResNet 等等。咱们的须要做的是从剖析数据集个性开始,而后在每一个局部中抉择适合的办法构建出一个残缺的我的项目,应用数据集训练、预测,最终保留合乎咱们要求的模型。
以上是集体的一些总结,有有余和谬误之处,还请多多留言领导。