前沿导论作业心得——利用RESNET预训练模型进行图片分类任务


学习重点

  • torchvision包的使用

代码实现

导入相关的库

from __future__ import print_function, division
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.optim import lr_scheduler
import numpy as np
import torchvision 
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import os
import copy

知识点一 torchvision

很多基于Pytorch的工具集都非常好用,比如处理自然语言的torchtext,处理音频的torchaudio,以及处理图像视频的torchvision。

torchvision包含一些常用的数据集、模型、转换函数。本实验由他来加载模型和进行数据加载器。

设置有关参数

data_dir = './'    # 设置文件的根目录
data_transforms = {
    # 训练中的数据增强和归一化
    'train': transforms.Compose([
        transforms.RandomResizedCrop(224), # 随机裁剪
        transforms.RandomHorizontalFlip(), # 左右翻转
        transforms.ToTensor(),   # 变为Tensor格式
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])     # 均值方差归一化
    ]), 
    # 验证集不增强,仅进行归一化
    'val': transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

知识点2

transforms.Compose函数就是将transforms组合在一起;而每一个transforms都有自己的功能。

transforms.Resize(256)是按照比例把图像最小的一个边长放缩到256,另一边按照相同比例放缩。

transforms.RandomResizedCrop(224,scale=(0.5,1.0))是把图像按照中心随机切割成224正方形大小的图片。

transforms.ToTensor() 转换为tensor格式,这个格式可以直接输入进神经网络了。

transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])对像素值进行归一化处理。

进行数据集的加载和预处理工作

数据集加载器

进行训练集和验证集的加载,输入的第一个参数是图片的路径,输入的第二个参数是转换的类型(由上所示)

image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
                                          data_transforms[x])
                  for x in ['train','val']}

由于数据集的格式解压后是这样的:

image

image

这种情况需要我们用datasets.ImageFolder中的方法

前面的dataset只是路径的集合,需要创建加载器便于加载,其中batch_size:一批输出的数据,shuffle:是否打乱,这对于用IMageFolder加载的数据是十分有必要的,因为有可能接连几个数据都属于同一类,不利于训练

dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
                                             shuffle=True, num_workers=4)
              for x in ['train','val']}

储存有用的常量

dataset_sizes = {x: len(image_datasets[x]) for x in ['train','val']}   # 得到数据集的长度
class_names = image_datasets['train'].classes   # 得到含有类名称的列表
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")      # 创建运行方式
class_nums = len(class_names)   # 设置待分类的个数

记载预训练模型并改写

# 从torchvision中载入resnet18模型,并且加载预训练
model_conv = torchvision.models.resnet18(pretrained=True)
# 这里的目的是防止预训练模型的准确结果随训练进行变化。
for param in model_conv.parameters():
    param.requires_grad = False
num_ftrs = model_conv.fc.in_features
model_conv.fc = nn.Linear(num_ftrs, class_nums)

这里的最后,我们把最后的全连接层改成我们分类需要的全连接层。

自定义模型和损失器

model_conv = model_conv.to(device)

criterion = nn.CrossEntropyLoss()

optimizer_conv = optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9)

exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)

对于exp_lr_scheduler,有:

  • optimizer (Optimizer):要更改学习率的优化器;
  • step_size(int):每训练step_size个epoch,更新一次参数;
  • gamma(float):更新lr的乘法因子;
  • last_epoch (int):最后一个epoch的index,如果是训练了很多个epoch后中断了,继续训练,这个值就等于加载的模型的epoch。默认为-1表示从头开始训练,即从epoch=1开始。

和optimizer

模型的训练

这里的一般思路是,每一轮词都对训练集进行训练,对验证集

看似是训练模型:

注意细节:

细节一:对于每一轮次设置计时器

since = time.time()

time_elapsed = time.time() - since

细节二:每一轮次设置输出信息

image

细节三:注意将输入的数据加载到gpu上

inputs = inputs.to(device)
labels = labels.to(device)

def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
    since = time.time()

    best_model_wts = copy.deepcopy(model.state_dict())
    
    best_acc = 0.0

    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # 每一个epoch都会进行一次验证
        for phase in ['train','val']:
            if phase == 'train':
                model.train()  # 设置模型为训练模式
            else:
                model.eval()   # 设置模型为验证模式

            running_loss = 0.0
            running_corrects = 0

            #  迭代所有样本
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)

                # 将梯度归零
                optimizer.zero_grad()

                # 前向传播网络,仅在训练状态记录参数的梯度从而计算loss
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels)

                    # 反向传播来进行梯度下降
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                # 统计loss值
                running_loss += loss.item() * inputs.size(0)  #item
                running_corrects += torch.sum(preds == labels.data)
            if phase == 'train':
                # 进行优化器的学习率优化
                scheduler.step()

            epoch_loss = running_loss / dataset_sizes[phase]
            epoch_acc = running_corrects.double() / dataset_sizes[phase]

            print('{} Loss: {:.4f} Acc: {:.4f}'.format(
                phase, epoch_loss, epoch_acc))

            # 依据验证集的准确率来更新最优模型
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())

        print()

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))

    # 载入最优模型
    model.load_state_dict(best_model_wts)
    return model

保存最好的模型

torch.save(model_ft, 'xybw_model')

image

最终的结果到达了很高的正确率

进行test集的加载和预测

进行类别转类别名的测试

id2class = {i:class_names[i] for i in range(len(class_names))}

这里进行转类别名测试的时候犯了难,如果利用ImageFolder的方式进行图片读取,这样会自动将用PIL格式读取,导致第64张图片无法被读取到,这里我们用dataset自定义数据加载器

from torch.utils import data
import cv2 as cv
class Animals(data.Dataset):
    def __init__(self, root):
        imgs=os.listdir(root)
        self.all_image_paths =[os.path.join(root,k) for k in imgs]
        mean = [0.485, 0.456, 0.406]
        std = [0.229, 0.224, 0.225]
        self.mean = np.array(mean).reshape((1, 1, 3))
        self.std = np.array(std).reshape((1, 1, 3))

    def __getitem__(self, index):
        img = cv.imread(self.all_image_paths[index])
        img = cv.resize(img, (224, 224))
        img = img / 255.
        img = (img - self.mean) / self.std
        img = np.transpose(img, [2, 0, 1])
        #label = self.all_image_labels[index]
        img = torch.tensor(img, dtype=torch.float32)

        return self.all_image_paths[index], img

    def __len__(self):
        return len(self.all_image_paths)

自己定义的dataset类需要继承: Dataset

需要实现必要的魔法方法:
-- __init__魔法方法里面进行读取数据文件
-- __getitem__魔法方法进行支持下标访问
-- __len__魔法方法返回自定义数据集的大小,方便后期遍历

之前之所以要设置dataloader的原因,就是要考虑批次(一次读入几组数据)的问题,是否打乱等问题,而在本实验中,最后进行测试集的测试没必要

实例化数据集

dataset = Animals('./test')

这个函数卡了好久的bug现在梳理一下。。。

img = img.unsqueeze(0).to(device)

这里用unsqueeze,是因为这是应该是因为网络的接收输入是一个mini-batch,image unsqueeze后第一个维度是留给batch size的即使要输入1,也要输入[[1]]

to(device)原因易知

pred = pred.cpu().numpy()[0]

得到结果是gpu上的Tensor,需要先到cpu上,在转化成numpy还要脱出一个

def model_predict(img, model, device):
    img = img.unsqueeze(0).to(device)
    id2class = {i:class_names[i] for i in range(len(class_names))}
    model.eval()
    outputs = model(img)
    _, pred = torch.max(outputs, 1)
    pred = pred.cpu().numpy()[0]
    return id2class[pred]

最终进行预测

for i in range(len(dataset)):
    k, j =dataset[i]
    print(k)
    print(model_predict(j, model_ft, device))

思考

dataloader与dataset

之前之所以要设置dataloader的原因,就是要考虑批次(一次读入几组数据)的问题,是否打乱等问题,而在本实验中,最后进行测试集的测试没必要,其实前面学sklearn的时候接触过,就是最后忘了(

torch.utils.data.Dataset 重要的特性是有__getitem____len__方法,这意味着可以用 value[index] 的方式访问内部元素(可以当作列表用)。

torch.utils.data.DataLoader 使用时,我们首先把torch.utils.data.Dataset类当作一个参数传递给 torch.utils.data.DataLoader类,得到一个数据加载器,这个数据加载器每次可以返回一个 Batch 的数据供模型训练使用。

Dataset子类 其实就是一个静态的数据池,这个数据池支持我们用 索引 得到某个数据,想要让这个数据池流动起来,源源不断地输出 Batch 还需要下一个工具 DataLoader类 。所以我们把创建的 Dataset子类 当参数传入 即将构建的DataLoader类才是使用Dataset子类最终目。

以上内容来自:https://www.jianshu.com/p/4818a1a4b5bd 作者:夜和大帝,写得太好了。

model.eval和with torch.no_grad()

在PyTorch中进行validation时,会使用model.eval()切换到测试模式,在该模式下,

主要用于通知dropout层和batchnorm层在train和val模式间切换

在train模式下,dropout网络层会按照设定的参数p设置保留激活单元的概率(保留概率=p); batchnorm层会继续计算数据的mean和var等参数并更新。

在val模式下,dropout层会让所有的激活单元都通过,而batchnorm层会停止计算和更新mean和var,直接使用在训练阶段已经学出的mean和var值。

该模式不会影响各层的gradient计算行为,即gradient计算和存储与training模式一样,只是不进行反传(backprobagation)

而with torch.no_grad()则主要是用于停止autograd模块的工作,以起到加速和节省显存的作用,具体行为就是停止gradient计算,从而节省了GPU算力和显存,但是并不会影响dropout和batchnorm层的行为。

上段摘自pytorch测试的时候为何要加上model.eval()? | w3c笔记 (w3cschool.cn)


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM