2.6 手写数字识别之优化算法

本文涉及的产品
函数计算FC,每月15万CU 3个月
简介: 这篇文章探讨了在手写数字识别任务中,如何通过优化算法来找到使损失函数达到最小的参数取值。文章首先讨论了学习率对模型训练的影响,然后介绍了四种主流的优化算法:SGD、Momentum、AdaGrad和Adam,并说明了每种算法的特点和适用场景。此外,文章还强调了模型参数初始化的重要性,并介绍了几种常用的参数初始化方法,最后指出在实际应用中,使用预训练模型可以加速网络训练并提高精度。

第2.5节我们明确了分类任务的损失函数(优化目标)的相关概念和实现方法,本节我们依旧横向展开"横纵式"教学法,如 图1 所示,本节主要探讨在手写数字识别任务中,使得损失达到最小的参数取值的实现方法。

图1:“横纵式”教学法 — 优化算法

前提条件

在优化算法之前,需要进行数据处理、设计神经网络结构,代码与上一节保持一致,此处直接调用封装好的代码。

In [1]

import paddle
from data_process import get_MNIST_dataloader
from MNIST_network import MNIST
train_loader, test_loader = get_MNIST_dataloader()

2.6.1 学习率

在深度学习神经网络模型中,通常使用标准的随机梯度下降算法更新参数,学习率代表参数更新幅度的大小,即步长。当学习率最优时,模型的有效容量最大,最终能达到的效果最好学习率和深度学习任务类型有关,合适的学习率往往需要大量的实验和调参经验。探索学习率最优值时需要注意如下两点:

  • 学习率不是越小越好。学习率越小,损失函数的变化速度越慢,意味着我们需要花费更长的时间进行收敛,如 图2左图所示。
  • 学习率不是越大越好。只根据总样本集中的一个批次计算梯度,抽样误差会导致计算出的梯度不是全局最优的方向,且存在波动。在接近最优解时,过大的学习率会导致参数在最优解附近震荡,损失难以收敛,如 图2 右图所示。

图2: 不同学习率(步长过大/过小)的示意图

在训练前,我们往往不清楚一个特定问题设置成怎样的学习率是合理的,因此在训练时可以尝试调小或调大,通过观察Loss下降的情况判断合理的学习率,设置学习率的代码如下所示。

In [2]

import paddle.nn.functional as F
#仅优化算法的设置有所差别deftrain(model):
    model.train()
    
    #设置不同初始学习率
    opt = paddle.optimizer.SGD(learning_rate=0.001, parameters=model.parameters())
    # opt = paddle.optimizer.SGD(learning_rate=0.0001, parameters=model.parameters())# opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters())
    
    EPOCH_NUM = 10
    loss_list = []
    for epoch_id inrange(EPOCH_NUM):
        for batch_id, data inenumerate(train_loader()):
            #准备数据
            images, labels = data
            images = paddle.to_tensor(images)
            labels = paddle.to_tensor(labels)
            
            #前向计算的过程
            predicts = model(images)
            
            #计算损失,取一个批次样本损失的平均值
            loss = F.cross_entropy(predicts, labels)
            avg_loss = paddle.mean(loss)
            
            #每训练了100批次的数据,打印下当前Loss的情况if batch_id % 200 == 0:
                loss = avg_loss.numpy()[0]
                loss_list.append(loss)
                print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, loss))
            
            #后向传播,更新参数的过程
            avg_loss.backward()
            # 最小化loss,更新参数
            opt.step()
            # 清除梯度
            opt.clear_grad()
   
    #保存模型参数
    paddle.save(model.state_dict(), 'mnist.pdparams')
    return loss_list
    
#创建模型    
model = MNIST()
#启动训练过程
loss_list = train(model)

W0902 13:59:43.727392 98 device_context.cc:447] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 11.2, Runtime API Version: 10.1

W0902 13:59:43.731014 98 device_context.cc:465] device: 0, cuDNN Version: 7.6.

epoch: 0, batch: 0, loss is: 2.672663927078247

epoch: 0, batch: 200, loss is: 1.5871143341064453

epoch: 0, batch: 400, loss is: 0.8906817436218262

epoch: 0, batch: 600, loss is: 0.8545184135437012

epoch: 0, batch: 800, loss is: 0.5248807668685913...

绘制loss变化曲线:

In [4]

from tools import plot
plot(loss_list)

2.6.2 学习率的四种主流优化算法

学习率是优化器的一个参数,调整学习率看似是一件非常麻烦的事情,需要不断的调整步长,观察训练时间和Loss的变化。经过科研人员的不断的实验,当前已经形成了四种比较成熟的优化算法:SGD、Momentum、AdaGrad和Adam,效果如 图3 所示。

图3: 不同学习率算法效果示意图

  • SGD: 随机梯度下降算法,每次训练少量数据,抽样偏差导致的参数收敛过程中震荡
  • Momentum: 引入物理“动量”的概念,累积速度,减少震荡,使参数更新的方向更稳定

每个批次的数据含有抽样误差,导致梯度更新的方向波动较大。如果我们引入物理动量的概念,给梯度下降的过程加入一定的“惯性”累积,就可以减少更新路径上的震荡,即每次更新的梯度由“历史多次梯度的累积方向”和“当次梯度”加权相加得到。历史多次梯度的累积方向往往是从全局视角更正确的方向,这与“惯性”的物理概念很像,也是为何其起名为“Momentum”的原因。类似不同品牌和材质的篮球有一定的重量差别,街头篮球队中的投手(擅长中远距离投篮)往往更喜欢稍重篮球。一个很重要的原因是,重的篮球惯性大,更不容易受到手势的小幅变形或风吹的影响。

  • AdaGrad: 根据不同参数距离最优解的远近,动态调整学习率。学习率逐渐下降,依据各参数变化大小调整学习率

通过调整学习率的实验可以发现:当某个参数的现值距离最优解较远时(表现为梯度的绝对值较大),我们期望参数更新的步长大一些,以便更快收敛到最优解。当某个参数的现值距离最优解较近时(表现为梯度的绝对值较小),我们期望参数的更新步长小一些,以便更精细的逼近最优解。类似于打高尔夫球,专业运动员第一杆开球时,通常会大力打一个远球,让球尽量落在洞口附近。当第二杆面对离洞口较近的球时,他会更轻柔而细致的推杆,避免将球打飞。与此类似,参数更新的步长应该随着优化过程逐渐减少,减少的程度与当前梯度的大小有关。根据这个思想编写的优化算法称为“AdaGrad”,Ada是Adaptive的缩写,表示“适应环境而变化”的意思。RMSProp是在AdaGrad基础上的改进,学习率随着梯度变化而适应,解决AdaGrad学习率急剧下降的问题。

  • Adam: 由于动量和自适应学习率两个优化思路是正交的,因此可以将两个思路结合起来,这是当前广泛应用的算法。

说明:

每种优化算法均有更多的参数设置,详情可查阅飞桨的官方API文档。理论最合理的未必在具体案例中最有效,所以模型调参是很有必要的,最优的模型配置往往是在一定“理论”和“经验”的指导下实验出来的。


我们可以尝试选择不同的优化算法训练模型,观察训练时间和损失变化的情况,代码实现如下。

In [5]

#仅优化算法的设置有所差别deftrain(model):
    model.train()
    
    #四种优化算法的设置方案,可以逐一尝试效果
    opt = paddle.optimizer.SGD(learning_rate=0.01, parameters=model.parameters())
    # opt = paddle.optimizer.Momentum(learning_rate=0.01, momentum=0.9, parameters=model.parameters())# opt = paddle.optimizer.Adagrad(learning_rate=0.01, parameters=model.parameters())# opt = paddle.optimizer.Adam(learning_rate=0.01, parameters=model.parameters())
    
    EPOCH_NUM = 3for epoch_id inrange(EPOCH_NUM):
        for batch_id, data inenumerate(train_loader()):
            #准备数据
            images, labels = data
            images = paddle.to_tensor(images)
            labels = paddle.to_tensor(labels)
            
            #前向计算的过程
            predicts = model(images)
            
            #计算损失,取一个批次样本损失的平均值
            loss = F.cross_entropy(predicts, labels)
            avg_loss = paddle.mean(loss)
            
            #每训练了100批次的数据,打印下当前Loss的情况if batch_id % 200 == 0:
                print("epoch: {}, batch: {}, loss is: {}".format(epoch_id, batch_id, avg_loss.numpy()))
            
            #后向传播,更新参数的过程
            avg_loss.backward()
            # 最小化loss,更新参数
            opt.step()
            # 清除梯度
            opt.clear_grad()
    #保存模型参数
    paddle.save(model.state_dict(), 'mnist.pdparams')
    
#创建模型    
model = MNIST()
#启动训练过程
train(model)

epoch: 0, batch: 0, loss is: [3.179514]

epoch: 0, batch: 200, loss is: [0.41207898]

epoch: 0, batch: 400, loss is: [0.14528263]

epoch: 0, batch: 600, loss is: [0.17151298]

epoch: 0, batch: 800, loss is: [0.20650986]

2.6.3 模型参数初始化

模型参数初始化是指在训练前,给要训练的参数一个初始的值。比如我们最终的目的是走到山谷底,如果一开始把你放到半山腰和放到山脚,那是否能够顺利走到谷底以及走到谷底的速度是有很大差距的。同样,在我们训练神经网络的时候,训练参数初始值不同也会导致模型收敛速度和最终训练效果的不同

在PaddlePaddle框架中,MNIST模型中用到的Conv2DLinear层都有weight_attrbias_attr两个参数,默认为None,表示使用默认的权重参数属性。这两个参数可以使用模型的named_parameters()函数获得。

通过下面的代码我们可以查看模型每层的参数情况

模型的state_dict函数可 获取当前层及其子层的所有参数和可持久性buffers。并 将所有参数和buffers存放在dict结构中。

In [6]

model = MNIST()
# 遍历所有的参数名和参数值for name, param in model.named_parameters():
    print(name, param)  
    
state_dict = model.state_dict() #collections.OrderedDict类型print(state_dict.keys())
# 根据某个参数的名字,打印对应的参数值# 如打印“conv1.weight”的参数值print("conv1.weight:",state_dict["conv1.weight"])

conv1.weight Parameter containing:

Tensor(shape=[20, 1, 5, 5], dtype=float32, place=CUDAPlace(0), stop_gradient=False,

[[[[ 0.00685916, -0.02827659, -0.00629800, 0.17840426, 0.07073421],

[ 0.15301149, 0.14512494, -0.08644208, -0.04089865, 0.06347622],

[-0.17223580, 0.53609502, -0.31562763, 0.31539580, 0.11763398],

[ 0.06057844, 0.12100003, -0.19036214, -0.32686502, -0.04253779],

[-0.24231870, -0.09162699, -0.00356645, 0.13624071, 0.35424659]]],

[[[ 0.19513041, -0.18949197, -0.06306379, -0.33953783, -0.12144631],

[-0.52912796, -0.04904719, 0.22196575, -0.21367665, -0.14066246],

[-0.47838733, 0.19762798, 0.32846957, -0.06876396, -0.05648636],

[-0.49627945, 0.04398900, -0.02770269, -0.09062629, -0.25601473],

[ 0.22629717, 0.12104782, -0.21879482, -0.29909152, 0.15422553]]],..

通过print(state_dict.keys())打印的结果为['conv1.weight', 'conv1.bias', 'conv2.weight', 'conv2.bias', 'fc.weight', 'fc.bias'],对应我们模型结构:两个卷积层,一个全连接层。每一层都有一个权重项和偏置项。由于有两个卷积层,所以卷积的权重名称有对应的编号。

paddle.ParamAttr可创建一个参数属性的对象,用户可设置参数的名称name、初始化方式initializer、学习率learning_rate、正则化规则regularizer、是否需要训练trainable、梯度裁剪方式need_clip、是否做模型平均do_model_average等属性。其参数初始化方式initializer默认值为None,表示权重参数采用Xavier初始化方式,偏置参数采用全0初始化方式。Xavier初始化方式可以缓解梯度消失的问题。

下面简单介绍几个常用的权重初始化方法:

下面我们通过paddle.ParamAttr基于常量初始化函数Constant完成全连接层的参数初始化,具体如下:

In [7]

import paddle
from paddle.nn import Linear
weight_attr = paddle.ParamAttr(initializer=paddle.nn.initializer.Constant(3))
#weight_attr = paddle.ParamAttr(initializer=paddle.nn.initializer.XavierNormal())
fc = Linear(in_features=20, out_features=10,weight_attr=weight_attr)
print(fc.state_dict())

OrderedDict([('weight', Parameter containing:

Tensor(shape=[20, 10], dtype=float32, place=CUDAPlace(0), stop_gradient=False,

[[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.],

[3., 3., 3., 3., 3., 3., 3., 3., 3., 3.]])), ('bias', Parameter containing:

Tensor(shape=[10], dtype=float32, place=CUDAPlace(0), stop_gradient=False,

[0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]))])

在我们的实际应用中,由于训练数据有限、数据获取较难、训练资源有限等原因,往往利用在大规模开源数据集上训练得到的模型参数作为我们自己模型的初始值******(也称为预训练模型),这样可以加速网络训练、并得到较高精度******

作业 2-3

在手写数字识别任务上,哪种优化算法的效果最好?多大的学习率最优?(可通过Loss的下降趋势来判断)

相关实践学习
【文生图】一键部署Stable Diffusion基于函数计算
本实验教你如何在函数计算FC上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。函数计算提供一定的免费额度供用户使用。本实验答疑钉钉群:29290019867
建立 Serverless 思维
本课程包括: Serverless 应用引擎的概念, 为开发者带来的实际价值, 以及让您了解常见的 Serverless 架构模式
相关文章
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
深度学习中的优化算法及其应用
【10月更文挑战第8天】 本文将探讨深度学习中常用的优化算法,包括梯度下降法、Adam和RMSProp等,介绍这些算法的基本原理与应用场景。通过实例分析,帮助读者更好地理解和应用这些优化算法,提高深度学习模型的训练效率与性能。
139 63
|
10天前
|
算法 调度
基于遗传模拟退火混合优化算法的车间作业最优调度matlab仿真,输出甘特图
车间作业调度问题(JSSP)通过遗传算法(GA)和模拟退火算法(SA)优化多个作业在并行工作中心上的加工顺序和时间,以最小化总完成时间和机器闲置时间。MATLAB2022a版本运行测试,展示了有效性和可行性。核心程序采用作业列表表示法,结合遗传操作和模拟退火过程,提高算法性能。
|
11天前
|
人工智能 算法 大数据
Linux内核中的调度算法演变:从O(1)到CFS的优化之旅###
本文深入探讨了Linux操作系统内核中进程调度算法的发展历程,聚焦于O(1)调度器向完全公平调度器(CFS)的转变。不同于传统摘要对研究背景、方法、结果和结论的概述,本文创新性地采用“技术演进时间线”的形式,简明扼要地勾勒出这一转变背后的关键技术里程碑,旨在为读者提供一个清晰的历史脉络,引领其深入了解Linux调度机制的革新之路。 ###
|
21天前
|
人工智能 算法 数据安全/隐私保护
基于遗传优化的SVD水印嵌入提取算法matlab仿真
该算法基于遗传优化的SVD水印嵌入与提取技术,通过遗传算法优化水印嵌入参数,提高水印的鲁棒性和隐蔽性。在MATLAB2022a环境下测试,展示了优化前后的性能对比及不同干扰下的水印提取效果。核心程序实现了SVD分解、遗传算法流程及其参数优化,有效提升了水印技术的应用价值。
|
20天前
|
存储 缓存 算法
优化轮询算法以提高资源分配的效率
【10月更文挑战第13天】通过以上这些优化措施,可以在一定程度上提高轮询算法的资源分配效率,使其更好地适应不同的应用场景和需求。但需要注意的是,优化策略的选择和实施需要根据具体情况进行详细的分析和评估,以确保优化效果的最大化。
|
22天前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。
|
22天前
|
存储 缓存 算法
前端算法:优化与实战技巧的深度探索
【10月更文挑战第21天】前端算法:优化与实战技巧的深度探索
19 1
|
23天前
|
机器学习/深度学习 算法 数据安全/隐私保护
基于贝叶斯优化CNN-LSTM网络的数据分类识别算法matlab仿真
本项目展示了基于贝叶斯优化(BO)的CNN-LSTM网络在数据分类中的应用。通过MATLAB 2022a实现,优化前后效果对比明显。核心代码附带中文注释和操作视频,涵盖BO、CNN、LSTM理论,特别是BO优化CNN-LSTM网络的batchsize和学习率,显著提升模型性能。
|
30天前
|
存储 缓存 算法
如何通过优化算法和代码结构来提升易语言程序的执行效率?
如何通过优化算法和代码结构来提升易语言程序的执行效率?
|
30天前
|
机器学习/深度学习 人工智能 算法
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解
70 0
[大语言模型-算法优化] 微调技术-LoRA算法原理及优化应用详解