【人工智能】ResNet与MobileNet实现猫狗识别

1 实验介绍

本次实验所使用的数据集为Cifar-10。该数据集共有60000张彩色图片,按照5:1的比例划分为训练集和测试集,每张图片的尺寸为32 x 32,共包含10大类别,每个类别含有6000张图片。最终进行预测时,只进行猫与狗两类图片的识别。
在这里插入图片描述

2 数据准备

2.1 导入所需要的包

# 
# 导入需要的包
import paddle
import numpy as np
from PIL import Image
import argparse 
import matplotlib.pyplot as plt
import os
import sys 
import pickle
from paddle.vision.transforms import Compose, Resize, RandomVerticalFlip, RandomHorizontalFlip, ColorJitter, RandomRotation, ToTensor, Normalize
from paddle.vision.datasets import Cifar10
import paddle.vision.transforms as T
from paddle.nn import CrossEntropyLoss
from paddle import nn
from paddle import metric as M
from paddle.io import DataLoader, Dataset
from paddle.nn import functional as F
from paddle.optimizer import Adam
from paddle.optimizer.lr import NaturalExpDecay
from paddle.optimizer.lr import PiecewiseDecay
from paddle.optimizer.lr import CosineAnnealingDecay

from paddle.metric import Accuracy
from visualdl import LogWriter
from paddle.vision.models import resnet18  # 也可以选择其他模型
print("本教程基于Paddle的版本号为:"+paddle.__version__)

2.2 数据预处理

创建类别字典,将标签与类别名对应

label_dict = {
'0':'airplane',
'1':'automobile',
'2':'bird',
'3':'cat',
'4':'deer',
'5':'dog',
'6':'frog',
'7':'horse',
'8':'ship',
'9':'truck'
}

  使用飞桨自带的接口导入Cifar-10数据集,分别定义训练集和测试集的数据处理方式。使用ToTensor 接口对数据进行转换,以匹配数据馈送格式,同时对图像的RGB 三通道分别进行标准化。为提高模型的泛化能力,降低过拟合,还需对训练集进行数据增强,使用到的方法包括随机水平翻转、随机垂直翻转、随机旋转、随机调整图像亮度等。注意到,实验所使用的预训练模型的输入尺寸为32 x 32,在后续的实验中,分别尝试将原始大小的图片与经过尺寸调整后的图片(224x224)导入模型进行训练。

#最开始使用32x32进行训练,后续为了进一步提高准确率,将图像尺寸修改为了224x224
train_transforms = T.Compose([
T.Resize(32),
T.RandomVerticalFlip(),
T.RandomHorizontalFlip(),
T.ColorJitter(0.4, 0.4, 0.4, 0.4),
T.RandomRotation(180),
T.ToTensor(),
T.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
eval_transforms = T.Compose([
T.Resize(32),
T.ToTensor(),
T.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
train_dataset = Cifar10(mode='train', transform=train_transforms)
eval_dataset = Cifar10(mode='test', transform=eval_transforms)

使用如下代码输出数据集的形状和标签

print('=============train_dataset =============')
#输出数据集的形状和标签
print(train_dataset.__getitem__(1)[0].shape,train_dataset.__getitem__(1)[1])
#输出数据集的长度
print(train_dataset.__len__())
print('=============eval_dataset =============')
#输出数据集的形状和标签
for data, label in eval_dataset:
    print(data.shape, label)
    break
#输出数据集的长度
print(eval_dataset.__len__())

得到训练集和测试集的大小:
![[Pasted image 20231228202050.png]]
至此,数据准备工作完成,可以开始进行后续的训练

3 网络配置

3.1 ResNet-18模型

CNN网络模型

  在CNN模型中,卷积神经网络能够更好的利用图像的结构信息。下面定义了一个较简单的卷积神经网络。显示了其结构:输入的二维图像,先经过三次卷积层、池化层和Batchnorm,再经过全连接层,最后使用softmax分类作为输出层。

Image

池化是非线性下采样的一种形式,主要作用是通过减少网络的参数来减小计算量,并且能够在一定程度上控制过拟合。通常在卷积层的后面会加上一个池化层。paddlepaddle池化默认为最大池化。是用不重叠的矩形框将输入层分成不同的区域,对于每个矩形框的数取最大值作为输出

BatchNorm2D顾名思义是对每batch个数据同时做一个norm。作用就是在深度神经网络训练过程中使得每一层神经网络的输入保持相同分布的.

  本实验最初选定resnet18模型,残差神经网络(ResNet)是由微软研究院的何恺明、张祥雨、任少卿、孙剑等人提出的。ResNet 在2015 年的ILSVRC(ImageNet Large Scale Visual Recognition Challenge)中取得了冠军,出自论文Deep Residual Learning for Iage Recognitiono该模型提出了残差学习的思想,改善了深度网络的退化问题,并针对退化现象发明了 “快捷连接(Shortcut connection)”,极大的消除了深度过大的神经网络训练困难问题。

  • ResidualBlock 类: 定义了 ResNet 中的残差块。每个残差块包含两个卷积层和 Batch Normalization 层,以及残差连接。当输入和输出通道数不同时,通过一个额外的卷积层来调整维度。

  • ResNet 类: 定义了整个 ResNet 模型。包含一个输入卷积层和四个阶段(每个阶段包含若干个残差块),最后接全连接层输出分类结果。

  • ResNet18 函数: 返回一个使用 ResNet 残差块构建的 ResNet-18 模型。

from paddle.nn import Conv2D, Sequential, BatchNorm2D, ReLU, Linear
import paddle.nn.functional as F

class ResidualBlock(paddle.nn.Layer):
    def __init__(self, inchannel, outchannel, stride=1):
        super(ResidualBlock, self).__init__()
        self.left = Sequential(
            Conv2D(inchannel, outchannel, kernel_size=3, stride=stride, padding=1, bias_attr=False),
            BatchNorm2D(outchannel),
            ReLU(),
            Conv2D(outchannel, outchannel, kernel_size=3, stride=1, padding=1, bias_attr=False),
            BatchNorm2D(outchannel)
        )
        self.shortcut = Sequential()
        if stride != 1 or inchannel != outchannel:
            self.shortcut = Sequential(
                Conv2D(inchannel, outchannel, kernel_size=1, stride=stride, bias_attr=False),
                BatchNorm2D(outchannel)
            )

    def forward(self, x):
        out = self.left(x)
        out += self.shortcut(x)
        out = F.relu(out)
        return out

class ResNet(paddle.nn.Layer):
    def __init__(self, ResidualBlock, num_classes=10):
        super(ResNet, self).__init__()
        self.inchannel = 64
        self.conv1 = Sequential(
            Conv2D(3, 64, kernel_size=3, stride=1, padding=1, bias_attr=False),
            BatchNorm2D(64),
            ReLU(),
        )
        self.layer1 = self.make_layer(ResidualBlock, 64,  2, stride=1)
        self.layer2 = self.make_layer(ResidualBlock, 128, 2, stride=2)
        self.layer3 = self.make_layer(ResidualBlock, 256, 2, stride=2)
        self.layer4 = self.make_layer(ResidualBlock, 512, 2, stride=2)
        self.fc = Linear(512, num_classes)

    def make_layer(self, block, channels, num_blocks, stride):
        strides = [stride] + [1] * (num_blocks - 1)   
        layers = []
        for stride in strides:
            layers.append(block(self.inchannel, channels, stride))
            self.inchannel = channels
        return Sequential(*layers)

    def forward(self, x):
        out = self.conv1(x)
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = F.avg_pool2d(out, 4)
        out = paddle.reshape(out, [out.shape[0], -1])
        out = self.fc(out)
        return out
def ResNet18():

    return ResNet(ResidualBlock)

使用paddle.summary查看resnet18的模型结构和部分参数如下:
在这里插入图片描述

后续本实验对resnet模型进行进一步调整,引入其他resnet模型,其他resnet模型的参数结构如下:
在这里插入图片描述

3.2 ResNet-101模型

  ResNet18是比较浅的网络,适合于小规模数据集的训练;ResNet101则是比较深的网络,适合于大规模数据集的训练。本次实验在调整ResNet-18的基础上进一步选择使用深度为101的ResNet101进行训练。同时,基于迁移学习的思想,引入预训练模型的同时,加载其在ImageNet上进行训练所得到的参数,获得预训练权重,由于本次实验是大量数据集,没有设置冻结层,在大规模数据集上,有足够的数据可以微调所有参数,以适应特定任务。这样可以更好地调整模型以适应特定任务的特征。以获得更好的结果。
  这里使用了paddlepaddle封装的ResNet101模型,直接加载进行训练。

resnet101 = paddle.vision.models.resnet101(num_classes=args.num_classes,pretrained=True)

3.3 MobileNetV3模型

MobileNetV3 是由 google 团队在 2019 年提出的,其原始论文为 Searching for MobileNetV3
MobileNetV3 有以下三点值得注意:

  • 更新 Block (bneck)
  • 使用 NAS 搜索参数 (Neural Architecture Search)
  • 重新设计耗时层结构
    在原论文摘要中,作者提到在 ImageNet 分类任务相比于 MobileNetV2 版本,正确率上升了 3.2%,计算延时还降低了 20%。
    整体来说MobileNetV3有两大创新点
    (1)互补搜索技术组合:由资源受限的NAS执行模块级搜索,NetAdapt执行局部搜索。
    (2)网络结构改进:将最后一步的平均池化层前移并移除最后一个卷积层,引入h-swish激活函数。
h-swish激活函数

作者发现swish激活函数能够有效提高网络的精度。然而,swish的计算量太大了。作者提出h-swish(hard version of swish)如下所示:
h − s w i s h [ x ] = x R e L U 6 ( x ) + 3 6 h-swish[x]=x\frac{ReLU6(x)+3}{6} hswish[x]=x6ReLU6(x)+3
h-swish如图所示:
在这里插入图片描述

  这里使用了paddlepaddle封装的MobileNetV3模型,直接加载模型进行训练。

from paddle.vision.models import mobilenet_v3_large
mobilenet_v3_large = mobilenet_v3_large(num_classes=args.num_classes,
pretrained=True)

4 模型训练

4.1 训练准备

首先,使用Python内置的Argparse 库进行参数解析,为参数调用提供统一的接口,也便于后续将参数信息写入日志。

def parse_args():
    """
    Add and load arguments.
    """
    parser = argparse.ArgumentParser(description='Hyperparameters')
    parser.add_argument('--use_gpu', type=bool, default=True)
    parser.add_argument('--img_size', type=int, default=224)
    parser.add_argument('--num_classes', type=int, default=10)
    parser.add_argument('--num_workers', type=int, default=4)
    parser.add_argument('--epoch', type=int, default=20)
    parser.add_argument('--batch_size', type=int, default=64)
    parser.add_argument('--weight_decay', type=float, default=0.0005)
    parser.add_argument('--learning_rate', type=float, default=0.0001)
    parser.add_argument('--patience', type=int, default=5)
    parser.add_argument('--save_dir', type=str, default='work')
    return parser.parse_args(args=[])
args = parse_args()

通过继承 paddle.callbacks 接口定义日志回调类 WriteLogs ,记录每次训练时的超参数信息,以及训练过程中每个epoch的训练结果,包括训练的开始与结束时间、训练集和测试集各自的损失与准确率。

class WriteLogs(paddle.callbacks.Callback):
    """
    Write logs during each epoch
    """
    def __init__(self, model_save_dir, file_name):
        self.file_path = os.path.join(model_save_dir, file_name)
        self.time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        self.logWriter = open(self.file_path, 'a')
        self.logWriter.write(str(args)+'\n')
        self.logWriter.write('Start training: '+self.time+'\n')
        self.logWriter.flush()
    def on_epoch_end(self, epoch, logs=None):
        self.epoch = epoch
        x = 'epoch: {}, train_loss: {}, '\
        'acc_top1: {:.4f}, acc_top5:{:.4f}, '.format(
        self.epoch+1, logs['loss'], logs['acc_top1'], logs['acc_top5']
        )
        self.logWriter.write(x)
    def on_eval_end(self, logs=None):
        x = 'eval_loss: {}, acc_top1: {:.4f}, acc_top5:{:.4f}'.format(
        logs['loss'], logs['acc_top1'], logs['acc_top5']
        )
        self.logWriter.write(x+'\n')
        self.logWriter.flush()
    def on_train_end(self, logs=None):
        self.time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        self.logWriter.write('End training'+self.time+'\n\n')
        self.logWriter.flush()
        self.logWriter.close()

接下来定义模型的训练过程。指定模型训练结果的保存路径,配置训练设备为GPU,并使用paddle.Model 接口封装模型;采用的优化器为AdamW ,同时使用CosineAnnealingDecay 作为学习率衰减策略;设置训练过程中的回调策略,以实现早停、训练过程可视化以及训练结果记录的功能;配置模型训练所需的部件,设置损失函数为交叉熵,评价指标为准确率;最后,启动训练,并保存模型训练结果。

def train(model, model_name):
    """
    Args:
    model: Input a model class.
    model_name (str): The name of the model.
    """
    model_save_dir = os.path.join(args.save_dir, model_name)
    file_name = model_name + '_logs.txt'
    # 创建文件夹
    if not os.path.exists(model_save_dir):
     os.mkdir(model_save_dir)
    # 配置训练设备
    if args.use_gpu:
     paddle.device.set_device('gpu:0')
    else:
     paddle.device.set_device('cpu')
# 封装模型
model = paddle.Model(resnet101)

模型训练部分代码如下:

from visualdl import LogWriter
import datetime
import time
current_datetime = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
file_name = f"logs_{current_datetime}.txt"
# 设置学习率衰减及优化策略
learning_rate = CosineAnnealingDecay(learning_rate=args.learning_rate,
                                    T_max=args.epoch,
                                    verbose=0)
optim = paddle.optimizer.AdamW(learning_rate=learning_rate,
                            weight_decay=args.weight_decay,
                            parameters=model.parameters())
# 设置可视化回调、停止训练回调、学习率回调及日志记录回调
visualDL = paddle.callbacks.VisualDL(log_dir=os.path.join(args.save_dir, 'VisualDL'))
earlyStop = paddle.callbacks.EarlyStopping(monitor='acc_top1',patience=args.patience)
lrScheduler = paddle.callbacks.LRScheduler(by_step=True,by_epoch=False)
writeLogs = WriteLogs(model_save_dir=args.save_dir,file_name=file_name)
callbacks = [visualDL, earlyStop, lrScheduler, writeLogs]
# 配置模型
model.prepare(optim,
    nn.CrossEntropyLoss(),
    paddle.metric.Accuracy(topk=(1, 5)))

4.2 训练过程

4.2.1 ResNet-18的训练过程
model.fit(train_dataset,            # 训练数据集
          eval_dataset,            # 评估数据集
          epochs=60,            # 总的训练轮次
          batch_size = 128,    # 批次计算的样本量大小
          shuffle=True,             # 是否打乱样本集
          verbose=1,                # 日志展示格式
          callbacks=[visualdl])     # 回调函数使用

训练过程可视化:
验证集:
在这里插入图片描述

训练集:
在这里插入图片描述

最终模型的训练Top1的准确度达到92.09%

4.2.2 ResNet-101的训练过程

  由于Cifar-10数据集的图片大小为32 x 32,经查阅相关资料后得知,若模型在输出时使用全局平均池化,即使输入图片的尺寸并非224 x 224、模型也能够进行训练。参考往届学长的经验以及PytorchCifar100项目发现,直接将原始大小的图片输入模型也能够得到较好的训练结果。因此我首先尝试使用原始大小的图片进行训练。
  起初,输入图像的大小为32 x 32时,训练结果一般,为作进一步探索,选择将图片尺寸更改为224 x 224后进行训练。增大图片尺寸后,训练耗时明显增加,但训练结果符合预期。进行多轮调参后,模型的最高Top-1准确率达到93.41% 。模型调参过程如下表所示,训练过程的可视化结果如下图所示。

model.fit(train_dataset,
    eval_dataset,
    epochs=args.epoch,
    batch_size=args.batch_size,
    save_dir=os.path.join(args.save_dir, 'model'),
    save_freq=args.epoch,
    verbose=1,
    shuffle=True,
    num_workers=args.num_workers,
    callbacks=callbacks)

模型的调参过程如下:
在这里插入图片描述

训练过程可视化:
在这里插入图片描述

4.2.3 MobileNetV3的训练过程

  接下来尝试使用轻量级网络进行训练。相较于重量级网络,轻量级网络拥有更少的参数、更低的计算量与更短的训练时间,更适合在移动端进行部署。MobileNetV3是轻量级网络中的佼佼者,该模型出自论文Searching for MobileNetV3,拥有Large和Small两个版本。本实验选择使用Large版本,同时加载其在ImageNet上的预训练参数进行训练。

model.fit(train_dataset,
    eval_dataset,
    epochs=args.epoch,
    batch_size=args.batch_size,
    save_dir=os.path.join(args.save_dir, 'model'),
    save_freq=args.epoch,
    verbose=1,
    shuffle=True,
    num_workers=args.num_workers,
    callbacks=callbacks)

  同样地,首先尝试导入原始大小的图像进行训练,结果未达预期,因而选择将输入图片的尺寸放大到224 x 224。同样在MobileNetV3上进行多组调参实验,得到模型的最佳Top-1准确率为93.03% 具体的调参过程如下表所示,训练过程的可视化结果如下图所示。
模型的调参过程如下:
在这里插入图片描述

训练过程可视化:
在这里插入图片描述

5 模型预测

  从本地导入照片输入模型进行预测。首先,通过继承 paddle.io.Dataset 类定义预测数据读取类,以便于模型读取预测数据,并使用 ToTensor 接口对数据进行转换,同时对图像的RGB二通道分别进行标准化。

class PredictDataset(paddle.io.Dataset):
    """
    Define the face dataset class.
    Args:
    data_list (list)
    """
    def __init__(self, data_list):
        """
        Initialize function. Randomly shuffle the data list.
        """
        super(PredictDataset, self).__init__()
        self.data_list = data_list
        self.transforms = T.Compose([
        T.ToTensor(),
        T.Normalize(mean=[0.485, 0.456, 0.406],
        std=[0.229, 0.224, 0.225])
        ])
    def __getitem__(self, idx):
        """
        Get a sample according to the index.
        """
        img, label = self.data_list[idx]
        img = self.transforms(img)
        return img, np.array(label, dtype='int64')
    def __len__(self):
        """Return the length of the data list.
        """
        return len(self.data_list)

  接下来定义预测函数。首先进行模型的封装以及最佳参数的导入,并配置模型预测所需的部件,再依据输入路径读取图像数据,将图像尺寸转化为224 224,展示图片,依据文件名(此处设置预测图片的文件名均以dog或cat开头) 为图片设置标签,最后使用 PredictDataset 类对图像与标签进行封装,传入模型进行预测并打印模型的预测结果。

def predict(img_save_dir, model):
    model = paddle.Model(resnet101)
    model.load(os.path.join('work', 'model/best_model'))
    model.prepare(loss=nn.CrossEntropyLoss(), metrics=paddle.metric.Accuracy(topk=(1, 5)))
    
    img_dirs = os.listdir(img_save_dir)
    
    # 设置九宫格的行数和列数
    rows = 3
    cols = 3
    fig, axs = plt.subplots(rows, cols, figsize=(12, 12))
    
    for i, img_dir in enumerate(img_dirs):
        if img_dir == '.ipynb_checkpoints':
            continue
        img_path = os.path.join(img_save_dir, img_dir)
        img = Image.open(img_path)
        
        # 图像预处理
        if img.mode != 'RGB':
            img = img.convert('RGB')
        img = img.resize((224, 224), Image.BICUBIC)
        
        # 显示图像在九宫格中的位置
        row_index = i // cols
        col_index = i % cols
        axs[row_index, col_index].imshow(img)
        axs[row_index, col_index].axis('off')  # 关闭坐标轴
        
        label = '3' if 'cat' in img_dir else '5'
        img_list = [[img, label]]
        img_dataset = PredictDataset(img_list)
        
        # 进行预测
        result = model.predict(img_dataset, verbose=0)
        predict_label = label_dict[str(result[0][0][0].argmax())]
        axs[row_index, col_index].set_title(f'Predicted: {predict_label}', fontsize=10)

    plt.tight_layout()
    plt.savefig("./data/result/result2.jpg")  # 保存图像
    plt.show()

# 调用函数
predict(img_save_dir='./data/test', model=resnet101)

ResNet101模型预测结果如下:
在这里插入图片描述

MobileNetV3的结果如下:
在这里插入图片描述

根据结果可以看出,无论是MobileNetV3还是ResNet101都对数据有很强的泛化能力,在选取的测试集是表情包时,仍然能对结果做出正确的判断,说明分类能力已经达到了预期结果,模型的泛化性都很好。

6 实验总结

不同模型取得最佳结果时的各项指标:
在这里插入图片描述

本次实验中,最佳Top-1准确率由ResNet101达到,而MobileNetV3的准确率虽稍逊于ResNet101,其存储开销有较大幅度领先。

7 实验心得

  • 在吸取了过往的各类经验教训后,本次实验的总体过程相对顺利,完成度也达到了历次实验中的最佳。
    -本次实验分别使用两种方法加载模型,第一种方法是自己编写模型结构再进行运行,灵活性强,但是复杂度高,相对费时,第二种方法是直接调用paddle库里封装的模型,灵活性差,但是很方便。在使用第一种方法时,我使用的图片尺寸是32x32,但是epoch的次数达到了60,因为在运行初期,模型的效果并不是很好,所以我通过增加epoch来增强训练的准确度,在使用第二种方法运行resnet时,我想到将图片的尺寸扩展为224x224,虽然epoch的次数减小但是运行所花费的时间大幅度上升,运行成本变高,准确度提升度也不是很高,这是此次实验的遗憾。
  • 在多次反复实验和调参过程中,我很希望能有一个自动调参工具方便我使用,而不是每次都手动调参,需要等待结果生成,实时监控,所谓我认为在之后的实验中我应该使用代码完成自动化调参。
  • 此次实验数据数目很大但是结过并没有之前的准确率高,让我感受到了猫脸狗脸识别的难度会相对较大。
  • 13
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值