(機器學習大作業)Pytorch 使用DCGAN實現二次元人物頭像生成(資料集+實現程式碼+數學原理)

marikaだよ發表於2020-10-17

Pytorch 使用DCGAN實現二次元人物頭像生成(實現程式碼+公式推導)

GAN介紹

  演算法主體

  推導證明(之後將補全完整過程)

  隨機梯度下降訓練D,G

  DCGAN介紹及相關原理

Pytorch實現二次元人物頭像生成

  如何使用GAN生成二次元頭像

   資料準備

   程式碼實現

    判別、生成模型均一輪迭代

    判別每一輪迭代,生成模型每五輪迭代

  圖片生成

總結

  本篇文章主要是關於李宏毅教授授課視訊中的作業進行介紹,小白博主所作工作只是將現有的知識內容結合網路上一些優秀作者的總結以博文的形式加上自己的理解複述一遍。本文主要還是我的學習總結,因為網上的一些知識分佈比較零散故作整理敘述。如果有不對的地方,還請幫忙指正,如有出現禁止轉載的圖片,文字內容請聯絡我修改。
 
相關參考:
 
  李宏毅2020機器學習深度學習(完整版)國語:連結: link.

  何之源:GAN學習指南:從原理入門到製作生成Demo連結: link

   張先生-您好.Pytorch實現GAN 生成動漫頭像 連結: link.

  Dean0Winchester.深度學習—影像卷積與反摺積(最完美的解釋)連結: link.

GAN介紹

  生成對抗網路 G A N GAN GAN, G e n e r a t i v e A d v e r s a r i a l N e t w o r k s Generative Adversarial Networks GenerativeAdversarialNetworks )是深度學習中一種無監督(即不需要給樣本資料打標籤)的學習方法, G A N GAN GAN的核心思想源自博弈論中的納什均衡,博弈的雙方分別是 G e n e r a t o Generato Generator(負責生成圖片的生成器 G G G)和 D i s c r i m i n a t o r Discriminator Discriminator(負責判斷圖片真假的判別器 D D D)。 D D D的目的在於揪出由 G G G “偽造”的“假”圖片給它打低分, G G G的目的在於盡最大可能地模模擬圖片從而“欺騙” D D D獲取高分。

  參與這場遊戲雙方再不斷地較量中完成自身地優化,從而實現了各自判別能力和生成能力的提升,直到雙方達到一種動態的平衡。
在這裡插入圖片描述
 
   如上圖所示,蝴蝶扮演 G A N GAN GAN中的生成器,而波波鳥則扮演判別器,蝴蝶為了逃避波波鳥(判別器)的捕食(識別)需要不斷的朝著樹葉(真樣本)進化,而波波鳥為了能夠捕食(識別)出蝴蝶(用假樣本偽裝自己的生成器)也需要不斷進化,比方說,最開始波波鳥理解的葉子(真樣本)只停留在不是彩色這一層面上,隨著訓練,波波鳥的認知開始進化,從不是彩色轉向棕色是葉子(真樣本),而蝴蝶得到反饋之後為了繼續欺騙波波鳥開始學會讓生成的翅膀是棕色。
 
   這個過程將反覆進行下去,最後可以達到,波波鳥可以真正認出葉子,蝴蝶可以讓生成的翅膀(假樣本)與葉子(真樣本)近乎一致為止。

演算法主體

演算法總流程:

   G A N GAN GAN演算法簡單來說,就是固定生成器,訓練判別器;然後固定訓練好的判別器,再訓練生成器的反覆過程。

在這裡插入圖片描述
逐條解讀演算法流程:
 
  首先初始化 D D D, G G G的引數,先固定生成器,進入判別器的學習過程。
 
    判別器學習過程

  1. 取樣 :從資料集中抽樣出 m m m張樣本,同時藉助某種分佈(通常使用正態分佈)生成 m m m n n n維噪聲樣本。

  2. 獲取假樣本:將噪聲樣本作為輸入,得到生成器輸出的假樣本。

  3. 判別器D的目標函式 :直觀上來看,我們要達到的目的,就是讓判別器對生成器生成的假樣本儘可能地嚴格,對資料集抽樣出來的真樣本儘可能地寬鬆,那麼我們可以構建這樣一個目標函式,讓判別器給真樣本儘可能高的分數,給假樣本儘可能低的分數。
     
                    m a x D max_D maxD v ∗ v^* v = 1 m \dfrac{1}{m} m1 ∑ i = 1 m \sum_{i=1}^{m} i=1m l o g log log( D D D( x i x^i xi)) + + + 1 m \dfrac{1}{m} m1 ∑ i = 1 m \sum_{i=1}^{m} i=1m l o g log log( D D D(1 − - x ∗ i x*^i xi))
     
       D D D( x x x)代表 x x x為真實圖片的概率,如果為 1 1 1,就代表100%是真實的圖片,而輸出為 0 0 0,就代表不可能是真實的圖片。程式碼實現中通常藉由 s i g m o i d sigmoid sigmoid函式來表達
     
      要讓 l o g log log( D D D( x ∗ i x*^i xi))儘可能小,等效於讓 l o g log log( D D D(1 − - x ∗ i x*^i xi))儘可能大。以此為我們的目的函式進行求導做梯度上升(亦可加負號求 m i n min min做梯度下降)從而更新判別器引數。
     
     
    生成器學習過程

  4. 噪聲取樣 :藉助同種分佈(通常使用正態分佈)生成另外 m m m n n n維噪聲樣本。

  5. 生成器G的目標函式
     
                               v ∗ v^* v = 1 m \dfrac{1}{m} m1 ∑ i = 1 m \sum_{i=1}^{m} i=1m l o g log log( D D D( G G G( z i z^i zi)))
     
      先直觀的理解為什麼要這樣定義 G G G目標函式,根據之前的分析,要讓生成器生成的圖片足夠接近真樣本,那麼就需要騙過判別器,從而我們可以讓最大化生成圖片在 D D D的打分為目標來引導我們的生成器完成引數迭代。
     
       在最理想的狀態下, G G G可以生成足以“以假亂真”的圖片 G G G( z z z)。對於 D D D來說,它難以判定 G G G生成的圖片究竟是不是真實的,因此 D D D( G G G( z z z)) = 0.5 0.5 0.5

推導證明

  說了那麼多,那麼我們如何用一條數學語言來描述 G A N GAN GAN的核心原理呢?
 
   G e n e r a t i v e A d v e r s a r i a l N e t w o r k s Generative Adversarial Networks GenerativeAdversarialNetworks 的作者 I a n Ian Ian G o o d f e l l o w Goodfellow Goodfellow大佬給出了答案:

在這裡插入圖片描述
分析這個公式:
 
1.整個式子由兩項構成。 x x x表示真實圖片, z z z表示輸入 G G G網路的噪聲,而 G G G( z z z)表示 G G G網路生成的圖片。
 
2. D D D( x x x)表示 D D D網路判斷真實圖片是否真實的概率(因為 x x x就是真實的,所以對於 D D D來說,這個值越接近 1 1 1越好)。而 D D D( G G G( z z z))是 D D D網路判斷 G G G生成的圖片的是否真實的概率。
 
3. D D D的目的: D D D的能力越強, D D D( x x x)應該越大, D D D( G G G( x x x))應該越小。這時 V V V( D D D, G G G)會變大。因此式子對於 D D D來說是求最大( m a x max max D D D)
 
4. G G G的目的: 上面提到過, D D D( G G G( z z z))是 D D D網路判斷 G G G生成的圖片是否真實的概率, G G G應該希望自己生成的圖片“越接近真實越好”。也就是說,(實際上,我們不是同時訓練兩者,當 D D D訓練好時,第一項好樣本的得分期望是定值), G G G希望 D D D( G G G( z z z))儘可能得大,那麼 V V V( D D D, G G G)會變小。因此我們看到式子的最前面的記號是 m i n min min G G G
 
5. 為什麼是期望: 我們假設我們取出的樣本的統計特性可以代表整個資料分佈(包括資料集資料分佈和 G G G生成器生成資料的分佈),那麼我們之前表示的均值可以近似等價於各自資料分佈的期望。
 
我們可以用張形象的圖來描述我們是如何選擇我們的生成器和判別器的。
 
在這裡插入圖片描述
 
   我們選取判別器的標準,是取 m a x max max V V V( D D D, G G G),那麼對應到圖片就是找到函式的最高點,在我們完成判別器的選取後(即找到函式最高點後),我們下一步的目標(更新生成器),就是找出 m i n min min m a x max max V V V( D D D, G G G)(此時 m a x max max V V V( D D D, G G G)即為紅點),我們要做的就是選取紅點中最低的,選出的 G i G_i Gi即為我們想要的生成器。

隨機梯度下降訓練D,G

   隨機梯度下降法用於減少陷入區域性最優的風險,,進而可以更接近全域性最優的方法。
   該怎麼做,論文也給出了演算法(不愧是大佬),本文程式碼使用的是 A d a m 優 化 算 法 Adam優化演算法 Adam(利用”慣性“來跳出區域性最優的方法),此處不做詳細說明:
在這裡插入圖片描述
  這裡紅框圈出的部分是我們要額外注意的。第一步我們訓練 D D D D D D是希望 V V V( G G G, D D D)越大越好,所以是加上梯度( a s c e n d i n g ascending ascending)。第二步訓練 G G G時, V V V( G G G, D D D)越小越好,所以是減去梯度( d e s c e n d i n g descending descending)。整個訓練過程交替進行。

DCGAN介紹及相關原理

 
  使用過 C N N CNN CNN的朋友一定不會對運用 C N N CNN CNN進行影像樣本分類的方法感到陌生,我們很容易就會想到, G A N GAN GAN中的判別器是否可以用 C N N CNN CNN做二元分類器來進行替代,那麼如何把 C N N CNN CNN G A N GAN GAN很好的結合起來?
 
U n s u p e r v i s e d R e p r e s e n t a t i o n L e a r n i n g w i t h D e e p C o n v o l u t i o n a l G e n e r a t i v e A d v e r s a r i a l N e t w o r k s Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks UnsupervisedRepresentationLearningwithDeepConvolutionalGenerativeAdversarialNetworks》論文告訴了我們答案。
 
  論文作者將 G A N GAN GAN中的生成器,判別器都用兩個卷積神經網路來進行替換。用二元分類的 C N N CNN CNN來做判別器,用反摺積代替卷積操作的卷積神經網路來做生成器,反摺積操作可簡單理解為往原有的小畫素矩陣中進行填充,之後再進行卷積操作,從而實現從”小到大”(從而可以實現用一組列向量生成一個畫素矩陣)的變化。
 
在這裡插入圖片描述
D C G A N DCGAN DCGAN對卷積神經網路的結構做了一些改變,以提高樣本的質量和收斂的速度,這些改變有:

1.取消所有 p o o l i n g pooling pooling層。 G G G網路中使用轉置卷積( t r a n s p o s e d c o n v o l u t i o n a l l a y e r transposed convolutional layer transposedconvolutionallayer)進行上取樣, D D D網路中用加入 s t r i d e stride stride的卷積代替 p o o l i n g pooling pooling
2.在 D D D G G G中均使用 b a t c h n o r m a l i z a t i o n batch normalization batchnormalization
3.去掉 F C FC FC層,使網路變為全卷積網路
4. G G G網路中使用 R e L U ReLU ReLU作為啟用函式,最後一層使用 t a n h tanh tanh
5. D D D網路中使用 L e a k y R e L U LeakyReLU LeakyReLU作為啟用函式

D C G A N DCGAN DCGAN中的 G G G網路示意:
在這裡插入圖片描述

Pytorch實現二次元人物頭像生成

 

如何使用GAN生成二次元頭像

 
  藉助我們之前的推導快速地理解一下動漫頭像生成的 G A N GAN GAN原理。如下圖所示,判別器將資料集中的取樣做真樣本,由噪聲取樣經生成器反摺積生成的影像做假樣本進行每代的更新,生成器則以讓判別器儘可能地給自己生成的影像打高分為目的進行模型的更新迭代。
 
在這裡插入圖片描述
 
  我們也可以用合作的思想去理解生成器同判別器的關係:
 在這裡插入圖片描述
 
  初始化兩個模型後,再依照我們之前提及的模型 G G G, D D D固定一方,更新另一方的方法進行反覆迭代。資料集提供的樣本做真樣本,由 G G G生成的樣本做假樣本。
 在這裡插入圖片描述
 
  之所以要這樣做,是因為,如果我們只用資料集抽樣出的樣本訓練 D D D,(而我們收集的圖片都是由真人繪製的好圖片),換言之,我們只能為判別器提供標籤為 1 1 1的樣本,除此之外沒有反例,這樣訓練出來的 D D D從來沒有見過反例,此時給他一張由 G G G生成的假圖片,它也會給它打高分,可以這樣理解,在它看來假圖片和真圖片都是圖片,都應該打高分( D D D並沒有學會我們想要它學會的特徵)。
 在這裡插入圖片描述
 
  所以,負面的例子對判別模型來說至關重要,於是我們可以讓資料集中抽樣出來的樣本做真樣本(得分為1,real),讓生成器生成的樣本做假樣本(得分為0,fake)。
 在這裡插入圖片描述
 
   生成演算法:給出一系列的正例,以及隨機生成的反例。在每一個輪次中讓 D D D學習這些正,反例,訓練好判別器之後,固定住。接著訓練我們的生成器,使它生成的反例能夠儘可能地在判別器中得高分。
 在這裡插入圖片描述
 
   演算法主體:和之前提及的一樣,初始引數,固定一方,訓練另一方,讓 G G G學會“欺騙”判別器。
 在這裡插入圖片描述
 
  另一種思路,我們可以認為所有的畫手在畫二次元美少女的時候都服從一種固定的規律,比如眼睛的位置,頭髮的走向,依照一些讓人感覺舒服的比例畫出好的圖片,又因為機器理解圖片是藉助畫素矩陣實現的(所以你喜歡的不是二次元美少女是矩陣啊),用數學的思維來理解,就是所有的美少女頭像都是服從一種複雜分佈的。如圖所示,我們在這個分佈裡面取樣本可以得到我們想要的五官分佈正常的好樣本,而脫離這個分佈之外,我們可以獲取各種“抽象派”畫作。
 在這裡插入圖片描述
 
  那麼生成器 G G G要做的就是找出這個分佈,並完成對它的模擬,從而讓隨機噪聲經過生成器後能夠得到滿足這個好分佈的影像。
 
 
  那麼我們可以從極大似然估計的方法去計算這個分佈的引數,通過圖中的數學變化可以發現,求引數的最大似然等價於求 P d a t a P_data Pdata分佈與 P G P_G PG分佈的 K L KL KL散度。那麼我們求似然最大就等價於求散度最小。
 
在這裡插入圖片描述
 
  同時我們再來看判別器的目標函式,經過求導帶入極值點後我們發現,判別器的目標函式最大值就是兩個 K L KL KL散度之和。從而判別器,生成器聯絡了起來,生成器的目標函式可以從 m i n min min K L KL KL改寫成 m i n min min m a x max max V V V ( G G G, D D D)。到這各位可以回過頭去再看看推到證明中的那個式子。
 在這裡插入圖片描述

 

資料準備

 
  本次採用的資料集約有 16000 16000 16000張,畫素為96X96,可以看到一些熟悉的角色(諸如涼宮,長門),下文提供下載連結,網路上也有很多其他的資料集,諸君也可以藉助爬蟲放自己喜歡的角色進去(京都粉可以嘗試爬京都的作畫,生成圖片會比較接近京都臉)。
 

在這裡插入圖片描述
 
網盤連結:連結:https://pan.baidu.com/s/1MFulwMQJ78U2MCqRUYjkMg
提取碼:58v6
複製這段內容後開啟百度網盤手機App,操作更方便哦
 

程式碼實現

程式碼部分主要參考:張先生-您好.Pytorch實現GAN 生成動漫頭像

from tqdm import tqdm
import torch
import torchvision as tv
from torch.utils.data import DataLoader
import torch.nn as nn

# config類中定義超引數,
class Config(object):
    """
    定義一個配置類
    """
    # 0.引數調整
    data_path = '/root/PycharmProjects/untitled/'
    virs = "result"
    num_workers = 4  # 多執行緒
    img_size = 96  # 剪下圖片的畫素大小
    batch_size = 256  # 批處理數量
    max_epoch = 400   # 最大輪次
    lr1 = 2e-4  # 生成器學習率
    lr2 = 2e-4  # 判別器學習率
    beta1 = 0.5  # 正則化係數,Adam優化器引數
    gpu = False  # 是否使用GPU運算(建議使用)
    nz = 100  # 噪聲維度
    ngf = 64  # 生成器的卷積核個數
    ndf = 64  # 判別器的卷積核個數

    # 1.模型儲存路徑
    save_path = 'imgs2/'  # opt.netg_path生成圖片的儲存路徑
    # 判別模型的更新頻率要高於生成模型
    d_every = 1  # 每一個batch 訓練一次判別器
    g_every = 5  # 每1個batch訓練一次生成模型
    save_every = 5  # 每save_every次儲存一次模型
    netd_path = None
    netg_path = None

    # 測試資料
    gen_img = "result.png"
    # 選擇儲存的照片
    # 一次生成儲存64張圖片
    gen_num = 64
    gen_search_num = 512
    gen_mean = 0    # 生成模型的噪聲均值
    gen_std = 1     # 噪聲方差

# 例項化Config類,設定超引數,並設定為全域性引數
opt = Config()

# 定義Generation生成模型,通過輸入噪聲向量來生成圖片
class NetG(nn.Module):
    # 構建初始化函式,傳入opt類
    def __init__(self, opt):
        super(NetG, self).__init__()
        # self.ngf生成器特徵圖數目
        self.ngf = opt.ngf
        self.Gene = nn.Sequential(
            # 假定輸入為1*1*opt.nz維的資料,opt.nz維的向量
            # output = (input - 1)*stride + output_padding - 2*padding + kernel_size
            nn.ConvTranspose2d(in_channels=opt.nz, out_channels=self.ngf * 8, kernel_size=4, stride=1, padding=0, bias =False),
            nn.BatchNorm2d(self.ngf * 8),
            nn.ReLU(inplace=True),

            # 輸入一個4*4*ngf*8
            nn.ConvTranspose2d(in_channels=self.ngf * 8, out_channels=self.ngf * 4, kernel_size=4, stride=2, padding=1, bias =False),
            nn.BatchNorm2d(self.ngf * 4),
            nn.ReLU(inplace=True),

            # 輸入一個8*8*ngf*4
            nn.ConvTranspose2d(in_channels=self.ngf * 4, out_channels=self.ngf * 2, kernel_size=4, stride=2, padding=1,bias=False),
            nn.BatchNorm2d(self.ngf * 2),
            nn.ReLU(inplace=True),

            # 輸入一個16*16*ngf*2
            nn.ConvTranspose2d(in_channels=self.ngf * 2, out_channels=self.ngf, kernel_size=4, stride=2, padding=1, bias =False),
            nn.BatchNorm2d(self.ngf),
            nn.ReLU(inplace=True),

            # 輸入一張32*32*ngf
            nn.ConvTranspose2d(in_channels=self.ngf, out_channels=3, kernel_size=5, stride=3, padding=1, bias =False),

            # Tanh收斂速度快於sigmoid,遠慢於relu,輸出範圍為[-1,1],輸出均值為0
            nn.Tanh(),

        )# 輸出一張96*96*3

    def forward(self, x):
        return self.Gene(x)

# 構建Discriminator判別器
class NetD(nn.Module):
    def __init__(self, opt):
        super(NetD, self).__init__()

        self.ndf = opt.ndf
        ### 為什麼不做池化??為什麼不加全連線層??
        self.Discrim = nn.Sequential(
            # 卷積層
            # 輸入通道數in_channels,輸出通道數(即卷積核的通道數)out_channels,此處設定filer過濾器有64個,輸出通道自然也就是64。
            # 因為對圖片作了灰度處理,此處通道數為1,
            # 卷積核大小kernel_size,步長stride,對稱填0行列數padding
            # input:(bitch_size, 3, 96, 96),bitch_size = 單次訓練的樣本量
            # output:(bitch_size, ndf, 32, 32), (96 - 5 +2 *1)/3 + 1 =32
            # LeakyReLu= x if x>0 else nx (n為第一個函式引數),開啟inplace(覆蓋)可以節省記憶體,取消反覆申請記憶體的過程
            # LeakyReLu取消了Relu的負數硬飽和問題,是否對模型優化有效有待考證
            nn.Conv2d(in_channels=3, out_channels= self.ndf, kernel_size= 5, stride= 3, padding= 1, bias=False),
            nn.LeakyReLU(negative_slope=0.2, inplace= True),

            # input:(ndf, 32, 32)
            nn.Conv2d(in_channels= self.ndf, out_channels= self.ndf * 2, kernel_size= 4, stride= 2, padding= 1, bias=False),
            nn.BatchNorm2d(self.ndf * 2),
            nn.LeakyReLU(0.2, True),

            # input:(ndf *2, 16, 16)
            nn.Conv2d(in_channels= self.ndf * 2, out_channels= self.ndf *4, kernel_size= 4, stride= 2, padding= 1,bias=False),
            nn.BatchNorm2d(self.ndf * 4),
            nn.LeakyReLU(0.2, True),

            # input:(ndf *4, 8, 8)
            nn.Conv2d(in_channels= self.ndf *4, out_channels= self.ndf *8, kernel_size= 4, stride= 2, padding= 1, bias=False),
            nn.BatchNorm2d(self.ndf *8),
            nn.LeakyReLU(0.2, True),

            # input:(ndf *8, 4, 4)
            # output:(1, 1, 1)
            nn.Conv2d(in_channels= self.ndf *8, out_channels= 1, kernel_size= 4, stride= 1, padding= 0, bias=True),

            # 呼叫sigmoid函式解決分類問題
            # 因為判別模型要做的是二分類,故用sigmoid即可,因為sigmoid返回值區間為[0,1],
            # 可作判別模型的打分標準
            nn.Sigmoid()
        )

    def forward(self, x):
        # 展平後返回
        return self.Discrim(x).view(-1)


def train(**kwargs):

    # 配置屬性
    # 如果函式無字典輸入則使用opt中設定好的預設超引數
    for k_, v_ in kwargs.items():
        setattr(opt, k_, v_)

    # device(裝置),分配裝置
    if opt.gpu:
        device = torch.device("cuda")
    else:
        device = torch.device('cpu')

    # 資料預處理1
    # transforms 模組提供一般影像轉換操作類的功能,最後轉成floatTensor
    # tv.transforms.Compose用於組合多個tv.transforms操作,定義好transforms組合操作後,直接傳入圖片即可進行處理
    # tv.transforms.Resize,對PIL Image物件作resize運算, 數值儲存型別為float64
    # tv.transforms.CenterCrop, 中心裁剪
    # tv.transforms.ToTensor,將opencv讀到的圖片轉為torch image型別(通道,畫素,畫素),且把畫素範圍轉為[0,1]
    # tv.transforms.Normalize,執行image = (image - mean)/std 資料歸一化操作,一引數是mean,二引數std
    # 因為是三通道,所以mean = (0.5, 0.5, 0.5),從而轉成[-1, 1]範圍
    transforms = tv.transforms.Compose([
        # 3*96*96
        tv.transforms.Resize(opt.img_size),   # 縮放到 img_size* img_size
        # 中心裁剪成96*96的圖片。因為本實驗資料已滿足96*96尺寸,可省略
        tv.transforms.CenterCrop(opt.img_size),

        # ToTensor 和 Normalize 搭配使用
        tv.transforms.ToTensor(),
        tv.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
    ])

    # 載入資料並使用定義好的transforms對圖片進行預處理,這裡用的是直接定義法
    # dataset是一個包裝類,將資料包裝成Dataset類,方便之後傳入DataLoader中
    # 寫法2:
    # 定義類Dataset(Datasets)包裝類,重寫__getitem__(進行transforms系列操作)、__len__方法(獲取樣本個數)
    # ### 兩種寫法有什麼區別
    dataset = tv.datasets.ImageFolder(root=opt.data_path, transform=transforms)

    # 資料預處理2
    # 檢視drop_last操作,
    dataloader = DataLoader(
        dataset,      # 資料載入
        batch_size=opt.batch_size,    # 批處理大小設定
        shuffle=True,     # 是否進行洗牌操作
        #num_workers=opt.num_workers,     # 是否進行多執行緒載入資料設定
        drop_last=True           # 為True時,如果資料集大小不能被批處理大小整除,則設定為刪除最後一個不完整的批處理。
    )

    # 初始化網路
    netg, netd = NetG(opt), NetD(opt)
    # 判斷網路是否有權重數值
    # ### storage儲存
    map_location = lambda storage, loc: storage


    # torch.load模型載入,即有模型載入模型在該模型基礎上進行訓練,沒有模型則從頭開始
    # f:類檔案物件,如果有模型物件路徑,則載入返回
    # map_location:一個函式或字典規定如何remap儲存位置
    # net.load_state_dict將載入出來的模型資料載入到構建好的net網路中去
    if opt.netg_path:
        netg.load_state_dict(torch.load(f=opt.netg_path, map_location=map_location))
    if opt.netd_path:
        netd.load_state_dict(torch.load(f=opt.netd_path, map_location=map_location))

    # 搬移模型到之前指定裝置,本文采用的是cpu,分配裝置
    netd.to(device)
    netg.to(device)

    # 定義優化策略
    # torch.optim包內有多種優化演算法,
    # Adam優化演算法,是帶動量的慣性梯度下降演算法
    optimize_g = torch.optim.Adam(netg.parameters(), lr=opt.lr1, betas=(opt.beta1, 0.999))
    optimize_d = torch.optim.Adam(netd.parameters(), lr=opt.lr2, betas=(opt.beta1, 0.999))

    # 計算目標值和預測值之間的交叉熵損失函式
    # BCEloss:-w(p(x)log x +(1 - p(x))log(1 - x))
    # to(device),用於指定CPU/GPU
    criterions = nn.BCELoss().to(device)

    # 定義標籤,並且開始注入生成器的輸入noise
    true_labels = torch.ones(opt.batch_size).to(device)
    fake_labels = torch.zeros(opt.batch_size).to(device)

    # 生成滿足N(1,1)標準正態分佈,opt.nz維(100維),opt.batch_size個數的隨機噪聲
    noises = torch.randn(opt.batch_size, opt.nz, 1, 1).to(device)

    # 用於儲存模型時作生成影像示例
    fix_noises = torch.randn(opt.batch_size, opt.nz, 1, 1).to(device)

    # 訓練網路
    # 設定迭代
    for epoch in range(opt.max_epoch):
        # tqdm(iterator()),函式內嵌迭代器,用作迴圈的進度條顯示
        for ii_, (img, _) in tqdm((enumerate(dataloader))):
            # 將處理好的圖片賦值
            real_img = img.to(device)

            # 開始訓練生成器和判別器
            # 注意要使得生成的訓練次數小於一些
            # 每一輪更新一次判別器
            if ii_ % opt.d_every == 0:
                # 優化器梯度清零
                optimize_d.zero_grad()

                # 訓練判別器
                # 把判別器的目標函式分成兩段分別進行反向求導,再統一優化
                # 真圖
                # 把所有的真樣本傳進netd進行訓練,
                output = netd(real_img)
                # 用之前定義好的交叉熵損失函式計算損失
                error_d_real = criterions(output, true_labels)
                # 誤差反向計算
                error_d_real.backward()

                # 隨機生成的假圖
                # .detach() 返回相同資料的 tensor ,且 requires_grad=False
                # ,但能通過 in-place 操作報告給 autograd 在進行反向傳播的時候
                noises = noises.detach()
                # 通過生成模型將隨機噪聲生成為圖片矩陣資料
                fake_image = netg(noises).detach()
                # 將生成的圖片交給判別模型進行判別
                output = netd(fake_image)
                # 再次計算損失函式的計算損失
                error_d_fake = criterions(output, fake_labels)
                # 誤差反向計算
                # 求導和優化(權重更新)是兩個獨立的過程,只不過優化時一定需要對應的已求取的梯度值。
                # 所以求得梯度值很關鍵,而且,經常會累積多種loss對某網路引數造成的梯度,一併更新網路。
                error_d_fake.backward()

                # 計算一次Adam演算法,完成判別模型的引數迭代
                # 多個不同loss的backward()來累積同一個網路的grad,計算一次Adam即可
                optimize_d.step()

            # 訓練判別器
            if ii_ % opt.g_every == 0:
                optimize_g.zero_grad()
                # 用於netd作判別訓練和用於netg作生成訓練兩組噪聲需不同
                noises.data.copy_(torch.randn(opt.batch_size, opt.nz, 1, 1))
                fake_image = netg(noises)
                output = netd(fake_image)
                error_g = criterions(output, true_labels)
                error_g.backward()

                # 計算一次Adam演算法,完成判別模型的引數迭代
                optimize_g.step()

        # 儲存模型
        if (epoch + 1) % opt.save_every == 0:
            fix_fake_image = netg(fix_noises)
            tv.utils.save_image(fix_fake_image.data[:64], "%s/%s.png" % (opt.save_path, epoch), normalize=True)

            torch.save(netd.state_dict(),  'imgs2/' + 'netd_{0}.pth'.format(epoch))
            torch.save(netg.state_dict(),  'imgs2/' + 'netg_{0}.pth'.format(epoch))



# @torch.no_grad():資料不需要計算梯度,也不會進行反向傳播
@torch.no_grad()
def generate(**kwargs):
    # 用訓練好的模型來生成圖片

    for k_, v_ in kwargs.items():
        setattr(opt, k_, v_)

    device = torch.device("cuda") if opt.gpu else torch.device("cpu")

    # 載入訓練好的權重資料
    netg, netd = NetG(opt).eval(), NetD(opt).eval()
    ### ?? 兩個引數返回第一個
    map_location = lambda storage, loc: storage

    # opt.netd_path等引數有待修改
    netd.load_state_dict(torch.load('imgs2/netd_399.pth', map_location=map_location), False)
    netg.load_state_dict(torch.load('imgs2/netg_399.pth', map_location=map_location), False)
    netd.to(device)
    netg.to(device)

    # 生成訓練好的圖片
    noise = torch.randn(opt.gen_search_num, opt.nz, 1, 1).normal_(opt.gen_mean, opt.gen_std)

    noise.to(device)

    fake_image = netg(noise)
    score = netd(fake_image).detach()

    # 挑選出合適的圖片
    # 取出得分最高的圖片
    indexs = score.topk(opt.gen_num)[1]

    result = []

    for ii in indexs:
        result.append(fake_image.data[ii])

    # 以opt.gen_img為檔名儲存生成圖片
    tv.utils.save_image(torch.stack(result), opt.gen_img, normalize=True, range=(-1, 1))

def main():
    # 訓練模型
    train()
    # 生成圖片
    generate()

if __name__ == '__main__':
    main()

 
 

判別、生成模型均一輪迭代,200輪次

 
50 50 50輪展示一次
 
 
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

 

判別每一輪迭代,生成模型每五輪迭代,400輪次

 
 
100 100 100輪展示一次
 
 
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述
在這裡插入圖片描述

 
 
在這裡插入圖片描述
 
 
理論上,生成器的更新頻率應該小於判別器,因為生成器變化太快,會導致 V V V( G G G, D D D)變化劇烈,從而最開始的最高點不再滿足最高要求。
 
 

圖片生成

 
  用訓練好的生成器,隨機輸入一組噪聲,生成結果如下。效果不理想,希望大佬們能夠給出意見讓我能夠學習改進一下。
 
在這裡插入圖片描述

總結

 
  博主第一次寫博文,很多不足之處希望多多包涵,之後會陸續補齊李教授的其他作業講解,希望能夠幫助到一些和我一樣剛入門的朋友,也希望大佬可以幫忙提意見幫助我改進不足之處。

相關文章