對抗生成網絡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