"数字识别"深度学习无法收敛问题求助

最近在学习paddlepaddle,跟着官方文档在写最基础的"数字识别"。发现自己写的实现无法收敛,初步判断是写的 样本数据采集处理 逻辑有问题,因为我改用官方实现的样本数据采集就能正常收敛。
调试了半天还是没发现是什么差异导致。想求助各位佬 :pray:

下图是自己写的 样本数据采集get_MNIST_dataloader(dataFrom=“local”) 收敛日志

改用官方的样本数据采集get_MNIST_dataloader(dataFrom=“remote”) 收敛日志

以下是源码

import json
import gzip
import paddle
from paddle.vision.transforms import Normalize
from paddle.io import Dataset
import numpy as np
import paddle.nn.functional as F
from paddle.nn import Linear
from paddle.nn import Conv2D, MaxPool2D, Linear

class MNISTDataset(Dataset):
    """
    步骤一:继承paddle.io.Dataset类
    """
    
    def __init__(self, datafile, mode='train', transform = None):
        """
        步骤二:实现构造函数
        """
        super().__init__()

        self.mode = mode
        self.transform = transform

        print('loading mnist dataset from {} ......'.format(datafile))
        # 加载json数据文件
        data = json.load(gzip.open(datafile))
        print('mnist dataset load done')
   
        # 读取到的数据区分训练集,验证集,测试集
        train_set, val_set, eval_set = data

        if mode=='train':
            # 获得训练数据集
            print("imgs",len(train_set[0][0]))
            self.imgs, self.labels = train_set[0], train_set[1]
        elif mode=='valid':
            # 获得验证数据集
            self.imgs, self.labels = val_set[0], val_set[1]
        elif mode=='test':
            # 获得测试数据集
            self.imgs, self.labels = eval_set[0], eval_set[1]
        else:
            raise Exception("mode can only be one of ['train', 'valid', 'test']")
    
    def __getitem__(self, index):
        """
        步骤三:实现__getitem__方法,定义指定index时如何获取数据
        """
        data, label = self.imgs[index], self.labels[index]
        data = np.reshape(data, [28, 28]).astype("float32")
        label = np.reshape(label,[1]).astype("int64")
        return self.transform(data),label

    def __len__(self):
        """
        步骤四:实现__len__方法,返回数据集总数目
        """
        return len(self.imgs)



def get_MNIST_dataloader(datafile = './dataset/mnist.json.gz',dataFrom = 'local'):

    # 定义图像归一化处理方法,这里的CHW指图像格式需为 [C通道数,H图像高度,W图像宽度]
    transform = Normalize(mean=[127.5], std=[127.5], data_format='CHW')

    # 下载数据集并初始化 DataSet
    if dataFrom == 'local' :
        train_dataset = MNISTDataset(datafile, mode='train', transform=transform)
    elif dataFrom == 'remote':
        train_dataset = paddle.vision.datasets.MNIST(mode='train', transform=transform)
    else:
        raise Exception("data_format can only be one of ['local', 'remote']")

    # 定义并初始化数据读取器
    train_loader = paddle.io.DataLoader(train_dataset, batch_size=64, shuffle=True,drop_last=True)
    
    return train_loader,''

# 定义多层全连接神经网络
class MNIST(paddle.nn.Layer):
     def __init__(self):
         super(MNIST, self).__init__()
         
         # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2
         self.conv1 = Conv2D(in_channels=1, out_channels=20, kernel_size=5, stride=1, padding=2)
         # 定义池化层,池化核的大小kernel_size为2,池化步长为2
         self.max_pool1 = MaxPool2D(kernel_size=2, stride=2)
         # 定义卷积层,输出特征通道out_channels设置为20,卷积核的大小kernel_size为5,卷积步长stride=1,padding=2
         self.conv2 = Conv2D(in_channels=20, out_channels=20, kernel_size=5, stride=1, padding=2)
         # 定义池化层,池化核的大小kernel_size为2,池化步长为2
         self.max_pool2 = MaxPool2D(kernel_size=2, stride=2)
         # 定义一层全连接层,输出维度是10
         self.fc = Linear(in_features=980, out_features=10)
         
    # 定义网络前向计算过程,卷积后紧接着使用池化层,最后使用全连接层计算最终输出
    # 卷积层激活函数使用Relu,全连接层不使用激活函数
     def forward(self, inputs):
         x = self.conv1(inputs)
         x = F.relu(x)
         x = self.max_pool1(x)
         x = self.conv2(x)
         x = F.relu(x)
         x = self.max_pool2(x)
         x = paddle.reshape(x, [x.shape[0], 980])
         x = self.fc(x)
         return x

#网络结构部分之后的代码,保持不变
def train(model,train_loader):
    model.train()
    learning_rate = 0.001 

    # 使用SGD优化器,设置learning_rate
    opt = paddle.optimizer.SGD(learning_rate=learning_rate, parameters=model.parameters())
    # 训练5轮
    EPOCH_NUM = 10
    # MNIST图像高和宽
    IMG_ROWS, IMG_COLS = 28, 28

    loss_list = []

    for epoch_id in range(EPOCH_NUM):
        for batch_id, data in enumerate(train_loader()):
            #准备数据
            images, labels = data
            images = paddle.to_tensor(images)
            labels = paddle.to_tensor(labels)
            #前向计算的过程
            predicts = model(images) # [batch_size, 1]

            #计算损失,使用交叉熵损失函数,取一个批次样本损失的平均值
            loss = F.cross_entropy(predicts, labels)
            avg_loss = paddle.mean(loss)

            #每训练200批次的数据,打印下当前Loss的情况
            if batch_id % 200 == 0:
                loss_list.append(avg_loss.numpy())
                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')

    return loss_list

def do_train():
    # 训练模型
    model = MNIST()
    params_info = paddle.summary(model, (1, 1, 28, 28))
    print(params_info)
    train_loader,_ = get_MNIST_dataloader(dataFrom="local") # local-本地采集器,remote-官方实现的采集器
    loss_list_conv = train(model,train_loader)
    
do_train()

以下是官方demo中样本数据集,我放wormhole上了,在24小时候或被下载100次后会被删除,如果过期了,还有需要的佬可以回复一下,我再补一个

https://wormhole.app/a054j#52quccu323Ts0ORQ-4HLpA

前排围观

简单看了一下代码,存在一个问题就是 数据的格式不统一,local的json数据已经实现[0,1]归一化,remote 不太清楚原始格式是不是 [0,256], 因此简单尝试修改了如下代码

def __getitem__(self, index):
        """
        步骤三:实现__getitem__方法,定义指定index时如何获取数据
        """
        data, label = self.imgs[index], self.labels[index]
        data = np.reshape(data, [1,28, 28]).astype("float32")*256 
        ## ⚠️上面此行
        label = np.reshape(label,[1]).astype("int64")
       ## 修改如下代码更鲁棒
        if self.transform:
            data = self.transform(data)
        return data,label

反正正常训练loss下降了

感谢!确实可以了 :pray:
思考了一下既然问题是已经归一化了,不能重复归一化
那根据Normalize的归一化公式 normalized = (x - mean) / std
可以得出 当 mean=0,std=1 的时候normalized的值和x相等
改一下归一化器的配置应该也可以

def get_MNIST_dataloader(datafile = './dataset/mnist.json.gz',dataFrom = 'local'):

    # 定义图像归一化处理方法,这里的CHW指图像格式需为 [C通道数,H图像高度,W图像宽度]
    transform = Normalize(mean=[0], std=[1], data_format='CHW')
    # ⚠️将mean改为0,将std改为1
   #  ...
   return train_loader,test_loader

那么__getitem__方法代码就可以保持不变

    def __getitem__(self, index):
            """
            步骤三:实现__getitem__方法,定义指定index时如何获取数据
            """
            data, label = self.imgs[index], self.labels[index]
            data = np.reshape(data, [28, 28]).astype("float32")
            ## ⚠️上面此行
            label = np.reshape(label,[1]).astype("int64")
        ## 修改如下代码更健壮
            if self.transform:
                data = self.transform(data)
            return data,label

这样正常训练 loss 也下降了。
还有一个问题想请教一下,多维数组的形状你写的是[1,28,28]
data = np.reshape(data, [1,28, 28]).astype("float32")
我测试了一下,好像形状写[28,28]
data = np.reshape(data, [28, 28]).astype("float32")
loss 也是能下降的,那究竟哪种写法才对,我要怎么验证比较好

又测试了一下,你写的多维数组的形状 [1,28,28]应该是才是对的

data = np.reshape(data, [1,28, 28]).astype("float32")

代码里的归一化器,其实就是为了帮我将不标准的格式转成标准的CHW格式,
他会判断如果格式为[28,28],会帮我自动多加一维度
transform = Normalize(mean=[0], std=[1], data_format='CHW')
如果我去掉这个归一化器,直接返回[28,28]就会报错,但是返回[1,28,28]就没问题

不太清除paddle的计算方法,日常使用pytorch,主要是取决于你的目的,你设计的输入和输出通道数。代码写的鲁棒可以适应不同类型的代码,还是规范明确一点好,尽可能减少隐操作。