【深度学习】CV_基于CNN的图像分类模型_代码逐行注释解析

目录

  • 前言
  • 一、任务描述和关键环节
    • (一)数据预处理
    • (二)网络模块设置
    • (三)网络模型保存与测试
  • 二、具体步骤
    • (一)任务分析与图像数据处理
      • 1.导包
      • 2.数据读取与预处理
        • 2.1 数据读取
        • 2.2 数据预处理
          • (1)制作数据源
          • (2)将预处理的数据指定好
        • 2.3读取标签对应的实际名字
    • (二)模型设置
      • 1.选用经典网络预训练模型
      • 2.结合实际更新模型参数
      • 3.重定义模型全连接层(将预训练模型改为自己需要的)
      • 4.模型初始化:设置需训练层参数
      • 5.优化器设置
      • 6.训练模型设置
    • (三)模型训练
      • 1.开始第一轮训练
      • 2.开始第二轮训练
    • (四)模型测试
      • 1.加载训练好的模型
      • 2.测试数据预处理
      • 3.取概率最大的结果
    • (五)预测结果展示
  • 三、任务总结
  • 感受


前言

本文为个人案例学习笔记,是深度学习CV领域的入门上手项目,通过blog梳理总结,形成整体建模思路、熟悉典型网络架构的搭建、掌握常用代码使用。


提示:以下是本篇文章正文内容,下面案例可供参考

一、任务描述和关键环节

花卉图片识别分类。根据训练集“样本-标签”的分类(本案例采用文件夹分类方法),训练能够识别并对花卉品种进行分类的网络模型。

(一)数据预处理

  • 数据增强
    torchvision中transforms模块自带功能。数据较少,为了有更好的效果,需数据增强操作,如翻转、平移、裁剪等,以增强样本多样性。
  • 数据预处理
    torchvision中transforms已封装,直接调用即可。
  • DataLoader
    DataLoader模块直接读取batch数据。

(二)网络模块设置

  • 加载预训练模型
    torchvision中有很多经典网络架构,调用起来十分方便,并且可以用人家训练好的权重参数来继续训练,也就是所谓的迁移学习。
  • 修改网络模型
    别人训练好的模型并不一定满足个人任务,需要把最后的head层改一改,一般也就是最后的全连接层,改成咱们自己的任务
  • 训练策略选择
    训练时可以全部从头训练,也可以只训练最后咱们任务的层,因为前几层都是做特征提取的,本质任务目标是一致的

(三)网络模型保存与测试

  • 保存
    模型保存的时候可以带有选择性,例如在验证集中如果当前效果好则保存
  • 读取
    读取模型进行实际测试

二、具体步骤

(一)任务分析与图像数据处理

1.导包

代码如下:

# ================导入所需模块================ #
import os               #导入标准库,利用其中API
import matplotlib.pyplot as plt
%matplotlib inline      #jupyter魔法命令,绘图直接嵌入在notebook行内
import numpy as np
import torch
from torch import nn    #导入torch中神经网络工具包
import torch.optim as optim    #pytorch中优化器模块,可以优化模型参数
import torchvision      #导入torchvision包
                        #pip install torchvision
from torchvision import transforms, models, datasets  
                        #tansforms包有内置数据增强策略,models封装好的神经网络模型,比如resnet,dataset数据目录结构
                        #https://pytorch.org/docs/stable/torchvision/index.html
import imageio          #python处理图像、视频的模块,读取、写入、转换格式等。
import time
import warnings
warnings.filterwarnings("ignore")    #忽略版本造成的告警
import random           #随机数模块,生成随机数。如:random.randint(0, 10)0-10之间生成1个随机数
import sys              #导入 Python 标准库中的 sys 模块,可以使用该模块获取版本信息、命令行参数等。如:print(sys.version)
import copy             #该模块用于使用复制副本功能,用其创建副本而不用引用原对象。
import json             #导入JSON数据格式的处理模块
from PIL import Image   #导入 Python Imaging Library(PIL)中的image 模块。PIL 是一个用于处理图像的 Python 库,image 模块包含了 PIL 中的图像处理功能
import os
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"    #防止anaconda内核挂掉的指令,别问,玄学!

2.数据读取与预处理

2.1 数据读取
  • 方法1:通过文件夹做数据集分类,文件夹名为label标签
  • 方法2:用txt写成标注文件。本案例数据少,需数据增强
  • 本案例数据集保存在conda目录下,名称为flower_data,内含train和valid2个文件夹,作为训练和验证集
data_dir = './flower_data/'        #指定数据读取目录
train_dir = data_dir + '/train'    #训练集
valid_dir = data_dir + '/valid'    #验证集
2.2 数据预处理
(1)制作数据源
  • data_transforms中指定了所有图像预处理操作
  • ImageFolder假设所有的文件按文件夹保存好,每个文件夹下面存贮同一类别的图片,文件夹的名字为分类的名字
  • 几种图像数据常用的数据增强操作,图像亮度、对比度、灰度等操作不常用,用在特殊场景下图像识别任务。
# 字典结构,2个key分别train和valid
data_transforms = {
    'train': 
        transforms.Compose([            #.compose指按顺序操作,以([])首尾括起来
        transforms.Resize([96, 96]),    #图像调整为相同大小,会丢失部分信息。一般分类任务多用正方形,size根据实际图像情况设定,不能丢失太多。
                                        #数据增强:如图像的翻转、旋转、平移等操作,由1张变化,得到多张图,数据更丰富。
                                        #一般操作有:旋转、裁剪、水平垂直翻转。
        transforms.RandomRotation(45),  #数据增强:随机旋转,-4545度之间随机选角度。
        transforms.CenterCrop(64),      #数据增强:从中心开始裁剪。理解:原始图像上随机选区域裁剪,增加数据多样性,残缺不全图像。
        transforms.RandomHorizontalFlip(p=0.5),    #数据增强:随机水平翻转 选择一个概率,执行该操作的概率
        transforms.RandomVerticalFlip(p=0.5),      #数据增强:随机垂直翻转
        transforms.ColorJitter(brightness=0.2, contrast=0.1, saturation=0.1, hue=0.1),    #数据增强:参数1为亮度,参数2为对比度,参数3为饱和度,参数4为色相
        transforms.RandomGrayscale(p=0.025),       #数据增强:概率转换成灰度图,3通道就是R=G=B。RGB转为RRR或BBB或BBB,一般不做。
        transforms.ToTensor(),  #数据转换为张量
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])  #标准化操作:均值,标准差,可以用imagnet大数据集的均值和标准差,括号3个值对应RGB三通道
    ]),
    'valid': 
        #验证集不再需要做数据增强,用实际图像做验证即可。
        transforms.Compose([
        transforms.Resize([64, 64]),  #保证验证和训练图像大小一样,训练中中心裁剪后,输出64*64的
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])  #须用于训练集中一样的均值和标准差,不能用先验知识做。
    ]),
}
(2)将预处理的数据指定好
batch_size = 128  #batch较大,因为输入图像小,64*64的,所以可以适当大batch

#本案例中,数据按分类存在文件夹中,文件夹名可作为标签,用于读取数据,不再通过datasets dataloader,通过文件夹形式读取数据,下文命令datasets.ImageFolder

#定义datasets: 2个文件夹train和valid,作为key值.指定好数据集和预处理操作,方法是ImageFolder
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x]) for x in ['train', 'valid']}
#定义dataloaders: 方法orch.utils...(参数datasets,batch,shuffle)
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=batch_size, shuffle=True) for x in ['train', 'valid']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'valid']}    #可有可无,用来算准确率
class_names = image_datasets['train'].classes    #预测顺序索引位置对应类的名称

#Tips:数据集flower_data放在notebook同一级文件夹内。
  • 打印上述image_datasets、class_names、dataloaders、dataset_sizes查看。
  • image_datasets,展示datasets数据集相关参数和相应数据增强操作。
  • class_names:最终预测顺序,本案例102个文件夹,102种类的花,最后图片分类任务会有102个特征值,而class展示出的特征不是1234顺序,而是1 10 100 102 2 20,因此索引位置不是按照1234。
  • 利用jupyter分块执行的特点,打印类别名查看,按照首位数字索引排列,如下:
class_names
---
'1',
 '10',
 '100',
 '101',
 '102',
 '11',
 '12',
 '13',
 '14',
 '15',
 '16',
 '17',
 '18',
 '19',
 '2',
 '20',
 '21',
 '22',
 '23',
 '24',
 '25',
 '26',
 '27',
 '28',
 '29',
 '3',
 '30',
 '31',
 ...]
  • 打印查看图像数据集的相关参数以及数据增强操作有哪些?
image_datasets
---
{'train': Dataset ImageFolder
     Number of datapoints: 6552
     Root location: ./flower_data/train
     StandardTransform
 Transform: Compose(
                Resize(size=[96, 96], interpolation=bilinear, max_size=None, antialias=None)
                RandomRotation(degrees=[-45.0, 45.0], interpolation=nearest, expand=False, fill=0)
                CenterCrop(size=(64, 64))    #中心裁剪
                RandomHorizontalFlip(p=0.5)  #随机水平
                RandomVerticalFlip(p=0.5)
                ColorJitter(brightness=[0.8, 1.2], contrast=[0.9, 1.1], saturation=[0.9, 1.1], hue=[-0.1, 0.1])
                RandomGrayscale(p=0.025)
                ToTensor()
                Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
            ),
 'valid': Dataset ImageFolder
     Number of datapoints: 818
     Root location: ./flower_data/valid
     StandardTransform
 Transform: Compose(
                Resize(size=[64, 64], interpolation=bilinear, max_size=None, antialias=None)
                ToTensor()  
                Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
            )}
  • 查看数据集
dataloaders
---
{'train': <torch.utils.data.dataloader.DataLoader at 0x2095ee35100>,
 'valid': <torch.utils.data.dataloader.DataLoader at 0x2095ee354f0>}
  • 查看数据集大小
dataset_sizes
---
{'train': 6552, 'valid': 818}
2.3读取标签对应的实际名字
with open('cat_to_name.json', 'r') as f:  #读取Json文件的操作。文件内是字典结构,对应文件夹标签即数字值得真实花名。参数r以只读方式读取文件。
    cat_to_name = json.load(f)            #理解:加载f(读取json文件)这一操作。
  • 打印抓取的名字
cat_to_name
---
{'21': 'fire lily',
 '3': 'canterbury bells',
 '45': 'bolero deep blue',
 '1': 'pink primrose',
 '34': 'mexican aster',
 '27': 'prince of wales feathers',
 '7': 'moon orchid',
 '16': 'globe-flower',
 '25': 'grape hyacinth',
 '26': 'corn poppy',
 '79': 'toad lily',
 '39': 'siam tulip',
 '24': 'red ginger',
 '67': 'spring crocus',
 '35': 'alpine sea holly',
...}

(二)模型设置

1.选用经典网络预训练模型

  • 加载models中提供的模型,并且直接用训练好的权重当做初始化参数
  • 第一次执行需要下载,可能会比较慢,我会提供给大家一份下载好的,可以直接放到相应路径
  • 迁移学习
    从0学效果并不好,在别人模型基础上微调。如:用resnet模型和参数,作为我们的初始化。即在经典预训练模型基础上做微调。
  • 迁移学习策略
    具体区分数据集大小,大、中、小,数据集小时,少量微调,经典模型中冻住大部分参数;中等时,前面冻住一部分权重参数;数据量大时,冻住小部分。
model_name = 'resnet'  #可选网络有 ['resnet', 'alexnet', 'vgg', 'squeezenet', 'densenet', 'inception'],可尝试多少种经典网络结构试验效果。
                       #用别人训练好的特征来做。用别人网络结构方法:①torchvision调包来用;②复制粘贴典型网络结构、配置文件。

feature_extract = True #都用人家特征,咱先不更新。True时,冻住所有特征,只保留最后输出层。
  • 选择运行设备
# 是否用GPU训练
train_on_gpu = torch.cuda.is_available()

if not train_on_gpu:
    print('CUDA is not available.  Training on CPU ...')
else:
    print('CUDA is available!  Training on GPU ...') 
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
---
CUDA is available!  Training on GPU ...

2.结合实际更新模型参数

  • 有时候用人家模型,就一直用了,更不更新可以自己定
  • 前面导入了torchvision模块中的models包,封装了典型网络模型,此处直接调用resnet,打印可查看网络内部结构
  • 18层Resnet网络结构10层,其中4层隐层layer中各包含2层Basic Block,最后2层平均池化、全连接层,但模型中输出分类是1000分类,需改动参数符合本案例。
model_ft = models.resnet18()  #torchvision封装了典型的包(模型),选用18层resnet做,18层的能快点,条件好点的也可以选152
model_ft
---
ResNet(
  (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
  (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu): ReLU(inplace=True)
  (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  (layer1): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
    (1): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer2): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer3): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer4): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
  (fc): Linear(in_features=512, out_features=1000, bias=True)    #原模型是1000分类的,我们任务102分类的,需再改。
)
  • 输出由hw维度的特征,reshape操作拉长为1个向量,才能做全连接从而实现分类任务。但在上述resnet模型中,最后做了全局平均池化操作,即将hw中的特征求其均值,变为11的特征,模型中维度512,即原先的hw512经全局平均池化变为11512排列的向量,最后再做1000分类的全连接,而本案例要102分类任务,对输出层做修改即可。
  • 改动模型输出参数,符合102分类案例需求。
# 通过设置参数反向传播时,是否更新梯度,从而冻住输出层之前层的参数更新。不再更新权重参数,也就停止更新。
def set_parameter_requires_grad(model, feature_extracting):  #model前面确定使用resnet,True用人家模型特征提取方法
    if feature_extracting:
        for param in model.parameters():
            param.requires_grad = False    #设置为False,即不更新参数了。

3.重定义模型全连接层(将预训练模型改为自己需要的)

def initialize_model(model_name, num_classes, feature_extract, use_pretrained=True):  #执行initialize_model时,会下载resnet18到C盘user_cach。同时,True代表使用预训练模型。
    
    model_ft = models.resnet18(pretrained=use_pretrained)  # use_pretrained使用resnet中训练好的参数,指定为True.
#     model_ft = model_name(pretrained=use_pretrained)  # use_pretrained使用resnet中训练好的参数,指定为True.
    set_parameter_requires_grad(model_ft, feature_extract)  #到输出层就停止更新了,要用自己的102分类全连接层。
    
    
    #通过名字找到别人模型的输出层及输入特征数。
    num_ftrs = model_ft.fc.in_features  #拿参数:定义的model_ft模型中,.fc在最后一层,.in_features是fc的上一层输出,即fc层输入特征数
    #重写自己的fc层,通过,fc覆盖之前的model_ft的fc层。
    model_ft.fc = nn.Linear(num_ftrs, num_classes)  #类别数自己根据自己任务来,nn.Linear全连接层,num_ftrs即特征个数/模型model_ft中.fc重赋值。num_classes=102
                            
    input_size = 64  #输入大小根据自己配置来

    return model_ft, input_size  #结果返回至模型中

4.模型初始化:设置需训练层参数

model_ft, input_size = initialize_model(model_name, 102, feature_extract, use_pretrained=True)  #feature_extract为true,把与训练模型冻住

#GPU还是CPU计算
model_ft = model_ft.to(device)

# 模型训练好后保存,名字自己起,实际保存的是graf网络结构,权重等参数,后面可直接用,不再做训练。(区分于案例已有best模型,用bset)
filename = 'bset.pt'

# 是否训练所有层
params_to_update = model_ft.parameters()  # model_ft.parameters()保存下了模型中所有参数和名字

print("Params to learn:")
if feature_extract:                           #条件语句布尔值
    params_to_update = []                     #里面存的就是要更新的东西
    for name,param in model_ft.named_parameters():
        if param.requires_grad == True:      # 如果需要权重参数,才会更新并存入 params_to_update = [],打印可知存了w和b
            # List[]保存了自己设置的全连接层,因此是需要更新参数的,保存后传给后面优化器optim
            params_to_update.append(param)
            print("\t",name)                  # 打印出参数名,虽然前面设置了False,但是设置自己的输出全连接层的操作默认了需要更新权重参数。
else:
    for name,param in model_ft.named_parameters():    #是否为偏置更新??????
        if param.requires_grad == True:
            print("\t",name)
---
Params to learn:
	 fc.weight
	 fc.bias
model_ft  #查看修改后的模型,输出层fc为102类特征分类任务,并且要进行权重参数更新
---
ResNet(
  (conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
  (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu): ReLU(inplace=True)
  (maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
  (layer1): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
    (1): BasicBlock(
      (conv1): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer2): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer3): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (layer4): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (downsample): Sequential(
        (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (relu): ReLU(inplace=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
    )
  )
  (avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
  (fc): Linear(in_features=512, out_features=102, bias=True)    #模型已改为102分类
)

5.优化器设置

# 优化器设置
optimizer_ft = optim.Adam(params_to_update, lr=1e-2)  #要训练啥参数,你来定。Adam最热门的优化器,学习率0.01

# 希望学习率不要一直不变,后期衰减。类似于打高尔夫,后期力度小。衰减策略可自定义。
# 选择StepLR衰减策略。step_size=10调用衰减策略,每10个epoch衰减1次。gamma=0.1衰减变为原来的1/10。
scheduler = optim.lr_scheduler.StepLR(optimizer_ft, step_size = 10, gamma=0.1)    #学习率每7个epoch衰减成原来的1/10
criterion = nn.CrossEntropyLoss()                                                 #交叉熵损失函数

6.训练模型设置

def train_model(model, dataloaders, criterion, optimizer, num_epochs=25,filename='bset.pt'):
    #dataloader用datafolder;criterion损失函数。
    
    #计算时间
    since = time.time()
    #记录最好的一次,因为迭代次数多,可能100次不如80次的,所以记录最好一次的。
    best_acc = 0
    #模型放到CPU或者GPU
    model.to(device)
    #训练过程中打印一堆损失和指标
    val_acc_history = []
    train_acc_history = []
    train_losses = []
    valid_losses = []
    #学习率
    LRs = [optimizer.param_groups[0]['lr']]  # 是字典结构,参数取当前学习率
        
    #最好的那次模型,后续会变的,先初始化
    #best_model_wts,当验证集比之前要好时,会更新。
    best_model_wts = copy.deepcopy(model.state_dict())  #保存好的权重参数值,state_dict()即当前权重参数,验证集期间若发现更好参数再更新赋值给best_model_wts
    
    
    #按epoch来遍历
    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))    #打印当前第几个epoch信息
        print('-' * 10)

        # 训练和验证
        for phase in ['train', 'valid']:    #phase阶段,用于遍历训练和验证两个阶段。
            if phase == 'train':
                model.train()  # 训练
            else:
                model.eval()   # 验证

            #初始化损失和正确个数,因为每次更新是一个epoch中的一部分,所以都初始化从0开始
            running_loss = 0.0
            running_corrects = 0

            
            ###  完成1次训练train,在上一层for循环,再去进行1次valid验证。都要走前向传播,只是train需要参数更新
            # 把数据都取到
            for inputs, labels in dataloaders[phase]:  #dataloader前面设置为字典格式,key名分别是train和valid,value值是里面data数据
                inputs = inputs.to(device)  #放到CPU或GPU
                labels = labels.to(device)  #device前面已经固定好

                # 梯度清零
                optimizer.zero_grad()
                # 只有训练的时候计算和更新梯度
                outputs = model(inputs)
                loss = criterion(outputs, labels)
                _, preds = torch.max(outputs, 1)
                # 训练阶段才更新权重(相当于1次迭代)
                if phase == 'train':
                    loss.backward()  #反向传播
                    optimizer.step()  #参数更新

                # 计算损失(running_loss当前迭代的损失)
                # 在for循环中使用+=,是需要把每次迭代的损失和正确值做累加,累加到1个epoch
                running_loss += loss.item() * inputs.size(0)    # inputs.size(0)是batch,0表示batch那个维度,loss.item()是batch里的损失
                running_corrects += torch.sum(preds == labels.data)    #预测结果最大的和真实值是否一致
                
            
            # 累加完成后,求平均(在for循环外,即完成一个epoch)
            epoch_loss = running_loss / len(dataloaders[phase].dataset)  #算平均,len(dataloaders[phase].dataset)是数据集总迭代次数
            epoch_acc = running_corrects.double() / len(dataloaders[phase].dataset)  #算1个epoch中正确率。正确/总数
            
            time_elapsed = time.time() - since  #一个epoch经历了多少时间
            print('Time elapsed {:.0f}m {:.0f}s'.format(time_elapsed // 60, time_elapsed % 60))
            print('{} Loss: {:.4f} Acc: {:.4f}'.format(phase, epoch_loss, epoch_acc))
            

            ### 完成了上述for循环,分别train和valid后
            # 得到最好那次的模型
            if phase == 'valid' and epoch_acc > best_acc:  #若当前处验证阶段,并且效果比之前最好结果要好,则更新
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())  #用当前一次替换更新
                state = {
                  'state_dict': model.state_dict(),  #字典里key就是各层的名字,值就是训练好的权重
                  'best_acc': best_acc,
                  'optimizer' : optimizer.state_dict(),
                }
                torch.save(state, filename)
            
            
            if phase == 'valid':
                val_acc_history.append(epoch_acc)
                valid_losses.append(epoch_loss)
                #scheduler.step(epoch_loss)#学习率衰减
            
            if phase == 'train':
                train_acc_history.append(epoch_acc)
                train_losses.append(epoch_loss)
        
        print('Optimizer learning rate : {:.7f}'.format(optimizer.param_groups[0]['lr']))
        LRs.append(optimizer.param_groups[0]['lr'])  #保存当前学习率
        print()
        scheduler.step()  #定义过的学习率衰减器:学习率衰减,累加10次执行1次衰减
        
        ###  1个epoch(for循环)结束

    
    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, val_acc_history, train_acc_history, valid_losses, train_losses, LRs

(三)模型训练

1.开始第一轮训练

  • 当前只训练输出层(全连接层FC.)
model_ft, val_acc_history, train_acc_history, valid_losses, train_losses, LRs  = train_model(model_ft, dataloaders, criterion, optimizer_ft, num_epochs=20)
---
Epoch 0/19
----------
Time elapsed 0m 36s
train Loss: 4.0560 Acc: 0.2410
Time elapsed 0m 39s
valid Loss: 3.5588 Acc: 0.2775
Optimizer learning rate : 0.0100000

Epoch 1/19
----------
Time elapsed 1m 11s
train Loss: 2.8984 Acc: 0.3926
Time elapsed 1m 14s
valid Loss: 3.6439 Acc: 0.3020
Optimizer learning rate : 0.0100000

...

2.开始第二轮训练

  • FC已经训练好,前期基于迁移学习策略,冻住已有模型参数,现在第二轮解冻模型,开始更新权重,第二轮所有层一起训练。
  • 发现102分类任务,30%的准确率,效果并不好。因此,在前述迁移学习中,现有自定义的102分类FC已经训练成功,那么将resnet前面冻结部分,全部解冻,一同参与权重参数更新。
  • 只需遍历模型model_ft中所有参数,并设置梯度更新为True.
  • 优化器,不再只优化FC,而是模型.所有参数都优化,学习率再小一点,因为整体参与训练,再加学习率衰减。
for param in model_ft.parameters():
    param.requires_grad = True

# 再继续训练所有的参数,学习率调小一点
optimizer = optim.Adam(model_ft.parameters(), lr=1e-3)  #model_ft.parameters()拿到模型中所有参数
scheduler = optim.lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)  #学习率衰减

# 损失函数
criterion = nn.CrossEntropyLoss()
# 加载之前训练好的权重参数

checkpoint = torch.load(filename)  #最好一次的模型(即保存到命名文件中的模型及其参数)加载进来
best_acc = checkpoint['best_acc']
model_ft.load_state_dict(checkpoint['state_dict'])  #当前所有权重参数,替代之前模型中的参数
model_ft, val_acc_history, train_acc_history, valid_losses, train_losses, LRs  = train_model(model_ft, dataloaders, criterion, optimizer, num_epochs=10,)

(四)模型测试

1.加载训练好的模型

# 若断了,在本地重新加载模型。保存好训练的模型,再重新加载进来。
model_ft, input_size = initialize_model(model_name, 102, feature_extract, use_pretrained=True)

# GPU模式
model_ft = model_ft.to(device)

# 保存文件的名字
filename='bset.pt'

# 加载模型
checkpoint = torch.load(filename)
best_acc = checkpoint['best_acc']
model_ft.load_state_dict(checkpoint['state_dict'])  #加载当前模型,用当前字典中的参数(字典结构'state_dict'),替换已有模型model_ft的参数。

#checkpoint加载的模型参数必须与之前保持一致,例如102分类保持一致。

2.测试数据预处理

  • 测试数据处理方法需要跟训练时一致才可以
  • crop操作的目的是保证输入的大小是一致的
  • 标准化操作也是必须的,用跟训练数据相同的mean和std,但是需要注意一点训练数据是在0-1上进行标准化,所以测试数据也需要先归一化
  • 最后一点,PyTorch中颜色通道是第一个维度,跟很多工具包都不一样,需要转换
# 案例求简便,用验证集valid充当test测试集
# 得到一个batch的测试数据
dataiter = iter(dataloaders['valid'])  #取一个验证集迭代器,dataiter
images, labels = next(dataiter)  #.next是按照batch大小按顺序取数据  # = dataiter.next()版本原因可能报错,改为 = next(dataiter)即可

model_ft.eval()  

if train_on_gpu:
    output = model_ft(images.cuda())  #网络模型model_ft前向传播,得到输出output
else:
    output = model_ft(images)
  • output表示对一个batch中每一个数据得到其属于各个类别的可能性
output.shape  #128是batch,会给128个batch里每个结果,都分配对应于102个结果的概率值,后面再.max取最大
---
torch.Size([128, 102])

3.取概率最大的结果

_, preds_tensor = torch.max(output, 1)    #_, preds_tensor得到最大概率样本类别的ID

#GPU活动的数据是tensor,因后期要画图,matplotlib只支持ndarray,所以要转换。前面CPU结果直接.numpy,后面GPU先.cpu再转换
preds = np.squeeze(preds_tensor.numpy()) if not train_on_gpu else np.squeeze(preds_tensor.cpu().numpy())
#np.squeeze是numpy库中的函数,可以用于从维数为1的数组中移除单维,让数组变得更紧凑。

preds    #得到预测结果
---
array([ 89,  38,  56,  26,  60,  43,  23,  78,  53,   5,  30,  23,  43,
        39,  89,  74,  87,  27,  43,  49,  54,  80,  43,  59,  56,  41,
        90,  81,  59,  88,  73,   2,  24,  99,  50,  27,  61,  78, 101,
        57,  49,  41,  62,  80,  61,  39,  49,  67,  48,   9,  52,  49,
        87,  71,  61,  35,  74,  93,  82,  95,  70,  52,  49,  96,  54,
        84,  20,  35,  59,  41,  62,  52,  75,  17,  17,  13,  60,  61,
        76,  45,  45,  53,  57,  33,  60,  67,  65,  73,  49,  73,  78,
        95,  84,  60,  16,  49,  15,  70,  87,   5,   1,   6,  57,  38,
        46,  86,  60,  95,  11,  56,  24,  50,  18,  45,  97,  84,  43,
        89,  49,  43,  49,  43,  52,  72,  81,  11,  28,  64], dtype=int64)

(五)预测结果展示

  • 画图
def im_convert(tensor):
    """ 展示数据"""
    
    #torch中dataloader取到图片image,所以画图需转成numpy格式
    image = tensor.to("cpu").clone().detach()  #将 tensor 移到 CPU 上的操作,并且保留 tensor 的拷贝,避免与原 tensor 发生冲突,detach是从计算图中分离出Tensor,使其不再具有梯度信息,在反向传播时不会产生梯度。
    image = image.numpy().squeeze()    #维度压缩squeeze()
    image = image.transpose(1,2,0)     #torch中,通道优先,转为图像格式长、宽、通道,012是位次
    image = image * np.array((0.229, 0.224, 0.225)) + np.array((0.485, 0.456, 0.406))    #标准差还原操作
    image = image.clip(0, 1)           #clip防止数据越界、异常

    return image    #数据还原,便于展示?
# 画图
fig = plt.figure(figsize = (20, 20))
columns = 4
rows = 2

for idx in range (columns*rows):
    ax = fig.add_subplot(rows, columns, idx+1, xticks=[], yticks=[])
    plt.imshow(im_convert(images[idx]))
    ax.set_title("{} ({})".format(cat_to_name[str(preds[idx])], cat_to_name[str(labels[idx].item())]),
                 color=("green" if cat_to_name[str(preds[idx])]==cat_to_name[str(labels[idx].item())] else "red"))
plt.show()
---
展示随机抽取预测的结果,按照前述设置,预测正确的会显示花名,括号中是正确花名(标签值)对错结果会区分红绿色展示图头。

三、任务总结

  • 本任务网络模型和训练策略通用性强,类似任务可参照进行修改。
  • 卷积运算中,数据标准化会受影响,因此每个卷积层后会再进行归一化,BatchNormal…
  • 案例选用18层Resnet模型,可改层数,层数越多,分类任务效果越好。通常几个常用层数18、50、101、152层。调用命令是
model_ft = models.resnet50    #resnet50是模型名,可改名调用

结构分析如图:(懒得敲了)
【深度学习】CV_基于CNN的图像分类模型_代码逐行注释解析_第1张图片
【深度学习】CV_基于CNN的图像分类模型_代码逐行注释解析_第2张图片

  • 迁移学习策略
    • phase1:冻住别人训练的模型,对自己末端的模型进行随机初始化,也就是修改了102分类后的全连接层,然后设置相应参数,完成FC训练。
    • phase2: FC训练好后,可以解冻前面经典模型,梯度改为True,即需要更新参数,开始第二轮全部层投入训练。
  • ACC出现浮动,多因学习率过大。
  • 注意区分全局平均池化和最大池化。

感受

感受:看完案例、推完代码,仍觉脑袋空空,缺乏整体思维。经讨论,对策:多写项目、多看模型。

你可能感兴趣的:(深度学习_CV,深度学习,cnn,分类)