深度學習之PyTorch實戰(4)——遷移學習

戰爭熱誠發表於2023-03-26

  (這篇部落格其實很早之前就寫過了,就是自己對當前學習pytorch的一個教程學習做了一個學習筆記,一直未發現,今天整理一下,發出來與前面基礎形成連載,方便初學者看,但是可能部分pytorch和torchvision的API介面已經更新了,導致部分程式碼會產生報錯,但是其思想還是可以借鑑的。

因為其中內容相對比較簡單,而且目前其實torchvision中已經存在現成的VGG模型及其預訓練模型,所以不建議看到這篇部落格的盆友花費過多時間敲寫學習)

========下面是5年前的正文:

  我們在前三篇部落格學會了使用自己搭建的卷積神經網路模型解決手寫圖片識別的問題,因為卷積神經網路在解決計算機視覺問題上有著獨特的優勢,所以採用簡單的神經網路模型就能使手寫圖片識別的準確率達到很高的水平。不過,用來訓練和測試模型的手寫圖片資料的特徵非常明顯,所以也很容易被卷積神經網路模型捕獲到。

  本次將搭建卷積神經網路模型對生活中普通圖片進行分類,並進入遷移學習(Transfer Learning)方法。為了驗證遷移學習方法的方便性和高效性,我們先使用自定義結構的卷積神經網路模型解決圖片的分類問題,然後透過使用遷移學習方法得到的模型來解決同樣的問題,以此來看看在效果上是傳統的方法更出色還是遷移學習方法更出色。

一:遷移學習入門

  在開始之前,我們先來了解一下什麼是遷移學習。在深度神經網路演算法的應用過程中,如果我們面對的是資料規模較大的問題,那麼在搭建好深度神經網路模型後,我們勢必要花費大量的算力和時間去訓練模型和最佳化引數,最後耗費了這麼多資源得到的模型只能解決這一個問題,價效比非常低。如果我們用這麼多資源訓練的模型能夠解決同一類問題,那麼模型的價效比會提高很多,這就促使使用遷移模型解決同一類問題的方法出現。因為該方法的出現,我們透過對一個訓練好的模型進行細微調整,就能將其應用到相似的問題中,最後還能取得很好的效果;另外,對於原始資料較少的問題,我們也能夠透過採用遷移模型進行有效解決,所以,如果能夠選取合適的遷移學習方法,則會對解決我們所面臨的問題有很大的幫助。
  假如我們現在需要解決一個計算機視覺的圖片分類問題,需要透過搭建一個模型對貓和狗的圖片進行分類,並且提供了大量的貓和狗的圖片資料集。假如我們選擇使用卷積神經網路模型來解決這個圖片分類問題,則首先要搭建模型,然後不斷對模型進行訓練,使其預測貓和狗的圖片的準確性達到要求的閾值,在這個過程中會消耗大量的時間在引數最佳化和模型訓練上。不久之後我們又面臨另一個圖片分類問題,這次需要搭建模型對貓和狗的圖片進行分類,同樣提供了大量的圖片資料集,如果已經掌握了遷移學習方法,就不必再重新搭建一套全新的模型,然後耗費大量的時間進行訓練了,可以直接使用之前已經得到的模型和模型的引數並稍加改動來滿足新的需求。不過,對遷移的模型需要進行重新訓練,這是因為最後分類的物件發生了變化,但是重新訓練的時間和搭建全新的模型進行訓練的時間相對很少,如果調整的僅僅是遷移模型的一小部分,那麼重新訓練所耗費的時間會更少。透過遷移學習可以節省大量的時間和精力,而且最終得到的結果不會太差,這就是遷移學習的優勢和特點。
  需要注意的是,在使用遷移學習的過程中有時會導致遷移模型出現負遷移,我們可以將其理解為模型的泛化能力惡化。假如我們將遷移學習用於解決兩個毫不相關的問題,則極有可能使最後遷移得到的模型出現負遷移。

  (下面遷移學習內容參考:https://my.oschina.net/u/876354/blog/1614883)

1.1,什麼是遷移學習?

  遷移學習(Transfer Learning)是一種機器學習方法,是把一個領域(即源領域)的知識,遷移到另外一個領域(即目標領域),使得目標領域能夠取得更好的學習效果。

  透過,源領域資料量充足,而目標領域資料量較小,這種場景就很適合做遷移學習,例如我們要對一個任務進行分類,但是此任務中資料不重複(目標域),然而卻又大量的相關的訓練資料(源域),但是此訓練資料與所需進行的分類任務中的測試資料特徵分佈不同(例如語音情感識別中,一種語言的語音資料充足,然而所需進行分類任務的情況資料卻極度缺乏),在這種情況下如果可以採用合適的遷移學習方法則可以大大提高樣本不充足任務的分類識別結果。

1.2,為什麼現在需要遷移學習?

  前百度首席科學家,史丹佛的教授吳恩達(Andrew Ng)在曾經說過:“遷移學習將會是繼監督學習之後的下一個機器學習商業成功的驅動力”。

  在2016年的NIPS會議上,吳恩達給出了一個未來AI方向的技術發展圖,還是很客觀的:

 

   毋庸置疑,監督學習是目前成熟度最高的,可以說已經成功商用,而下一個商用的技術將會是遷移學習(Transfer Learning),這也是Andrew 預測未來五年最有可能走向商用的 AI 技術。

  吳恩達在一次採訪中,也提到遷移學習會是一個很有活力的領域,我們之所以對遷移學習感到興奮,其原因在於現代深度學習的巨大價值是針對我們擁有海量資料的問題。但是,也有很多問題領域,我們沒有足夠資料。比如語音識別。在一些語言中,比如普通話,我們有很多資料,但是那些只有少數人說的語言,我們的資料就不夠龐大。所以,為了針對資料量不那麼多的中國少數人所說的方言進行語音識別,能將從學習普通話中得到的東西進行遷移嗎?我們的技術確實可以做到這一點,我們也正在做,但是,這一領域的進步能讓人工智慧有能力解決廣泛得多的問題。

1.3,傳統機器學習和遷移學習有什麼不同呢?

  在機器學習的經典監督學習場景中,如果我們要針對一些任務和域A訓練一個模型,我們會假設被提供了針對同一個域和任務的標籤資料。如下圖所示,其中我們的模型A在訓練資料和測試資料中的域和任務都是一樣的。

 

   即使是跟遷移學習比較相似的多工學習,多工學習是對目標域和源域進行共同學習,而遷移學習主要是對透過對源域的學習解決目標域的識別任務。下圖就展示了傳統的機器學習和遷移學習的區別:

 

 1.4,什麼適合遷移?

  在一些學習任務中有一些特徵是個體所特有的,這些特徵不可以遷移。而有些特徵是所有的個體中具有貢獻的,這些可以進行遷移。

  有些時候如果遷移的不合適則導致負遷移,例如當源域和目標域的任務毫不相關時有可能會導致負遷移。

1.5,遷移學習的分類

  根據 Sinno  Jialin Pan 和 Qiang Yang 在TKDE 2010上的文章,可將遷移學習演算法,根據所需要的遷移知識表示形式(即 what to  transfer ),分為四類:

  • 1,基於例項的遷移學習(instance-based  transfer learning):源領域(source domain)中的資料(data)的某一部分可以透過 reweighting 的方法重用,用於 target domain 的學習。
  • 2,基於特徵表示的遷移學習(feature-representation  transfer learning):透過 source domain 學習一個好的(good)的特徵表示,把知識透過特徵的形式進行編碼,並從 source domain 傳遞到 target domain ,提升 target domain 任務效果。
  • 3,基於引數的遷移學習(parameter-transfer learning):target domain和source domain的任務之間共享相同的模型引數(model parameters)或者是服從相同的先驗分佈(prior distribution)。
  • 4,基於關係知識遷移學習(relational-knowledge transfer learning):相關領域之間的知識遷移,假設 source demain 和 target domain中,資料(data)之間聯絡關係是相同的。

  前三類遷移學習方式都要求資料(data)獨立同分布假設。同時,四類遷移學習方式都要求選擇 source domain和 target domain 相關。

  下表給出了遷移內容的遷移學習分類:

 

1.6,遷移學習的應用與價值

1.6.1 遷移學習的應用

  用於情感分類,影像分類,命名實體識別,WiFi訊號定位,自動化設計,中文到英文翻譯等問題。

1.6.2 遷移學習的價值

  • 1,複用現有知識域資料,已有的大量的工作不至於完全丟棄
  • 2,不需要再去花費巨大代價去重新採集和標定龐大的新資料局,也有可能資料根本無法獲取
  • 3,對於快速出現的新領域,能夠快速遷移和應用,體現時效性優勢

  總之,遷移學習將會成為接下來令人興奮的研究方向,特別是許多應用需要能夠將知識遷移到新的任務和域中的模型,將會成為人工智慧的又一個重要助推力。

二:資料集處理

  本文使用的資料集來自Kaggle網站上的“Dogs vs.Cats”競賽專案,可以透過網路免費下載這些資料集。在這個資料集的訓練資料集中一共有25000張貓和狗的圖片,其中包含12500張貓的圖片和12500張狗的圖片。在測試資料集中有12500張圖片,不過其中的貓狗圖片是無序混雜的,而且沒有對應的標籤。這些資料集將被用於對模型進行訓練和對引數進行最佳化,以及在最後對模型的泛化能力進行驗證。

官方下載地地址 https://www.kaggle.com/c/dogs-vs-cats/data

2.1 驗證資料集和測試資料集

  在實踐中,我們不會直接使用測試資料集對搭建的模型進行訓練和最佳化,而是在訓練資料集中劃出一部分作為驗證集,來評估在每個批次的訓練後模型的泛化能力。這樣做的原因是如果我們使用測試資料集進行模型訓練和最佳化,那麼模型最終會對測試資料集產生擬合傾向,換而言之,我們的模型只有在對測試資料集中圖片的類別進行預測時才有極強的準確率,而在對測試資料集以外的圖片類別進行預測時會出現非常多的錯誤,這樣的模型缺少泛化能力。所以,為了防止這種情況的出
現,我們會把測試資料集從模型的訓練和最佳化過程中隔離出來,只在每輪訓練結束後使用。如果模型對驗證資料集和測試資料集的預測同時具備高準確率和低損失值,就基本說明模型的引數最佳化是成功的,模型將具備極強的泛化能力。在本章的實踐中我們分別從訓練資料集的貓和狗的圖片中各抽出 2500 張圖片組成一個具有5000張圖片的驗證資料集。
  我們也可以將驗證資料集看作考試中的模擬訓練測試,將測試資料集看作考試中的最終測試,透過兩個結果看測試的整體能力,但是測試資料集最後會有絕對的主導作用。不過本章使用的測試資料集是沒有標籤的,而且本章旨在證明遷移學習比傳統的訓練高效,所以暫時不使用在資料集中提供的測試資料集,我們進行的只是模型對驗證資料集的準確性的橫向比較。

2.2 資料預覽

  在劃分好資料集之後,就可以進行資料預覽了,我們透過資料預覽可以掌握資料的基本資訊,從而更好地決定如何使用這些資料。

  開始的部分程式碼如下:

import torch 
import torchvision
from torchvision import datasets
from torchvision import transforms
import os
import matplotlib.pyplot as plt
import time

   在以上的程式碼中先匯入了必要的包,之之前不同的是新增加了os包和time包,os包整合了一些對檔案路徑和目錄進行操作的類,time包主要是一些和時間相關的方法。

  在獲取全部的資料集之後,我們就可以對這些資料進行簡單分類了。新建一個名為DogsVSCats的資料夾,在該資料夾下面新建一個名為train和一個名為valid的子資料夾,在子資料夾下面再分別新建一個名為cat的資料夾和一個名為dog的資料夾,最後將資料集中對應部分的資料放到對應名字的資料夾中,之後就可以進行資料的載入了。對資料進行載入的程式碼如下:

data_dir = "DogsVSCats"
data_transform = {x:transforms.Compose([transforms.Scale([64,64]),
                                        transforms.ToTensor()])
                  for x in ["train","valid  "]}
image_datasets = {x:datasets.ImageFolder(root=os.path.join(data_dir,x),
                                         transform=data_transform[x])
                  for x in ["train","valid"]}
dataloader = {x:torch.utils.data.DataLoader(dataset = image_datasets[x],
                                            batch_size = 16,
                                            shuffle = True)
              for x in ["train","valid"]}

   在進行資料的載入時我們使用torch.transforms中的Scale類將原始圖片的大小統一縮放至64×64。在以上程式碼中對資料的變換和匯入都使用了字典的形式,因為我們需要分別對訓練資料集和驗證資料集的資料載入方法進行簡單定義,所以使用字典可以簡化程式碼,也方便之後進行相應的呼叫和操作。

  os.path.join就是來自之前提到的 os包的方法,它的作用是將輸入引數中的兩個名字拼接成一個完整的檔案路徑。其他常用的os.path類方法如下:

(1)os.path.dirname :用於返回一個目錄的目錄名,輸入引數為檔案的目錄。
(2)os.path.exists :用於測試輸入引數指定的檔案是否存在。
(3)os.path.isdir :用於測試輸入引數是否是目錄名。
(4)os.path.isfile :用於測試輸入引數是否是一個檔案。
(5)os.path.samefile :用於測試兩個輸入的路徑引數是否指向同一個檔案。
(6)os.path.split :用於對輸入引數中的目錄名進行分割,返回一個元組,該元組由目錄名和檔名組成

   下面獲取一個批次的資料並進行資料預覽和分析,程式碼如下:

X_example,y_example = next(iter(dataloader['train']))

   以上程式碼透過next和iter迭代操作獲取一個批次的裝載資料,不過因為受到我們之前定義的batch_size值的影響,這一批次的資料只有16張圖片,所以X_example和y_example的長度也全部是16,可以透過列印這兩個變數來確認。列印輸出的程式碼如下:

print(u"X_example 個數{}".format(len(X_example)))
print(u"y_example 個數{}".format(len(y_example)))

   輸出結果如下:

X_example 個數16
y_example 個數16

  其中,X_example是Tensor資料型別的變數,因為做了圖片大小的縮放變換,所以現在圖片的大小全部是64×64了,那麼X_example的維度就是(16, 3, 64, 64),16代表在這個批次中有16張圖片;3代表色彩通道數,因為原始圖片是彩色的,所以使用了R、G、B這三個通道;64代表圖片的寬度值和高度值。
  y_example也是Tensor資料型別的變數,不過其中的元素全部是0和1。為什麼會出現0和1?這是因為在進行資料裝載時已經對dog資料夾和cat資料夾下的內容進行了獨熱編碼(One-Hot Encoding),所以這時的0和1不僅是每張圖片的標籤,還分別對應貓的圖片和狗的圖片。我們可以做一個簡單的列印輸出,來驗證這個獨熱編碼的對應關係,程式碼如下:

index_classes = image_datasets["train"].class_to_idx
print(index_classes)

   輸出的結果如下:

{'cat': 0, 'dog': 1}

  這樣就很明顯了,貓的圖片標籤和狗的圖片標籤被獨熱編碼後分別被數字化了,相較於使用文字作為圖片的標籤而言,使用0和1也可以讓之後的計算方便很多。不過,為了增加之後繪製的影像標籤的可識別性,我們還需要透過image_datasets["train"].classes將原始標籤的結果儲存在名為example_clasees的變數中。程式碼如下:

['cat', 'dog']

   example_clasees變數其實是一個列表,而且在這個列表中只有兩個元素,分別是dog和cat。

  列印輸出的該批次的所有圖片的標籤結果如下:

['dog', 'dog', 'cat', 'cat', 'dog', 'dog', 'dog', 'dog', 'cat', 'dog', 'cat', 
'dog', 'cat', 'cat', 'dog', 'dog']

   標籤對應的圖片如圖所示

三:模型搭建和引數最佳化

   本節會先基於一個簡化的VGGNet架構搭建卷積神經網路模型並進行模型訓練和引數最佳化,然後遷移一個完整的VGG16架構的卷積神經網路模型,最後遷移一個ResNet50架構的卷積神經網路模型,並對比這三個模型在預測結果上的準確性和在泛化能力上的差異。

3.1 自定義VGGNet

  我們首先需要搭建一個卷積神經網路模型,考慮到訓練時間的成本,我們基於VGG16架構來搭建一個簡化版的VGGNet模型,這個簡化版模型要求輸入的圖片大小全部縮放到64×64,而在標準的VGG16架構
模型中輸入的圖片大小應當是224×224的;同時簡化版模型刪除了VGG16最後的三個卷積層和池化層,也改變了全連線層中的連線引數,這一系列的改變都是為了減少整個模型參與訓練的引數數量。簡化版模型的搭建程式碼如下:

# 自定義VGGNet
# 基於VGG16架構一個簡化版的VGGNet模型 簡化吧的VGG16圖片全部縮放到64*64
# 而標準的VGG模型輸入的圖片應該是224*224
class Modles(torch.nn.Module):
    def __init__(self):
        super(Modles,self).__init__()
        self.Conv = torch.nn.Sequential(
            torch.nn.Conv2d(3,64,kernel_size=3,stride=1,padding=1),
            torch.nn.ReLU(),
            torch.nn.Conv2d(64,64, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(kernel_size=2,stride=2),

            torch.nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.Conv2d(128,128, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(kernel_size=2, stride=2),

            torch.nn.Conv2d(128,256, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.Conv2d(256,256, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(kernel_size=2, stride=2),

            torch.nn.Conv2d(256,512, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.Conv2d(512,512, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.Conv2d(512,512, kernel_size=3, stride=1, padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(kernel_size=2, stride=2),

        )
        self.Classes = torch.nn.Sequential(
            torch.nn.Linear(4*4*512,1024),
            torch.nn.ReLU(),
            torch.nn.Dropout(p=0.5),
            torch.nn.Linear(1024,1024),
            torch.nn.ReLU(),
            torch.nn.Dropout(p=0.5),
            torch.nn.Linear(1024,2)
        )

        def forward(self, input):
            x = self.Conv(input)
            x = x.view(-1,4*4,512)
            x = self.Classes(x)
            return x


   在搭建好模型後,透過 print 對搭建的模型進行列印輸出來顯示模型中的細節,列印輸出的程式碼如下:

model = Models()
print(model)

   輸出的內容如下:

Modles(
  (Conv): Sequential(
    (0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU()
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU()
    (7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU()
    (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU()
    (12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU()
    (14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (15): ReLU()
    (16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (18): ReLU()
    (19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (20): ReLU()
    (21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (22): ReLU()
    (23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  )
  (Classes): Sequential(
    (0): Linear(in_features=8192, out_features=1024, bias=True)
    (1): ReLU()
    (2): Dropout(p=0.5)
    (3): Linear(in_features=1024, out_features=1024, bias=True)
    (4): ReLU()
    (5): Dropout(p=0.5)
    (6): Linear(in_features=1024, out_features=2, bias=True)
  )
)

   然後,定義好模型的損失函式和對引數進行最佳化的最佳化函式,程式碼如下;

loss_f = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),lr = 0.00001)

epoch_n = 10
time_open = time.time()

for epoch in range(epoch_n):
    print("Epoch {}/{}".format(epoch,epoch_n-1))
    print("-"*10)

    for phase in ["train","valid"]:
        if phase == "train":
            print("Training...")
            model.train(True)
        else:
            print("Validing...")
            model.train(False)

        running_loss = 0.0
        running_corrscts = 0

        for batch,data in enumerate(dataloader[phase],1):
            X,y = data
            X,y = Variable(X),Variable(y)

            y_pred = model(X)
            _,pred = torch.max(y_pred.data,1)
            optimizer.zero_grad()

            loss = loss_f(y_pred,y)

            if phase == "train":
                loss.backward()
                optimizer.step()

            running_loss += loss.data[0]
            running_corrects += torch.sum(pred == y.data)

            if batch%500 == 0 and phase == 'train':
                print("Batch{},Train Loss:{:.4f},Train ACC:{:.4f}".format(
                    batch,running_loss/batch,100*running_corrects/(16*batch)
                ))
        epoch_loss = running_loss*16/len(image_datasets[phase])
        epoch_acc = 100*running_corrects/len(image_datasets[phase])

        print("{} Loss:{:.4f} Acc:{:.4f}%".format(phase,epoch_loss,epoch_acc))

    time_end = time.time() - time_open
    print(time_end)

 

  在程式碼中最佳化函式使用的是Adam,損失函式使用的是交叉熵,訓練次數總共是10 次,最後的輸出結果如下(這是書原文的結果,這裡直接複製貼上過來)

  雖然準確率不錯,但因為全程使用了計算機的 CPU進行計算,所以整個過程非常耗時,約為492分鐘(492=29520/60)。下面我們對原始程式碼進行適當調整,將在模型訓練的過程中需要計算的引數全部遷移至GPUs上,這個過程非常簡單和方便,只需重新對這部分引數進行型別轉換就可以了,當然,在此之前,我們需要先確認GPUs硬體是否可用,具體的程式碼如下:

# print(torch.cuda.is_available())
# Use_gpu = torch.cuda.is_available()

   列印輸出的結果如下:

True

   返回的值是True,這說明我們的GPUs已經具備了被使用的全部條件,如果遇到False,則說明顯示卡暫時不支援,如果是驅動存在問題,則最簡單的辦法是將顯示卡驅動升級到最新版本。
  在完成對模型訓練過程中引數的遷移之後,新的訓練程式碼如下:

print(torch.cuda.is_available())
Use_gpu = torch.cuda.is_available()


if Use_gpu:
    model = model.cuda()

epoch_n = 10
time_open = time.time()

for epoch in range(epoch_n):
    print("Epoch {}/{}".format(epoch,epoch_n-1))
    print("-"*10)

    for phase in ["train","valid"]:
        if phase == "train":
            print("Training...")
            model.train(True)
        else:
            print("Validing...")
            model.train(False)

        running_loss = 0.0
        running_corrscts = 0

        for batch,data in enumerate(dataloader[phase],1):
            X,y = data
            if Use_gpu:
                X, y = Variable(X.cuda()), Variable(y.cuda())
            else:
                X,y = Variable(X),Variable(y)

            y_pred = model(X)
            _,pred = torch.max(y_pred.data,1)
            optimizer.zero_grad()

            loss = loss_f(y_pred,y)

            if phase == "train":
                loss.backward()
                optimizer.step()

            running_loss += loss.item()
            running_corrscts += torch.sum(pred == y.data)

            if batch%500 == 0 and phase == 'train':
                print("Batch{},Train Loss:{:.4f},Train ACC:{:.4f}".format(
                    batch,running_loss/batch,100*running_corrscts/(16*batch)
                ))
        epoch_loss = running_loss*16/len(image_datasets[phase])
        epoch_acc = 100*running_corrscts/len(image_datasets[phase])

        print("{} Loss:{:.4f} Acc:{:.4f}%".format(phase,epoch_loss,epoch_acc))

    time_end = time.time() - time_open
    print(time_end)

   在以上程式碼中,model = model.cuda()和X, y = Variable(X.cuda()),Variable(y.cuda())就是參與遷移至GPUs的具體程式碼,在進行10次訓練後,輸出的結果如下

  從結果可以看出,不僅驗證測試集的準確率提升了近10%,而且最後輸出的訓練耗時縮短到了大約14分鐘(14=855/60),與之前的訓練相比,耗時大幅下降,明顯比使用CPU進行引數計算在效率上高出不少。
  到目前為止,我們構建的卷積神經網路模型已經具備了較高的預測準確率了,下面引入遷移學習來看看預測的準確性還能提升多少,看看計算耗時能否進一步縮短。在使用遷移學習時,我們只需對原模型的結構進行很小一部分重新調整和訓練,所以預計最後的結果能夠有所突破。

 3.2  遷移VGG16

  下面看看遷移學習的具體實施過程,首先需要下載已經具備最優引數的模型,這需要對我們之前使用的  model = Models() 程式碼部分進行替換,因為我們不需要再自己搭建和定義訓練的模型了,而是透過程式碼自動下載模型並直接呼叫,具體程式碼如下:

model = models.vgg16(prepare = True)

  在以上程式碼中,我們指定進行下載的模型是VGG16,並透過設定prepare = True 中的值為True,來實現下載的模型附帶了已經最佳化好的模型引數。這樣,遷移的第一步就完成了,如果想要看遷移的細節,就可以透過print 將其列印輸出,輸出的結果如下:

  下面開始進行遷移學習的第二步,對當前遷移過來的模型進行調整,儘管遷移學習要求我們需要解決的問題之間最好具有很強的相似性,但是每個問題對最後輸出的結果會有不一樣的要求,而承擔整個模型輸出分類工作的是卷積神經網路模型中的全連線層,所以在遷移學習的過程中調整最多的也是全連線層部分。其基本思想是凍結卷積神經網路中全連線層之前的全部網路層次,讓這些被凍結的網路層次中的引數在模型的訓練過程中不進行梯度更新,能夠被最佳化的引數僅僅是沒有被凍結的全連線層的全部引數。

  下面看看具體的程式碼。首先,遷移過來的VGG16架構模型在最後輸出的結果是1000個,在我們的問題中只需要兩個輸出結果,所以全連線層必須進行調整。模型調整的具體程式碼如下:

for parma in model.parameters():
    parma.requires_grad = False
    
model.classifier = torch.nn.Sequential(torch.nn.Linear(25088, 4096),
                                       torch.nn.ReLU(),
                                       torch.nn.Dropout(p=0.5),
                                       torch.nn.Linear(4096, 4096),
                                       torch.nn.ReLU(),
                                       torch.nn.Dropout(p=0.5),
                                       torch.nn.Linear(4096, 2))

if Use_gpu:
    model = model.cuda()

cost = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.classifier.parameters(), lr=0.00001)

  首先,對原模型中的引數進行遍歷操作,將引數中的parma.requires_grad全部設定為False,這樣對應的引數將不計算梯度,當然也不會進行梯度更新了,這就是之前說到的凍結操作;然後,定義新的全連線層結構並重新賦值給model.classifier。在完成了新的全連線層定義後,全連線層中的parma.requires_grad引數會被預設重置為True,所以不需要再次遍歷引數來進行解凍操作。損失函式的loss值依然使用交叉熵進行計算,但是在最佳化函式中負責最佳化的引數變成了全連線層中的所有引數,即對 model.classifier.parameters這部分引數進行最佳化。在調整完模型的結構之後,我們透過列印輸出對比其與模型沒有進行調整前有什麼不同,結果如下:

 

 

相關文章