昇思MindSpore 应用学习-ResNet50迁移学习-CSDN

日期

心得

昇思MindSpore 应用学习-ResNet50迁移学习 (AI 代码解析)

在实际应用场景中,由于训练数据集不足,所以很少有人会从头开始训练整个网络。普遍的做法是,在一个非常大的基础数据集上训练得到一个预训练模型,然后使用该模型来初始化网络的权重参数或作为固定特征提取器应用于特定的任务中。本章将使用迁移学习的方法对ImageNet数据集中的狼和狗图像进行分类。
迁移学习详细内容见Stanford University CS231n

数据准备

下载数据集

下载案例所用到的狗与狼分类数据集,数据集中的图像来自于ImageNet,每个分类有大约120张训练图像与30张验证图像。使用download接口下载数据集,并将下载后的数据集自动解压到当前目录下。

from download import download

# 定义数据集的URL
dataset_url = "https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/datasets/intermediate/Canidae_data.zip"

# 使用download函数下载数据集
# 参数解释:
# - dataset_url: 数据集的URL
# - "./datasets-Canidae": 下载文件的保存路径
# - kind="zip": 指定下载的文件类型为zip
# - replace=True: 如果目标路径已存在同名文件,则替换它
download(dataset_url, "./datasets-Canidae", kind="zip", replace=True)
  1. 导入模块:
from download import download

这里导入了download模块中的download函数。这个函数通常用于下载文件,支持多种文件类型和选项。

  1. 定义数据集URL:
dataset_url = "https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/datasets/intermediate/Canidae_data.zip"

这里定义了数据集的URL,指向一个zip文件。

  1. 调用download函数:
download(dataset_url, "./datasets-Canidae", kind="zip", replace=True)

这里调用了download函数来下载数据集。函数的参数解释如下:

  • dataset_url: 数据集的URL,指定要下载的文件。
  • "./datasets-Canidae": 下载文件的保存路径。如果路径不存在,函数会尝试创建它。
  • kind="zip": 指定下载的文件类型为zip。这告诉函数如何处理下载的文件。
  • replace=True: 如果目标路径已存在同名文件,则替换它。这确保了每次下载都是最新的文件。
  • download函数:
    • 参数:
      • url: 要下载的文件的URL。
      • path: 下载文件的保存路径。
      • kind: 下载文件的类型,例如"zip""tar"等。
      • replace: 布尔值,如果为True,则替换目标路径中已存在的同名文件。
    • 功能: 从指定的URL下载文件,并保存到指定的路径。根据kind参数处理不同类型的文件,并根据replace参数决定是否替换已存在的文件。

加载数据集

狼狗数据集提取自ImageNet分类数据集,使用mindspore.dataset.ImageFolderDataset接口来加载数据集,并进行相关图像增强操作。
首先执行过程定义一些输入:

batch_size = 18                             # 批量大小
image_size = 224                            # 训练图像的尺寸
num_epochs = 10                             # 训练周期数
lr = 0.001                                  # 学习率
momentum = 0.9                              # 动量
workers = 4                                 # 并行线程个数

import mindspore as ms
import mindspore.dataset as ds
import mindspore.dataset.vision as vision

# 数据集目录路径
data_path_train = "./datasets-Canidae/data/Canidae/train/"
data_path_val = "./datasets-Canidae/data/Canidae/val/"

# 创建训练数据集函数
def create_dataset_canidae(dataset_path, usage):
    """数据加载"""
    data_set = ds.ImageFolderDataset(dataset_path,
                                     num_parallel_workers=workers,
                                     shuffle=True,)

    # 数据增强操作
    mean = [0.485 * 255, 0.456 * 255, 0.406 * 255]
    std = [0.229 * 255, 0.224 * 255, 0.225 * 255]
    scale = 32

    if usage == "train":
        # 定义训练数据集的映射操作
        trans = [
            vision.RandomCropDecodeResize(size=image_size, scale=(0.08, 1.0), ratio=(0.75, 1.333)),
            vision.RandomHorizontalFlip(prob=0.5),
            vision.Normalize(mean=mean, std=std),
            vision.HWC2CHW()
        ]
    else:
        # 定义验证数据集的映射操作
        trans = [
            vision.Decode(),
            vision.Resize(image_size + scale),
            vision.CenterCrop(image_size),
            vision.Normalize(mean=mean, std=std),
            vision.HWC2CHW()
        ]

    # 数据映射操作
    data_set = data_set.map(
        operations=trans,
        input_columns='image',
        num_parallel_workers=workers)

    # 批量操作
    data_set = data_set.batch(batch_size)

    return data_set

# 加载训练数据集
dataset_train = create_dataset_canidae(data_path_train, "train")
step_size_train = dataset_train.get_dataset_size()

# 加载验证数据集
dataset_val = create_dataset_canidae(data_path_val, "val")
step_size_val = dataset_val.get_dataset_size()
  1. 参数定义:
batch_size = 18                             # 批量大小
image_size = 224                            # 训练图像的尺寸
num_epochs = 10                             # 训练周期数
lr = 0.001                                  # 学习率
momentum = 0.9                              # 动量
workers = 4                                 # 并行线程个数

这些参数用于设置训练的基本配置,包括批量大小、图像尺寸、训练周期数、学习率、动量和并行线程个数。

  1. 导入模块:
import mindspore as ms
import mindspore.dataset as ds
import mindspore.dataset.vision as vision

这里导入了MindSpore框架及其数据集处理和图像处理模块。

  1. 数据集目录路径:
data_path_train = "./datasets-Canidae/data/Canidae/train/"
data_path_val = "./datasets-Canidae/data/Canidae/val/"

定义了训练和验证数据集的目录路径。

  1. 创建训练数据集函数:
def create_dataset_canidae(dataset_path, usage):
    """数据加载"""
    data_set = ds.ImageFolderDataset(dataset_path,
                                     num_parallel_workers=workers,
                                     shuffle=True,)

定义了一个函数create_dataset_canidae,用于创建数据集。这里使用ds.ImageFolderDataset加载指定路径下的图像数据集,并进行并行处理和随机打乱。

  1. 数据增强操作定义:
mean = [0.485 * 255, 0.456 * 255, 0.406 * 255]
std = [0.229 * 255, 0.224 * 255, 0.225 * 255]
scale = 32

定义了用于图像标准化的数据增强参数meanstd,以及用于调整图像尺寸的scale

  1. 定义映射操作:
if usage == "train":
    trans = [
        vision.RandomCropDecodeResize(size=image_size, scale=(0.08, 1.0), ratio=(0.75, 1.333)),
        vision.RandomHorizontalFlip(prob=0.5),
        vision.Normalize(mean=mean, std=std),
        vision.HWC2CHW()
    ]
else:
    trans = [
        vision.Decode(),
        vision.Resize(image_size + scale),
        vision.CenterCrop(image_size),
        vision.Normalize(mean=mean, std=std),
        vision.HWC2CHW()
    ]

根据usage参数(训练或验证)定义不同的数据增强和处理操作。训练数据集进行随机裁剪、水平翻转和标准化,验证数据集进行解码、调整尺寸、中心裁剪和标准化。

  1. 数据映射操作:
data_set = data_set.map(
    operations=trans,
    input_columns='image',
    num_parallel_workers=workers)

使用map函数将定义好的映射操作应用到数据集的图像列,并进行并行处理。

  1. 批量操作:
data_set = data_set.batch(batch_size)

使用batch函数将处理好的数据集按批量大小进行分组。

  1. 加载训练和验证数据集:
dataset_train = create_dataset_canidae(data_path_train, "train")
step_size_train = dataset_train.get_dataset_size()

dataset_val = create_dataset_canidae(data_path_val, "val")
step_size_val = dataset_val.get_dataset_size()

调用create_dataset_canidae函数加载训练和验证数据集,并获取各自的数据集大小。

  • ds.ImageFolderDataset:
    • 参数:
      • dataset_path: 数据集的目录路径。
      • num_parallel_workers: 并行线程个数。
      • shuffle: 是否对数据进行随机打乱。
    • 功能: 从指定路径加载图像数据集,并支持并行处理和随机打乱。
  • map函数:
    • 参数:
      • operations: 要应用的数据增强和处理操作列表。
      • input_columns: 输入列名,指定操作应用于图像列。
      • num_parallel_workers: 并行线程个数。
    • 功能: 将数据增强和处理操作应用到数据集的指定列,并进行并行处理。
  • batch函数:
    • 参数:
      • batch_size: 批量大小。
    • 功能: 将数据集按指定批量大小进行分组,以便于批量处理和训练。

数据集可视化
mindspore.dataset.ImageFolderDataset接口中加载的训练数据集返回值为字典,用户可通过 create_dict_iterator 接口创建数据迭代器,使用 next 迭代访问数据集。本章中 batch_size 设为18,所以使用 next 一次可获取18个图像及标签数据。

data = next(dataset_train.create_dict_iterator())
images = data["image"]
labels = data["label"]

print("Tensor of image", images.shape)
print("Labels:", labels)
  1. 获取数据集的下一个批次:
data = next(dataset_train.create_dict_iterator())

使用create_dict_iterator方法创建一个字典迭代器,然后调用next函数获取数据集的下一个批次。这个批次包含图像和标签数据。

  1. 提取图像和标签:
images = data["image"]
labels = data["label"]

从获取的数据字典中提取图像和标签。data["image"]包含图像数据,data["label"]包含对应的标签数据。

  1. 打印图像张量的形状和标签:
print("Tensor of image", images.shape)
print("Labels:", labels)

打印图像张量的形状和标签。images.shape显示图像张量的维度,通常是(batch_size, channels, height, width)labels显示对应的标签数据。

  • create_dict_iterator:
    • 功能: 创建一个字典迭代器,用于遍历数据集的批次。每个批次以字典形式返回,包含图像和标签等数据。
  • next函数:
    • 功能: 从迭代器中获取下一个批次的数据。通常用于获取数据集的第一个批次或进行迭代处理。

通过这段代码,你可以查看数据集的第一个批次的图像和标签数据,了解数据的形状和内容。这对于调试和验证数据集加载和处理是否正确非常有用。

对获取到的图像及标签数据进行可视化,标题为图像对应的label名称。

import matplotlib.pyplot as plt
import numpy as np

# class_name对应label,按文件夹字符串从小到大的顺序标记label
class_name = {0: "dogs", 1: "wolves"}

plt.figure(figsize=(5, 5))
for i in range(4):
    # 获取图像及其对应的label
    data_image = images[i].asnumpy()
    data_label = labels[i]
    # 处理图像供展示使用
    data_image = np.transpose(data_image, (1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    data_image = std * data_image + mean
    data_image = np.clip(data_image, 0, 1)
    # 显示图像
    plt.subplot(2, 2, i+1)
    plt.imshow(data_image)
    plt.title(class_name[int(data_label.asnumpy())])
    plt.axis("off")

plt.show()
  1. 导入模块:
import matplotlib.pyplot as plt
import numpy as np

导入matplotlib.pyplot用于绘制图像,导入numpy用于数值计算。

  1. 定义类别名称:
class_name = {0: "dogs", 1: "wolves"}

定义一个字典,将类别标签映射到类别名称。根据文件夹字符串从小到大的顺序标记标签。

  1. 创建绘图窗口:
plt.figure(figsize=(5, 5))

创建一个图形窗口,大小为5x5英寸。

  1. 循环显示图像:
for i in range(4):
    # 获取图像及其对应的label
    data_image = images[i].asnumpy()
    data_label = labels[i]

使用一个循环从批次数据中获取前4张图像及其对应的标签。images[i].asnumpy()将图像张量转换为NumPy数组。

  1. 处理图像以供展示:
data_image = np.transpose(data_image, (1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
data_image = std * data_image + mean
data_image = np.clip(data_image, 0, 1)
  • np.transpose(data_image, (1, 2, 0)): 将图像数据的维度从(channels, height, width)转换为(height, width, channels),以便于显示。
  • meanstd: 定义用于标准化的均值和标准差。
  • data_image = std * data_image + mean: 反标准化图像数据,将其转换回原始图像范围。
  • data_image = np.clip(data_image, 0, 1): 将图像数据限制在0到1之间。
  1. 绘制图像:
plt.subplot(2, 2, i+1)
plt.imshow(data_image)
plt.title(class_name[int(data_label.asnumpy())])
plt.axis("off")
  • plt.subplot(2, 2, i+1): 创建一个2x2的子图网格,每个子图显示一个图像。
  • plt.imshow(data_image): 显示图像。
  • plt.title(class_name[int(data_label.asnumpy())]): 设置图像的标题,显示类别名称。
  • plt.axis("off"): 关闭坐标轴。
  1. 显示图像:
plt.show()

显示绘制的图像窗口。

总结

这段代码通过从训练数据集中提取前4张图像及其标签,并进行反标准化处理后,使用matplotlib将图像显示出来,同时在每张图像上方显示对应的类别名称。这有助于可视化和验证数据预处理以及数据集加载是否正确。

训练模型

本章使用ResNet50模型进行训练。搭建好模型框架后,通过将pretrained参数设置为True来下载ResNet50的预训练模型并将权重参数加载到网络中。

构建Resnet50网络

from typing import Type, Union, List, Optional
from mindspore import nn, train
from mindspore.common.initializer import Normal

# 初始化权重和gamma参数的均值和标准差
weight_init = Normal(mean=0, sigma=0.02)
gamma_init = Normal(mean=1, sigma=0.02)
  1. 导入模块:
from typing import Type, Union, List, Optional
from mindspore import nn, train
from mindspore.common.initializer import Normal

导入相关模块和类:

  • Type, Union, List, Optional:类型提示模块。
  • nn, train:MindSpore中的神经网络构建和训练模块。
  • Normal:MindSpore中的正态分布初始化器,用于参数初始化。
  1. 初始化参数:
weight_init = Normal(mean=0, sigma=0.02)
gamma_init = Normal(mean=1, sigma=0.02)

使用Normal初始化器初始化权重和γ参数:

  • weight_init = Normal(mean=0, sigma=0.02):创建一个正态分布初始化器,均值为0,标准差为0.02,用于初始化网络权重。

  • gamma_init = Normal(mean=1, sigma=0.02):创建一个正态分布初始化器,均值为1,标准差为0.02,用于初始化BatchNorm层的γ参数。

  • Normal:

    • 参数:
      • mean: 正态分布的均值。
      • sigma: 正态分布的标准差。
    • 功能: 创建一个正态分布初始化器,用于初始化神经网络的权重或其他参数。

总结

这段代码旨在为神经网络中的权重和BatchNorm层的γ参数提供初始化方法。通过使用MindSpore的Normal初始化器,可以确保权重和γ参数按照指定的正态分布进行初始化,从而有助于稳定和加速神经网络的训练过程。

class ResidualBlockBase(nn.Cell):
    expansion: int = 1  # 最后一个卷积核数量与第一个卷积核数量相等

    def __init__(self, in_channel: int, out_channel: int,
                 stride: int = 1, norm: Optional[nn.Cell] = None,
                 down_sample: Optional[nn.Cell] = None) -> None:
        super(ResidualBlockBase, self).__init__()
        if not norm:
            self.norm = nn.BatchNorm2d(out_channel)
        else:
            self.norm = norm

        self.conv1 = nn.Conv2d(in_channel, out_channel,
                               kernel_size=3, stride=stride,
                               weight_init=weight_init)
        self.conv2 = nn.Conv2d(out_channel, out_channel,
                               kernel_size=3, pad_mode='same', weight_init=weight_init)
        self.relu = nn.ReLU()
        self.down_sample = down_sample

    def construct(self, x):
        """ResidualBlockBase construct."""
        identity = x  # shortcuts分支

        out = self.conv1(x)  # 主分支第一层:3*3卷积层
        out = self.norm(out)
        out = self.relu(out)
        out = self.conv2(out)  # 主分支第二层:3*3卷积层
        out = self.norm(out)

        if self.down_sample is not None:
            identity = self.down_sample(x)
        out += identity  # 输出为主分支与shortcuts之和
        out = self.relu(out)

        return out

这段代码定义了一个残差网络块(Residual Block),它是深度残差网络(ResNet)的基础组成部分。残差块可以让网络更深,而不会导致训练困难。

  1. 类定义:
    • expansion: 一个类属性,表示最后一个卷积层输出通道数与第一个卷积层输出通道数的比例。在这个ResidualBlockBase类中,这个比例被设置为1,意味着输入和输出通道数相同。
  2. 初始化方法__init__:
    • 初始化残差块,包含两个3x3的卷积层,ReLU激活函数,一个可选的批量归一化层norm,以及一个可选的下采样层down_sample
    • 如果未提供norm,则使用输出通道数创建一个BatchNorm2d
    • conv1conv2是两个卷积层。注意在conv2卷积层的初始化中,输入通道应为out_channel,以保持通道一致性。
    • weight_init是此前定义的正态分布初始化器,用于初始化卷积层的权重。
  3. 构建方法construct:
    • 残差块的前向传播定义。输入x通过主分支的卷积层,然后进行批量归一化和ReLU激活。
    • identity变量是shortcut连接的输入,如果定义了down_sample,则应用其进行下采样。
    • 主分支输出与shortcut连接相加,然后再应用ReLU激活函数。
    • 返回最终的输出。

代码修正

注意,这里有一个小错误:conv2的初始化应该使用out_channel作为输入通道数,并且通常卷积层conv2不会改变特征图的大小,因此可能需要设置pad_mode='same'
修正后的代码如下:

self.conv2 = nn.Conv2d(out_channel, out_channel,
                       kernel_size=3, stride=1, pad_mode='same', 
                       weight_init=weight_init)

这里,stride被设置为1(默认值),pad_mode='same'确保了输出特征图大小与输入相同,这样就可以直接与shortcut连接相加。

  • nn.Conv2d:
    • 参数:
      • in_channel: 输入特征图的通道数。
      • out_channel: 输出特征图的通道数。
      • kernel_size: 卷积核的大小。
      • stride: 卷积核移动的步长。
      • pad_mode: 填充模式,默认是’valid’,设置为’same’表示填充输入以使输出具有与原始输入相同的高度和宽度。
      • weight_init: 权重初始化器。
    • 功能: 创建一个卷积层。
  • nn.BatchNorm2d:
    • 参数:
      • out_channel: 批量归一化层正则化的特征数。
    • 功能: 创建一个批量归一化层,用于正则化输入特征图。
  • nn.ReLU:
    • 不需要特定参数。
    • 功能: 创建一个ReLU激活函数,对输入执行非线性变换。

总结

这段代码定义了一个基础版的残差块,它对输入特征图进行了两次卷积操作,并加上了输入特征图(可能经过下采样)作为shortcut连接,来实现残差学习。这种结构可以帮助避免在深层网络中出现的梯度消失问题。

class ResidualBlock(nn.Cell):
    expansion = 4  # 最后一个卷积核的数量是第一个卷积核数量的4倍

    def __init__(self, in_channel: int, out_channel: int,
                 stride: int = 1, down_sample: Optional[nn.Cell] = None) -> None:
        super(ResidualBlock, self).__init__()

        # 第一层 1x1 卷积,改变通道数
        self.conv1 = nn.Conv2d(in_channel, out_channel,
                               kernel_size=1, weight_init=weight_init)
        self.norm1 = nn.BatchNorm2d(out_channel)
        
        # 第二层 3x3 卷积,进行特征提取
        self.conv2 = nn.Conv2d(out_channel, out_channel,
                               kernel_size=3, stride=stride, pad_mode='same',
                               weight_init=weight_init)
        self.norm2 = nn.BatchNorm2d(out_channel)
        
        # 第三层 1x1 卷积,调整通道数到 out_channel * expansion
        self.conv3 = nn.Conv2d(out_channel, out_channel * self.expansion,
                               kernel_size=1, weight_init=weight_init)
        self.norm3 = nn.BatchNorm2d(out_channel * self.expansion)

        self.relu = nn.ReLU()
        
        # 下采样层,如果需要对输入进行下采样以匹配输出形状
        self.down_sample = down_sample

    def construct(self, x):
        identity = x  # shortcuts分支

        out = self.conv1(x)  # 主分支第一层:1x1卷积层
        out = self.norm1(out)
        out = self.relu(out)
        
        out = self.conv2(out)  # 主分支第二层:3x3卷积层
        out = self.norm2(out)
        out = self.relu(out)
        
        out = self.conv3(out)  # 主分支第三层:1x1卷积层
        out = self.norm3(out)

        # 判断是否需要下采样,并将结果添加到主分支上
        if self.down_sample is not None:
            identity = self.down_sample(x)

        out += identity  # 输出为主分支与shortcuts之和
        out = self.relu(out)

        return out

这段代码定义了一个更复杂的残差网络块(Residual Block),它是ResNet-50和更深层模型的基础组成部分。这个块相比于基本残差块,引入了两个1x1的卷积层用于调整通道数。

  1. 类定义:
    • expansion: 一个类属性,表示最后一个卷积层输出通道数是第一个卷积层输出通道数的4倍。
  2. 初始化方法__init__:
    • 初始化残差块,包含三个卷积层(1x1, 3x3, 1x1)、批量归一化层和ReLU激活函数。
    • conv1: 1x1 卷积层,用于调整通道数。
    • conv2: 3x3 卷积层,用于特征提取。
    • conv3: 1x1 卷积层,将通道数扩展到 out_channel * expansion
    • weight_init: 正态分布初始化器,用于初始化卷积层的权重。
    • norm1, norm2, norm3: 批量归一化层,分别对应每个卷积层。
    • relu: ReLU激活函数。
    • down_sample: 可选的下采样层,用于调整输入特征图的尺寸和通道数以匹配输出。
  3. 构建方法construct:
    • 残差块的前向传播定义。输入x通过三个卷积层,每个卷积层后跟随批量归一化和ReLU激活。
    • identity变量是shortcut连接的输入,如果定义了down_sample,则应用其进行下采样。
    • 主分支输出与shortcut连接相加,然后再应用ReLU激活函数。
    • 返回最终的输出。
  • nn.Conv2d:
    • 参数:
      • in_channel: 输入特征图的通道数。
      • out_channel: 输出特征图的通道数。
      • kernel_size: 卷积核的大小。
      • stride: 卷积核移动的步长。
      • pad_mode: 填充模式。pad_mode='same'确保输出特征图大小与输入相同。
      • weight_init: 权重初始化器。
    • 功能: 创建一个卷积层。
  • nn.BatchNorm2d:
    • 参数:
      • out_channel: 批量归一化层正则化的特征数。
    • 功能: 创建一个批量归一化层,用于正则化输入特征图。
  • nn.ReLU:
    • 不需要特定参数。
    • 功能: 创建一个ReLU激活函数,对输入执行非线性变换。

总结

这段代码定义了一个更复杂的残差块,它包含三个卷积层(1x1, 3x3, 1x1),其中1x1卷积层用于调整通道数,3x3卷积层用于特征提取。这种三层的残差块常用于ResNet-50及更深层的网络结构中,可以显著提高网络的表达能力。通过引入shortcut连接和下采样机制,这种结构可以有效地缓解梯度消失问题,并允许构建非常深的神经网络。

def make_layer(last_out_channel, block: Type[Union[ResidualBlockBase, ResidualBlock]],
               channel: int, block_nums: int, stride: int = 1):
    down_sample = None  # shortcuts分支的下采样层,初始化为None

    # 判断是否需要下采样
    if stride != 1 or last_out_channel != channel * block.expansion:
        # 创建下采样层
        down_sample = nn.SequentialCell([
            nn.Conv2d(last_out_channel, channel * block.expansion,
                      kernel_size=1, stride=stride, weight_init=weight_init),
            nn.BatchNorm2d(channel * block.expansion, gamma_init=gamma_init)
        ])

    layers = []  # 存储残差块的列表
    # 创建第一个残差块,可能包含下采样
    layers.append(block(last_out_channel, channel, stride=stride, down_sample=down_sample))

    in_channel = channel * block.expansion  # 更新输入通道数
    # 循环创建剩余的残差块
    for _ in range(1, block_nums):
        layers.append(block(in_channel, channel))  # 添加新的残差块到列表中

    return nn.SequentialCell(layers)  # 返回包含所有残差块的顺序容器

这个函数make_layer负责创建一个残差网络层,这个网络层由多个残差块(ResidualBlockBaseResidualBlock)组成。这个函数是构建深度残差网络(如ResNet)的关键部分,允许灵活地堆叠不同类型的残差块以构建不同深度的网络。

  1. 参数说明:
    • last_out_channel: 上一个残差层的输出通道数。
    • block: 用于构建该层的残差块类型,可以是ResidualBlockBaseResidualBlock
    • channel: 当前层中每个残差块的输出通道数。
    • block_nums: 当前层中残差块的数量。
    • stride: 第一个残差块的步长,用于调整特征图的大小。
  2. 下采样层:
    • 如果stride不等于1或者last_out_channel与该层残差块的输出通道数不同,表明需要对输入进行下采样以匹配维度或特征图大小。此时,会构建一个顺序容器nn.SequentialCell,其中包括一个1x1卷积层和一个批量归一化层,用于实现下采样。
  3. 构建残差层:
    • 首先,利用给定的参数创建第一个残差块,可能包含下采样层。
    • 然后,更新输入通道数为channel * block.expansion,这是因为在使用ResidualBlock时,最后一个卷积层的输出通道数是输入通道数的4倍。
    • 最后,循环添加剩余的残差块到残差层中。这些残差块的步长默认为1,不包含下采样层,因为只有第一个残差块可能需要改变特征图的大小或通道数。
  4. 返回值:
    • 使用nn.SequentialCell将所有残差块封装成一个顺序容器并返回,这样可以将这些残差块作为一个整体在网络中使用。

总结

make_layer函数通过灵活组合残差块来构建残差层,是构建深度残差网络的基石。通过调整block_numsstride参数,可以轻松实现不同深度和复杂度的网络结构,从而适应不同的应用场景。

from mindspore import load_checkpoint, load_param_into_net

class ResNet(nn.Cell):
    def __init__(self, block: Type[Union[ResidualBlockBase, ResidualBlock]],
                 layer_nums: List[int], num_classes: int, input_channel: int) -> None:
        super(ResNet, self).__init__()

        self.relu = nn.ReLU()
        # 第一个卷积层,输入channel为3(彩色图像),输出channel为64
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, weight_init=weight_init)
        self.norm = nn.BatchNorm2d(64)
        # 最大池化层,缩小图片的尺寸
        self.max_pool = nn.MaxPool2d(kernel_size=3, stride=2, pad_mode='same')
        # 各个残差网络结构块定义,
        self.layer1 = make_layer(64, block, 64, layer_nums[0])
        self.layer2 = make_layer(64 * block.expansion, block, 128, layer_nums[1], stride=2)
        self.layer3 = make_layer(128 * block.expansion, block, 256, layer_nums[2], stride=2)
        self.layer4 = make_layer(256 * block.expansion, block, 512, layer_nums[3], stride=2)
        # 平均池化层
        self.avg_pool = nn.AvgPool2d()
        # flattern层
        self.flatten = nn.Flatten()
        # 全连接层
        self.fc = nn.Dense(in_channels=input_channel, out_channels=num_classes)

    def construct(self, x):
        x = self.conv1(x)
        x = self.norm(x)
        x = self.relu(x)
        x = self.max_pool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avg_pool(x)
        x = self.flatten(x)
        x = self.fc(x)

        return x

def _resnet(model_url: str, block: Type[Union[ResidualBlockBase, ResidualBlock]],
            layers: List[int], num_classes: int, pretrained: bool, pretrianed_ckpt: str,
            input_channel: int):
    model = ResNet(block, layers, num_classes, input_channel)

    if pretrained:
        # 加载预训练模型
        download(url=model_url, path=pretrianed_ckpt, replace=True)
        param_dict = load_checkpoint(pretrianed_ckpt)
        load_param_into_net(model, param_dict)

    return model

def resnet50(num_classes: int = 1000, pretrained: bool = False):
    "ResNet50模型"
    resnet50_url = "https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/models/application/resnet50_224_new.ckpt"
    resnet50_ckpt = "./LoadPretrainedModel/resnet50_224_new.ckpt"
    return _resnet(resnet50_url, ResidualBlock, [3, 4, 6, 3], num_classes,
                   pretrained, resnet50_ckpt, 2048)

这段代码定义了一个ResNet模型类和相关的辅助函数,用于构建和加载预训练的ResNet50模型。

  1. ResNet类:
    • 初始化方法__init__:
      • 定义了ResNet模型的各个层,包括卷积层、批量归一化层、最大池化层、残差块层、平均池化层、展平层和全连接层。
      • conv1是一个7x7的卷积层,用于提取输入图像的特征。
      • norm是一个批量归一化层,用于正则化卷积层的输出。
      • max_pool是一个最大池化层,用于减小特征图的尺寸。
      • layer1layer4是四个残差块层,每个层由多个残差块组成,用于提取更深层次的特征。
      • avg_pool是一个平均池化层,用于进一步减小特征图的尺寸。
      • flatten是一个展平层,用于将特征图展平为一维向量。
      • fc是一个全连接层,用于将展平后的特征向量映射到最终的类别数。
    • 构建方法construct:
      • 定义了ResNet模型的前向传播过程,依次通过各个层,最终输出分类结果。
  2. _resnet函数:
    • 用于创建ResNet模型实例,并根据需要加载预训练的权重。
    • 如果pretrained为True,则从指定的URL下载预训练模型文件,并加载到模型中。
  3. resnet50函数:
    • 是一个便捷函数,用于创建ResNet50模型实例,并可以选择是否加载预训练的权重。
    • 指定了ResNet50模型的URL和预训练模型文件的路径,以及模型的层数和输出类别数。

总结

这段代码提供了一个完整的ResNet50模型的实现,包括模型的定义、预训练权重的加载和模型的实例化。通过调用resnet50函数,可以方便地创建一个ResNet50模型,并根据需要加载预训练的权重,适用于各种图像分类任务。

模型微调

由于ResNet50中的预训练模型是针对ImageNet数据集中的1000个类别进行分类的,在本章只对狼和狗两个类别进行分类,所以需要重置预训练模型中的分类器,然后重新微调网络。

import mindspore as ms

# 加载预训练的 ResNet50 网络
network = resnet50(pretrained=True)

# 获取原全连接层的输入通道数
in_channels = network.fc.in_channels
# 创建新的全连接层,将输出通道数设置为2(狼狗分类数)
head = nn.Dense(in_channels, 2)
# 替换原网络的全连接层
network.fc = head

# 创建新的平均池化层,kernel size 为7
avg_pool = nn.AvgPool2d(kernel_size=7)
# 替换原网络的平均池化层
network.avg_pool = avg_pool

这段代码将预训练的 ResNet50 模型进行微调,以适应一个新的二分类任务,例如分类“狼”和“狗”。

  1. 加载预训练模型:
    • 使用resnet50(pretrained=True)加载预训练的 ResNet50 模型。
  2. 修改全连接层:
    • 获取原全连接层fc的输入通道数in_channels
    • 新建一个全连接层head,其输入通道数为in_channels,输出通道数为2(对应两个分类)。
    • 将网络的全连接层fc替换为新的全连接层head
  3. 修改平均池化层:
    • 新建一个平均池化层avg_pool,其核大小(kernel size)为7。
    • 将网络的平均池化层avg_pool替换为新的平均池化层avg_pool

总结

这段代码实现了对预训练 ResNet50 模型的微调,通过替换最后的全连接层和平均池化层,使其适应新的二分类任务。这种方法非常适合在已有的预训练模型基础上进行特定任务的微调,提高模型在新任务上的性能,同时减少训练时间和所需的数据量。

import mindspore as ms
import mindspore.nn as nn
import mindspore.train as train

# 定义优化器和损失函数
opt = nn.Momentum(params=network.trainable_params(), learning_rate=lr, momentum=momentum)
loss_fn = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')

# 实例化模型
model = train.Model(network, loss_fn, opt, metrics={"Accuracy": train.Accuracy()})

def forward_fn(inputs, targets):
    logits = network(inputs)  # 获取网络输出
    loss = loss_fn(logits, targets)  # 计算损失值
    return loss

# 获取损失和梯度的计算函数
grad_fn = ms.value_and_grad(forward_fn, None, opt.parameters)

def train_step(inputs, targets):
    loss, grads = grad_fn(inputs, targets)  # 计算损失和梯度
    opt(grads)  # 更新梯度
    return loss

这段代码定义了一个训练过程,包括优化器、损失函数和训练步骤,并使用 MindSpore 框架实现。

  1. 定义优化器和损失函数:
    • opt 是一个动量优化器(Momentum optimizer),使用模型的可训练参数、指定的学习率 lr 和动量 momentum 进行初始化。
    • loss_fn 是一个交叉熵损失函数(SoftmaxCrossEntropyWithLogits),用于计算预测值和真实值之间的损失。参数 sparse=True 表示标签是稀疏的(即标签为整数而不是 one-hot 编码),reduction='mean' 表示对所有样本的损失取平均值。
  2. 实例化模型:
    • model 是一个模型实例,包含网络 network、损失函数 loss_fn 和优化器 optmetrics={"Accuracy": train.Accuracy()} 表示在训练过程中使用准确度作为评估指标。
  3. 定义前向传播函数:
    • forward_fn 定义了前向传播过程,接收输入 inputs 和目标 targets,计算模型输出 logits 和损失值 loss
  4. 定义梯度计算函数:
    • grad_fn 使用 ms.value_and_grad 函数,获取前向传播函数 forward_fn 的损失值和梯度信息。None 表示不需要额外的返回值,opt.parameters 表示需要对优化器参数计算梯度。
  5. 定义训练步骤函数:
    • train_step 接收输入 inputs 和目标 targets,计算损失值 loss 和梯度 grads
    • 使用优化器 opt 更新梯度。
    • 返回当前批次的损失值 loss

总结

这段代码实现了一个简单的训练过程,定义了优化器、损失函数和训练步骤,并使用 MindSpore 框架进行了前向传播和梯度计算。通过这种方式,可以在训练过程中更新模型参数,从而逐步优化模型性能。

训练和评估

训练并评估网络,且在训练完成后,保存评估精度最高的ckpt文件(resnet50-best.ckpt)到当前路径的/BestCheckpoint下,保存路径和ckpt文件名可自行调整。

# 创建迭代器
data_loader_train = dataset_train.create_tuple_iterator(num_epochs=num_epochs)

# 最佳模型保存路径
best_ckpt_dir = "./BestCheckpoint"
best_ckpt_path = "./BestCheckpoint/resnet50-best.ckpt"

这段代码用于创建数据集的迭代器,并指定最佳模型的保存路径。

  1. 创建迭代器:
    • data_loader_train 是使用 dataset_train.create_tuple_iterator 方法创建的数据集迭代器。
    • num_epochs 指定了整个数据集应该被迭代的次数。
    • 通过迭代器,可以在训练过程中逐批次地获取数据,从而实现逐步训练模型。
  2. 最佳模型保存路径:
    • best_ckpt_dir 是一个字符串,表示保存最佳模型的目录路径。
    • best_ckpt_path 是一个字符串,表示保存最佳模型的文件路径,包括文件名resnet50-best.ckpt
    • 在训练过程中,可以通过比较验证集上的性能指标(如准确率)来判断当前模型是否是最佳模型,如果是,则将其保存到指定路径。

总结

这段代码为训练过程中的数据迭代和模型保存做了准备。通过创建数据集迭代器,可以方便地获取训练数据进行批次训练;通过指定最佳模型的保存路径,可以在训练过程中保存性能最好的模型,以便后续加载和使用。

import os
import time

# 开始循环训练
print("Start Training Loop ...")

best_acc = 0  # 初始化最佳准确率

for epoch in range(num_epochs):
    losses = []  # 存储每个epoch的损失值
    network.set_train()  # 设置网络为训练模式

    epoch_start = time.time()  # 记录epoch的开始时间

    # 为每轮训练读入数据
    for i, (images, labels) in enumerate(data_loader_train):
        labels = labels.astype(ms.int32)  # 将标签转换为int32类型
        loss = train_step(images, labels)  # 执行训练步骤,计算损失
        losses.append(loss)  # 将损失值添加到列表中

    # 每个epoch结束后,验证模型在验证集上的准确率
    acc = model.eval(dataset_val)['Accuracy']  # 评估模型在验证集上的准确率

    epoch_end = time.time()  # 记录epoch的结束时间
    epoch_seconds = (epoch_end - epoch_start) * 1000  # 计算每个epoch的时间(毫秒)
    step_seconds = epoch_seconds / step_size_train  # 计算每步的时间

    # 打印当前epoch的训练信息
    print("-" * 20)
    print("Epoch: [%3d/%3d], Average Train Loss: [%5.3f], Accuracy: [%5.3f]" % (
        epoch + 1, num_epochs, sum(losses) / len(losses), acc
    ))
    print("epoch time: %5.3f ms, per step time: %5.3f ms" % (
        epoch_seconds, step_seconds
    ))

    # 如果当前epoch的验证准确率高于最佳准确率,则保存当前模型
    if acc > best_acc:
        best_acc = acc  # 更新最佳准确率
        if not os.path.exists(best_ckpt_dir):  # 如果保存路径不存在,则创建路径
            os.mkdir(best_ckpt_dir)
        ms.save_checkpoint(network, best_ckpt_path)  # 保存当前最优模型

# 打印训练结束信息
print("=" * 80)
print(f"End of validation the best Accuracy is: {best_acc: 5.3f}, "
      f"save the best ckpt file in {best_ckpt_path}", flush=True)

这段代码实现了一个完整的训练循环,包括训练过程、验证过程以及保存最佳模型。

  1. 初始化和准备工作:
    • best_acc 初始化为0,用于保存训练过程中最高的验证准确率。
    • 使用 print("Start Training Loop ...") 打印训练开始的提示信息。
  2. 训练循环:
    • 外层 for epoch in range(num_epochs) 循环控制训练的轮数。
    • losses 列表用于存储每个epoch中每个batch的损失。
    • network.set_train() 将网络设置为训练模式。
    • epoch_start = time.time() 记录本epoch开始时间。
  3. 数据迭代和训练:
    • 内层 for i, (images, labels) in enumerate(data_loader_train) 循环从数据加载器中获取每个batch的数据。
    • labels = labels.astype(ms.int32) 将标签转换为 int32 类型。
    • loss = train_step(images, labels) 调用 train_step 函数进行单步训练,并获取损失值。
    • losses.append(loss) 将损失值添加到列表中。
  4. 验证和评估:
    • acc = model.eval(dataset_val)['Accuracy'] 在验证集上评估模型,并获取准确率。
    • 记录epoch结束时间 epoch_end = time.time(),计算时间间隔 epoch_secondsstep_seconds
  5. 打印训练信息:
    • 打印当前epoch的平均损失值、准确率和时间信息。
  6. 保存最佳模型:
    • 如果当前epoch的验证准确率 acc 高于之前的最佳准确率 best_acc,则更新 best_acc 并保存模型。
    • if not os.path.exists(best_ckpt_dir): os.mkdir(best_ckpt_dir) 确保保存路径存在。
    • ms.save_checkpoint(network, best_ckpt_path) 保存当前最优模型。
  7. 结束提示:
    • 打印训练结束后的提示信息,显示最佳验证准确率 best_acc 和模型保存路径 best_ckpt_path

总结

这段代码实现了一个完整的训练过程,包括逐epoch的训练、验证、评估和保存最佳模型的逻辑。通过这种方式,可以有效地训练模型,并确保在验证集上获得最佳性能的模型被保存下来。

可视化模型预测

定义 visualize_mode 函数,可视化模型预测。

visualize_model(best_ckpt_path, dataset_val)

整体代码

#!/usr/bin/env python
# coding: utf-8

# # ResNet50迁移学习

# 下载案例所用到的[狗与狼分类数据集](https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/datasets/intermediate/Canidae_data.zip)

# In[2]:

# 引入download模块中的download函数
from download import download

# 定义数据集URL
dataset_url = "https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/datasets/intermediate/Canidae_data.zip"

# 使用下载函数下载并解压数据集
download(dataset_url, "./datasets-Canidae", kind="zip", replace=True)

解析:

  1. 引入所需的download函数。
  2. 定义数据集的URL。
  3. 使用download函数下载并解压数据集到指定路径。
# In[3]:

# 定义超参数
batch_size = 18                             # 批量大小
image_size = 224                            # 训练图像空间大小
num_epochs = 10                             # 训练周期数
lr = 0.001                                  # 学习率
momentum = 0.9                              # 动量
workers = 4                                 # 并行线程个数

# 加载所需的模块
import mindspore as ms
import mindspore.dataset as ds
import mindspore.dataset.vision as vision

# 数据集目录路径
data_path_train = "./datasets-Canidae/data/Canidae/train/"
data_path_val = "./datasets-Canidae/data/Canidae/val/"

# 创建训练数据集

def create_dataset_canidae(dataset_path, usage):
    """数据加载"""
    # 使用ImageFolderDataset加载数据集
    data_set = ds.ImageFolderDataset(dataset_path, num_parallel_workers=workers, shuffle=True)

    # 数据增强参数
    mean = [0.485 * 255, 0.456 * 255, 0.406 * 255]
    std = [0.229 * 255, 0.224 * 255, 0.225 * 255]
    scale = 32

    if usage == "train":
        # 定义训练数据的map操作
        trans = [
            vision.RandomCropDecodeResize(size=image_size, scale=(0.08, 1.0), ratio=(0.75, 1.333)),
            vision.RandomHorizontalFlip(prob=0.5),
            vision.Normalize(mean=mean, std=std),
            vision.HWC2CHW()
        ]
    else:
        # 定义验证数据的map操作
        trans = [
            vision.Decode(),
            vision.Resize(image_size + scale),
            vision.CenterCrop(image_size),
            vision.Normalize(mean=mean, std=std),
            vision.HWC2CHW()
        ]

    # 应用数据增强操作
    data_set = data_set.map(operations=trans, input_columns='image', num_parallel_workers=workers)

    # 批量操作
    data_set = data_set.batch(batch_size)

    return data_set

# 创建训练和验证数据集
dataset_train = create_dataset_canidae(data_path_train, "train")
step_size_train = dataset_train.get_dataset_size()

dataset_val = create_dataset_canidae(data_path_val, "val")
step_size_val = dataset_val.get_dataset_size()

解析:

  1. 定义了一系列超参数,如批量大小、图像大小、训练周期数、学习率、动量值和并行线程个数。
  2. 使用mindspore.dataset.ImageFolderDataset加载数据集,并进行数据增强操作。
  3. 创建用于训练和验证的数据集。
# In[5]:

# 从数据集中获取一批数据进行可视化
data = next(dataset_train.create_dict_iterator())
images = data["image"]
labels = data["label"]

print("Tensor of image", images.shape)
print("Labels:", labels)

# 使用matplotlib进行图像和标签的可视化
import matplotlib.pyplot as plt
import numpy as np

# class_name对应label,按文件夹字符串从小到大的顺序标记label
class_name = {0: "dogs", 1: "wolves"}

plt.figure(figsize=(5, 5))
for i in range(4):
    # 获取图像及其对应的label
    data_image = images[i].asnumpy()
    data_label = labels[i]
    # 处理图像供展示使用
    data_image = np.transpose(data_image, (1, 2, 0))
    mean = np.array([0.485, 0.456, 0.406])
    std = np.array([0.229, 0.224, 0.225])
    data_image = std * data_image + mean
    data_image = np.clip(data_image, 0, 1)
    # 显示图像
    plt.subplot(2, 2, i+1)
    plt.imshow(data_image)
    plt.title(class_name[int(labels[i].asnumpy())])
    plt.axis("off")

plt.show()

解析:

  1. 从训练数据集中获取一批数据用于可视化。
  2. 使用matplotlib将图像和其对应的标签进行展示。
# In[25]:

# 定义残差块
from typing import Type, Union, List, Optional
from mindspore import nn, train
from mindspore.common.initializer import Normal

# 初始化权重
weight_init = Normal(mean=0, sigma=0.02)
gamma_init = Normal(mean=1, sigma=0.02)

class ResidualBlockBase(nn.Cell):
    expansion: int = 1  # 最后一个卷积核数量与第一个卷积核数量相等

    def __init__(self, in_channel: int, out_channel: int, stride: int = 1, norm: Optional[nn.Cell] = None, down_sample: Optional[nn.Cell] = None) -> None:
        super(ResidualBlockBase, self).__init__()
        if not norm:
            self.norm = nn.BatchNorm2d(out_channel)
        else:
            self.norm = norm

        self.conv1 = nn.Conv2d(in_channel, out_channel, kernel_size=3, stride=stride, weight_init=weight_init)
        self.conv2 = nn.Conv2d(in_channel, out_channel, kernel_size=3, weight_init=weight_init)
        self.relu = nn.ReLU()
        self.down_sample = down_sample

    def construct(self, x):
        """ResidualBlockBase construct."""
        identity = x  # shortcuts分支

        out = self.conv1(x)  # 主分支第一层:3*3卷积层
        out = self.norm(out)
        out = self.relu(out)
        out = self.conv2(out)  # 主分支第二层:3*3卷积层
        out = self.norm(out)

        if self.down_sample is not None:
            identity = self.down_sample(x)
        out += identity  # 输出为主分支与shortcuts之和
        out = self.relu(out)

        return out

解析:

  1. 定义了基本的残差块ResidualBlockBase,包含两个卷积层。
  2. 初始化卷积层和批量归一化层。
# In[27]:

# 定义更复杂的残差块
class ResidualBlock(nn.Cell):
    expansion = 4  # 最后一个卷积核的数量是第一个卷积核数量的4倍

    def __init__(self, in_channel: int, out_channel: int, stride: int = 1, down_sample: Optional[nn.Cell] = None) -> None:
        super(ResidualBlock, self).__init__()

        self.conv1 = nn.Conv2d(in_channel, out_channel, kernel_size=1, weight_init=weight_init)
        self.norm1 = nn.BatchNorm2d(out_channel)
        self.conv2 = nn.Conv2d(out_channel, out_channel, kernel_size=3, stride=stride, weight_init=weight_init)
        self.norm2 = nn.BatchNorm2d(out_channel)
        self.conv3 = nn.Conv2d(out_channel, out_channel * self.expansion, kernel_size=1, weight_init=weight_init)
        self.norm3 = nn.BatchNorm2d(out_channel * self.expansion)

        self.relu = nn.ReLU()
        self.down_sample = down_sample

    def construct(self, x):

        identity = x  # shortscuts分支

        out = self.conv1(x)  # 主分支第一层:1*1卷积层
        out = self.norm1(out)
        out = self.relu(out)
        out = self.conv2(x)  # 主分支第二层:3*3卷积层
        out = self.norm2(out)
        out = self.relu(out)
        out = self.conv3(x)  # 主分支第三层:1*1卷积层
        out = self.norm3(out)

        if self.down_sample is not None:
            identity = self.down_sample(x)

        out += identity  # 输出为主分支与shortcuts之和
        out = self.relu(out)

        return out

解析:

  1. 定义了更复杂的残差块ResidualBlock,包括三个卷积层和批量归一化层。
  2. 增加了expansion参数使得最后一个卷积层的输出通道数量是第一个的四倍。
# In[28]:

# 定义构建残差网络的方法
def make_layer(last_out_channel, block: Type[Union[ResidualBlockBase, ResidualBlock]], channel: int, block_nums: int, stride: int = 1):
    down_sample = None  # shortcuts分支

    if stride != 1 or last_out_channel != channel * block.expansion:
        down_sample = nn.SequentialCell([
            nn.Conv2d(last_out_channel, channel * block.expansion, kernel_size=1, stride=stride, weight_init=weight_init),
            nn.BatchNorm2d(channel * block.expansion, gamma_init=gamma_init)
        ])

    layers = []
    layers.append(block(last_out_channel, channel, stride=stride, down_sample=down_sample))

    in_channel = channel * block.expansion
    # 堆叠残差网络
    for _ in range(1, block_nums):
        layers.append(block(in_channel, channel))

    return nn.SequentialCell(layers)

解析:

  1. 定义了make_layer方法,用于创建多个残差块并堆叠形成残差网络。
  2. 判断是否需要down_sample以匹配输入和输出的通道数。
# In[29]:

from mindspore import load_checkpoint, load_param_into_net

# 定义ResNet网络
class ResNet(nn.Cell):
    def __init__(self, block: Type[Union[ResidualBlockBase, ResidualBlock]], layer_nums: List[int], num_classes: int, input_channel: int) -> None:
        super(ResNet, self).__init__()

        self.relu = nn.ReLU()
        # 第一个卷积层,输入channel为3(彩色图像),输出channel为64
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, weight_init=weight_init)
        self.norm = nn.BatchNorm2d(64)
        # 最大池化层,缩小图片的尺寸
        self.max_pool = nn.MaxPool2d(kernel_size=3, stride=2, pad_mode='same')
        # 各个残差网络结构块定义,
        self.layer1 = make_layer(64, block, 64, layer_nums[0])
        self.layer2 = make_layer(64 * block.expansion, block, 128, layer_nums[1], stride=2)
        self.layer3 = make_layer(128 * block.expansion, block, 256, layer_nums[2], stride=2)
        self.layer4 = make_layer(256 * block.expansion, block, 512, layer_nums[3], stride=2)
        # 平均池化层
        self.avg_pool = nn.AvgPool2d()
        # flattern层
        self.flatten = nn.Flatten()
        # 全连接层
        self.fc = nn.Dense(in_channels=input_channel, out_channels=num_classes)

    def construct(self, x):
        x = self.conv1(x)
        x = self.norm(x)
        x = self.relu(x)
        x = self.max_pool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avg_pool(x)
        x = self.flatten(x)
        x = self.fc(x)

        return x

# 加载ResNet模型
def _resnet(model_url: str, block: Type[Union[ResidualBlockBase, ResidualBlock]], layers: List[int], num_classes: int, pretrained: bool, pretrianed_ckpt: str, input_channel: int):
    model = ResNet(block, layers, num_classes, input_channel)

    if pretrained:
        # 加载预训练模型
        download(url=model_url, path=pretrianed_ckpt, replace=True)
        param_dict = load_checkpoint(pretrianed_ckpt)
        load_param_into_net(model, param_dict)

    return model

def resnet50(num_classes: int = 1000, pretrained: bool = False):
    "ResNet50模型"
    resnet50_url = "https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/models/application/resnet50_224_new.ckpt"
    resnet50_ckpt = "./LoadPretrainedModel/resnet50_224_new.ckpt"
    return _resnet(resnet50_url, ResidualBlock, [3, 4, 6, 3], num_classes, pretrained, resnet50_ckpt, 2048)

解析:

  1. 定义了ResNet类,包含卷积层、池化层、残差块等组成部分。
  2. 加载预训练模型的函数_resnet和具体的resnet50函数。
# In[30]:

import mindspore as ms

# 使用预训练的ResNet50模型
network = resnet50(pretrained=True)

# 全连接层输入层的大小
in_channels = network.fc.in_channels
# 输出通道数大小为狼狗分类数2
head = nn.Dense(in_channels, 2)
# 重置全连接层
network.fc = head

# 平均池化层kernel size为7
avg_pool = nn.AvgPool2d(kernel_size=7)
# 重置平均池化层
network.avg_pool = avg_pool

解析:

  1. 加载预训练的ResNet50模型。
  2. 修改全连接层和平均池化层以适应当前数据集的分类任务。
# In[31]:

import mindspore as ms

# 定义优化器和损失函数
opt = nn.Momentum(params=network.trainable_params(), learning_rate=lr, momentum=momentum)
loss_fn = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')

# 实例化模型
model = train.Model(network, loss_fn, opt, metrics={"Accuracy": train.Accuracy()})

def forward_fn(inputs, targets):
    logits = network(inputs)
    loss = loss_fn(logits, targets)
    return loss

grad_fn = ms.value_and_grad(forward_fn, None, opt.parameters)

def train_step(inputs, targets):
    loss, grads = grad_fn(inputs, targets)
    opt(grads)
    return loss

解析:

  1. 定义了优化器和损失函数。
  2. 实例化模型,并定义了前向传播和训练步骤函数。
# In[32]:

# 创建迭代器
data_loader_train = dataset_train.create_tuple_iterator(num_epochs=num_epochs)

# 最佳模型保存路径
best_ckpt_dir = "./BestCheckpoint"
best_ckpt_path = "./BestCheckpoint/resnet50-best.ckpt"

import os
import time

# 开始循环训练
print("Start Training Loop ...")

best_acc = 0

for epoch in range(num_epochs):
    losses = []
    network.set_train()

    epoch_start = time.time()

    # 为每轮训练读入数据
    for i, (images, labels) in enumerate(data_loader_train):
        labels = labels.astype(ms.int32)
        loss = train_step(images, labels)
        losses.append(loss)

    # 每个epoch结束后,验证准确率
    acc = model.eval(dataset_val)['Accuracy']

    epoch_end = time.time()
    epoch_seconds = (epoch_end - epoch_start) * 1000
    step_seconds = epoch_seconds/step_size_train

    print("-" * 20)
    print("Epoch: [%3d/%3d], Average Train Loss: [%5.3f], Accuracy: [%5.3f]" % (
        epoch+1, num_epochs, sum(losses)/len(losses), acc
    ))
    print("epoch time: %5.3f ms, per step time: %5.3f ms" % (
        epoch_seconds, step_seconds
    ))

    if acc > best_acc:
        best_acc = acc
        if not os.path.exists(best_ckpt_dir):
            os.mkdir(best_ckpt_dir)
        ms.save_checkpoint(network, best_ckpt_path)

print("=" * 80)
print(f"End of validation the best Accuracy is: {best_acc: 5.3f}, save the best ckpt file in {best_ckpt_path}", flush=True)

解析:

  1. 创建用于训练的数据迭代器。
  2. 定义并进行训练循环,并在每个epoch结束后评估模型,保存最佳模型。
# In[36]:

import matplotlib.pyplot as plt
import mindspore as ms

def visualize_model(best_ckpt_path, val_ds):
    net = resnet50()
    # 全连接层输入层的大小
    in_channels = net.fc.in_channels
    # 输出通道数大小为狼狗分类数2
    head = nn.Dense(in_channels, 2)
    # 重置全连接层
    net.fc = head
    # 平均池化层kernel size为7
    avg_pool = nn.AvgPool2d(kernel_size=7)
    # 重置平均池化层
    net.avg_pool = avg_pool
    # 加载模型参数
    param_dict = ms.load_checkpoint(best_ckpt_path)
    ms.load_param_into_net(net, param_dict)
    model = train.Model(net)
    # 加载验证集的数据进行验证
    data = next(val_ds.create_dict_iterator())
    images = data["image"].asnumpy()
    labels = data["label"].asnumpy()
# class_name对应label,按文件夹字符串从小到大的顺序标记label
    class_name = {0: "dogs", 1: "wolves"}
    # 预测图像类别
    output = model.predict(ms.Tensor(data['image']))
    pred = np.argmax(output.asnumpy(), axis=1)

    # 显示图像及图像的预测值
    plt.figure(figsize=(5, 5))
    for i in range(4):
        plt.subplot(2, 2, i + 1)
        # 若预测正确,显示为蓝色;若预测错误,显示为红色
        color = 'blue' if pred[i] == labels[i] else 'red'
        plt.title('predict:{}'.format(class_name[pred[i]]), color=color)
        picture_show = np.transpose(images[i], (1, 2, 0))
        mean = np.array([0.485, 0.456, 0.406])
        std = np.array([0.229, 0.224, 0.225])
        picture_show = std * picture_show + mean
        picture_show = np.clip(picture_show, 0, 1)
        plt.imshow(picture_show)
        plt.axis('off')

    plt.show()
    
# 使用模型微调得到的best.ckpt文件对验证集的狼和狗图像数据进行预测。若预测字体为蓝色表示预测正确,若预测字体为红色表示预测错误。
visualize_model(best_ckpt_path, dataset_val)

解析:

  1. 定义了visualize_model函数,用于加载最佳模型并进行图像预测和可视化。
  2. 使用model.predict进行预测,并显示预测结果与实际标签的对比。
# In[38]:

# 使用固定特征进行训练
net_work = resnet50(pretrained=True)

# 全连接层输入层的大小
in_channels = net_work.fc.in_channels
# 输出通道数大小为狼狗分类数2
head = nn.Dense(in_channels, 2)
# 重置全连接层
net_work.fc = head

# 平均池化层kernel size为7
avg_pool = nn.AvgPool2d(kernel_size=7)
# 重置平均池化层
net_work.avg_pool = avg_pool

# 冻结除最后一层外的所有参数
for param in net_work.get_parameters():
    if param.name not in ["fc.weight", "fc.bias"]:
        param.requires_grad = False

# 定义优化器和损失函数
opt = nn.Momentum(params=net_work.trainable_params(), learning_rate=lr, momentum=0.5)
loss_fn = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')

# 定义前向传播和训练步骤
def forward_fn(inputs, targets):
    logits = net_work(inputs)
    loss = loss_fn(logits, targets)
    return loss

grad_fn = ms.value_and_grad(forward_fn, None, opt.parameters)

def train_step(inputs, targets):
    loss, grads = grad_fn(inputs, targets)
    opt(grads)
    return loss

# 实例化模型
model1 = train.Model(net_work, loss_fn, opt, metrics={"Accuracy": train.Accuracy()})

解析:

  1. 加载预训练的ResNet50模型,并重置全连接层和平均池化层。
  2. 冻结除最后一层外的所有参数,以固定特征进行训练。
  3. 定义优化器和损失函数,并实例化模型。
# In[39]:

# 重新创建训练和验证数据集
dataset_train = create_dataset_canidae(data_path_train, "train")
step_size_train = dataset_train.get_dataset_size()

dataset_val = create_dataset_canidae(data_path_val, "val")
step_size_val = dataset_val.get_dataset_size()

# 更新训练周期数
num_epochs = 10

# 创建迭代器
data_loader_train = dataset_train.create_tuple_iterator(num_epochs=num_epochs)
data_loader_val = dataset_val.create_tuple_iterator(num_epochs=num_epochs)
best_ckpt_dir = "./BestCheckpoint"
best_ckpt_path = "./BestCheckpoint/resnet50-best-freezing-param.ckpt"

# 开始循环训练
print("Start Training Loop ...")

best_acc = 0

for epoch in range(num_epochs):
    losses = []
    net_work.set_train()

    epoch_start = time.time()

    # 为每轮训练读入数据
    for i, (images, labels) in enumerate(data_loader_train):
        labels = labels.astype(ms.int32)
        loss = train_step(images, labels)
        losses.append(loss)

    # 每个epoch结束后,验证准确率
    acc = model1.eval(dataset_val)['Accuracy']

    epoch_end = time.time()
    epoch_seconds = (epoch_end - epoch_start) * 1000
    step_seconds = epoch_seconds/step_size_train

    print("-" * 20)
    print("Epoch: [%3d/%3d], Average Train Loss: [%5.3f], Accuracy: [%5.3f]" % (
        epoch+1, num_epochs, sum(losses)/len(losses), acc
    ))
    print("epoch time: %5.3f ms, per step time: %5.3f ms" % (
        epoch_seconds, step_seconds
    ))

    if acc > best_acc:
        best_acc = acc
        if not os.path.exists(best_ckpt_dir):
            os.mkdir(best_ckpt_dir)
        ms.save_checkpoint(net_work, best_ckpt_path)

print("=" * 80)
print(f"End of validation the best Accuracy is: {best_acc: 5.3f}, save the best ckpt file in {best_ckpt_path}", flush=True)

解析:

  1. 重新创建训练和验证数据集。
  2. 定义并进行训练循环,并在每个epoch结束后评估模型,保存最佳模型。
  3. 更新最佳模型的保存路径。
# In[41]:

# 使用固定特征得到的best.ckpt文件对对验证集的狼和狗图像数据进行预测。若预测字体为蓝色即为预测正确,若预测字体为红色则预测错误。
visualize_model(best_ckpt_path, dataset_val)

解析:

  1. 使用固定特征训练得到的最佳模型进行预测和可视化。
  2. 预测结果与实际标签对比,预测正确的显示为蓝色,预测错误的显示为红色。
# 输出结果示例
# Start Training Loop ...
# 
# --------------------
# Epoch: [  1/ 10], Average Train Loss: [0.582], Accuracy: [1.000]
# epoch time: 201648.793 ms, per step time: 14403.485 ms
# --------------------
# Epoch: [  2/ 10], Average Train Loss: [0.290], Accuracy: [0.967]
# epoch time: 171260.342 ms, per step time: 12232.882 ms
# --------------------
# Epoch: [  3/ 10], Average Train Loss: [0.126], Accuracy: [1.000]
# epoch time: 204468.052 ms, per step time: 14604.861 ms
#
# ......
#
# --------------------
# Epoch: [ 10/ 10], Average Train Loss: [0.057], Accuracy: [1.000]
# epoch time: 1339063.144 ms, per step time: 95647.367 ms
# ================================================================================
# End of validation the best Accuracy is:  1.000, save the best ckpt file in ./BestCheckpoint/resnet50-best.ckpt

解析:

  1. 输出训练过程中的日志信息,包括每个epoch的训练损失和评估准确率。
  2. 显示最佳模型的保存路径及其对应的准确率。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值