pytorch==1.1.0
cuda==10.0
python==3.7
1.导入相关包
from __future__ import print_function, division
import torch
import torch.nn as nn
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
plt.ion() # 开启交互模式
2.加载数据
今天要解决的问题是训练一个模型来分类蚂蚁ants和蜜蜂bees。ants和bees各有约120张训练图片。每个类有75张验证图片。从零开始在如此小的数据集上进行训练通常是很难泛化的。由于我们使用迁移学习,模型的泛化能力会相当好。 该数据集是imagenet的一个非常小的子集。从此处 下载数据,并将其解压缩到当前目录。
# 数据增强
# 训练集数据扩充和归一化
# 在验证集上仅需要归一化
data_transforms = {
'train': transforms.Compose([ # 所有的转换使用Compose链接在一起
transforms.RandomResizedCrop(224), # 随机裁剪224×224图像,然后再resize
transforms.RandomHorizontalFlip(), # 随机水平翻转图像,默认概率为50%。
transforms.ToTensor(), # 将图像数据,转换为网络训练所需的tensor向量
# ToTensor将数值范围为0-255的PIL Image转换为一个浮点张量,并通过将其除以255将其归一化为0-1。
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
#transforms.Normalize(norm_mean, norm_std), # 标准化均值为0标准差为1
# 归一化使用一个3通道张量,每个通道被归一化为T = (T - mean)/(标准差)
]),
# 注意:对于验证和测试数据,不执行RandomResizedCrop、RandomRotation和RandomHorizontalFlip转换。
'val': transforms.Compose([
transforms.Resize(256), # 按照比例把图像最小的一个边长放缩到256,另一边按照相同比例放缩
transforms.CenterCrop(224), # 从中心裁剪224×224图像
transforms.ToTensor(), # 转化为张量
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) # 归一化
]),
}
# 数据加载
data_dir = 'data/hymenoptera_data'
# Datasets子类的实例
# datasets.ImageFolder 返回的dataset其属性
#self.classes:用一个 list 保存类别名称
#但由于torchvision.datasets.ImageFolder函数的使用必须对数据的放置有要求,必须在data_dir目录下放置train和val两个文件夹,然后每个文件夹下,每一类图片单独放在一个文件夹里。官方的例子是ants和bees,所以在train和val文件夹下都有ants和bees这两个文件夹,分别放置相应的文件。
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),data_transforms[x])for x in ['train', 'val']}
#Dataloader类:对Dataset挑选后的数据进行打包,为后面的网络提供不同的数据形式。
# 工具函数torch.utils.data.DataLoader,让数据变成mini-batch,且在准备mini-batch的时候可以多线程并行处理
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x],
batch_size=4,
shuffle=True, # 在每个 epoch 开始时,是否对数据进行打乱
num_workers=0) # 使用几个进程来获取并处理数据,num_workers=0代表不使用多进程
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")
3.可视化部分训练图像
def imshow(inp, title=None):
inp = inp.numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
inp = std * inp + mean
inp = np.clip(inp, 0, 1)#限制上下界
plt.imshow(inp)
if title is not None:
plt.title(title)
plt.pause(0.001)
# 获取一批训练数据
inputs, classes = next(iter(dataloaders['train']))
# 批量制作网格
out = torchvision.utils.make_grid(inputs)
imshow(out, title=[class_names[x] for x in classes])
输出
4.训练模型
下面的参数scheduler是一个来自 torch.optim.lr_scheduler 的学习速率调整类的对象(LRscheduler object)。
# 所有训练样本都已输入到模型中,称为一个epoch
def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
since = time.time()
best_model_wts = copy.deepcopy(model.state_dict())
# copy.copy(xxx)浅拷贝只拷贝最外层的数值和指针,不会去拷贝最外层“指针”所指向的内层的东西
# copy.deepcopy(xxx)深拷贝则会拷贝全部层的东西
# torch.nn.Module模块中的state_dict 保存模型中的weight权值和bias偏置值
best_acc = 0.0
for epoch in range(num_epochs):
print('Epoch {}/{}'.format(epoch, num_epochs - 1))
print('-' * 10)
# 每个epoch都有一个训练和验证阶段,每个epoch都会历遍一次整个训练集
for phase in ['train', 'val']:
if phase == 'train':
# scheduler.step()通常用在epoch里面,更新优化器的学习率lr
# UserWarning: Detected call of `lr_scheduler.step()` before `optimizer.step();
# 解决:warnings.filterwarnings("ignore")
scheduler.step()
# 如果模型中有BN层(Batch Normalization, 批量归一化)和Dropout,需要在训练时添加model.train(),在测试时添加model.eval()。
# 其中model.train()是保证BN层用每一批数据的均值和方差,而model.eval()是保证BN用全部训练数据的均值和方差;
# 而对于Dropout,model.train()是随机取一部分网络连接来训练更新参数,而model.eval()是利用到了所有网络连接。
# 训练过程中使用model.train(),作用是启用batch normalization和drop out。
model.train() # set model to training mode
else:
# 测试过程中使用model.eval(),这时神经网络会沿用batch normalization的值,并不使用drop out。
model.eval() # set model to evaluate mode
running_loss = 0.0
running_corrects = 0.0
# 迭代数据
for inputs, labels in dataloaders[phase]:
# .to(device): copy一份到device所指定的GPU上去,之后的运算都在GPU上进行。
inputs = inputs.to(device)
labels = labels.to(device)
# 梯度参数清零
# 因为如果不清零,那么使用的这个grad就得同上一个mini-batch有关,这不是我们需要的结果。
optimizer.zero_grad()
# 与with torch.no_grad() 相似,参数是一个bool类型的值,决定了梯度计算启动(True)或禁用(False)。
# 训练阶段启动自动求导,验证阶段关掉梯度计算,节省eval的时间;预测阶段并不反向求导,只有前向计算,求导及梯度更新只出现在训练阶段
# 只进行inference时,model.eval()是必须使用的,否则会影响结果准确性。 而torch.no_grad()并不是强制的,只影响运行效率。
# track history if only in train
with torch.set_grad_enabled(phase == 'train'):
# 前向计算
outputs = model(inputs)
_, preds = torch.max(outputs, 1) # 1表示取每行的最大值;_,表示只返回最大值所在位置
# 计算loss
loss = criterion(outputs, labels)
# 后向计算 + 仅在训练阶段进行优化
if phase == 'train':
# optimizer更新参数空间需要基于反向梯度
loss.backward()
# optimizer.step()通常用在每个mini-batch之中,更新模型参数
# mini-batch训练模式是假定每一个训练集就只有mini-batch这样大,一次训练更新一次参数空间
optimizer.step()
# 在每个epoch内累积统计running_loss
running_loss += loss.item() * inputs.size(0) # .size(0)取行数
# .data 取出本体tensor数据,舍弃了grad,grad_fn等额外反向图计算过程需保存的额外信息。
running_corrects += torch.sum(preds == labels.data)
# 计算损失的平均值
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()) # 深度拷贝
#state_dict就是一个简单的Python字典,它将模型中的可训练参数(比如weights和biases,batchnorm的running_mean、torch.optim参数等)通过将模型每层与层的参数张量之间一一映射,实现保存、更新、变化和再存储
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))
# 加载最佳模型权重
# load_state_dict将预训练的参数权重加载到新的模型中
model.load_state_dict(best_model_wts)
return model
5.可视化模型的预测结果
def visualize_model(model, num_images=6):
# 默认情况下model.train(),model.training为True;
# 若model.eval(),model.training为False
was_training = model.training
# 使用model.eval()时,它指示模型不需要学习任何新的内容,并且模型用于测试。
# model.eval()等价于model.train(mode=False)
model.eval()
images_so_far = 0
fig = plt.figure()
with torch.no_grad(): # 验证阶段关掉梯度计算
for i, (inputs, labels) in enumerate(dataloaders['val']):
# .to(device): copy到device所指定的GPU上去
inputs = inputs.to(device)
labels = labels.to(device)
outputs = model(inputs)
_, preds = torch.max(outputs, 1)
for j in range(inputs.size()[0]): # 等价于.size(0)
images_so_far += 1
ax = plt.subplot(num_images // 2, 2, images_so_far)
ax.axis('off')
ax.set_title('predicted: {}'.format(class_names[preds[j]]))
# .cpu是把数据转移到cpu上, .data是读取Variable中的tensor本体
# 如果tensor是放在GPU上,先得用.cpu()把它传到cpu上
imshow(inputs.cpu().data[j])
if images_so_far == num_images:
model.train(mode=was_training) # False,测试模式
return
model.train(mode=was_training)
迁移学习的两个主要场景:
1.微调Convnet:使用预训练的网络(如在 imagenet 1000上训练而来的网络)来初始化自己的网络,而不是随机初始化。其他的训练步骤不变。
2.将Convnet看成固定的特征提取器:首先固定ConvNet除了最后的全连接层外的其他所有层。最后的全连接层被替换成一个新的随机初始化的层,只有这个新的层会被训练(只有这层参数会在反向传播时更新)。
1.微调
往往为了加快学习的进度,在训练的初期我们直接加载pre-train模型中预先训练好的参数, 作为backbone(主干网络)。torchvision.models这个包中包含alexnet、densenet、inception、resnet、 squeezenet、vgg等常用的网络结构,并且提供了预训练模型。
注意:基本所有预训练模型都是基于ImageNet数据集训练的。
我们从预训练模型开始,更新我们新任务的所有模型参数,实质上是重新训练整个模型。
if __name__=='__main__':
# pretrained设定是否导入预训练的参数
model_ft = models.resnet18(pretrained=True)
# 内层与预先训练的模型保持一致,只有最后的层被更改以适应我们的类数量
# 修改预训练模型的参数
# 提取fc(全连接)层中固定的参数
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, 2)# 修改类别为2
model_ft = model_ft.to(device)#将模型加载到相应的设备:把model.parameters()移动到GPU上面去
criterion = nn.CrossEntropyLoss()## 定义损失函数:交叉熵损失函数CrossEntropyLoss
##定义优化算法
# SGD指stochastic gradient descent,即随机梯度下降。是梯度下降的batch版本。
# 缺点是,其更新方向完全依赖于当前的batch
# 引入momentum动量法,模拟物体运动时的惯性,即更新的时候在一定程度上保留之前更新的方向
# 参考:https://blog.csdn.net/tsyccnh/article/details/76270707
# optimizer_ft 更新mini-batch
optimizier_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9) # 观察所有参数都正在优化
# torch.optim.lr_scheduler.StepLR 根据epoch训练次数来调整学习率
# 更新策略:每过step_size个epoch,做一次更新;gamma是更新lr的乘法因子
# 每7个epochs衰减LR通过设置gamma=0.1
# exp_lr_scheduler 更新epoch
exp_lr_scheduler = lr_scheduler.StepLR(optimizier_ft, step_size=7, gamma=0.1)# 每7个epochs衰减LR通过设置gamma=0.1
'''
torch.optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1, last_epoch=-1)
参数:
step_size(int)
学习率下降间隔数,若为 30,则会在 30、 60、 90…个 step 时,将学习率调整为 lr*gamma。
gamma(float)
学习率调整倍数,默认为 0.1 倍,即下降 10 倍。
last_epoch(int)
上一个 epoch 数,这个变量用来指示学习率是否需要调整。当last_epoch 符合设定的间隔时,就会对学习率进行调整。当为-1 时,学习率设置为初始值。
'''
##训练模型
model_ft = train_model(model_ft, criterion, optimizier_ft, exp_lr_scheduler, num_epochs=25)
# 保存模型
torch.save(model_ft, 'models/model') # 此处暂时以保存整个模型结构和参数为例
输出
Epoch 0/24
----------
train Loss: 1.0259 Acc: 0.5779
val Loss: 0.2637 Acc: 0.8889
Epoch 1/24
----------
train Loss: 0.4967 Acc: 0.8033
val Loss: 0.2769 Acc: 0.8889
Epoch 2/24
----------
train Loss: 0.5174 Acc: 0.7705
val Loss: 0.2791 Acc: 0.8824
Epoch 3/24
----------
train Loss: 0.4558 Acc: 0.8033
val Loss: 0.1408 Acc: 0.9477
Epoch 4/24
----------
train Loss: 0.6160 Acc: 0.7623
val Loss: 0.2746 Acc: 0.9085
Epoch 5/24
----------
train Loss: 0.5924 Acc: 0.7828
val Loss: 0.4119 Acc: 0.8039
Epoch 6/24
----------
train Loss: 0.4640 Acc: 0.8279
val Loss: 0.2652 Acc: 0.8954
Epoch 7/24
----------
train Loss: 0.3876 Acc: 0.8279
val Loss: 0.2352 Acc: 0.9085
Epoch 8/24
----------
train Loss: 0.2969 Acc: 0.8648
val Loss: 0.2547 Acc: 0.9020
Epoch 9/24
----------
train Loss: 0.3104 Acc: 0.8443
val Loss: 0.2225 Acc: 0.9281
Epoch 10/24
----------
train Loss: 0.3371 Acc: 0.8770
val Loss: 0.2065 Acc: 0.9477
Epoch 11/24
----------
train Loss: 0.3467 Acc: 0.8607
val Loss: 0.2208 Acc: 0.9216
Epoch 12/24
----------
train Loss: 0.2591 Acc: 0.8811
val Loss: 0.2084 Acc: 0.9346
Epoch 13/24
----------
train Loss: 0.3585 Acc: 0.8648
val Loss: 0.2010 Acc: 0.9216
Epoch 14/24
----------
train Loss: 0.2993 Acc: 0.8770
val Loss: 0.2033 Acc: 0.9412
Epoch 15/24
----------
train Loss: 0.2513 Acc: 0.8975
val Loss: 0.1933 Acc: 0.9477
Epoch 16/24
----------
train Loss: 0.2700 Acc: 0.8648
val Loss: 0.1956 Acc: 0.9346
Epoch 17/24
----------
train Loss: 0.2791 Acc: 0.8730
val Loss: 0.2212 Acc: 0.9150
Epoch 18/24
----------
train Loss: 0.2940 Acc: 0.8648
val Loss: 0.2397 Acc: 0.9281
Epoch 19/24
----------
train Loss: 0.2921 Acc: 0.8811
val Loss: 0.1939 Acc: 0.9542
Epoch 20/24
----------
train Loss: 0.3482 Acc: 0.8402
val Loss: 0.2195 Acc: 0.9346
Epoch 21/24
----------
train Loss: 0.2135 Acc: 0.9303
val Loss: 0.2011 Acc: 0.9346
Epoch 22/24
----------
train Loss: 0.3331 Acc: 0.8525
val Loss: 0.2046 Acc: 0.9346
Epoch 23/24
----------
train Loss: 0.2960 Acc: 0.8852
val Loss: 0.1906 Acc: 0.9477
Epoch 24/24
----------
train Loss: 0.2609 Acc: 0.8893
val Loss: 0.1947 Acc: 0.9412
Training complete in 10m 59s
Best val Acc: 0.9542
模型评估与可视化
visualize_model(model_ft)
# 如果在脚本中使用ion()命令开启了交互模式,没有使用ioff()关闭的话,则图像会一闪而过,并不会常留。
# 要想防止这种情况,需要在plt.show()之前加上ioff()命令。
plt.ioff()
plt.show()
场景2: 特征提取
我们从预训练模型开始,仅更新从中导出预测的最终图层权重。
首先固定ConvNet除了最后的全连接层外的其他所有层。最后的全连接层被替换成一个新的随机初始化的层,只有这个新的层会被训练(只有这层参数会在反向传播时更新)。
注意:当一个模型在PyTorch中加载时,它的所有参数的requires_grad字段默认设置为True。这意味着对参数值的每一次更改都将被存储,以便在用于训练的反向传播图中使用。这增加了内存需求。由于我们的预训练模型中的大多数参数已经被训练,我们将requires_grad字段重置为false。
# 加载预训练模型
model_conv = torchvision.models.resnet18(pretrained=True)
# 通过设置 requires_grad == False来冻结网络参数,这样在反向传播backward()的时候他们的梯度就不会被计算。
for param in model_conv.parameters():
param.requires_grad = False
# Parameters of newly constructed modules have requires_grad=True by default
# 提取fc(全连接)层中固定的参数
num_ftrs = model_conv.fc.in_features
# 修改类别为2
model_conv.fc = nn.Linear(num_ftrs, 2)
# 将模型加载到相应的设备中
model_conv = model_conv.to(device)
# 定义损失函数,交叉熵损失函数CrossEntropyLoss
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)
#训练模型
model_conv = train_model(model_conv, criterion, optimizer_conv, exp_lr_scheduler, num_epochs=25)
输出
Epoch 0/24
----------
train Loss: 0.5811 Acc: 0.6721
val Loss: 0.1988 Acc: 0.9542
Epoch 1/24
----------
train Loss: 0.4550 Acc: 0.7951
val Loss: 0.4463 Acc: 0.8170
Epoch 2/24
----------
train Loss: 0.6513 Acc: 0.7582
val Loss: 0.1597 Acc: 0.9608
Epoch 3/24
----------
train Loss: 0.6122 Acc: 0.7336
val Loss: 0.1700 Acc: 0.9412
Epoch 4/24
----------
train Loss: 0.3723 Acc: 0.8402
val Loss: 0.1713 Acc: 0.9542
Epoch 5/24
----------
train Loss: 0.5031 Acc: 0.7705
val Loss: 0.2177 Acc: 0.9412
Epoch 6/24
----------
train Loss: 0.3255 Acc: 0.8566
val Loss: 0.1692 Acc: 0.9477
Epoch 7/24
----------
train Loss: 0.4114 Acc: 0.8074
val Loss: 0.1600 Acc: 0.9542
Epoch 8/24
----------
train Loss: 0.4377 Acc: 0.8238
val Loss: 0.1631 Acc: 0.9608
Epoch 9/24
----------
train Loss: 0.3700 Acc: 0.8361
val Loss: 0.1646 Acc: 0.9542
Epoch 10/24
----------
train Loss: 0.4176 Acc: 0.8197
val Loss: 0.1815 Acc: 0.9608
Epoch 11/24
----------
train Loss: 0.3712 Acc: 0.8279
val Loss: 0.1785 Acc: 0.9608
Epoch 12/24
----------
train Loss: 0.3660 Acc: 0.8320
val Loss: 0.1780 Acc: 0.9542
Epoch 13/24
----------
train Loss: 0.3335 Acc: 0.8484
val Loss: 0.1654 Acc: 0.9542
Epoch 14/24
----------
train Loss: 0.3933 Acc: 0.8115
val Loss: 0.1697 Acc: 0.9608
Epoch 15/24
----------
train Loss: 0.3851 Acc: 0.8238
val Loss: 0.1891 Acc: 0.9608
Epoch 16/24
----------
train Loss: 0.3221 Acc: 0.8730
val Loss: 0.1841 Acc: 0.9542
Epoch 17/24
----------
train Loss: 0.3079 Acc: 0.8648
val Loss: 0.1883 Acc: 0.9542
Epoch 18/24
----------
train Loss: 0.2861 Acc: 0.8648
val Loss: 0.1708 Acc: 0.9477
Epoch 19/24
----------
train Loss: 0.3241 Acc: 0.8607
val Loss: 0.1712 Acc: 0.9608
Epoch 20/24
----------
train Loss: 0.3766 Acc: 0.8361
val Loss: 0.1798 Acc: 0.9608
Epoch 21/24
----------
train Loss: 0.3171 Acc: 0.8566
val Loss: 0.1721 Acc: 0.9542
Epoch 22/24
----------
train Loss: 0.3336 Acc: 0.8484
val Loss: 0.1621 Acc: 0.9542
Epoch 23/24
----------
train Loss: 0.3068 Acc: 0.8689
val Loss: 0.1799 Acc: 0.9608
Epoch 24/24
----------
train Loss: 0.3369 Acc: 0.8525
val Loss: 0.1730 Acc: 0.9608
Training complete in 10m 23s
Best val Acc: 0.9608
模型评估与可视化
visualize_model(model_ft)
plt.ioff()
plt.show()
%%%
大佬