16.模型保存與加載、finetune


本課程來自深度之眼deepshare.net,部分截圖來自課程視頻。

序列化與反序列化

把模型從內存保存到硬盤上的過程也成爲序列化與反序列化,在內存中,模型中的數據或者程序是一個個的對象,而在硬盤中是以二進制(序列)進行保存的。所以,序列化也就是指把內存當中的對象保存到硬盤中,以二進制序列的形式存儲下來。反之稱爲反序列化
在這裏插入圖片描述
在這裏插入圖片描述

PyTorch中的序列化與反序列化

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 & Model Finetune

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層分類器,如果我們做二分類問題就需要對這一層 進行改動並重新學習。
在這裏插入圖片描述

PyTorch中的Finetune

模型微調步驟:
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()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章