PyTorch 遷移學習實戰

1. 實驗環境

  • Jupyter Notebook
  • Python 3.7
  • PyTorch 1.4.0

2. 實驗目的

遷移學習,讓機器擁有能夠“舉一反三”的能力。
本次實驗就以“是螞蟻還是蜜蜂”為例,探索如何將已訓練好的大網絡遷移到小數據集上,並經過少量數據集的訓練就讓它獲得非常出眾的效果。

3. 相關原理

使用 PyTorch 的數據集套件從本地加載數據的方法
遷移訓練好的大型神經網絡模型到自己模型中的方法
遷移學習與普通深度學習方法的效果區別
兩種遷移學習方法的區別

4. 實驗步驟

# 下載實驗所需數據並解壓
!wget http://labfile.oss.aliyuncs.com/courses/1073/transfer-data.zip
!unzip transfer-data.zip

4.1 數據收集

實驗中的數據是已經準備好的,訓練數據集在 ./data/train 中,校驗數據集在 ./data/val 中。(推薦直接到藍橋雲課上進行實驗)。如果使用自己的環境隻需要自己準備相關圖片數據,並將代碼中的路徑改成你自己的數據集路徑。

#引入實驗所需要的包
import torch
import torch.nn as nn
import torch.optim as optim
from torch.autograd import Variable
import torch.nn.functional as F
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import copy
import os

4.1.1加載數據

使用 datasets 的 ImageFolder 方法就可以實現自動加載數據,因為數據集中的數據可能分別在不同的文件夾中,要讓所有的數據一起加載。

# 數據存儲總路徑
data_dir = 'transfer-data'
# 圖像的大小為224*224
image_size = 224
# 從data_dir/train加載文件
# 加載的過程將會對圖像自動作如下的圖像增強操作:
# 1. 隨機從原始圖像中切下來一塊224*224大小的區域
# 2. 隨機水平翻轉圖像
# 3. 將圖像的色彩數值標準化
train_dataset = datasets.ImageFolder(os.path.join(data_dir, 'train'),
                                    transforms.Compose([
                                        transforms.RandomResizedCrop(image_size),
                                        transforms.RandomHorizontalFlip(),
                                        transforms.ToTensor(),
                                        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
                                    ])
                                    )

# 加載校驗數據集,對每個加載的數據進行如下處理:
# 1. 放大到256*256像素
# 2. 從中心區域切割下224*224大小的圖像區域
# 3. 將圖像的色彩數值標準化
val_dataset = datasets.ImageFolder(os.path.join(data_dir, 'val'),
                                    transforms.Compose([
                                        transforms.Resize(256),
                                        transforms.CenterCrop(image_size),
                                        transforms.ToTensor(),
                                        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
                                    ])
                                    )

下面要為每個數據集創建數據加載器。

# 創建相應的數據加載器
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size = 4, shuffle = True, num_workers=4)
val_loader = torch.utils.data.DataLoader(val_dataset, batch_size = 4, shuffle = True, num_workers=4)

# 讀取得出數據中的分類類別數
# 如果隻有蜜蜂和螞蟻,那麼是2
num_classes = len(train_dataset.classes)
num_classes

輸出:2

4.1.2 GPU運算

第一次瞭解GPU運算是在第一篇博客PyTorch,簡單的瞭解瞭一下。

深度學習可以通過 GPU 並行運算加速模型的訓練。
PyTorch 是支持使用 GPU 並行運算的。但是能不能使用 GPU 加速運算還取決於硬件,支持 GPU 的硬件(顯卡)一般是比較昂貴的。
如果你想讓自己的程序能夠自動識別 GPU 計算環境,並且在 GPU 不具備的情況下也能自動使用 CPU 正常運行,可以這麼做:
這三個變量,之後會用來靈活判斷是否需要采用 GPU 運算。

# 檢測本機器是否安裝GPU,將檢測結果記錄在佈爾變量use_cuda中
use_cuda = torch.cuda.is_available()

# 當可用GPU的時候,將新建立的張量自動加載到GPU中
dtype = torch.cuda.FloatTensor if use_cuda else torch.FloatTensor
itype = torch.cuda.LongTensor if use_cuda else torch.LongTensor

4.2 數據預處理

該函數作用:將數據集中的某張圖片打印出來。

def imshow(inp, title=None):
    # 將一張圖打印顯示出來,inp為一個張量,title為顯示在圖像上的文字

    # 一般的張量格式為:channels * image_width * image_height
    # 而一般的圖像為 image_width * image_height * channels 
    # 所以,需要將張量中的 channels 轉換到最後一個維度
    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)  # 暫停一會是為瞭能夠將圖像顯示出來。

將訓練數據集的第一個 batch 繪制出來:

#獲取第一個圖像batch和標簽
images, labels = next(iter(train_loader))

# 將這個batch中的圖像制成表格繪制出來
out = torchvision.utils.make_grid(images)

imshow(out, title=[train_dataset.classes[x] for x in labels])

4.3 創建模型

該實驗先訓練一個普通的卷積神經網絡,但正確率勉強達到50%上下。模型預測的效果很差。因為該實驗選用的是螞蟻和蜜蜂的圖像數據,本身就很難識別,簡單的卷積神經網絡應付不瞭這種復雜的情況。其次,該實驗的圖片訓練樣本隻有244個,數量級太小。

代碼略

簡單卷積神經網絡取得的效果:(黃色曲線是測試數據集錯誤率,藍色曲線是訓練數據集錯誤率。)

因此,這裡提到使用“加載已訓練好的 ResNet 進行遷移學習”。
ResNet 是微軟亞洲研究院何凱明團隊開發的一種極深的特殊的卷積神經網絡。該網絡的原始版本曾號稱是“史上最深的網絡”,有 152 層,在物體分類等任務上具有較高的準確度。

考慮到原始的 ResNet 具有較大的復雜性,在本次實驗中,實際遷移的是一個具有 18 層的精簡版的 ResNet。該網絡由 18 個串聯在一起的卷積模塊構成,其中每一個卷積模塊都包括一層卷積一層池化。下面將加載 ResNet 模型,並觀察模型的組成部分。如果是第一次運行,那麼模型會被下載到 ~/.torch/models/ 文件夾中。

torch.utils.model_zoo.load_url('http://labfile.oss.aliyuncs.com/courses/1073/resnet18-5c106cde.pth')
# 加載模型庫中的residual network,並設置pretrained為true,這樣便可加載相應的權重
net = models.resnet18(pretrained=True)
#如果存在GPU,就將網絡加載到GPU上
net = net.cuda() if use_cuda else net
# 將網絡的架構打印出來
net

從模型的組成部分中,可以看到最後有一層全連接層,也就是 (fc): Linear(in_features=512, out_features=1000)。

4.3.1 構建遷移模型

下面把 ResNet18 中的卷積模塊作為特征提取層遷移過來,用於提取局部特征。同時,將 ResNet18 中最後的全連接層(fc)替換,構建一個包含 512 個隱含節點的全連接層,後接兩個結點的輸出層,用於最後的分類輸出。
整個模型的前面大部分的結構都是 ResNet,最後兩層被替換成瞭自定義的全連接層。

# 讀取最後線性層的輸入單元數,這是前面各層卷積提取到的特征數量
num_ftrs = net.fc.in_features

# 重新定義一個全新的線性層,它的輸出為2,原本是1000
net.fc = nn.Linear(num_ftrs, 2)

#如果存在GPU則將網絡加載到GPU中
net.fc = net.fc.cuda() if use_cuda else net.fc

criterion = nn.CrossEntropyLoss() #Loss函數的定義
# 將網絡的所有參數放入優化器中
optimizer = optim.SGD(net.parameters(), lr = 0.0001, momentum=0.9)

4.3.2 訓練模型+測試+繪制圖表

在訓練階段,遷移過來的 ResNet 模塊的結構和所有超參數都可以保持不變,但是權重參數則有可能被新的數據重新訓練。是否要更新這些舊模塊的權重參數完全取決於我們采取的遷移學習方式。
遷移學習主要有兩種模式:預訓練模式固定值模式
接下來會分別介紹

4.3.2.1 預訓練模式

record = [] #記錄準確率等數值的容器

#開始訓練循環
num_epochs = 20
net.train(True) # 給網絡模型做標記,標志說模型在訓練集上訓練
best_model = net
best_r = 0.0
for epoch in range(num_epochs):
    #optimizer = exp_lr_scheduler(optimizer, epoch)
    train_rights = [] #記錄訓練數據集準確率的容器
    train_losses = []
    for batch_idx, (data, target) in enumerate(train_loader):  #針對容器中的每一個批進行循環
        data, target = Variable(data), Variable(target) #將Tensor轉化為Variable,data為圖像,target為標簽
        #如果存在GPU則將變量加載到GPU中
        if use_cuda:
            data, target = data.cuda(), target.cuda()
        output = net(data) #完成一次預測
        loss = criterion(output, target) #計算誤差
        optimizer.zero_grad() #清空梯度
        loss.backward() #反向傳播
        optimizer.step() #一步隨機梯度下降
        right = rightness(output, target) #計算準確率所需數值,返回正確的數值為(正確樣例數,總樣本數)
        train_rights.append(right) #將計算結果裝到列表容器中
        loss = loss.cpu() if use_cuda else loss
        train_losses.append(loss.data.numpy())


        #if batch_idx % 20 == 0: #每間隔100個batch執行一次
     #train_r為一個二元組,分別記錄訓練集中分類正確的數量和該集合中總的樣本數
    train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))

    #在測試集上分批運行,並計算總的正確率
    net.eval() #標志模型當前為運行階段
    test_loss = 0
    correct = 0
    vals = []
    #對測試數據集進行循環
    for data, target in val_loader:
        #如果存在GPU則將變量加載到GPU中
        if use_cuda:
            data, target = data.cuda(), target.cuda()
        data, target = Variable(data, requires_grad=True), Variable(target)
        output = net(data) #將特征數據喂入網絡,得到分類的輸出
        val = rightness(output, target) #獲得正確樣本數以及總樣本數
        vals.append(val) #記錄結果

    #計算準確率
    val_r = (sum([tup[0] for tup in vals]), sum([tup[1] for tup in vals]))
    val_ratio = 1.0*val_r[0].numpy()/val_r[1]

    if val_ratio > best_r:
        best_r = val_ratio
        best_model = copy.deepcopy(net)
    #打印準確率等數值,其中正確率為本訓練周期Epoch開始後到目前撮的正確率的平均值
    print('訓練周期: {} \tLoss: {:.6f}\t訓練正確率: {:.2f}%, 校驗正確率: {:.2f}%'.format(
        epoch, np.mean(train_losses), 100. * train_r[0].numpy() / train_r[1], 100. * val_r[0].numpy()/val_r[1]))       
    record.append([np.mean(train_losses), 1. * train_r[0].data.numpy() / train_r[1], 1. * val_r[0].data.numpy() / val_r[1]])

#繪制訓練誤差曲線
x = [x[0] for x in record]
y = [1 - x[1] for x in record]
z = [1 - x[2] for x in record]
#plt.plot(x)
plt.figure(figsize = (10, 7))
plt.plot(y)
plt.plot(z)
plt.xlabel('Epoch')
plt.ylabel('Error Rate')

測試模型,繪制分類效果

def visualize_model(model, num_images=6):
    images_so_far = 0
    fig = plt.figure(figsize=(15,10))

    for i, data in enumerate(val_loader):
        inputs, labels = data
        inputs, labels = Variable(inputs), Variable(labels)
        if use_cuda:
            inputs, labels = inputs.cuda(), labels.cuda()
        outputs = model(inputs)
        _, preds = torch.max(outputs.data, 1)
        preds = preds.cpu().numpy() if use_cuda else preds.numpy()
        for j in range(inputs.size()[0]):
            images_so_far += 1
            ax = plt.subplot( 2,num_images//2, images_so_far)
            ax.axis('off')

            ax.set_title('predicted: {}'.format(val_dataset.classes[preds[j]]))
            imshow(data[0][j])

            if images_so_far == num_images:
                return
visualize_model(net)

plt.ioff()
plt.show()

4.3.2.2 固定值模式

遷移過來的部分網絡在結構和權重上都保持固定的數值不會改變。
要想讓模型在固定值模式下訓練,需要先鎖定網絡模型相關位置的參數。鎖定的方法非常簡單,隻要把網絡的梯度反傳標志 requires_grad 設置為 False 就可以瞭。

# 加載residual網絡模型
net = torchvision.models.resnet18(pretrained=True)
# 將模型放入GPU中
net = net.cuda() if use_cuda else net

# 循環網絡,將所有參數設為不更新梯度信息
for param in net.parameters():
    param.requires_grad = False

# 將網絡最後一層線性層換掉
num_ftrs = net.fc.in_features
net.fc = nn.Linear(num_ftrs, 2)
net.fc = net.fc.cuda() if use_cuda else net.fc

criterion = nn.CrossEntropyLoss() #Loss函數的定義
# 僅將線性層的參數放入優化器中
optimizer = optim.SGD(net.fc.parameters(), lr = 0.001, momentum=0.9)


#訓練模型
record = [] #記錄準確率等數值的容器

#開始訓練循環
num_epochs = 4
net.train(True) # 給網絡模型做標記,標志說模型在訓練集上訓練
best_model = net
best_r = 0.0
for epoch in range(num_epochs):
    #optimizer = exp_lr_scheduler(optimizer, epoch)
    train_rights = [] #記錄訓練數據集準確率的容器
    train_losses = []
    for batch_idx, (data, target) in enumerate(train_loader):  #針對容器中的每一個批進行循環
        data, target = Variable(data), Variable(target) #將Tensor轉化為Variable,data為圖像,target為標簽
        if use_cuda:
            data, target = data.cuda(), target.cuda()
        output = net(data) #完成一次預測
        loss = criterion(output, target) #計算誤差
        optimizer.zero_grad() #清空梯度
        loss.backward() #反向傳播
        optimizer.step() #一步隨機梯度下降
        right = rightness(output, target) #計算準確率所需數值,返回正確的數值為(正確樣例數,總樣本數)
        train_rights.append(right) #將計算結果裝到列表容器中
        loss = loss.cpu() if use_cuda else loss
        train_losses.append(loss.data.numpy())


     #train_r為一個二元組,分別記錄訓練集中分類正確的數量和該集合中總的樣本數
    train_r = (sum([tup[0] for tup in train_rights]), sum([tup[1] for tup in train_rights]))

    #在測試集上分批運行,並計算總的正確率
    net.eval() #標志模型當前為運行階段
    test_loss = 0
    correct = 0
    vals = []
    #對測試數據集進行循環
    for data, target in val_loader:
        data, target = Variable(data, requires_grad=True), Variable(target)
        if use_cuda:
            data, target = data.cuda(), target.cuda()
        output = net(data) #將特征數據喂入網絡,得到分類的輸出
        val = rightness(output, target) #獲得正確樣本數以及總樣本數
        vals.append(val) #記錄結果

    #計算準確率
    val_r = (sum([tup[0] for tup in vals]), sum([tup[1] for tup in vals]))
    val_ratio = 1.0*val_r[0].numpy()/val_r[1]

    if val_ratio > best_r:
        best_r = val_ratio
        best_model = copy.deepcopy(net)
    #打印準確率等數值,其中正確率為本訓練周期Epoch開始後到目前撮的正確率的平均值
    print('訓練周期: {} \tLoss: {:.6f}\t訓練正確率: {:.2f}%, 校驗正確率: {:.2f}%'.format(
        epoch, np.mean(train_losses), 100. * train_r[0].numpy() / train_r[1], 100. * val_r[0].numpy()/val_r[1]))       
    record.append([np.mean(train_losses), 1. * train_r[0].data.numpy() / train_r[1], 1. * val_r[0].data.numpy() / val_r[1]])


# 繪制誤差曲線
x = [x[0] for x in record]
y = [1 - x[1] for x in record]
z = [1 - x[2] for x in record]
#plt.plot(x)
plt.figure(figsize = (10, 7))
plt.plot(y)
plt.plot(z)
plt.xlabel('Epoch')
plt.ylabel('Error Rate')


#展示分類結果
visualize_model(best_model)

plt.ioff()
plt.show()

4.4 結論

該實驗中,預訓練遷移模型取得的效果整體的錯誤率比簡單卷積神經網絡低瞭很多。訓練錯誤率可以穩定在 0.02 之下,測試錯誤率大約在 0.07 左右。因為在預訓練模式下,模型對訓練數據的擬合性比較強,所以訓練錯誤率與測試錯誤率差別較大。
固定值遷移模式下,訓練錯誤率可以在 0.02 ~ 0.04 之間,比預訓練模式稍高。測試錯誤率大約在 0.07 左右,與預訓練模式差不多。
因為固定值模式鎖定瞭大部分權重,模型對訓練數據的擬合性沒那麼強,所以訓練錯誤率與測試錯誤率的差別也沒那麼大。

到此這篇關於PyTorch 遷移學習實戰的文章就介紹到這瞭,更多相關PyTorch 遷移內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: