日期
心得
昇思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)
- 导入模块:
from download import download
这里导入了download
模块中的download
函数。这个函数通常用于下载文件,支持多种文件类型和选项。
- 定义数据集URL:
dataset_url = "https://mindspore-website.obs.cn-north-4.myhuaweicloud.com/notebook/datasets/intermediate/Canidae_data.zip"
这里定义了数据集的URL,指向一个zip文件。
- 调用
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()
- 参数定义:
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
这里导入了MindSpore框架及其数据集处理和图像处理模块。
- 数据集目录路径:
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,)
定义了一个函数create_dataset_canidae
,用于创建数据集。这里使用ds.ImageFolderDataset
加载指定路径下的图像数据集,并进行并行处理和随机打乱。
- 数据增强操作定义:
mean = [0.485 * 255, 0.456 * 255, 0.406 * 255]
std = [0.229 * 255, 0.224 * 255, 0.225 * 255]
scale = 32
定义了用于图像标准化的数据增强参数mean
和std
,以及用于调整图像尺寸的scale
。
- 定义映射操作:
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
参数(训练或验证)定义不同的数据增强和处理操作。训练数据集进行随机裁剪、水平翻转和标准化,验证数据集进行解码、调整尺寸、中心裁剪和标准化。
- 数据映射操作:
data_set = data_set.map(
operations=trans,
input_columns='image',
num_parallel_workers=workers)
使用map
函数将定义好的映射操作应用到数据集的图像列,并进行并行处理。
- 批量操作:
data_set = data_set.batch(batch_size)
使用batch
函数将处理好的数据集按批量大小进行分组。
- 加载训练和验证数据集:
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)
- 获取数据集的下一个批次:
data = next(dataset_train.create_dict_iterator())
使用create_dict_iterator
方法创建一个字典迭代器,然后调用next
函数获取数据集的下一个批次。这个批次包含图像和标签数据。
- 提取图像和标签:
images = data["image"]
labels = data["label"]
从获取的数据字典中提取图像和标签。data["image"]
包含图像数据,data["label"]
包含对应的标签数据。
- 打印图像张量的形状和标签:
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()
- 导入模块:
import matplotlib.pyplot as plt
import numpy as np
导入matplotlib.pyplot
用于绘制图像,导入numpy
用于数值计算。
- 定义类别名称:
class_name = {0: "dogs", 1: "wolves"}
定义一个字典,将类别标签映射到类别名称。根据文件夹字符串从小到大的顺序标记标签。
- 创建绘图窗口:
plt.figure(figsize=(5, 5))
创建一个图形窗口,大小为5x5英寸。
- 循环显示图像:
for i in range(4):
# 获取图像及其对应的label
data_image = images[i].asnumpy()
data_label = labels[i]
使用一个循环从批次数据中获取前4张图像及其对应的标签。images[i].asnumpy()
将图像张量转换为NumPy数组。
- 处理图像以供展示:
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)
,以便于显示。mean
和std
: 定义用于标准化的均值和标准差。data_image = std * data_image + mean
: 反标准化图像数据,将其转换回原始图像范围。data_image = np.clip(data_image, 0, 1)
: 将图像数据限制在0到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")
: 关闭坐标轴。
- 显示图像:
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)
- 导入模块:
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中的正态分布初始化器,用于参数初始化。
- 初始化参数:
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)的基础组成部分。残差块可以让网络更深,而不会导致训练困难。
- 类定义:
expansion
: 一个类属性,表示最后一个卷积层输出通道数与第一个卷积层输出通道数的比例。在这个ResidualBlockBase
类中,这个比例被设置为1,意味着输入和输出通道数相同。
- 初始化方法__init__:
- 初始化残差块,包含两个3x3的卷积层,ReLU激活函数,一个可选的批量归一化层
norm
,以及一个可选的下采样层down_sample
。 - 如果未提供
norm
,则使用输出通道数创建一个BatchNorm2d
。 conv1
和conv2
是两个卷积层。注意在conv2
卷积层的初始化中,输入通道应为out_channel
,以保持通道一致性。weight_init
是此前定义的正态分布初始化器,用于初始化卷积层的权重。
- 初始化残差块,包含两个3x3的卷积层,ReLU激活函数,一个可选的批量归一化层
- 构建方法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的卷积层用于调整通道数。
- 类定义:
expansion
: 一个类属性,表示最后一个卷积层输出通道数是第一个卷积层输出通道数的4倍。
- 初始化方法__init__:
- 初始化残差块,包含三个卷积层(1x1, 3x3, 1x1)、批量归一化层和ReLU激活函数。
conv1
: 1x1 卷积层,用于调整通道数。conv2
: 3x3 卷积层,用于特征提取。conv3
: 1x1 卷积层,将通道数扩展到out_channel * expansion
。weight_init
: 正态分布初始化器,用于初始化卷积层的权重。norm1
,norm2
,norm3
: 批量归一化层,分别对应每个卷积层。relu
: ReLU激活函数。down_sample
: 可选的下采样层,用于调整输入特征图的尺寸和通道数以匹配输出。
- 构建方法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
负责创建一个残差网络层,这个网络层由多个残差块(ResidualBlockBase
或ResidualBlock
)组成。这个函数是构建深度残差网络(如ResNet)的关键部分,允许灵活地堆叠不同类型的残差块以构建不同深度的网络。
- 参数说明:
last_out_channel
: 上一个残差层的输出通道数。block
: 用于构建该层的残差块类型,可以是ResidualBlockBase
或ResidualBlock
。channel
: 当前层中每个残差块的输出通道数。block_nums
: 当前层中残差块的数量。stride
: 第一个残差块的步长,用于调整特征图的大小。
- 下采样层:
- 如果
stride
不等于1或者last_out_channel
与该层残差块的输出通道数不同,表明需要对输入进行下采样以匹配维度或特征图大小。此时,会构建一个顺序容器nn.SequentialCell
,其中包括一个1x1卷积层和一个批量归一化层,用于实现下采样。
- 如果
- 构建残差层:
- 首先,利用给定的参数创建第一个残差块,可能包含下采样层。
- 然后,更新输入通道数为
channel * block.expansion
,这是因为在使用ResidualBlock
时,最后一个卷积层的输出通道数是输入通道数的4倍。 - 最后,循环添加剩余的残差块到残差层中。这些残差块的步长默认为1,不包含下采样层,因为只有第一个残差块可能需要改变特征图的大小或通道数。
- 返回值:
- 使用
nn.SequentialCell
将所有残差块封装成一个顺序容器并返回,这样可以将这些残差块作为一个整体在网络中使用。
- 使用
总结
make_layer
函数通过灵活组合残差块来构建残差层,是构建深度残差网络的基石。通过调整block_nums
和stride
参数,可以轻松实现不同深度和复杂度的网络结构,从而适应不同的应用场景。
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模型。
- ResNet类:
- 初始化方法__init__:
- 定义了ResNet模型的各个层,包括卷积层、批量归一化层、最大池化层、残差块层、平均池化层、展平层和全连接层。
conv1
是一个7x7的卷积层,用于提取输入图像的特征。norm
是一个批量归一化层,用于正则化卷积层的输出。max_pool
是一个最大池化层,用于减小特征图的尺寸。layer1
到layer4
是四个残差块层,每个层由多个残差块组成,用于提取更深层次的特征。avg_pool
是一个平均池化层,用于进一步减小特征图的尺寸。flatten
是一个展平层,用于将特征图展平为一维向量。fc
是一个全连接层,用于将展平后的特征向量映射到最终的类别数。
- 构建方法construct:
- 定义了ResNet模型的前向传播过程,依次通过各个层,最终输出分类结果。
- 初始化方法__init__:
- _resnet函数:
- 用于创建ResNet模型实例,并根据需要加载预训练的权重。
- 如果
pretrained
为True,则从指定的URL下载预训练模型文件,并加载到模型中。
- 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 模型进行微调,以适应一个新的二分类任务,例如分类“狼”和“狗”。
- 加载预训练模型:
- 使用
resnet50(pretrained=True)
加载预训练的 ResNet50 模型。
- 使用
- 修改全连接层:
- 获取原全连接层
fc
的输入通道数in_channels
。 - 新建一个全连接层
head
,其输入通道数为in_channels
,输出通道数为2(对应两个分类)。 - 将网络的全连接层
fc
替换为新的全连接层head
。
- 获取原全连接层
- 修改平均池化层:
- 新建一个平均池化层
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 框架实现。
- 定义优化器和损失函数:
opt
是一个动量优化器(Momentum optimizer),使用模型的可训练参数、指定的学习率lr
和动量momentum
进行初始化。loss_fn
是一个交叉熵损失函数(SoftmaxCrossEntropyWithLogits),用于计算预测值和真实值之间的损失。参数sparse=True
表示标签是稀疏的(即标签为整数而不是 one-hot 编码),reduction='mean'
表示对所有样本的损失取平均值。
- 实例化模型:
model
是一个模型实例,包含网络network
、损失函数loss_fn
和优化器opt
。metrics={"Accuracy": train.Accuracy()}
表示在训练过程中使用准确度作为评估指标。
- 定义前向传播函数:
forward_fn
定义了前向传播过程,接收输入inputs
和目标targets
,计算模型输出logits
和损失值loss
。
- 定义梯度计算函数:
grad_fn
使用ms.value_and_grad
函数,获取前向传播函数forward_fn
的损失值和梯度信息。None
表示不需要额外的返回值,opt.parameters
表示需要对优化器参数计算梯度。
- 定义训练步骤函数:
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"
这段代码用于创建数据集的迭代器,并指定最佳模型的保存路径。
- 创建迭代器:
data_loader_train
是使用dataset_train.create_tuple_iterator
方法创建的数据集迭代器。num_epochs
指定了整个数据集应该被迭代的次数。- 通过迭代器,可以在训练过程中逐批次地获取数据,从而实现逐步训练模型。
- 最佳模型保存路径:
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)
这段代码实现了一个完整的训练循环,包括训练过程、验证过程以及保存最佳模型。
- 初始化和准备工作:
best_acc
初始化为0,用于保存训练过程中最高的验证准确率。- 使用
print("Start Training Loop ...")
打印训练开始的提示信息。
- 训练循环:
- 外层
for epoch in range(num_epochs)
循环控制训练的轮数。 losses
列表用于存储每个epoch中每个batch的损失。network.set_train()
将网络设置为训练模式。epoch_start = time.time()
记录本epoch开始时间。
- 外层
- 数据迭代和训练:
- 内层
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)
将损失值添加到列表中。
- 内层
- 验证和评估:
acc = model.eval(dataset_val)['Accuracy']
在验证集上评估模型,并获取准确率。- 记录epoch结束时间
epoch_end = time.time()
,计算时间间隔epoch_seconds
和step_seconds
。
- 打印训练信息:
- 打印当前epoch的平均损失值、准确率和时间信息。
- 保存最佳模型:
- 如果当前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)
保存当前最优模型。
- 如果当前epoch的验证准确率
- 结束提示:
- 打印训练结束后的提示信息,显示最佳验证准确率
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)
解析:
- 引入所需的
download
函数。 - 定义数据集的URL。
- 使用
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()
解析:
- 定义了一系列超参数,如批量大小、图像大小、训练周期数、学习率、动量值和并行线程个数。
- 使用
mindspore.dataset.ImageFolderDataset
加载数据集,并进行数据增强操作。 - 创建用于训练和验证的数据集。
# 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()
解析:
- 从训练数据集中获取一批数据用于可视化。
- 使用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
解析:
- 定义了基本的残差块
ResidualBlockBase
,包含两个卷积层。 - 初始化卷积层和批量归一化层。
# 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
解析:
- 定义了更复杂的残差块
ResidualBlock
,包括三个卷积层和批量归一化层。 - 增加了
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)
解析:
- 定义了
make_layer
方法,用于创建多个残差块并堆叠形成残差网络。 - 判断是否需要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)
解析:
- 定义了ResNet类,包含卷积层、池化层、残差块等组成部分。
- 加载预训练模型的函数
_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
解析:
- 加载预训练的ResNet50模型。
- 修改全连接层和平均池化层以适应当前数据集的分类任务。
# 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
解析:
- 定义了优化器和损失函数。
- 实例化模型,并定义了前向传播和训练步骤函数。
# 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)
解析:
- 创建用于训练的数据迭代器。
- 定义并进行训练循环,并在每个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)
解析:
- 定义了
visualize_model
函数,用于加载最佳模型并进行图像预测和可视化。 - 使用
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()})
解析:
- 加载预训练的ResNet50模型,并重置全连接层和平均池化层。
- 冻结除最后一层外的所有参数,以固定特征进行训练。
- 定义优化器和损失函数,并实例化模型。
# 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)
解析:
- 重新创建训练和验证数据集。
- 定义并进行训练循环,并在每个epoch结束后评估模型,保存最佳模型。
- 更新最佳模型的保存路径。
# In[41]:
# 使用固定特征得到的best.ckpt文件对对验证集的狼和狗图像数据进行预测。若预测字体为蓝色即为预测正确,若预测字体为红色则预测错误。
visualize_model(best_ckpt_path, dataset_val)
解析:
- 使用固定特征训练得到的最佳模型进行预测和可视化。
- 预测结果与实际标签对比,预测正确的显示为蓝色,预测错误的显示为红色。
# 输出结果示例
# 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
解析:
- 输出训练过程中的日志信息,包括每个epoch的训练损失和评估准确率。
- 显示最佳模型的保存路径及其对应的准确率。