神经网络所拟合的函数是高度非凸函数,理想的训练目标是,优化这类函数,达到函数最小值点或接近最小值的极小值点。选择合适的优化器和学习率是实现训练目标的关键因素。接下来主要探讨如何设置合理的优化器和学习率参数,保证训练结果达到理想目标。
注:设置模型的优化算法在启动训练前,所以加载数据和网络结构的代码均不变。
1 # 加载相关库 2 import os 3 import random 4 import paddle 5 import paddle.fluid as fluid 6 from paddle.fluid.dygraph.nn import Conv2D, Pool2D, FC 7 import numpy as np 8 from PIL import Image 9 10 import gzip 11 import json 12 13 # 定义数据集读取器 14 def load_data(mode='train'): 15 16 # 读取数据文件 17 datafile = './work/mnist.json.gz' 18 print('loading mnist dataset from {} ......'.format(datafile)) 19 data = json.load(gzip.open(datafile)) 20 # 读取数据集中的训练集,验证集和测试集 21 train_set, val_set, eval_set = data 22 23 # 数据集相关参数,图片高度IMG_ROWS, 图片宽度IMG_COLS 24 IMG_ROWS = 28 25 IMG_COLS = 28 26 # 根据输入mode参数决定使用训练集,验证集还是测试 27 if mode == 'train': 28 imgs = train_set[0] 29 labels = train_set[1] 30 elif mode == 'valid': 31 imgs = val_set[0] 32 labels = val_set[1] 33 elif mode == 'eval': 34 imgs = eval_set[0] 35 labels = eval_set[1] 36 # 获得所有图像的数量 37 imgs_length = len(imgs) 38 # 验证图像数量和标签数量是否一致 39 assert len(imgs) == len(labels), 40 "length of train_imgs({}) should be the same as train_labels({})".format( 41 len(imgs), len(labels)) 42 43 index_list = list(range(imgs_length)) 44 45 # 读入数据时用到的batchsize 46 BATCHSIZE = 100 47 48 # 定义数据生成器 49 def data_generator(): 50 # 训练模式下,打乱训练数据 51 if mode == 'train': 52 random.shuffle(index_list) 53 imgs_list = [] 54 labels_list = [] 55 # 按照索引读取数据 56 for i in index_list: 57 # 读取图像和标签,转换其尺寸和类型 58 img = np.reshape(imgs[i], [1, IMG_ROWS, IMG_COLS]).astype('float32') 59 label = np.reshape(labels[i], [1]).astype('int64') 60 imgs_list.append(img) 61 labels_list.append(label) 62 # 如果当前数据缓存达到了batch size,就返回一个批次数据 63 if len(imgs_list) == BATCHSIZE: 64 yield np.array(imgs_list), np.array(labels_list) 65 # 清空数据缓存列表 66 imgs_list = [] 67 labels_list = [] 68 69 # 如果剩余数据的数目小于BATCHSIZE, 70 # 则剩余数据一起构成一个大小为len(imgs_list)的mini-batch 71 if len(imgs_list) > 0: 72 yield np.array(imgs_list), np.array(labels_list) 73 74 return data_generator 75 76 77 # 定义模型结构 78 class MNIST(fluid.dygraph.Layer): 79 def __init__(self, name_scope): 80 super(MNIST, self).__init__(name_scope) 81 name_scope = self.full_name() 82 # 定义卷积层,输出通道20,卷积核大小为5,步长为1,padding为2,使用relu激活函数 83 self.conv1 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act='relu') 84 # 定义池化层,池化核为2,采用最大池化方式 85 self.pool1 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max') 86 # 定义卷积层,输出通道20,卷积核大小为5,步长为1,padding为2,使用relu激活函数 87 self.conv2 = Conv2D(name_scope, num_filters=20, filter_size=5, stride=1, padding=2, act='relu') 88 # 定义池化层,池化核为2,采用最大池化方式 89 self.pool2 = Pool2D(name_scope, pool_size=2, pool_stride=2, pool_type='max') 90 # 定义全连接层,输出节点数为10,激活函数使用softmax 91 self.fc = FC(name_scope, size=10, act='softmax') 92 93 # 定义网络的前向计算过程 94 def forward(self, inputs): 95 x = self.conv1(inputs) 96 x = self.pool1(x) 97 x = self.conv2(x) 98 x = self.pool2(x) 99 x = self.fc(x) 100 return x
最优的学习率需要多轮调试
在深度学习神经网络模型中,通常使用标准的随机梯度下降算法更新参数,学习率代表参数更新幅度的大小。当学习率最优时,模型的有效容量最大。学习率设置和当前深度学习任务有关,合适的学习率往往需要调参经验和大量的实验,总结来说,学习率选取需要注意以下两点:
- 学习率不是越小越好。学习率越小,损失函数的变化速度越慢,意味着我们需要花费更长的时间进行收敛。
- 学习率不是越大越好。因为只根据总样本集中的一个批次计算梯度,抽样误差会导致计算出的梯度不是全局最优的方向,且存在波动。同时,在接近最优解时,过大的学习率会导致参数在最优解附近震荡,导致损失难以收敛。
上图左边,学习率合理下降均匀;右边,学习率过大,导致在底部最小点附近震荡。
1 #仅优化算法的设置有所差别 2 with fluid.dygraph.guard(): 3 model = MNIST("mnist") 4 model.train() 5 #调用加载数据的函数 6 train_loader = load_data('train') 7 8 #设置不同初始学习率 9 optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.01) #018 10 #optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.001) #0.23 11 #optimizer = fluid.optimizer.SGDOptimizer(learning_rate=0.1) #0.07 12 13 EPOCH_NUM = 5 14 for epoch_id in range(EPOCH_NUM): 15 for batch_id, data in enumerate(train_loader()): 16 #准备数据,变得更加简洁 17 image_data, label_data = data 18 image = fluid.dygraph.to_variable(image_data) 19 label = fluid.dygraph.to_variable(label_data) 20 21 #前向计算的过程 22 predict = model(image) 23 24 #计算损失,取一个批次样本损失的平均值 25 loss = fluid.layers.cross_entropy(predict, label) 26 avg_loss = fluid.layers.mean(loss) 27 28 #每训练了100批次的数据,打印下当前Loss的情况 29 if batch_id % 200 == 0: 30 print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy())) 31 32 #后向传播,更新参数的过程 33 avg_loss.backward() 34 optimizer.minimize(avg_loss) 35 model.clear_gradients() 36 37 #保存模型参数 38 fluid.save_dygraph(model.state_dict(), 'mnist')
loading mnist dataset from ./work/mnist.json.gz ...... epoch: 0, batch: 0, loss is: [2.6056936] epoch: 0, batch: 200, loss is: [0.47590914] epoch: 0, batch: 400, loss is: [0.23337086] epoch: 1, batch: 0, loss is: [0.29190642] epoch: 1, batch: 200, loss is: [0.27118027] epoch: 1, batch: 400, loss is: [0.1074637] epoch: 2, batch: 0, loss is: [0.158496] epoch: 2, batch: 200, loss is: [0.30373794] epoch: 2, batch: 400, loss is: [0.1660601] epoch: 3, batch: 0, loss is: [0.15808547] epoch: 3, batch: 200, loss is: [0.26393268] epoch: 3, batch: 400, loss is: [0.09973648] epoch: 4, batch: 0, loss is: [0.08508419] epoch: 4, batch: 200, loss is: [0.10338296] epoch: 4, batch: 400, loss is: [0.04516026]
选择适合的优化算法训练模型
学习率是优化器的一个参数,虽然参数更新都是采用梯度下降算法,但是不同的梯度下降算法影响着神经网络的收敛效果。当随机梯度下降算法SGD无法满足我们的需求时,可以尝试如下三个思路选取优化器。
-
加入“动量”,参数更新的方向更稳定,比如Momentum优化器。每个批次的数据含有抽样误差,导致梯度更新的方向波动较大。如果我们引入物理动量的概念,给梯度下降的过程加入一定的“惯性”累积,就可以减少更新路径上的震荡!即每次更新的梯度由“历史多次梯度的累积方向”和“当次梯度”加权相加得到。历史多次梯度的累积方向往往是从全局视角更正确的方向,这与“惯性”的物理概念很像,也是为何其起名为“Momentum”的原因。类似不同品牌和材质的篮球有一定的重量差别,街头篮球队中的投手(擅长中远距离投篮)喜欢稍重篮球的比例较高。一个很重要的原因是,重的篮球惯性大,更不容易受到手势的小幅变形或风吹的影响。
-
根据不同参数距离最优解的远近,动态调整学习率,比如AdaGrad优化器。通过调整学习率的实验可以发现:当某个参数的现值距离最优解较远时(表现为梯度的绝对值较大),我们期望参数更新的步长大一些,以便更快收敛到最优解。当某个参数的现值距离最优解较近时(表现为梯度的绝对值较小),我们期望参数的更新步长小一些,以便更精细的逼近最优解。类似于打高尔夫球,专业运动员第一杆开球时,通常会大力打一个远球,让球尽量落在洞口附近。当第二杆面对离洞口较近的球时,他会更轻柔而细致的推杆,避免将球打飞。与此类似,参数更新的步长应该随着优化过程逐渐减少,减少的程度与当前梯度的大小有关。根据这个思想编写的优化算法称为“AdaGrad”,Ada是Adaptive的缩写,表示“适应环境而变化”的意思。
-
因为上述两个优化思路是正交的,所以可以将两个思路结合起来,这就是当前广泛应用的Adam算法。
1 #仅优化算法的设置有所差别 2 with fluid.dygraph.guard(): 3 model = MNIST("mnist") 4 model.train() 5 #调用加载数据的函数 6 train_loader = load_data('train') 7 8 #四种优化算法的设置方案,可以逐一尝试效果 9 opti_SGD = fluid.optimizer.SGDOptimizer(learning_rate=0.01) #0.12 10 #opti_Mom = fluid.optimizer.MomentumOptimizer(learning_rate=0.01, momentum=0.9) 11 #opti_Adag = fluid.optimizer.AdagradOptimizer(learning_rate=0.01)#0.2 12 #opti_Adam = fluid.optimizer.AdamOptimizer(learning_rate=0.01) #0.03 13 x=[] 14 y1=[] 15 y2=[] 16 y3=[] 17 y4=[] 18 iter_num=0 19 20 EPOCH_NUM = 5 21 for epoch_id in range(EPOCH_NUM): 22 for batch_id, data in enumerate(train_loader()): 23 #准备数据,变得更加简洁 24 image_data, label_data = data 25 image = fluid.dygraph.to_variable(image_data) 26 label = fluid.dygraph.to_variable(label_data) 27 28 #前向计算的过程 29 predict = model(image) 30 31 #计算损失,取一个批次样本损失的平均值 32 loss1 = fluid.layers.cross_entropy(predict, label) 33 avg_loss1 = fluid.layers.mean(loss1) 34 #loss2 = fluid.layers.cross_entropy(predict, label) 35 #avg_loss2 = fluid.layers.mean(loss2) 36 #loss3 = fluid.layers.cross_entropy(predict, label) 37 #avg_loss3 = fluid.layers.mean(loss3) 38 #loss4 = fluid.layers.cross_entropy(predict, label) 39 #avg_loss4 = fluid.layers.mean(loss4) 40 41 #每训练了100批次的数据,打印下当前Loss的情况 42 if batch_id % 200 == 0: 43 print("epoch: {}, batch: {}, loss is: {},iter: {}".format(epoch_id, batch_id, avg_loss1.numpy(),iter_num)) 44 y1.append(avg_loss1.numpy()) 45 #y2.append(avg_loss2.numpy()) 46 #y3.append(avg_loss3.numpy()) 47 #y4.append(avg_loss4.numpy()) 48 iter_num+=1 49 #y1.append(avg_loss1.numpy()) 50 #y2.append(avg_loss2.numpy()) 51 #y3.append(avg_loss3.numpy()) 52 #y4.append(avg_loss4.numpy()) 53 54 #后向传播,更新参数的过程 55 avg_loss1.backward() 56 #avg_loss2.backward() 57 #avg_loss3.backward() 58 #avg_loss4.backward() 59 opti_SGD.minimize(avg_loss1) 60 #opti_Mom.minimize(avg_loss2) 61 #opti_Adag.minimize(avg_loss3) 62 #opti_Adam.minimize(avg_loss4) 63 model.clear_gradients() 64 65 x=range(iter_num) 66 import matplotlib.pyplot as plt 67 #plt.title('Compare loss tendency with different Optimizer') 68 '''plt.plot(x,y1,'b',label="SGD") 69 plt.plot(x,y2,'g',label="Mom") 70 plt.plot(x,y3,'r',label="Adag") 71 plt.plot(x,y4,'y',label="Adam") 72 plt.legend() 73 plt.xlabel("Every 2400 Batch") 74 plt.ylabel("Loss") 75 plt.show()''' 76 fig,ax=plt.subplots() 77 #ax.plot(x,y1,label='SGD') 78 #ax.plot(x,y2,label='Mom') 79 #ax.plot(x,y3,label='Adag') 80 #ax.plot(x,y4,label='Adam') 81 ax.legend(loc='upper right',frameon=False) 82 plt.plot(x,y1,label='SGD') 83 #plt.plot(x,y2,label='second') 84 #plt.plot(x,y[:,2]) 85 plt.legend(frameon=True,framealpha=1) 86 87 #保存模型参数 88 fluid.save_dygraph(model.state_dict(), 'mnist')
学习率相同,SGD的测试效果曲线如下(暂时无法同时在一张图上画出四个曲线)。
loading mnist dataset from ./work/mnist.json.gz ...... epoch: 0, batch: 0, loss is: [2.5861013],iter: 0 epoch: 0, batch: 200, loss is: [0.3419642],iter: 1 epoch: 0, batch: 400, loss is: [0.3126796],iter: 2 epoch: 1, batch: 0, loss is: [0.2929134],iter: 3 epoch: 1, batch: 200, loss is: [0.22857675],iter: 4 epoch: 1, batch: 400, loss is: [0.2534462],iter: 5 epoch: 2, batch: 0, loss is: [0.16139439],iter: 6 epoch: 2, batch: 200, loss is: [0.23658308],iter: 7 epoch: 2, batch: 400, loss is: [0.15185605],iter: 8 epoch: 3, batch: 0, loss is: [0.27450126],iter: 9 epoch: 3, batch: 200, loss is: [0.20561613],iter: 10 epoch: 3, batch: 400, loss is: [0.14927392],iter: 11 epoch: 4, batch: 0, loss is: [0.21470158],iter: 12 epoch: 4, batch: 200, loss is: [0.10310929],iter: 13 epoch: 4, batch: 400, loss is: [0.07067939],iter: 14
No handles with labels found to put in legend.
下面就是测试效果:
1 # 预测100张图片准确率 2 with fluid.dygraph.guard(): 3 print('start evaluation .......') 4 datafile = './work/mnist.json.gz' 5 print('loading mnist dataset from {} ......'.format(datafile)) 6 data = json.load(gzip.open(datafile)) 7 # 读取数据集中的训练集,验证集和测试集 8 _, _, eval_set = data 9 # 随机抽取100张测试数据(图片) 10 num_img = 100 11 test_imgs = eval_set[0] 12 test_labs = eval_set[1] 13 index = list(range(len(test_imgs))) 14 random.shuffle(index) # 随机图片排序 15 imgs_list = [] 16 labels_list = [] 17 # 按照索引读取数据 18 for i in range(num_img): 19 # 读取图像和标签,转换其尺寸和类型 20 img = np.reshape(test_imgs[index[i]], [1, 28, 28]).astype('float32') 21 label = np.array(test_labs[index[i]]).astype('int64') 22 imgs_list.append(img) 23 labels_list.append(label) 24 test_img = np.array(imgs_list) 25 test_lab = np.array(labels_list) 26 27 print(f"There are {test_img.shape[0]} eval images in total.") 28 29 # 加载模型 30 model = MNIST("mnist") 31 model_state_dict, _ = fluid.load_dygraph('mnist') 32 model.load_dict(model_state_dict) 33 model.eval() 34 35 # 预测图片 36 test_img = fluid.dygraph.to_variable(test_img) # 转化为paddle数据格式 37 results = model(test_img) 38 results_num = np.argmax(results.numpy(), axis=1) # 获取概率最大值的标签 39 correct_cls = (results_num == test_lab) 40 acc = np.sum(correct_cls) / num_img 41 42 print(f"{num_img}张测试图片测试的准确率是: {acc*100}%。")
start evaluation ....... loading mnist dataset from ./work/mnist.json.gz ...... There are 100 eval images in total. 100张测试图片测试的准确率是: 97.0%。