🍨 本文為[🔗365天深度學習訓練營]內部限免文章(版權歸 *K同學啊* 所有)
🍖 作者:[K同學啊]
一、理論基礎
生成對抗網絡(Generative Adversarial Networks, GAN)是近年來深度學習領域的一個熱點方向。GAN并不指代某一個具體的神經網絡,而是指一類基于博弈思想而設計的神經網絡。GAN由兩個分別被稱為生成器(Generator)和判別器(Discriminator)的神經網絡組成。其中,生成器從某種噪聲分布中隨機采樣作為輸入,輸出與訓練集中真實樣本非常相似的人工樣本;判別器的輸入則為真實樣本或人工樣本,其目的是將人工樣本與真實樣本盡可能地區分出來。生成器和判別器交替運行,相互博弈,各自的能力都得到升。理想情況下,經過足夠次數的博弈之后,判別器無法判斷給定樣本的真實性,即對于所有樣本都輸出50%真,50%假的判斷。此時,生成器輸出的人工樣本已經逼真到使判別器無法分辨真假,停止博弈。這樣就可以得到一個具有“偽造”真實樣本能力的生成器。
1. 生成器
GANs中,生成器 G 選取隨機噪聲 z 作為輸入,通過生成器的不斷擬合,最終輸出一個和真實樣本尺寸相同,分布相似的偽造樣本G(z)。生成器的本質是一個使用生成式方法的模型,它對數據的分布假設和分布參數進行學習,然后根據學習到的模型重新采樣出新的樣本。
從數學上來說,生成式方法對于給定的真實數據,首先需要對數據的顯式變量或隱含變量做分布假設;然后再將真實數據輸入到模型中對變量、參數進行訓練;最后得到一個學習后的近似分布,這個分布可以用來生成新的數據。從機器學習的角度來說,模型不會去做分布假設,而是通過不斷地學習真實數據,對模型進行修正,最后也可以得到一個學習后的模型來做樣本生成任務。這種方法不同于數學方法,學習的過程對人類理解較不直觀。
2. 判別器
GANs中,判別器 D 對于輸入的樣本 x,輸出一個[0,1]之間的概率數值D(x)。x 可能是來自于原始數據集中的真實樣本 x,也可能是來自于生成器 G 的人工樣本G(z)。通常約定,概率值D(x)越接近于1就代表此樣本為真實樣本的可能性更大;反之概率值越小則此樣本為偽造樣本的可能性越大。也就是說,這里的判別器是一個二分類的神經網絡分類器,目的不是判定輸入數據的原始類別,而是區分輸入樣本的真偽。可以注意到,不管在生成器還是判別器中,樣本的類別信息都沒有用到,也表明 GAN 是一個無監督的學習過程。
3. 基本原理
GAN是博弈論和機器學習相結合的產物,于2014年Ian Goodfellow的論文中問世,一經問世即火爆足以看出人們對于這種算法的認可和狂熱的研究熱忱。想要更詳細的了解GAN,就要知道它是怎么來的,以及這種算法出現的意義是什么。研究者最初想要通過計算機完成自動生成數據的功能,例如通過訓練某種算法模型,讓某模型學習過一些蘋果的圖片后能自動生成蘋果的圖片,具備些功能的算法即認為具有生成功能。但是GAN不是第一個生成算法,而是以往的生成算法在衡量生成圖片和真實圖片的差距時采用均方誤差作為損失函數,但是研究者發現有時均方誤差一樣的兩張生成圖片效果卻截然不同,鑒于此不足Ian Goodfellow提出了GAN。
那么GAN是如何完成生成圖片這項功能的呢,如圖1所示,GAN是由兩個模型組成的:生成模型G和判別模型D。首先第一代生成模型1G的輸入是隨機噪聲z,然后生成模型會生成一張初級照片,訓練一代判別模型1D另其進行二分類操作,將生成的圖片判別為0,而真實圖片判別為1;為了欺瞞一代鑒別器,于是一代生成模型開始優化,然后它進階成了二代,當它生成的數據成功欺瞞1D時,鑒別模型也會優化更新,進而升級為2D,按照同樣的過程也會不斷更新出N代的G和D。
二、前期準備工作
1. 定義超參數
import argparse
import os
import numpy as np
import torchvision.transforms as transforms
from torchvision.utils import save_image
from torch.utils.data import DataLoader
from torchvision import datasets
from torch.autograd import Variable
import torch.nn as nn
import torch## 創建文件夾
os.makedirs("./images/", exist_ok=True) ## 記錄訓練過程的圖片效果
os.makedirs("./save/", exist_ok=True) ## 訓練完成時模型保存的位置
os.makedirs("./datasets/mnist", exist_ok=True) ## 下載數據集存放的位置## 超參數配置
n_epochs=50
batch_size=512
lr=0.0002
b1=0.5
b2=0.999
n_cpu=2
latent_dim=100
img_size=28
channels=1
sample_interval=500## 圖像的尺寸:(1, 28, 28), 和圖像的像素面積:(784)
img_shape = (channels, img_size, img_size)
img_area = np.prod(img_shape)## 設置cuda:(cuda:0)
cuda = True if torch.cuda.is_available() else False
print(cuda)
## mnist數據集下載
mnist = datasets.MNIST(root='./datasets/', train=True, download=True, transform=transforms.Compose([transforms.Resize(img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]),
)
## 配置數據到加載器
dataloader = DataLoader(mnist,batch_size=batch_size,shuffle=True,
)
class Discriminator(nn.Module):def __init__(self):super(Discriminator, self).__init__()self.model = nn.Sequential(nn.Linear(img_area, 512), # 輸入特征數為784,輸出為512nn.LeakyReLU(0.2, inplace=True), # 進行非線性映射nn.Linear(512, 256), # 輸入特征數為512,輸出為256nn.LeakyReLU(0.2, inplace=True), # 進行非線性映射nn.Linear(256, 1), # 輸入特征數為256,輸出為1nn.Sigmoid(), # sigmoid是一個激活函數,二分類問題中可將實數映射到[0, 1],作為概率值, 多分類用softmax函數)def forward(self, img):img_flat = img.view(img.size(0), -1) # 鑒別器輸入是一個被view展開的(784)的一維圖像:(64, 784)validity = self.model(img_flat) # 通過鑒別器網絡return validity # 鑒別器返回的是一個[0, 1]間的概率
class Generator(nn.Module):def __init__(self):super(Generator, self).__init__()## 模型中間塊兒def block(in_feat, out_feat, normalize=True): # block(in, out )layers = [nn.Linear(in_feat, out_feat)] # 線性變換將輸入映射到out維if normalize:layers.append(nn.BatchNorm1d(out_feat, 0.8)) # 正則化layers.append(nn.LeakyReLU(0.2, inplace=True)) # 非線性激活函數return layers## prod():返回給定軸上的數組元素的乘積:1*28*28=784self.model = nn.Sequential(*block(latent_dim, 128, normalize=False), # 線性變化將輸入映射 100 to 128, 正則化, LeakyReLU*block(128, 256), # 線性變化將輸入映射 128 to 256, 正則化, LeakyReLU*block(256, 512), # 線性變化將輸入映射 256 to 512, 正則化, LeakyReLU*block(512, 1024), # 線性變化將輸入映射 512 to 1024, 正則化, LeakyReLUnn.Linear(1024, img_area), # 線性變化將輸入映射 1024 to 784nn.Tanh() # 將(784)的數據每一個都映射到[-1, 1]之間)## view():相當于numpy中的reshape,重新定義矩陣的形狀:這里是reshape(64, 1, 28, 28)def forward(self, z): # 輸入的是(64, 100)的噪聲數據imgs = self.model(z) # 噪聲數據通過生成器模型imgs = imgs.view(imgs.size(0), *img_shape) # reshape成(64, 1, 28, 28)return imgs # 輸出為64張大小為(1, 28, 28)的圖像
## 創建生成器,判別器對象
generator = Generator()
discriminator = Discriminator()## 首先需要定義loss的度量方式 (二分類的交叉熵)
criterion = torch.nn.BCELoss()## 其次定義 優化函數,優化函數的學習率為0.0003
## betas:用于計算梯度以及梯度平方的運行平均值的系數
optimizer_G = torch.optim.Adam(generator.parameters(), lr=lr, betas=(b1, b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=lr, betas=(b1, b2))## 如果有顯卡,都在cuda模式中運行
if torch.cuda.is_available():generator = generator.cuda()discriminator = discriminator.cuda()criterion = criterion.cuda()
for epoch in range(n_epochs): # epoch:50for i, (imgs, _) in enumerate(dataloader): # imgs:(64, 1, 28, 28) _:label(64)imgs = imgs.view(imgs.size(0), -1) # 將圖片展開為28*28=784 imgs:(64, 784)real_img = Variable(imgs).cuda() # 將tensor變成Variable放入計算圖中,tensor變成variable之后才能進行反向傳播求梯度real_label = Variable(torch.ones(imgs.size(0), 1)).cuda() ## 定義真實的圖片label為1fake_label = Variable(torch.zeros(imgs.size(0), 1)).cuda() ## 定義假的圖片的label為0real_out = discriminator(real_img) # 將真實圖片放入判別器中loss_real_D = criterion(real_out, real_label) # 得到真實圖片的lossreal_scores = real_out # 得到真實圖片的判別值,輸出的值越接近1越好## 計算假的圖片的損失## detach(): 從當前計算圖中分離下來避免梯度傳到G,因為G不用更新z = Variable(torch.randn(imgs.size(0), latent_dim)).cuda() ## 隨機生成一些噪聲, 大小為(128, 100)fake_img = generator(z).detach() ## 隨機噪聲放入生成網絡中,生成一張假的圖片。fake_out = discriminator(fake_img) ## 判別器判斷假的圖片loss_fake_D = criterion(fake_out, fake_label) ## 得到假的圖片的lossfake_scores = fake_out## 損失函數和優化loss_D = loss_real_D + loss_fake_D # 損失包括判真損失和判假損失optimizer_D.zero_grad() # 在反向傳播之前,先將梯度歸0loss_D.backward() # 將誤差反向傳播optimizer_D.step() # 更新參數z = Variable(torch.randn(imgs.size(0), latent_dim)).cuda() ## 得到隨機噪聲fake_img = generator(z) ## 隨機噪聲輸入到生成器中,得到一副假的圖片output = discriminator(fake_img) ## 經過判別器得到的結果## 損失函數和優化loss_G = criterion(output, real_label) ## 得到的假的圖片與真實的圖片的label的lossoptimizer_G.zero_grad() ## 梯度歸0loss_G.backward() ## 進行反向傳播optimizer_G.step() ## step()一般用在反向傳播后面,用于更新生成網絡的參數## 打印訓練過程中的日志## item():取出單元素張量的元素值并返回該值,保持原元素類型不變if ( i + 1 ) % 100 == 0:print("[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f] [D real: %f] [D fake: %f]"% (epoch, n_epochs, i, len(dataloader), loss_D.item(), loss_G.item(), real_scores.data.mean(), fake_scores.data.mean()))## 保存訓練過程中的圖像batches_done = epoch * len(dataloader) + iif batches_done % sample_interval == 0:save_image(fake_img.data[:25], "./images/%d.png" % batches_done, nrow=5, normalize=True)
torch.save(generator.state_dict(), './generator.pth')
torch.save(discriminator.state_dict(), './discriminator.pth')
部分運行截圖: