把模型从内存保存到硬盘上的过程也成为序列化与反序列化,在内存中,模型中的数据或者程序是一个个的对象,而在硬盘中是以二进制(序列)进行保存的。所以,序列化也就是指把内存当中的对象保存到硬盘中,以二进制序列的形式存储下来。反之称为反序列化
1.torch.save
主要参数:
·obj:对象
·f:输出路径
2.torch.load主要参数
·f:文件路径
·map_location:指定存放位置,cpu or gpu,CPU模式下这个不用关心。
法1:保存整个Module ,速度比较慢,不推荐。这个方法保存训练之后的模型参数。
torch.save(net,path)
法2:保存模型参数
state_dict=net.state_dict()
torch.save(state_dict,path)
模型如下图所示,在训练过程中,数据是不变的,损失函数是一个公式,也不变的。变的是模型和优化器(动量优化器要利用之前的信息不断更新当前值),还有迭代次数、loss等要保存。
checkpoint不要写在iteration里面,要写在epoch循环中。
保存:
checkpoint = {"model_state_dict": net.state_dict(),
"optimizer_state_dict": optimizer.state_dict(),
"epoch": epoch}
path_checkpoint = "./checkpoint_{}_epoch.pkl".format(epoch)
torch.save(checkpoint, path_checkpoint)
恢复
path_checkpoint = "./checkpoint_4_epoch.pkl"#恢复的文件路径
checkpoint = torch.load(path_checkpoint)#load文件
net.load_state_dict(checkpoint['model_state_dict'])#恢复模型参数
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])#恢复优化器参数
start_epoch = checkpoint['epoch']#设置要恢复的epoch
scheduler.last_epoch = start_epoch#设置学习率
Transfer Learning:机器学习分支,研究源域(source domain)的知识如何应用到目标域(target domain)
文献:A Survey on Transfer Learning.
Model Finetune:模型的迁移学习
文献:How transfer-able are features in deep neural networks?
例如下面alexnet,feature extractor一般是模型共性的地方,而最后的classifier是我们需要重新学习的地方,原文的Alexnet最后的output是10个神经元的FC层分类器,如果我们做二分类问题就需要对这一层 进行改动并重新学习。
模型微调步骤:
1.获取预训练模型参数。
2.加载模型(load_state_dict)
3.修改输出层
模型微调训练方法:
1.固定预训练的参数(requires_grad=False;Ir=0):这里学习率为0就是不更新。
2.Features Extractor较小学习率(params_group):常用的方法,就是分类器部分用较大学习率,记得paramter是可以分组的,这里把大学习率和小学习率进行分组。
Finetune Resnet-18(已经训练好的)用于二分类
蚂蚁蜜蜂二分类数据
训练集:各120~ 张验证集:各70~ 张
数据下载
模型下载
Resnet-18模型结构如下图所示:
前面四层是特征提取,接下来四层(layer1~layer4)是残差网络,然后接avgpool池化层,最后接FC分类(原模型是1000分类,ImageNet上训练的)。
下面是不采用Finetune,直接随机初始化一个Resnet-18模型,然后进行25个epoch的训练,最后的loss曲线如下图所示:
最后的验证集上准确率大概为70%左右。
主要代码和注释如下:
# -*- coding: utf-8 -*-
"""
# @file name : finetune_resnet18.py
# @author : TingsongYu https://github.com/TingsongYu
# @date : 2019-11-05
# @brief : 模型finetune方法
"""
import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
import torchvision.transforms as transforms
import torch.optim as optim
from matplotlib import pyplot as plt
from tools.my_dataset import AntsDataset
from tools.common_tools import set_seed
import torchvision.models as models
import torchvision
BASEDIR = os.path.dirname(os.path.abspath(__file__))
#这里设置的gpu训练,共有三处
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")#获取device,具体要看97和133行有用到device
print("use device :{}".format(device))
set_seed(1) # 设置随机种子
label_name = {"ants": 0, "bees": 1}
# 参数设置
MAX_EPOCH = 25
BATCH_SIZE = 16
LR = 0.001
log_interval = 10
val_interval = 1
classes = 2#这里是二分类
start_epoch = -1
lr_decay_step = 7
# ============================ step 1/5 数据 ============================
data_dir = os.path.join(BASEDIR, "..", "..", "data/hymenoptera_data")
train_dir = os.path.join(data_dir, "train")
valid_dir = os.path.join(data_dir, "val")
norm_mean = [0.485, 0.456, 0.406]
norm_std = [0.229, 0.224, 0.225]
train_transform = transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize(norm_mean, norm_std),
])
valid_transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(norm_mean, norm_std),
])
# 构建MyDataset实例
train_data = AntsDataset(data_dir=train_dir, transform=train_transform)
valid_data = AntsDataset(data_dir=valid_dir, transform=valid_transform)
# 构建DataLoder
train_loader = DataLoader(dataset=train_data, batch_size=BATCH_SIZE, shuffle=True)
valid_loader = DataLoader(dataset=valid_data, batch_size=BATCH_SIZE)
# ============================ step 2/5 模型 ============================
# 1/3 构建模型
resnet18_ft = models.resnet18()#PyTorch自带resnet18模型
# 2/3 加载参数
# flag = 0
flag = 1
if flag:
path_pretrained_model = os.path.join(BASEDIR, "..", "..", "data/resnet18-5c106cde.pth")
state_dict_load = torch.load(path_pretrained_model)#本节的内容,直接加载模型参数
resnet18_ft.load_state_dict(state_dict_load)
#这里演示了两种finetune的方式
# 法1 : 冻结卷积层
flag_m1 = 0
# flag_m1 = 1
if flag_m1:
for param in resnet18_ft.parameters():
param.requires_grad = False#设置参数不在求取梯度
print("conv1.weights[0, 0, ...]:\n {}".format(resnet18_ft.conv1.weight[0, 0, ...]))
# 3/3 替换fc层
num_ftrs = resnet18_ft.fc.in_features#获取原始的输入神经元个数
resnet18_ft.fc = nn.Linear(num_ftrs, classes)#重新设置分类数量,输入神经元个数不变
resnet18_ft.to(device)#这里把模型设置到指定device上,cpu或者gpu
# ============================ step 3/5 损失函数 ============================
criterion = nn.CrossEntropyLoss() # 选择损失函数
# ============================ step 4/5 优化器 ============================
# 法2 : conv 小学习率
# flag = 0
flag = 1
if flag:
fc_params_id = list(map(id, resnet18_ft.fc.parameters())) # 返回的是parameters的 内存地址,并保存为list(fc_params_id是一个list)
base_params = filter(lambda p: id(p) not in fc_params_id, resnet18_ft.parameters())#这里过滤掉不是fc层的参数(not in fc_params_id)
optimizer = optim.SGD([
{'params': base_params, 'lr': LR*0.1}, # 如果这里设置0,就和方法一是一样的,固定住非fc层参数。
{'params': resnet18_ft.fc.parameters(), 'lr': LR}], momentum=0.9)#两个参数组的momentum是一样的,初始学习率LR不一样
else:
optimizer = optim.SGD(resnet18_ft.parameters(), lr=LR, momentum=0.9) # 选择优化器,这里设置的是所有参数用一种学习率
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=lr_decay_step, gamma=0.1) # 设置学习率下降策略
# ============================ step 5/5 训练 ============================
train_curve = list()
valid_curve = list()
for epoch in range(start_epoch + 1, MAX_EPOCH):
loss_mean = 0.
correct = 0.
total = 0.
resnet18_ft.train()
for i, data in enumerate(train_loader):
# forward
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)#数据也要放GPU上
outputs = resnet18_ft(inputs)
# backward
optimizer.zero_grad()
loss = criterion(outputs, labels)
loss.backward()
# update weights
optimizer.step()
# 统计分类情况
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).squeeze().cpu().sum().numpy()
# 打印训练信息
loss_mean += loss.item()
train_curve.append(loss.item())
if (i+1) % log_interval == 0:
loss_mean = loss_mean / log_interval
print("Training:Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
epoch, MAX_EPOCH, i+1, len(train_loader), loss_mean, correct / total))
loss_mean = 0.
# if flag_m1:
print("epoch:{} conv1.weights[0, 0, ...] :\n {}".format(epoch, resnet18_ft.conv1.weight[0, 0, ...]))
scheduler.step() # 更新学习率
# validate the model
if (epoch+1) % val_interval == 0:
correct_val = 0.
total_val = 0.
loss_val = 0.
resnet18_ft.eval()
with torch.no_grad():
for j, data in enumerate(valid_loader):
inputs, labels = data
inputs, labels = inputs.to(device), labels.to(device)
outputs = resnet18_ft(inputs)
loss = criterion(outputs, labels)
_, predicted = torch.max(outputs.data, 1)
total_val += labels.size(0)
correct_val += (predicted == labels).squeeze().cpu().sum().numpy()
loss_val += loss.item()
loss_val_mean = loss_val/len(valid_loader)
valid_curve.append(loss_val_mean)
print("Valid:\t Epoch[{:0>3}/{:0>3}] Iteration[{:0>3}/{:0>3}] Loss: {:.4f} Acc:{:.2%}".format(
epoch, MAX_EPOCH, j+1, len(valid_loader), loss_val_mean, correct_val / total_val))
resnet18_ft.train()
train_x = range(len(train_curve))
train_y = train_curve
train_iters = len(train_loader)
valid_x = np.arange(1, len(valid_curve)+1) * train_iters*val_interval # 由于valid中记录的是epochloss,需要对记录点进行转换到iterations
valid_y = valid_curve
plt.plot(train_x, train_y, label='Train')
plt.plot(valid_x, valid_y, label='Valid')
plt.legend(loc='upper right')
plt.ylabel('loss value')
plt.xlabel('Iteration')
plt.show()