图像分类代码学习笔记

发布时间 2023-03-29 10:55:38作者: 杨谖之

图像分类代码学习笔记

数据集描述

数据集为美国手语图像数据(American Sign Language),包含87000张图像,每张图片都是200×200像素,总共有29个类,其中26个类表示字母 A-Z,剩下三种表示空格、删除和什么都没有。

代码笔记

train_data_path = '../input/asl-alphabet/asl_alphabet_train/asl_alphabet_train/'

train_data_path 存储训练数据集的轮径。

train_transforms = transforms.Compose([
    transforms.Resize(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225])
])
test_transforms = transforms.Compose([
    transforms.Resize(224),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406],
                         std=[0.229, 0.224, 0.225])
])

原始数据的格式不一定和训练所需的数据格式相同。我们使用transforms模块对数据进行操作,使其适合训练。

transforms.Compose() 的主要作用是串联多个图片变换的操作。

transforms.Resize() 是调整 PILImage 对象的尺寸,注意不能是用 io.imread 或者 cv2.imread 读取的图片,这两种方法得到的是 ndarray。

将图片短边缩放至x,长宽比保持不变:

transforms.Resize(x)

而一般输入深度网络的特征图长宽是相等的,就不能采取等比例缩放的方式了,需要同时指定长宽:

transforms.Resize([h, w])

transform.ToTensor() 将PILImage 或者 numpy 的 ndarray 转化成 Tensor。

transform.Normalize() 逐channel的对图像进行标准化(均值变为0,标准差变为1),可以加快模型的收敛。

这里的参数 mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) 使用的数据是 ImageNet 数据集的均值和标准差。

如何计算图片的 mean 和 std:如何计算pytorch中图像输入的均值和方差

train_dataset = datasets.ImageFolder(train_data_path, transform=train_transforms)
val_dataset = datasets.ImageFolder(train_data_path, transform=test_transforms)

Datasets 都是 torch.utils.data.Dataset 的子类,所以,他们也可以通过 torch.utils.data.DataLoader 使用多线程(python的多进程)。

ImageFolder假设所有的文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类名,其构造函数如下:

ImageFolder(root, transform=None, target_transform=None, loader=default_loader)
  • root:在 root 指定的路径下寻找图片
  • transform:对 PIL Image 进行的转换操作,transform 的输入是使用 loader 读取图片的返回对象
  • target_transform:对 label 的转换
  • loader:给定路径后如何读取图片,默认读取为 RGB 格式的 PIL Image 对象
# 数据集划分
torch.manual_seed(1)
num_train_samples = len(full_dataset)
# num_train_samples = 5000
full_dataset, _ = torch.utils.data.random_split(full_dataset, [num_train_samples, len(full_dataset)-num_train_samples])

train_size = int(0.6 * num_train_samples)
val_size = int(0.2 * num_train_samples)
test_size = num_train_samples - train_size - val_size

train_dataset, val_dataset, test_dataset = torch.utils.data.random_split(full_dataset, [train_size, val_size, test_size])
len(train_dataset), len(val_dataset), len(test_dataset)

torch.manual_seed() 设置随机数种子。

torhc.utils.data.random_split对数据集进行随机划分,其中第一个参数是待划分的数据集,第二个参数是一个列表,其中存储划分后各个数据集占原数据集的比例。

这里划分后训练集、验证集和测试集的比例为 6:2:2。

# 设置 Dataloader
batch_size = 64 # 设置 batch_size

train_dataloader = torch.utils.data.DataLoader(
    dataset=train_dataset,
    batch_size=batch_size,
    shuffle=True
)

val_dataloader = torch.utils.data.DataLoader(
    dataset=val_dataset,
    batch_size=batch_size,
    shuffle=False
)

这一步将数据集装入 Pytorch 的 DataLoader,DataLoader 可以在训练时自动从 Dataset 中抽取样本进行训练,并且能够成批抽取,形成 Batch。

resnet = models.resnet50(pretrained=True)   # 直接使用 Pytorch 官方提供的 resnet 模型,并且获取预训练参数

Pytorch 封装了许多常见的模型,这里使用 Pytorch 提供的 ResNet50 模型,并且加载了模型提供的预训练参数。

# 冻结特征层的参数,进行迁移学习
for param in resnet.parameters():
    param.requires_grad = False
# 更换全连接层,调整输入和输出,释放全连接层的参数
in_features = resnet.fc.in_features
fc = nn.Linear(in_features=in_features, out_features=len(classes))
resnet.fc = fc

这一步冻结了特征层的参数,在后续的训练中不再需要对特征层的参数进行反向传播和更新,降低了训练的难度,这一步属于迁移学习。同时,由于全连接层的大小取决于输出的类别,所以需要对 ResNet50 模型的全连接层进行修改。

# 需要更新的参数(上一步中更换的全连接层参数)
params_to_update = []
for name, param in resnet.named_parameters():
    if param.requires_grad:
        params_to_update.append(param)

确定需要更新的参数,其实就是全连接层的参数,最后的全连接层主要负责分类,而之前的层叫做特征层,主要用于特征提取。

criterion = nn.CrossEntropyLoss()   # 交叉熵损失
optimizer = torch.optim.Adam(params_to_update, lr=0.001)    # Adam 优化器

设置损失函数和优化器,多分类任务常用的损失函数就是交叉熵损失,这里优化器选择 Adam 优化器。

# 选择显卡进行训练
device = 'cuda' if torch.cuda.is_available() else 'cpu'
device

设置显卡。

from time import time
from tqdm import tqdm   # 进度条

# 训练过程
def train(model,
          criterion,
          optimizer,
          train_dataloader,
          val_dataloader,
          print_every,
          num_epoch):
    '''
    :param model: Pytorch模型
    :param criterion: 损失函数
    :param optimizer:  优化器
    :param train_dataloader: 训练集
    :param val_dataloader: 验证集
    :param print_every: 多少个 epochs 输出一次
    :param num_epoch: 训练多少个 epochs
    :returns model: 模型
    :returns train_losses 训练集损失
    :returns val_losses 验证集损失
    '''
    steps = 0
    train_losses, val_losses = [], []
    train_accuracy, val_accuracy = [], []

    model.to(device)
    for epoch in tqdm(range(num_epoch)):
        running_loss = 0
        correct_train = 0
        total_train = 0
        start_time = time()
        iter_time = time()
        
        model.train()
        for i, (images, labels) in enumerate(train_dataloader):
            steps += 1
            # 把数据放到 GPU
            images = images.to(device)
            labels = labels.to(device)

            if epoch == 0 and i == 0:
                torch.onnx.export(resnet, images, 'resnet.pth')

            # 前向传播
            output = model(images)
            loss = criterion(output, labels)

            correct_train += (torch.max(output, dim=1)[1] == labels).sum().item()
            total_train += labels.size(0)

            # 反向传播与优化
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            running_loss += loss.item()

            # 记录输出
            if steps % print_every == 0:
                print(f'Epoch [{epoch + 1}]/[{num_epoch}]. Batch [{i + 1}]/[{len(train_dataloader)}].', end=' ')    # epoch数量
                print(f'Train loss {running_loss / steps:.3f}.', end=' ')
                print(f'Train acc {correct_train / total_train * 100:.3f}.', end=' ')
                train_accuracy.append(correct_train / total_train)
                # 使用验证集评估模型训练过程中的表现
                with torch.no_grad():
                    model.eval()
                    correct_val, total_val = 0, 0
                    val_loss = 0
                    for images, labels in val_dataloader:
                        images = images.to(device)
                        labels = labels.to(device)
                        output = model(images)
                        loss = criterion(output, labels)
                        val_loss += loss.item()

                        correct_val += (torch.max(output, dim=1)[1] == labels).sum().item()
                        total_val += labels.size(0)
                print(f'Val loss {val_loss / len(val_dataloader):.3f}. Val acc {correct_val / total_val * 100:.3f}.', end=' ')
                print(f'Took {time() - iter_time:.3f} seconds')
                val_accuracy.append(correct_val / total_val)
                iter_time = time()

                train_losses.append(running_loss / total_train)
                val_losses.append(val_loss / total_val)


        print(f'Epoch took {time() - start_time}')

        # 保存断点
        checkpoint = {"model_state_dict": model.state_dict(),
                      "optimizer_state_dict": optimizer.state_dict(),
                      "epoch": epoch}
        path_checkpoint = "./checkpoint_{}_epoch.pkl".format(epoch)
        torch.save(checkpoint, path_checkpoint)
        
    return model, train_losses, val_losses, train_accuracy, val_accuracy

定义训练过程,包括前向传播、反向传播、优化,每50次迭代使用验证集进行评估测试,并且输出信息。

print_every = 50    # 每50次迭代输出一次
num_epoch = 5   # 训练的epochs数量

resnet, train_losses, val_losses, train_accuracy, val_accuracy = train(
    model=resnet,
    criterion=criterion,
    optimizer=optimizer,
    train_dataloader=train_dataloader,
    val_dataloader=val_dataloader,
    print_every=print_every,
    num_epoch=num_epoch
)

实际调用训练过程,设置多少次迭代输出一次,以及设置训练多少个 epoch。

# 召回变化
plt.rcParams.update({"font.size": 10})
plt.plot(train_losses, label='Training loss')
plt.plot(val_losses, label='Validation loss')
plt.legend(frameon=False)
plt.savefig('/kaggle/working/loss', dpi=250)
plt.show()

使用 matplotlib 绘制损失变化曲线。

# 准确率变化
plt.rcParams.update({"font.size": 10})
plt.plot(train_accuracy, label='Training Accuracy')
plt.plot(val_accuracy, label='Validation Accuracy')
plt.legend(frameon=False)
plt.savefig('/kaggle/working/accuracy', dpi=250)
plt.show()

准确率变化曲线。

y_true = []
y_pred = []

# 测试过程
i, j = 0, 0
for img, label in test_dataset:
    img = torch.Tensor(img[None])
    img = img.to(device)
    resnet.eval()
    prediction = resnet(img)  # 在 img 最前面加上一维(batch维度)
    prediction_class = int(torch.max(prediction, dim=1)[1])
    y_pred.append(prediction_class)
    y_true.append(label)

测试过程,后续使用 scikit-learn 对测试结果进行评估,评估过程记录每个样本的真实标签 y_true 和预测值(标签)y_pred。

# 正确率
from sklearn.metrics import accuracy_score

accuracy_score(y_true, y_pred)
# 混淆矩阵
from sklearn.metrics import confusion_matrix

conf_matrix = confusion_matrix(y_true, y_pred)
xtick = classes
ytick = classes

plt.rcParams.update({"font.size": 5})
plt.savefig('/kaggle/working/conf_matrix', dpi=250)
sns.heatmap(conf_matrix, fmt='g', cmap='Blues', annot=True, cbar=False, xticklabels=xtick, yticklabels=ytick)
# 精确率
from sklearn.metrics import precision_score

precision_list = precision_score(y_true, y_pred, average=None)
precision_list
# 精确率
from sklearn.metrics import recall_score

recall_list = recall_score(y_true, y_pred, average=None)
recall_list
# 分类准确率/雅卡得相似系数
from sklearn.metrics import jaccard_score

accuracy_list = jaccard_score(y_true, y_pred, average=None)
accuracy_list
# f1 and macro-f1
from sklearn.metrics import f1_score

f1_list = f1_score(y_true, y_pred, average=None)
macro_f1 = f1_score(y_true, y_pred, average='macro')
f1_list, macro_f1

使用 scikit-learn 对结果进行评估,包括正确率、分类准确率、分类召回率、混淆矩阵、F1分数等指标,具体评估方法可以参考 scikit-learn 的参考手册。