Deep Learning-深度学习(四)
深度学习入门
1、数据处理优化
1.1 前提条件
即多方的paddlepaddle库的导入,加载飞桨平台和数据处理库。
1 #数据处理部分之前的代码,加入部分数据处理的库 2 import paddle 3 from paddle.nn import Linear 4 import paddle.nn.functional as F 5 import os 6 import gzip 7 import json 8 import random 9 import numpy as np
1.2 读入数据并划分数据集
首先,我们要明白整个MNIST数据集的存储结构,如下图:
其次,了解各个组成成分的意义,data包含三种元素,即①train-set是训练集,用于确定模型参数,是包含两个元素的列表,train-images是[50000,784]的二维列表,包含50000张可以用长度为784的向量表示的图片,train-labels为[50000,]的列表,表示这些图片对应的分类标签,即所对应的0-9的值。②val-set,为验证集,用于调节模型超参数。③test-set,为测试集,用来估算模型的实际应用效果的列表。这里要注意的是他们都是列表。
将文件名称为mnist.json.gz
的MNIST数据,并拆分成训练集、验证集和测试集,实现方法为:
1 # 声明数据集文件位置 2 datafile = './work/mnist.json.gz' 3 print('loading mnist dataset from {} ......'.format(datafile)) 4 # 加载json数据文件 5 data = json.load(gzip.open(datafile)) 6 print('mnist dataset load done') 7 # 读取到的数据区分训练集,验证集,测试集 8 train_set, val_set, eval_set = data 9 10 # 观察训练集数据 11 imgs, labels = train_set[0], train_set[1] 12 print("训练数据集数量: ", len(imgs)) 13 14 # 观察验证集数量 15 imgs, labels = val_set[0], val_set[1] 16 print("验证数据集数量: ", len(imgs)) 17 18 # 观察测试集数量 19 imgs, labels = val= eval_set[0], eval_set[1] 20 print("测试数据集数量: ", len(imgs))
结果为:
1.3 训练样本
使得样本乱序,即先将样本按顺序进行编号,建立ID集合index_list。然后将index_list乱序,最后按乱序后的顺序读取数据;生成批次数据,即先设置合理的batch_size,再将数据转变成符合模型输入要求的np.array格式返回。同时,在返回数据时将Python生成器设置为yield
模式,以减少内存占用。这样做的原因在于模型会对最后面的数据较为敏感,因此要进行乱序操作。
执行操作前,需要先将数据处理代码封装成load_data函数,方便后续调用。load_data有三种模型:train
、valid
、eval
,分为对应返回的数据是训练集、验证集、测试集。
1 imgs, labels = train_set[0], train_set[1] 2 print("训练数据集数量: ", len(imgs)) 3 # 获得数据集长度 4 imgs_length = len(imgs) 5 # 定义数据集每个数据的序号,根据序号读取数据 6 index_list = list(range(imgs_length)) 7 # 读入数据时用到的批次大小 8 BATCHSIZE = 100 9 10 # 随机打乱训练数据的索引序号 11 random.shuffle(index_list) 12 13 # 定义数据生成器,返回批次数据 14 def data_generator(): 15 imgs_list = [] 16 labels_list = [] 17 for i in index_list: 18 # 将数据处理成希望的类型 19 img = np.array(imgs[i]).astype('float32') 20 label = np.array(labels[i]).astype('float32') 21 imgs_list.append(img) 22 labels_list.append(label) 23 if len(imgs_list) == BATCHSIZE: 24 # 获得一个batchsize的数据,并返回 25 yield np.array(imgs_list), np.array(labels_list) 26 # 清空数据读取列表 27 imgs_list = [] 28 labels_list = [] 29 30 # 如果剩余数据的数目小于BATCHSIZE, 31 # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch 32 if len(imgs_list) > 0: 33 yield np.array(imgs_list), np.array(labels_list) 34 return data_generator
结果为:
这里就是实现50000条数据的乱序,以及100为batche-size进行分组,方便之后进行SGD运算。要注意的是yield关键字,带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方执行的,所以调用next的时候,生成器并不会从foo函数的开始执行,只是接着上一步停止的地方开始,然后遇到yield后,return出要生成的数,此步就结束。
1 # 声明数据读取函数,从训练集中读取数据 2 train_loader = data_generator 3 # 以迭代的形式读取数据 4 for batch_id, data in enumerate(train_loader()): 5 image_data, label_data = data 6 if batch_id == 0: 7 # 打印数据shape和类型 8 print("打印第一个batch数据的维度:") 9 print("图像维度: {}, 标签维度: {}".format(image_data.shape, label_data.shape)) 10 break
结果为:
1.4 校验数据有效性
在实际当中,原始数据可能出现标注、格式、数据杂乱等情况,所以需要对数据及逆行校验,有两种方式,①机器校验,即利用机器加入相关的操作。②人工校验,即先打印数据输出结果,观察是否是设置的格式。再从训练的结果验证数据处理和读取的有效性。
机器校验:如果数据集中的图片数量和标签数量不等,说明数据逻辑存在问题,可使用assert语句校验图像数量和标签数据是否一致。
1 imgs_length = len(imgs) 2 3 assert len(imgs) == len(labels), \ 4 "length of train_imgs({}) should be the same as train_labels({})"\ 5 .format(len(imgs), len(label))
人工校验:打印数据输出结果,观察是否是预期的格式。实现数据处理和加载函数后,我们可以调用它读取一次数据,观察数据的shape和类型是否与函数中设置的一致。
1 # 声明数据读取函数,从训练集中读取数据 2 train_loader = data_generator 3 # 以迭代的形式读取数据 4 for batch_id, data in enumerate(train_loader()): 5 image_data, label_data = data 6 if batch_id == 0: 7 # 打印数据shape和类型 8 print("打印第一个batch数据的维度,以及数据的类型:") 9 print("图像维度: {}, 标签维度: {}, 图像数据类型: {}, 标签数据类型: {}".format(image_data.shape, label_data.shape, type(image_data), type(label_data))) 10 break
结果为:
1.5 封装数据
即将上诉流程封装在一个函数当中,方便在之后的神经网络中进行使用,得到的函数为:
1 #数据处理部分之前的代码,加入部分数据处理的库 2 import paddle 3 from paddle.nn import Linear 4 import paddle.nn.functional as F 5 import os 6 import gzip 7 import json 8 import random 9 import numpy as np 10 11 def load_data(mode='train'): 12 datafile = './work/mnist.json.gz' 13 print('loading mnist dataset from {} ......'.format(datafile)) 14 # 加载json数据文件 15 data = json.load(gzip.open(datafile)) 16 print('mnist dataset load done') 17 18 # 读取到的数据区分训练集,验证集,测试集 19 train_set, val_set, eval_set = data 20 if mode=='train': 21 # 获得训练数据集 22 imgs, labels = train_set[0], train_set[1] 23 elif mode=='valid': 24 # 获得验证数据集 25 imgs, labels = val_set[0], val_set[1] 26 elif mode=='eval': 27 # 获得测试数据集 28 imgs, labels = eval_set[0], eval_set[1] 29 else: 30 raise Exception("mode can only be one of ['train', 'valid', 'eval']") 31 print("训练数据集数量: ", len(imgs)) 32 33 # 校验数据 34 imgs_length = len(imgs) 35 36 assert len(imgs) == len(labels), \ 37 "length of train_imgs({}) should be the same as train_labels({})".format(len(imgs), len(labels)) 38 39 # 获得数据集长度 40 imgs_length = len(imgs) 41 42 # 定义数据集每个数据的序号,根据序号读取数据 43 index_list = list(range(imgs_length)) 44 # 读入数据时用到的批次大小 45 BATCHSIZE = 100 46 47 # 定义数据生成器 48 def data_generator(): 49 if mode == 'train': 50 # 训练模式下打乱数据 51 random.shuffle(index_list) 52 imgs_list = [] 53 labels_list = [] 54 for i in index_list: 55 # 将数据处理成希望的类型 56 img = np.array(imgs[i]).astype('float32') 57 label = np.array(labels[i]).astype('float32') 58 imgs_list.append(img) 59 labels_list.append(label) 60 if len(imgs_list) == BATCHSIZE: 61 # 获得一个batchsize的数据,并返回 62 yield np.array(imgs_list), np.array(labels_list) 63 # 清空数据读取列表 64 imgs_list = [] 65 labels_list = [] 66 67 # 如果剩余数据的数目小于BATCHSIZE, 68 # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch 69 if len(imgs_list) > 0: 70 yield np.array(imgs_list), np.array(labels_list) 71 return data_generator
结果为:
1.6 一层神经网络
定义一层神经网络,利用定义好的数据处理函数,完成神经网络的训练。即所有过程,数据处理,前向计算、损失计算、SGD等。
1 class MNIST(paddle.nn.Layer): 2 def __init__(self): 3 super(MNIST, self).__init__() 4 # 定义一层全连接层,输出维度是1 5 self.fc = paddle.nn.Linear(in_features=784, out_features=1) 6 7 def forward(self, inputs): 8 outputs = self.fc(inputs) 9 return outputs 10 11 # 训练配置,并启动训练过程 12 def train(model): 13 model = MNIST() 14 model.train() 15 #调用加载数据的函数 16 train_loader = load_data('train') 17 opt = paddle.optimizer.SGD(learning_rate=0.001, parameters=model.parameters()) 18 EPOCH_NUM = 10 19 for epoch_id in range(EPOCH_NUM): 20 for batch_id, data in enumerate(train_loader()): 21 #准备数据,变得更加简洁 22 images, labels = data 23 images = paddle.to_tensor(images) 24 labels = paddle.to_tensor(labels) 25 26 #前向计算的过程 27 predits = model(images) 28 29 #计算损失,取一个批次样本损失的平均值 30 loss = F.square_error_cost(predits, labels) 31 avg_loss = paddle.mean(loss) 32 33 #每训练了200批次的数据,打印下当前Loss的情况 34 if batch_id % 200 == 0: 35 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 36 37 #后向传播,更新参数的过程 38 avg_loss.backward() 39 opt.step() 40 opt.clear_grad() 41 42 # 保存模型 43 paddle.save(model.state_dict(), './mnist.pdparams') 44 # 创建模型 45 model = MNIST() 46 # 启动训练过程 47 train(model)
结果为:
2、异步数据读取
即适当利用一部分内存换取数据读取效率的提升。形式为:
-
同步数据读取:数据读取与模型训练串行。当模型需要数据时,才运行数据读取函数获得当前批次的数据。在读取数据期间,模型一直等待数据读取结束才进行训练,数据读取速度相对较慢。
-
异步数据读取:数据读取和模型训练并行。读取到的数据不断的放入缓存区,无需等待模型训练就可以启动下一轮数据读取。当模型训练完一个批次后,不用等待数据读取过程,直接从缓存区获得下一批次数据进行训练,从而加快了数据读取速度。
-
异步队列:数据读取和模型训练交互的仓库,二者均可以从仓库中读取数据,它的存在使得两者的工作节奏可以解耦。
利用飞桨平台实现异步数据读取,一共两步:①构建一个继承paddle.io.Dataset类的数据读取器。②通过paddle.io.DataLoader创建异步数据读取的迭代器。
1 import numpy as np 2 from paddle.io import Dataset 3 # 构建一个类,继承paddle.io.Dataset,创建数据读取器 4 class RandomDataset(Dataset): 5 def __init__(self, num_samples): 6 # 样本数量 7 self.num_samples = num_samples 8 9 def __getitem__(self, idx): 10 # 随机产生数据和label 11 image = np.random.random([784]).astype('float32') 12 label = np.random.randint(0, 9, (1, )).astype('float32') 13 return image, label 14 15 def __len__(self): 16 # 返回样本总数量 17 return self.num_samples 18 19 # 测试数据读取器 20 dataset = RandomDataset(10) 21 for i in range(len(dataset)): 22 print(dataset[i])
结果为:
在定义完paddle.io.Dataset后,使用paddle.io.DataLoader API即可实现异步数据读取,数据会由Python线程预先读取,并异步送入一个队列中。即:
class paddle.io.DataLoader(dataset, batch_size=100, shuffle=True, num_workers=2)
DataLoader支持单进程和多进程的数据加载方式。当 num_workers=0时,使用单进程方式异步加载数据;当 num_workers=n(n>0)时,主进程将会开启n个子进程异步加载数据。 DataLoader返回一个迭代器,迭代的返回dataset中的数据内容;dataset是支持 map-style 的数据集。
以MNIST数据为例,生成对应的Dataset和DataLoader:
1 import paddle 2 import json 3 import gzip 4 import numpy as np 5 6 # 创建一个类MnistDataset,继承paddle.io.Dataset 这个类 7 # MnistDataset的作用和上面load_data()函数的作用相同,均是构建一个迭代器 8 class MnistDataset(paddle.io.Dataset): 9 def __init__(self, mode): 10 datafile = './work/mnist.json.gz' 11 data = json.load(gzip.open(datafile)) 12 # 读取到的数据区分训练集,验证集,测试集 13 train_set, val_set, eval_set = data 14 if mode=='train': 15 # 获得训练数据集 16 imgs, labels = train_set[0], train_set[1] 17 elif mode=='valid': 18 # 获得验证数据集 19 imgs, labels = val_set[0], val_set[1] 20 elif mode=='eval': 21 # 获得测试数据集 22 imgs, labels = eval_set[0], eval_set[1] 23 else: 24 raise Exception("mode can only be one of ['train', 'valid', 'eval']") 25 26 # 校验数据 27 imgs_length = len(imgs) 28 assert len(imgs) == len(labels), \ 29 "length of train_imgs({}) should be the same as train_labels({})".format(len(imgs), len(labels)) 30 31 self.imgs = imgs 32 self.labels = labels 33 34 def __getitem__(self, idx): 35 img = np.array(self.imgs[idx]).astype('float32') 36 label = np.array(self.labels[idx]).astype('float32') 37 38 return img, label 39 40 def __len__(self): 41 return len(self.imgs) 42 43 44 # 声明数据加载函数,使用训练模式,MnistDataset构建的迭代器每次迭代只返回batch=1的数据 45 train_dataset = MnistDataset(mode='train') 46 # 使用paddle.io.DataLoader 定义DataLoader对象用于加载Python生成器产生的数据, 47 # DataLoader 返回的是一个批次数据迭代器,并且是异步的; 48 data_loader = paddle.io.DataLoader(train_dataset, batch_size=100, shuffle=True) 49 # 迭代的读取数据并打印数据的形状 50 for i, data in enumerate(data_loader()): 51 images, labels = data 52 print(i, images.shape, labels.shape) 53 if i>=2: 54 break
结果为:
异步数据读取并训练的完整案例:
1 def train(model): 2 model = MNIST() 3 model.train() 4 opt = paddle.optimizer.SGD(learning_rate=0.001, parameters=model.parameters()) 5 EPOCH_NUM = 3 6 for epoch_id in range(EPOCH_NUM): 7 for batch_id, data in enumerate(data_loader()): 8 images, labels = data 9 images = paddle.to_tensor(images) 10 labels = paddle.to_tensor(labels).astype('float32') 11 12 #前向计算的过程 13 predicts = model(images) 14 15 #计算损失,取一个批次样本损失的平均值 16 loss = F.square_error_cost(predicts, labels) 17 avg_loss = paddle.mean(loss) 18 19 #每训练了200批次的数据,打印下当前Loss的情况 20 if batch_id % 200 == 0: 21 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 22 23 #后向传播,更新参数的过程 24 avg_loss.backward() 25 opt.step() 26 opt.clear_grad() 27 28 #保存模型参数 29 paddle.save(model.state_dict(), 'mnist') 30 31 #创建模型 32 model = MNIST() 33 #启动训练过程 34 train(model)
结果为:
可以发现与同步读取数据时的结果是一样的,但是这样能够在数据规模很大的时候带来性能的显著提升。
3、网络结构
3.1 前提条件
在之前的模型建立过程当中,无论是牛顿第二运动定律还是房价预测模型,都是可以用线性关系进行模拟出来的,然而对于手写数字的识别,很明显是无法满足其中的要求的,因此我们需要引入非线性的关系来完善其中的模型过程。
3.2 全连接神经网络
经典的全连接神经网络来包含四层网络:输入层、两个隐含层和输出层,将手写数字识别任务通过全连接神经网络表示,如图所示。
-
输入层:将数据输入给神经网络。在该任务中,输入层的尺度为28×28的像素值。
-
隐含层:增加网络深度和复杂度,隐含层的节点数是可以调整的,节点数越多,神经网络表示能力越强,参数量也会增加。在该任务中,中间的两个隐含层为10×10的结构,通常隐含层会比输入层的尺寸小,以便对关键信息做抽象,激活函数使用常见的Sigmoid函数。
-
输出层:输出网络计算结果,输出层的节点数是固定的。如果是回归问题,节点数量为需要回归的数字数量。如果是分类问题,则是分类标签的数量。在该任务中,模型的输出是回归一个数字,输出层的尺寸为1。
手写数字识别网络设计:
-
输入层的尺度为28×28,但批次计算的时候会统一加1个维度(大小为batch size)。
-
中间的两个隐含层为10×10的结构,激活函数使用常见的Sigmoid函数。
-
与房价预测模型一样,模型的输出是回归一个数字,输出层的尺寸设置成1。
全连接神经网络实现(其余各个部分仍旧和以前的地方相同):
1 # 定义多层全连接神经网络 2 class MNIST(paddle.nn.Layer): 3 def __init__(self): 4 super(MNIST, self).__init__() 5 # 定义两层全连接隐含层,输出维度是10,当前设定隐含节点数为10,可根据任务调整 6 self.fc1 = Linear(in_features=784, out_features=10) 7 self.fc2 = Linear(in_features=10, out_features=10) 8 # 定义一层全连接输出层,输出维度是1 9 self.fc3 = Linear(in_features=10, out_features=1) 10 11 # 定义网络的前向计算,隐含层激活函数为sigmoid,输出层不使用激活函数 12 def forward(self, inputs): 13 # inputs = paddle.reshape(inputs, [inputs.shape[0], 784]) 14 outputs1 = self.fc1(inputs) 15 outputs1 = F.sigmoid(outputs1) 16 outputs2 = self.fc2(outputs1) 17 outputs2 = F.sigmoid(outputs2) 18 outputs_final = self.fc3(outputs2) 19 return outputs_final
3.3 卷积神经网络的简单运用
为了不影响图像识别方面的数据的空间性的缺失,卷积神经网络针对视觉问题的特点进行了网络结构优化,可以直接处理原始形式的图像数据,保留像素间的空间信息,更适合处理视觉问题。这里只是为了能够对卷积神经网络有一个感性的认识,具体的实现将在之后的学习中进行深度学习。
过程为:从原始信号
—>发现边缘和方向
—>不断抽象
—>不断抽象
问题定义:图像分类,使用LeNet-5网络完成手写数字识别图片的分类
1 addle 2 import numpy as np 3 import matplotlib.pyplot as plt
数据加载和预处理:一个归一化处理,把数据归一到(-1,1)的区间
1 import paddle.vision.transforms as T 2 3 transform = T.Normalize(mean=[127.5], std=[127.5]) 4 5 # 训练数据集 6 train_dataset = paddle.vision.datasets.MNIST(mode='train', transform=transform) 7 8 # 验证数据集 9 eval_dataset = paddle.vision.datasets.MNIST(mode='test', transform=transform) 10 11 print('训练样本量:{},测试样本量:{}'.format(len(train_dataset), len(eval_dataset)))
结果为:
数据查看:
1 print('图片:') 2 print(type(train_dataset[0][0])) 3 print(train_dataset[0][0].shape) 4 print('标签:') 5 print(type(train_dataset[0][1])) 6 print(train_dataset[0][1]) 7 8 # 可视化展示 9 plt.figure() 10 plt.imshow(train_dataset[0][0].reshape([28,28]), cmap=plt.cm.binary) 11 plt.show()
结果为:
模型选择:LeNet-5网络结构
单通道卷积神经网络:
其中有两个概念,即计算和步长:
计算:
步长:卷积核在你原图上每一步移动的距离
多通道卷积神经网络:
这里多通道对应的卷积核也是相应维度的,分别计算出对应维度的结果如何把对应位置的加起来就是最后输出的降维结果 1101 = 805 + 271 + 25
这里三通道的数据经过一个卷积核得到了一个单通道的表示。
多通道输出:
是否为多通道取决于有多少个卷积核。
其中有个填充的步骤,即padding,因为边缘的一些内容有可能就只被收集了一次特征,但是中心的确有多次,那么边上的信息数据就会丢失或者木有那么的清晰,这个是如果填充空白的数据那么就可以比较好的解决问题, 同时可以解决图像变小带来的某些时候不必要的麻烦。
池化层:池化层是特征选择和信息过滤的过程,过程中会损失一部分信息,但是会同时会减少参数和计算量,在模型效果和计算性能之间寻找平衡,随着运算速度的不断提高,慢慢可能会有一些设计上的变化,现在有些网络已经开始少用或者不用池化层。
平均池化(Avg Pooling) 对邻域内特征点求平均
-
优缺点:能很好的保留背景,但容易使得图片变模糊
-
正向传播:邻域内取平均
-
反向传播:特征值根据领域大小被平均,然后传给每个索引位置
最大池化(Max Pooling)
对邻域内特征点取最大
-
优缺点:能很好的保留一些关键的纹理特征,现在更多的再使用Max Pooling而很少用Avg Pooling
-
正向传播:取邻域内最大,并记住最大值的索引位置,以方便反向传播
-
反向传播:将特征值填充到正向传播中,值最大的索引位置,其他位置补0
网络结构代码实现(这里有两种方式进行网络结构的实现):
①用subclass方法:
1 class LeNet(nn.Layer): 2 """ 3 继承paddle.nn.Layer定义网络结构 4 """ 5 6 def __init__(self, num_classes=10): 7 """ 8 初始化函数 9 """ 10 super(LeNet, self).__init__() 11 12 self.features = nn.Sequential( 13 nn.Conv2D(in_channels=1, out_channels=6, kernel_size=3, stride=1, padding=1), # 第一层卷积 14 nn.ReLU(), # 激活函数 15 nn.MaxPool2D(kernel_size=2, stride=2), # 最大池化,下采样 16 nn.Conv2D(in_channels=6, out_channels=16, kernel_size=5, stride=1, padding=0), # 第二层卷积 17 nn.ReLU(), # 激活函数 18 nn.MaxPool2D(kernel_size=2, stride=2) # 最大池化,下采样 19 ) 20 21 self.fc = nn.Sequential( 22 nn.Linear(400, 120), # 全连接 23 nn.Linear(120, 84), # 全连接 24 nn.Linear(84, num_classes) # 输出层 25 ) 26 27 def forward(self, inputs): 28 """ 29 前向计算 30 """ 31 y = self.features(inputs) 32 y = paddle.flatten(y, 1) 33 out = self.fc(y) 34 35 return out 36 37 network_3 = LeNet()
②直接运用高层API中封装好的LeNet网络接口:
1 network_4 = paddle.vision.models.LeNet(num_classes=10)
可以利用命令:paddle.summary(network_4, (1, 1, 28, 28)) 来可视化模型。
模型训练和优化:
模型配置
-
优化器:随机梯度下滑(SGD)
-
损失函数:交叉熵(cross entropy)
-
评估指标:Accuracy
1 # 模型封装 2 model = paddle.Model(network_4) 3 4 # 模型配置 5 model.prepare(paddle.optimizer.Adam(learning_rate=0.001, parameters=model.parameters()), # 优化器 6 paddle.nn.CrossEntropyLoss(), # 损失函数 7 paddle.metric.Accuracy()) # 评估指标 8 9 # 启动全流程训练 10 model.fit(train_dataset, # 训练数据集 11 eval_dataset, # 评估数据集 12 epochs=5, # 训练轮次 13 batch_size=64, # 单次计算数据样本量 14 verbose=1) # 日志展示形式
模型评估:
1 result = model.evaluate(eval_dataset, verbose=1) 2 3 print(result)
模型预测(批量预测):
可使用model.predict接口来完成对大量数据集的批量预测:
1 # 进行预测操作 2 result = model.predict(eval_dataset) 3 4 # 定义画图方法 5 def show_img(img, predict): 6 plt.figure() 7 plt.title('predict: {}'.format(predict)) 8 plt.imshow(img.reshape([28, 28]), cmap=plt.cm.binary) 9 plt.show() 10 11 # 抽样展示 12 indexs = [2, 15, 38, 211] 13 14 for idx in indexs: 15 show_img(eval_dataset[idx][0], np.argmax(result[0][idx]))
结果为:
保存模型:
1 model.save('finetuning/mnist')
4、交叉熵的实现
定义:在模型输出为分类标签的概率时,直接以标签和概率做比较也不够合理,人们更习惯使用交叉熵误差作为分类问题的损失衡量。交叉熵损失函数的设计是基于最大似然思想:最大概率得到观察结果的假设是真的。
这里学到一个例子,如图:
问题是:有两个外形相同的盒子,甲盒中有99个白球,1个蓝球;乙盒中有99个蓝球,1个白球。一次试验取出了一个蓝球,请问这个球应该是从哪个盒子中取出的?
简单思考后均会得出更可能是从乙盒中取出的,因为从乙盒中取出一个蓝球的概率更高,然后科学家通过分析,使得某二分类模型“生成”n个训练样本的概率(概率论中的二项分布):
等价于最小化交叉熵,得到交叉熵的损失函数。交叉熵的公式如下:
以上为二分类,此外还有多分类,也就是对二分类的扩展。
代码实现:
-
在读取数据部分,将标签的类型设置成int,体现它是一个标签而不是实数值(飞桨框架默认将标签处理成int64)。
-
在网络定义部分,将输出层改成“输出十个标签的概率”的模式。
-
在训练过程部分,将损失函数从均方误差换成交叉熵。
在数据处理部分,需要修改标签变量Label的格式,代码如下所示。
- 从:label = np.reshape(labels[i], [1]).astype('float32')
- 到:label = np.reshape(labels[i], [1]).astype('int64')
数据处理:使用之前的代码
网络定义:需要修改输出层结构,代码如下所示。
- 从:self.fc = Linear(in_features=980, out_features=1)
- 到:self.fc = Linear(in_features=980, out_features=10)
1 # 定义 SimpleNet 网络结构 2 import paddle 3 from paddle.nn import Conv2D, MaxPool2D, Linear 4 import paddle.nn.functional as F 5 # 多层卷积神经网络实现 6 class MNIST(paddle.nn.Layer): 7 def __init__(self): 8 super(MNIST, self).__init__() 9 10 # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2 11 self.conv1 = Conv2D(in_channels=1, out_channels=20, kernel_size=5, stride=1, padding=2) 12 # 定义池化层,池化核的大小kernel_size为2,池化步长为2 13 self.max_pool1 = MaxPool2D(kernel_size=2, stride=2) 14 # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2 15 self.conv2 = Conv2D(in_channels=20, out_channels=20, kernel_size=5, stride=1, padding=2) 16 # 定义池化层,池化核的大小kernel_size为2,池化步长为2 17 self.max_pool2 = MaxPool2D(kernel_size=2, stride=2) 18 # 定义一层全连接层,输出维度是10 19 self.fc = Linear(in_features=980, out_features=10) 20 21 # 定义网络前向计算过程,卷积后紧接着使用池化层,最后使用全连接层计算最终输出 22 # 卷积层激活函数使用Relu,全连接层激活函数使用softmax 23 def forward(self, inputs): 24 x = self.conv1(inputs) 25 x = F.relu(x) 26 x = self.max_pool1(x) 27 x = self.conv2(x) 28 x = F.relu(x) 29 x = self.max_pool2(x) 30 x = paddle.reshape(x, [x.shape[0], 980]) 31 x = self.fc(x) 32 return x
损失函数:从均方误差(常用于回归问题)到交叉熵误差(常用于分类问题):
1 # 声明数据加载函数,使用训练模式,MnistDataset构建的迭代器每次迭代只返回batch=1的数据 2 train_dataset = MnistDataset(mode='train') 3 # 使用paddle.io.DataLoader 定义DataLoader对象用于加载Python生成器产生的数据, 4 # DataLoader 返回的是一个批次数据迭代器,并且是异步的; 5 data_loader = paddle.io.DataLoader(train_dataset, batch_size=100, shuffle=True)
-
从:loss = paddle.nn.functional.square_error_cost(predict, label)
-
到:loss = paddle.nn.functional.cross_entropy(predict, label)
1 def evaluation(model, datasets): 2 model.eval() 3 4 acc_set = list() 5 for batch_id, data in enumerate(datasets()): 6 images, labels = data 7 images = paddle.to_tensor(images) 8 labels = paddle.to_tensor(labels) 9 pred = model(images) # 获取预测值 10 acc = paddle.metric.accuracy(input=pred, label=labels) 11 acc_set.extend(acc.numpy()) 12 13 # #计算多个batch的准确率 14 acc_val_mean = np.array(acc_set).mean() 15 return acc_val_mean 16 #仅修改计算损失的函数,从均方误差(常用于回归问题)到交叉熵误差(常用于分类问题) 17 def train(model): 18 model.train() 19 #调用加载数据的函数 20 # train_loader = load_data('train') 21 # val_loader = load_data('valid') 22 opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters()) 23 EPOCH_NUM = 10 24 for epoch_id in range(EPOCH_NUM): 25 for batch_id, data in enumerate(data_loader()): 26 #准备数据 27 images, labels = data 28 images = paddle.to_tensor(images) 29 labels = paddle.to_tensor(labels) 30 #前向计算的过程 31 predicts = model(images) 32 33 #计算损失,使用交叉熵损失函数,取一个批次样本损失的平均值 34 loss = F.cross_entropy(predicts, labels) 35 avg_loss = paddle.mean(loss) 36 37 #每训练了200批次的数据,打印下当前Loss的情况 38 if batch_id % 200 == 0: 39 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 40 41 #后向传播,更新参数的过程 42 avg_loss.backward() 43 # 最小化loss,更新参数 44 opt.step() 45 # 清除梯度 46 opt.clear_grad() 47 # acc_train_mean = evaluation(model, train_loader) 48 # acc_val_mean = evaluation(model, val_loader) 49 # print('train_acc: {}, val acc: {}'.format(acc_train_mean, acc_val_mean)) 50 #保存模型参数 51 paddle.save(model.state_dict(), 'mnist.pdparams') 52 53 #创建模型 54 model = MNIST() 55 #启动训练过程 56 train(model)
结果为:
五、总结
通过这一周的学习,对神经网络有一个更加清晰的认识,能够完成从简单线性过程到非线性激活函数运用进行处理的转变,同时对整个建模过程更加清晰。但是对于卷积函数以及交叉熵的运用还并未完全理解,只是停留能够进行简单使用的过程。
六、参考资料
卷积神经简易实验:https://aistudio.baidu.com/aistudio/projectdetail/1514092?channelType=0&channel=0
交叉熵:https://zhuanlan.zhihu.com/p/35709485
MNIST数据集:https://aistudio.baidu.com/aistudio/datasetdetail/93735