首頁 > 軟體

PyTorch 遷移學習實戰

2023-01-16 14:00:29

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 遷移內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com