以MNIST手写体数字识别为例自定义实现梯度下降(MindSpore框架)

1. 原理介绍 

 

        定义梯度下降的公式如下:

w^{t+1} = w^t - learning\_rate * \nabla J(w^t)

        其中w表示待更新的参数值,t表示迭代轮数,learning_rate表示学习率,J(w)表示目标函数。假设第t轮迭代时,w位于上图中红色箭头指向的位置。此时,目标函数的梯度方向如绿色箭头所示,目标函数的梯度值为负数。结合梯度下降的公式,我们可以清晰地知道在第t+1轮迭代时,w的值增大了。直观上,离目标函数的极小值处更近了。这就是梯度下降的原理。对于学习率lr而言,学习率设置过大的话,那么利用梯度下降进行参数更新的过程将在极小值处附近不断震荡,无法收敛,导致训练出的神经网络性能效果较差。学习率设置如果过小的话,会存在训练速度过慢等问题。 

2. 加载外部库 

import mindspore
from mindspore import nn
from mindspore.dataset import vision, transforms
from mindspore.dataset import MnistDataset
import mindspore.context as context
from mindspore import ops
from download import download
import matplotlib.pyplot as plt
import time

        将实验所需要的模块通过import命令进行导入。mindspore模块用于构造全连接神经网络分类器,完成MNIST手写体数字识别任务。download模块用于下载MNIST数据集。matplotlib模块用于可视化训练过程。time模块用于DeBug。 

3. 设备设置  

context.set_context(mode = context.GRAPH_MODE, device_target = 'GPU')

        在实验中,我将神经网络模型的训练过程部署到GPU上进行,相比于CPU而言,模型的训练速度更快。 

4. 下载数据集 

url = "https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/" \
      "notebook/datasets/MNIST_Data.zip"
path = download(url, "./", kind = "zip", replace = True)

         通过download模块下载MNIST数据集。MNIST数据集包含10种数据类型,分别对应阿拉伯数字0至9。

 

5. 超参数设置 

batch_size = 16
lr = 1e-3 # 1e-2
epochs = 5
weight_decay = 1e-2

        对于batch_size而言,如果batch_size太大的话,参数更新的次数将会变少,尽管模型训练的时间会缩短,但神经网络的分类效果将会降低,如果batch_size太小的话,模型训练的时间会大大增加,同时在收敛过程中容易发生振荡。对于lr而言,学习率设置过大的话,那么利用梯度下降进行参数更新的过程将在极小值点附近不断震荡,无法收敛,导致训练出的神经网络性能效果较差,学习率设置如果过低的话,会存在训练速度过慢等问题。对于epoch而言,如果epoch较小,那么参数更新的次数将会偏少,训练出的神经网络其分类效果和泛化能力将会变弱,如果epoch较大,那么训练时间将会大幅度增加,且训练出的神经网络其分类效果先会取得明显提升,但后续可能变化不大。值得注意的是weight_decay,weight_decay是放在L2正则项前的系数,weight_decay越大,模型参数越趋近于0。weight_decay可以控制参数值的变化幅度,如果模型参数值变化幅度很大,则表明模型本身变化很大,容易过拟合训练样本。同时weight_decay可以保证模型参数值较小,进而避免梯度爆炸。

6. 数据集加载  

train_dataset = MnistDataset('MNIST_Data/train')
test_dataset = MnistDataset('MNIST_Data/test')

        通过MindSpore框架提供的 mindspore.dataset.MnistDataset API接口加载下载到本地的MNIST数据集。生成的数据集有两列[image,label]。image 列的数据类型为unit8,label列的数据类型为unit32。API接口的转入参数中,’MNIST_Data/train’ 表示包含数据集文件的根目录路径。 

 

7. 数据预处理 

def data_pre_processing(dataset):
    image_transforms = [
        vision.Rescale(1.0 / 255.0, 0), # 归一化
        vision.Normalize(mean=(0.1307,), std=(0.3081,)), # 正则化
        vision.HWC2CHW()
    ]
    label_transform = transforms.TypeCast(mindspore.int32)

    dataset = dataset.map(image_transforms, 'image')
    dataset = dataset.map(label_transform, 'label')
    return dataset

# 数据预处理
train_dataset = data_pre_processing(train_dataset)
test_dataset = data_pre_processing(test_dataset)

        通过MindSpore框架提供的 mindspore.dataset.Dataset.map API接口按顺序将数据增强作用在数据集对象上。传入参数中,image_transformers / label_transformers是用户自定义的数据增强操作,’image’ / ‘label’ 指定数据增强操作作用在的数据列。 

        通过MindSpore框架提供的 mindspore.dataset.vision.Rescale API接口基于给定的缩放因子和平移因子调整图像的像素值大小。传入参数中,1.0 / 255.0是缩放因子,0是平移因子。该数据增强操作实现了归一化处理。

        通过MindSpore框架提供的 mindspore.dataset.vision.Normalize API接口根据给定均值和标准差对输入图像进行归一化处理。传入参数中,mean是均值,std是标准差。

        通过MindSpore框架提供的 mindspore.dataset.vision.HWC2CHW API接口将输入图像的shape从<H,W,C>转换为<C,H,W>,其中H表示图像的高度,W表示图像的宽度,C表示图像的通道数。

        通过MindSpore框架提供的 mindspore.dataset.transforms.TypeCast API接口将输入的Tensor转换为指定的数据类型。传入参数中,int32表示目标数据类型。

8. 数据混洗与划分  

train_dataset = train_dataset.shuffle(60000)
train_dataset = train_dataset.batch(batch_size)
test_dataset = test_dataset.shuffle(10000)
test_dataset = test_dataset.batch(batch_size)

        通过MindSpore框架提供的.shuffle方法,创建 buffer_size 大小的缓存来混洗数据集。将 buffer_size 设置为数据集大小时将进行全局混洗。混洗步骤如下,1. 生成一个混洗缓冲区包含 buffer_size 条数据行;2. 从混洗缓冲区中随机选择一个数据行,传递给下一个操作;3. 从上一个操作获取下一个数据行(如果有的话),并将其放入混洗缓冲区中;4. 重复步骤2和3,直到混洗缓冲区中没有数据行为止。如果不进行shuffle,神经网络模型一段时间内容易过拟合于某一标签数据,导致模型的性能降低,泛化能力变差。

        通过MindSpore框架提供的.batch方法,将数据集中连续 batch_size 条数据组合为一个批数据,batch 操作要求每列中的数据具有相同的shape。 

9. 构造全连接神经网络 

class Network(nn.Cell):
    def __init__(self):
        super().__init__()
        self.flatten = nn.Flatten()
        self.Dense1 = nn.Dense(28*28, 1024)
        self.ReLU1 = nn.ReLU()
        self.Dense2 = nn.Dense(1024, 512)
        self.ReLU2 = nn.ReLU()
        self.Dense3 = nn.Dense(512, 10)
        

    def construct(self, x):
        x = self.flatten(x)
        x = self.Dense1(x)
        x = self.ReLU1(x)
        x = self.Dense2(x)
        x = self.ReLU2(x)
        x = self.Dense3(x)
        return x

model = Network()

        构造了四层全连接神经网络。先通过一层神经网络将原始低维输入特征映射到高维空间,有助于特征交互和特征提取。之后通过两层神经网络不断降维,试图提取出有价值的特征,增加全连接神经网络的性能表现。每层神经网络的输出需要通过激活函数进行非线性变换,这样可以解决线性模型不能处理的问题,增加全连接神经网络的表达能力。激活函数我选择了ReLU。对于Sigmoid激活函数而言,存在三个问题1. Sigmoid函数的导数最大值为0.25且饱和区域占比较大,饱和区域的导数趋近于0,这样在反向传播的过程中很容易造成梯度消失;2. Sigmoid函数的输出值恒大于0,而不是以0为中心,这会导致神经网络模型的收敛速度变慢;3. Sigmoid函数执行的是幂运算,时间开销更大。相比而言,ReLU激活函数在正区间解决了梯度消失问题,同时会使一部分神经元的输出为0,这就造成了神经网络的稀疏性,并且减少了参数间的相互依存关系,缓解了过拟合问题的发生,而且ReLU激活函数收敛速度更快,计算效率更高。 

 

10. 构造训练函数 

# 定义损失函数和优化器
loss_fn = nn.CrossEntropyLoss()
optimizer = nn.Adam(model.trainable_params(), learning_rate = lr) # nn.Adagrad nn.RMSProp nn.Adam

# 前向传播 不会计算梯度
def forward(data, label):
    logits = model(data)
    loss = loss_fn(logits, label)
    return loss, logits

# 生成求导函数 用于计算给定函数的前向传播结果和梯度
grad_fn = mindspore.value_and_grad(forward, None, optimizer.parameters, has_aux=True)

# one-step
def train_step(data, label):
    (loss, logits), grads = grad_fn(data, label)
    
    # 调用API实现优化器
    optimizer(grads)

    # 自定义实现优化器
    # 尝试一: 运行时无法赋值
    # for i, parameter in enumerate(model.get_parameters()):
    #     parameter -= lr * grads[i]
    # 尝试二: 官方文档提供运行时赋值方法
    # index = 0
    # for (name, _) in model.parameters_and_names():
    #     lay_name, attr_name = name.split('.')
    #     lay = getattr(model, lay_name)
    #     attr = getattr(lay, attr_name)
    #     ops.assign(attr, attr - lr * grads[index])
    #     index += 1

    return loss, logits

def train(model, dataset):
    num_batches = dataset.get_dataset_size()
    model.set_train()
    total, train_loss, accuracy = 0, 0, 0
    for batch, (data, label) in enumerate(dataset.create_tuple_iterator()):
        total += len(data)
        loss, pred = train_step(data, label)
        train_loss += loss.asnumpy()
        accuracy += (pred.argmax(1) == label).asnumpy().sum()
    train_loss /= num_batches
    accuracy /= total
    print(f"Train: \n Accuracy: {(100*accuracy):>0.1f}%, Avg loss: {train_loss:>8f} \n")

    return accuracy

        首先定义了前向传播函数forward。在PyTorch中,默认情况下,执行前向传播计算时会记录反向传播所需的梯度信息。在推理阶段,这一操作是冗余的,会额外耗时,因此PyTorch提供了torch.no_grad来取消该过程。而MindSpore只有在调用grad时才会根据正向图结构来构建反向图,前向传播时不会记录任何梯度信息,即MindSpore的正向计算均在torch. no_grad情况下进行。因此,我们需要调用mindspore.value_and_grad,获得微分函数,用于计算前向传播结果和梯度。然后,我们可以调用MindSpore框架提供的API实现优化器,也可以根据官方文档提供的网络参数运行时赋值方法,自定义梯度下降优化器,完成对网络参数的更新。接下来,通过调用MindSpore框架提供的API接口create_tuple_iterator,基于数据集对象创建批迭代器,循环迭代训练全连接神经网络。

        损失函数我们选择了交叉熵损失函数。相比而言,均方误差对参数的偏导结果都乘了激活函数的导数, 而部分激活函数饱和区域过大, 容易造成偏导数为0, 参数不进行更新。而交叉熵损失描述了预测数据分布与真实分布之间的差异, 可以很好地对模型性能进行建模,而且其对参数的偏导数没有激活函数的导数, 不存在上述问题, 同时更便于进行梯度计算。经实验验证,当前任务上交叉熵损失函数的效果要优化均方误差损失函数。

        值得一提的是,MindSpore框架提供的交叉熵损失函数API接口nn.CrossEntropyLoss 与PyTorch的功能几乎一致,不需要输入one-hot编码向量,同时还内置了soft-max函数。

11. 构造测试函数 

def test(model, dataset, loss_fn):
    num_batches = dataset.get_dataset_size()
    model.set_train(False)
    total, test_loss, accuracy = 0, 0, 0
    for data, label in dataset.create_tuple_iterator():
        pred = model(data)
        total += len(data)
        test_loss += loss_fn(pred, label).asnumpy()
        accuracy += (pred.argmax(1) == label).asnumpy().sum()
    test_loss /= num_batches
    accuracy /= total
    print(f"Test: \n Accuracy: {(100*accuracy):>0.1f}%, Avg loss: {test_loss:>8f} \n")

    return accuracy

        实现思路与构造训练函数时的思路大体一致,这里不过多赘述。值得注意的是,在将测试集数据输入到模型之前,我们需要调用set_train(False),调用后模型的Dropout层和批归一化层BatchNorm将会关闭。与PyTorch不同,PyTorch在推理时需要调用torch.no_grad(),保证在前向传播的过程中不计算梯度,以节省显存,加快计算速度。而MindSpore在前向传播时不会记录任何梯度信息。在推理输入测试数据的类别时,由于soft - max函数能将输出值转化为概率并扩大差异,但不影响分类判断。因此我们可以将输出向量中最大分量的下标直接视为推理结果。 

12. 模型训练与测试 

Train_Accuracy_List = []
Test_Accurary_List = []
for epoch in range(epochs):
    print(f"Epoch {epoch + 1}\n-------------------------------")
    train_accuracy = train(model, train_dataset)
    Train_Accuracy_List.append(train_accuracy)
    test_accuracy = test(model, test_dataset, loss_fn)
    Test_Accurary_List.append(test_accuracy)
print("Done!")

  

13. 实验结果 

14. 总结  

        相信小白通过与阅读本篇博客,可以增强自己的理论知识,也可以清晰地了解如何运用MindSpore框架搭建神经网络模型,并完成神经网络模型的训练与推理,同时读者可以从本篇博客中了解到如何自定义梯度下降算法更新神经网络参数。 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值