本文为个人案例学习笔记,是深度学习CV领域的入门上手项目,通过blog梳理总结,形成整体建模思路、熟悉典型网络架构的搭建、掌握常用代码使用。
提示:以下是本篇文章正文内容,下面案例可供参考
花卉图片识别分类。根据训练集“样本-标签”的分类(本案例采用文件夹分类方法),训练能够识别并对花卉品种进行分类的网络模型。
代码如下:
# ================导入所需模块================ #
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内核挂掉的指令,别问,玄学!
data_dir = './flower_data/' #指定数据读取目录
train_dir = data_dir + '/train' #训练集
valid_dir = data_dir + '/valid' #验证集
# 字典结构,2个key分别train和valid
data_transforms = {
'train':
transforms.Compose([ #.compose指按顺序操作,以([])首尾括起来
transforms.Resize([96, 96]), #图像调整为相同大小,会丢失部分信息。一般分类任务多用正方形,size根据实际图像情况设定,不能丢失太多。
#数据增强:如图像的翻转、旋转、平移等操作,由1张变化,得到多张图,数据更丰富。
#一般操作有:旋转、裁剪、水平垂直翻转。
transforms.RandomRotation(45), #数据增强:随机旋转,-45到45度之间随机选角度。
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]) #须用于训练集中一样的均值和标准差,不能用先验知识做。
]),
}
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同一级文件夹内。
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}
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',
...略}
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 ...
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分类的,需再改。
)
# 通过设置参数反向传播时,是否更新梯度,从而冻住输出层之前层的参数更新。不再更新权重参数,也就停止更新。
def set_parameter_requires_grad(model, feature_extracting): #model前面确定使用resnet,True用人家模型特征提取方法
if feature_extracting:
for param in model.parameters():
param.requires_grad = False #设置为False,即不更新参数了。
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 #结果返回至模型中
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分类
)
# 优化器设置
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() #交叉熵损失函数
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
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
...略
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,)
# 若断了,在本地重新加载模型。保存好训练的模型,再重新加载进来。
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分类保持一致。
# 案例求简便,用验证集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.shape #128是batch,会给128个batch里每个结果,都分配对应于102个结果的概率值,后面再.max取最大
---
torch.Size([128, 102])
_, 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()
---
展示随机抽取预测的结果,按照前述设置,预测正确的会显示花名,括号中是正确花名(标签值)对错结果会区分红绿色展示图头。
model_ft = models.resnet50 #resnet50是模型名,可改名调用
感受:看完案例、推完代码,仍觉脑袋空空,缺乏整体思维。经讨论,对策:多写项目、多看模型。