對抗生成網路GAN系列——DCGAN簡介及人臉影象生成案例

語言: CN / TW / HK

theme: fancy

本文為稀土掘金技術社群首發簽約文章,14天內禁止轉載,14天后未獲授權禁止轉載,侵權必究!

🍊作者簡介:禿頭小蘇,致力於用最通俗的語言描述問題

🍊往期回顧:對抗生成網路GAN系列——GAN原理及手寫數字生成小案例

🍊近期目標:寫好專欄的每一篇文章

🍊支援小蘇:點贊👍🏼、收藏⭐、留言📩

 

對抗生成網路GAN系列——DCGAN簡介及人臉影象生成案例

寫在前面

  前段時間,我已經寫過一篇關於GAN的理論講解,並且結合理論做了一個手寫數字生成的小案例,對GAN原理不清楚的可以點選☞☞☞跳轉了解詳情。🌱🌱🌱

  為喚醒大家的記憶,這裡我再來用一句話對GAN的原理進行總結:GAN網路即是通過生成器和判別器的不斷相互對抗,不斷優化,直到判別器難以判斷生成器生成影象的真假。

  那麼接下來我就要開始講述DCGAN了喔,讀到這裡我就預設大家對GAN的原理已經掌握了,開始發車。🚖🚖🚖

 

DCGAN重點知識把握

DCGAN簡介

  我們先來看一下DCGAN的全稱——Deep Convolutional Genrative Adversarial Networks。這大家應該都能看懂叭,就是說這次我們將生成對抗網路和深度學習結合到一塊兒了,現在看這篇文章的一些觀點其實覺得是很平常的,沒有特別出彩之處,但是這篇文章是在16年釋出的,在當時能提出一些思想確實是難得。

  其實呢,這篇文章的原理和GAN基本是一樣的。不同之處只在生成網路模型和判別網路模型的搭建上,因為這篇文章結合了深度學習嘛,所以在模型搭建中使用了卷積操作【注:在上一篇GAN網路模型搭建中我們只使用的全連線層】。介於此,我不會再介紹DCGAN的原理,重點將放在DCGAN網路模型的搭建上。【注:這樣看來DCGAN就很簡單了,確實也是這樣的。但是大家也不要掉以輕心喔,這裡還是有一些細節的,我也是花了很長的時間來閱讀文件和做實驗來理解的,覺得理解差不多了,才來寫了這篇文章。】

  那麼接下來就來講講DCGAN生成模型和判別模型的設計,跟我一起來看看叭!!!

   

DCGAN生成模型、判別模型設計✨✨✨

  在具體到生成模型和判別模型的設計前,我們先來看論文中給出的一段話,如下圖所示:

image-20220722151528431

  這裡我還是翻譯一下,如下圖所示:

image-20220722153212125

  上圖給出了設計生成模型和判別模型的基本準則,後文我們搭建模型時也是嚴格按照這個來的。【注意上圖黃色背景的分數卷積喔,後文會詳細敘述】

 

生成網路模型🧅🧅🧅

  話不多說,直接放論文中生成網路結構圖,如下:

image-20220722154308221

圖1 生成網路模型

  看到這張圖不知道大家是否有幾秒的遲疑,反正我當時是這樣的,這個結構給人一種熟悉的感覺,但又覺得非常的陌生。好了,不賣關子了,我們一般看到的卷積結構都是特徵圖的尺寸越來越小,是一個下采樣的過程;而這個結構特徵圖的尺寸越來越大,是一個上取樣的過程。那麼這個上取樣是怎麼實現的呢,這就要說到主角==分數卷積==了。【又可以叫轉置卷積(transposed convolution)和反捲積(deconvolution),但是pytorch官方不建議取反捲積的名稱,論文中更是說這個叫法是錯誤的,所以我們儘量不要去用反捲積這個名稱,同時後文我會統一用轉置卷積來表述,因為這個叫法最多,我認為也是最貼切的】

 


⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔ 轉置卷積專場⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔

轉置卷積理論📝📝📝

  這裡我將通過一個小例子來講述轉置卷積的步驟,並通過程式碼來驗證這個步驟的正確性。首先我們先來看看轉置卷積的步驟,如下:

  • 在輸入特徵圖元素間填充s-1行、列0(其中s表示轉置卷積的步距,注意這裡的步長s和卷積操作中的有些不同)
  • 在輸入特徵圖四周填充k-p-1行、列0(其中k表示轉置卷積kernel_size大小,p為轉置卷積的padding,注意這裡的padding和卷積操作中的有些不同)
  • 將卷積核引數上下、左右翻轉
  • 做正常卷積運算(padding=0,s=1)

  是不是還是懵逼的狀態呢,不用急,現在就通過一個例子來講述這個過程。首先我們假設輸入特徵圖的尺寸為2*2大小,s=2,k=3,p=0,如下圖所示:

  第一步我們需要在特徵圖元素間填充s-1=1 行、列 0 (即填充1行0,1列0),變換後特徵圖如下:

  第二步我們需要在輸入特徵圖四周填充k-p-1=2 行、列0(即填充2行0,2列0),變換後特徵圖如下:

  第三步我們需要將卷積核上下、左右翻轉,得到新的卷積核【卷積核尺寸為k=3】,卷積核變化過程如下:

image-20220722171034209

  最後一步,我們做正常的卷積即可【注:拿第二步得到的特徵圖和第三步翻轉後得到的卷積核做正常卷積】,結果如下:

image-20220722180107129

  至此我們就從完成了轉置卷積,從一個2*2大小的特徵圖變成了一個5*5大小的特徵圖,如下圖所示(忽略了中間步驟):

image-20220722171802281

​   為了讓大家更直觀的感受轉置卷積的過程,我從Github上down了一個此過程動態圖供大家參考,如下:【注:需要動態圖點選☞☞☞自取】

轉置卷積s=2 k=3 p=0

​   通過上文的講述,相信你已經對轉置卷積的步驟比較清楚了。這時候你就可以試試圖1中結構,看看應用上述的方法能否得到對應的結構。需要注意的是,在第一次轉置卷積時,使用的引數k=4,s=1,p=0,後面的引數都為k=4,s=2,p=1,如下圖所示:

image-20220722185445223

​   如果你按照我的步驟試了試,可能會發出一些吐槽,這也太麻煩了,我只想計算一下經過轉置卷積後特徵圖的的變化,即知道輸入特徵圖尺寸以及k、s、p算出輸出特徵圖尺寸,這步驟也太複雜了。於是好奇有沒有什麼公式可以很方便的計算呢?enmmm,我這麼說,那肯定有嘛,公式如下圖所示:

image-20220722190016752

​ 對於上述公式我做3點說明:

  1. 在轉置卷積的官方文件中,引數還有output_padding 和dilation引數也會影響輸出特徵圖的大小,但這裡我們沒使用,公式就不加上這倆了,感興趣的可以自己去閱讀一下文件,寫的很詳細。🌵🌵🌵
  2. 對於stride[0],stride[1]、padding[0],padding[1]、kernel_size[0],kernel_size[1]該怎麼理解?其實啊這些都是卷積的基本知識,這些引數設定時可以設定一個整數或者一個含兩個整數的元組,*[0]表示在高度上進行操作,*[1]表示在寬度上進行操作。有關這部分在官方文件上也有寫,大家可自行檢視。為方便大家,我截了一下這部分的圖片,如下:
  3. 這點我帶大家巨集觀的理解一下這個公式,在傳統卷積中,往往卷積核k越小、padding越大,得到的特徵圖尺寸越大;而在轉置卷積中,從公式可以看出,卷積核k越大,padding越小,得到的特徵圖尺寸越大,關於這一點相信你也能從前文所述的轉置卷積理論部分有所感受。🌿🌿🌿

​   現在有了這個公式,大家再去試試叭。


轉置卷積實驗📝📝📝

​   接下來我將通過一個小實驗驗證上面的過程,程式碼如下:

```python import torch import torch.nn as nn

轉置卷積

def transposed_conv_official(): feature_map = torch.as_tensor([[1, 2], [0, 1]], dtype=torch.float32).reshape([1, 1, 2, 2]) print(feature_map) trans_conv = nn.ConvTranspose2d(in_channels=1, out_channels=1, kernel_size=3, stride=2, bias=False) trans_conv.load_state_dict({"weight": torch.as_tensor([[1, 0, 1], [1, 1, 0], [0, 0, 1]], dtype=torch.float32).reshape([1, 1, 3, 3])}) print(trans_conv.weight) output = trans_conv(feature_map) print(output)

def transposed_conv_self(): feature_map = torch.as_tensor([[0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 2, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0]], dtype=torch.float32).reshape([1, 1, 7, 7]) print(feature_map) conv = nn.Conv2d(in_channels=1, out_channels=1, kernel_size=3, stride=1, bias=False) conv.load_state_dict({"weight": torch.as_tensor([[1, 0, 0], [0, 1, 1], [1, 0, 1]], dtype=torch.float32).reshape([1, 1, 3, 3])}) print(conv.weight) output = conv(feature_map) print(output)

def main(): transposed_conv_official() print("---------------") transposed_conv_self()

if name == 'main': main()

```

​   首先我們先通過transposed_conv_official()函式來封裝一個轉置卷積過程,可以看到我們的輸入為[[1,2],[0,1]],卷積核為[[1,0,1],[1,1,0],[0,0,1]],採用k=3,s=2,p=0進行轉置卷積【注:這些引數和我前文講解轉置卷積步驟的用例引數是一致的】,我們來看一下程式輸出的結果:可以發現程式輸出和我們前面理論計算得到的結果是一致的。

image-20220722195837221

​   接著我們封裝了transposed_conv_self函式,這個函式定義的是一個正常的卷積,輸入是理論第2步得到的特徵圖,卷積核是第三步翻轉後得到的卷積核,經過卷積後輸出結果如下:結果和前面的一致。

image-20220722200836873

​   那麼通過這個例子就大致證明了轉置卷積的步驟確實是我們理論步驟所述。


【呼~~這部分終於講完了,其實我覺得轉置卷積倒是這篇論文很核心的一個知識點,這部分參考連結如下:參考視訊🥎🥎🥎】

⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔轉置卷積專場⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔⚔


判別模型網路🧅🧅🧅

​  同樣的,直接放出判別模型的網路結構圖,如下:【注:這部分原論文中沒有給出圖例,我自己簡單畫了一個,沒有論文中圖示美觀,但也大致能表示卷積的過程,望大家見諒】

​   判別網路真的沒什麼好講的,就是傳統的卷積操作,對卷積不瞭解的建議閱讀一下我的這篇文章🧨🧨🧨

​   這裡我給出程式執行的網路模型結構的結果,這部分就結束了:

image-20220722221744353

   

DCGAN人臉生成實戰✨✨✨

​   這部分我們將來實現一個人臉生成的實戰專案,我們先來看一下人臉一步步生成的動畫效果,如下圖所示:

動畫

​   我們可以看到隨著迭代次數增加,人臉生成的效果是越來越好的,說句不怎麼恰當的話,最後生成的圖片是像個人的。看到這裡,是不是都興致勃勃了呢,下面就讓我們一起來學學叭。🏆🏆🏆

​   秉持著授人以魚不如授人以漁的原則,這裡我就不帶大家一句一句的分析程式碼了,都是比較簡單的,官方文件寫的也非常詳細,我再敘述一篇也沒有什麼意義。哦,對了,這部分程式碼參考的是pytorch官網上DCGAN的教程,連結如下:DCGAN實戰教程🎈🎈🎈

​    我來簡單介紹一下官方教程的使用,點選上文連結會進入下圖的介面:這個介面正常滑動就是對這個專案的解釋,包括原理、程式碼及程式碼執行結果,大家首先要做的應該是閱讀一遍這個文件,基本可以解決大部分的問題。那麼接下來對於不明白的就可以點選下圖中綠框連結修改一些程式碼來除錯我們不懂的問題,這樣基本就都會明白了。【框1是google提供的一個免費的GPU運算平臺,就類似是雲端的jupyter notebook ,但這個需要梯子,大家自備;框2 是下載notebook到本地;框3是專案的Github地址】

image-20220722235309784

​   那方法都教給大家了,大家快去試試叭!!!

​   作為一個負責的博主👨‍🦳👨‍🦳👨‍🦳,當然不會就甩一個連結就走人啦,下面我會幫助大家排查一下程式碼中的一些難點,大家看完官方文件後如果有不明白的記得回來看看喔。🥂🥂🥂當然,如果有什麼不理解的地方且我下文沒有提及歡迎評論區討論交流。🛠🛠🛠


資料集載入🧅🧅🧅

​   首先我來說一下資料集的載入,這部分不難,卻十分重要。對於我們自己的資料集,我們先用ImageFolder方法建立dataset,程式碼如下:

```python

Create the dataset

dataset = dset.ImageFolder(root=dataroot, transform=transforms.Compose([ transforms.Resize(image_size), transforms.CenterCrop(image_size), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), ])) ```

​   需要強調的是root=dataroot表示我們自己資料集的路徑,在這個路徑下必須還有一個子目錄。怎麼理解呢,我舉個例子。比如我現在有一個人臉圖片資料集,其存放在資料夾2下面,我們不能將root的路徑指定為資料夾2,而是將資料夾2放入一個新資料夾1裡面,root的路徑指定為資料夾1。

​   對於上面程式碼的transforms操作做一個簡要的概括,transforms.Resize將圖片尺寸進行縮放、transforms.CenterCrop對圖片進行中心裁剪、transforms.ToTensor、transforms.Normalize最終會將圖片資料歸一化到[-1,1]之間,這部分不懂的可以參考我的這篇博文:pytorch中的transforms.ToTensor和transforms.Normalize理解🍚🍚🍚

​   有了dataset後,就可以通過DataLoader方法來載入資料集了,程式碼如下:

```python

Create the dataloader

dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=workers) ```


生成模型搭建🧅🧅🧅

​   接下來我們來說說生成網路模型的搭建,程式碼如下:不知道大家有沒有發現pytorch官網此部分搭建的網路模型和論文中給出的是有一點差別的,這裡我修改成了和論文中一樣的模型,從訓練效果來看,兩者差別是不大的。【注:下面程式碼是我修改過的】

```python

Generator Code

class Generator(nn.Module): def init(self, ngpu): super(Generator, self).init() self.ngpu = ngpu self.main = nn.Sequential( # input is Z, going into a convolution nn.ConvTranspose2d( nz, ngf * 16, 4, 1, 0, bias=False), nn.BatchNorm2d(ngf * 16), nn.ReLU(True), # state size. (ngf16) x 4 x 4 nn.ConvTranspose2d(ngf * 16, ngf * 8, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), # state size. (ngf8) x 8 x 8 nn.ConvTranspose2d( ngf * 8, ngf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), # state size. (ngf*4) x 16 x 16 nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), # state size. (ngf * 2) x 32 x 32 nn.ConvTranspose2d( ngf * 2, nc, 4, 2, 1, bias=False), nn.Tanh() # state size. (nc) x 64 x 64 )

def forward(self, input):
    return self.main(input)

```

​   我覺得這個模型搭建步驟大家應該都是較為清楚的,但我當時對這個第一步即從一個100維的噪聲向量如何變成變成一個1024*4*4的特徵圖還是比較疑惑的。這裡就為大家解答一下,我們可以看看在訓練過程中傳入的噪聲程式碼,即輸入為:noise = torch.randn(b_size, nz, 1, 1, device=device),這是一個100*1*1的特徵圖,這樣是不是一下子恍然大悟了呢,那我們的第一步也就是從100*1*1的特徵圖經轉置卷積變成1024*4*4的特徵圖。


模型訓練🧅🧅🧅

​   這部分我在上一篇GAN網路講解中已經介紹過,但是我沒有細講,這裡我想重點講一下BCELOSS損失函式。【就是二值交叉熵損失函式啦】我們先來看一下pytorch官網對這個函式的解釋,如下圖所示:

image-20220723142323032

​   其中N表示batch_size,$w_n$應該表示一個權重係數,預設為1【這個是我猜的哈,在官網沒看到對這一部分的解釋】,$y_n$表示標籤值,$x_n$表示資料。我們會對每個batch_size的資料都計算一個$l_n$ ,最後求平均或求和。【預設求均值】

​   看到這裡大家可能還是一知半解,不用擔心,我舉一個小例子大家就明白了。首先我們初始化一些輸入資料和標籤:

python import torch import math input = torch.randn(3,3) target = torch.FloatTensor([[0, 1, 1], [1, 1, 0], [0, 0, 0]])

​   來看看輸入資料和標籤的結果:

image-20220723144544905

​   接著我們要讓輸入資料經過Sigmoid函式將其歸一化到[0,1]之間【BCELOSS函式要求】:

python m = torch.nn.Sigmoid() m(input)

​   輸出的結果如下:

image-20220723145022493

​   最後我們就可以使用BCELOSS函式計算輸入資料和標籤的損失了:

python loss =torch.nn.BCELoss() loss(m(input), target)

​   輸出結果如下:

​   大家記住這個值喔!!!

​   上文似乎只是介紹了BCELOSS怎麼用,具體怎麼算的好像並不清楚,下面我們就根據官方給的公式來一步一步手動計算這個損失,看看結果和呼叫函式是否一致,如下:

```python r11 = 0 * math.log(0.8172) + (1-0) * math.log(1-0.8172) r12 = 1 * math.log(0.8648) + (1-1) * math.log(1-0.8648) r13 = 1 * math.log(0.4122) + (1-1) * math.log(1-0.4122)

r21 = 1 * math.log(0.3266) + (1-1) * math.log(1-0.3266) r22 = 1 * math.log(0.6902) + (1-1) * math.log(1-0.6902) r23 = 0 * math.log(0.5620) + (1-0) * math.log(1-0.5620)

r31 = 0 * math.log(0.2024) + (1-0) * math.log(1-0.2024) r32 = 0 * math.log(0.2884) + (1-0) * math.log(1-0.2884) r33 = 0 * math.log(0.5554) + (1-0) * math.log(1-0.5554)

BCELOSS = -(1/9) * (r11 + r12+ r13 + r21 + r22 + r23 + r31 + r32 + r33) ```

​   來看看結果叭:

image-20220723145941661

​   你會發現呼叫BCELOSS函式和手動計算的結果是一致的,只是精度上有差別,這說明我們前面所說的理論公式是正確的。【注:官方還提供了一種函式——BCEWithLogitsLoss,其和BCELOSS大致一樣,只是對輸入的資料不需要再呼叫Sigmoid函式將其歸一化到[0,1]之間,感興趣的可以閱讀看看】

​   這個損失函式講完訓練部分就真沒什麼可講的了,哦,這裡得提一下,在計算生成器的損失時,我們不是最小化$log(1-D(G(Z)))$ ,而是最大化$logD(G(z))$ 。這個在GAN網路論文中也有提及,我上一篇沒有說明這點,這裡說聲抱歉,論文中說是這樣會更好的收斂,這裡大家注意一下就好。

   

番外篇——使用伺服器訓練如何儲存圖片和訓練損失✨✨✨

​   不知道大家執行這個程式碼有沒有遇到這樣尬尷的處境:

  1. 無法科學上網,用不了google提供的免費GPU
  2. 自己電腦沒有GPU,這個模型很難跑完
  3. 有伺服器,但是官方提供的程式碼並沒有儲存最後生成的圖片和損失,自己又不會改

​   前兩個我沒法幫大家解決,那麼我就來說說怎麼來儲存圖片和訓練損失。首先來說說怎麼儲存圖片,這個就很簡單啦,就使用一個save_image函式即可,具體如下圖所示:【在訓練部分新增】

image-20220723162639573

​   接下來說說怎麼儲存訓練損失,通過torch.save()方法儲存程式碼如下:

```python

儲存LOSS

G_losses = torch.tensor(G_losses) D_losses = torch.tensor(D_losses) torch.save(G_losses, 'LOSS\GL') torch.save(D_losses, 'LOSS\DL') ```

​   程式碼執行完後,損失儲存在LOSS資料夾下,一個檔案為GL,一個為DL。這時候我們需要建立一個.py檔案來載入損失並可視化,.py檔案內容如下:

```python import torch import torch.utils.data import matplotlib.pyplot as plt

繪製LOSS曲線

G_losses = torch.load('F:\老師發放論文\經典網路模型\GAN系列\DCGAN\LOSS\GL') D_losses = torch.load('F:\老師發放論文\經典網路模型\GAN系列\DCGAN\LOSS\DL')

plt.figure(figsize=(10,5)) plt.title("Generator and Discriminator Loss During Training") plt.plot(G_losses,label="G") plt.plot(D_losses,label="D") plt.xlabel("iterations") plt.ylabel("Loss") plt.legend() plt.show() ```

​   最後來看看儲存的圖片和損失,如下圖所示:

image_4_3165

image-20220723163500724

 

小結

​   至此,DCGAN就全部講完啦,希望大家都能有所收穫。有什麼問題歡迎評論區討論交流!!!GAN系列近期還會出cycleGAN的講解和四季風格轉換的demo,後期會考慮出瑕疵檢測方面的GAN網路,如AnoGAN等等,敬請期待。🏵🏵🏵

   

如若文章對你有所幫助,那就🛴🛴🛴

         一鍵三連 (1).gif