對抗生成網絡GAN(Generative Adversarial Networks)是由蒙特利爾大學Ian Goodfellow在2014年提出的機器學習架構,與之前介紹的神經網絡不同,GAN最初是作為一種無監督的機器學習模型,對抗生成網絡的變體也有很多,如GAN、DCGAN、CGAN、ACGAN等,無論對抗生成網絡形式為何種,對抗生成網絡都由兩部分組成:判別器(Discriminator)常用D表示;另一個稱為生成器(Generator)用G表示。判別器與生成器的博弈過程是對抗生成網絡學習過程,判別器通過不斷學習提高自身的識別能力,而生成器利用判別器不斷提升生成樣本能力,當判別器對生成器生成的樣本判斷真偽概率為50%時,生成器訓練完時說明生成器生成的樣本達到了以假亂真的效果了,對抗生成網絡整個訓練過程類似金庸小說《神雕俠侶》中的周伯通,通過左右互搏術提高自身的武學修為。
一、對抗生成網絡訓練過程
1.1、判別器與生成器相互博弈過程
以最簡單的GAN為例,用xi代表判別器輸入數據,xi有兩種可能,當xi來自真實樣本數據輸入時標記為yi=1,而當xi來自生成器輸入時數據標記為yi=0,訓練判別器使用交叉熵作為損失函數:
①
上式中pi代表真實的概率分布,在①式中,pi等於0或1,即pi等於yi;qi代表判別器模擬的概率分布,可用qi=D(xi)表示,有了這些設定后,①式可寫為:
②
由於yi取值只有0,1兩種可能,所以上面表達式
最終形式是
(yi=1),要么是
(yi=0),公式②是兩分類問題的交叉熵函數。在訓練判別器過程中往往是輸入一條真實樣本,再輸入一條來自生成器生成數據,輸入數據xi來自真實樣本或生成器的概率為50%,將公式②兩邊同乘以1/2有:
③
推導過程使用數學期望的離散公式:

判別器訓練過程為求③交叉熵最小值,換言之,是求下面方程的最大值:
④
④中函數V(D)中的D代表判別器中未知參數集合。通過上面訓練過程,判別器能逐漸識別出生成數據的真偽。當判別器輸入xi來自生成器時,G將噪音數據zi處理后得到輸入xi,用代數式表達為:

生成器目標是使得生成的圖像G(z)與真實的數據沒有差異,即使得D(G(z))函數值變大,代入④后有:

上式中的G代表生成器中未知參數集合,D(G(z))函數值變大時,
變小,所以生成器G的訓練目標是讓公式④變小。綜合起來看,對於函數V(D,G)先將G的參數集合視為符號常量,判別器的參數集合D視為變量求函數V最大值,該最大值是含G的符號常量代數式,此時再求函數最小值即可獲得生成器的最優參數值,整個訓練過程用公式表示為:

這個最優點叫做鞍點,在svm支持向量機詳解一篇中曾詳細介紹過,如下圖所示:

上圖中曲線代表函數V(D,G*),其含義為參數集合G固定為G*,以D為自變量生成的一個函數,求V(D,G*)最大值,參數集合G取不同值就相應的生成無數個曲線,這曲線最大值形成一個馬鞍形背脊線:

背脊線上最小值為鞍點,即為參數集合G和D的最優解。GAN的訓練過程也可以歸納為:第一步max過程,訓練判別器使D能最大程度的識別輸入數據真偽;第二步min過程,利用逐漸提升的判別器來訓練生成器,使生成器G能生成以假亂真的數據。先max后min反應出判別器與生成器相互博弈的過程,而最優解是納什均衡點。
1.2、實現GAN網絡
利用上面推導可以實現一個簡單的對抗生成網絡GAN,下面的代碼中利用minist數據集生成0到9的圖片,判別器和生成器都是簡單的全連接神經網絡,判別器輸出是一個0到1的小數,代表判別器識別樣本真偽的概率;生成器輸入以一組標准正態分布噪音,輸出是一個28*28的圖片,利用GAN生成器最后能生成較為逼真的手寫數字圖片。
minist數據集下載地址:minist數據集下載,下載后放入程序目錄下的dataset目錄中。
=imgdataset.py=:圖片加載輔助類
import os import numpy as np import gzip import torch.utils.data as Data class DataSet(Data.Dataset): """ 讀取數據、初始化數據 """ def __init__(self, folder, data_name, label_name,transform=None): (train_set, train_labels) = self.load_data(folder, data_name, label_name) # 其實也可以直接使用torch.load(),讀取之后的結果為torch.Tensor形式 self.train_set = train_set self.train_labels = train_labels self.transform = transform pass def load_data(self,data_folder, data_name, label_name): """ data_folder: 文件目錄 data_name: 數據文件名 label_name:標簽數據文件名 """ with gzip.open(os.path.join(data_folder, label_name), 'rb') as lbpath: # rb表示的是讀取二進制數據 y_train = np.frombuffer(lbpath.read(), np.uint8, offset=8) with gzip.open(os.path.join(data_folder, data_name), 'rb') as imgpath: x_train = np.frombuffer( imgpath.read(), np.uint8, offset=16).reshape(len(y_train), 28, 28) return (x_train, y_train) def __getitem__(self, index): img, target = self.train_set[index], int(self.train_labels[index]) if self.transform is not None: img = self.transform(img) return img, target def __len__(self): return len(self.train_set)
=GAN_Net.py=:判別器與生成器
import torch import torch.nn as nn class D_Net(nn.Module): def __init__(self): super().__init__() self.dnet = nn.Sequential( nn.Linear(784,512), nn.ReLU(), nn.Linear(512,256), nn.ReLU(), nn.Linear(256,1), nn.Sigmoid() ) def forward(self,x): x = self.dnet(x) return x class G_Net(nn.Module): def __init__(self,noise_size): super().__init__() self.gnet = nn.Sequential( nn.Linear(noise_size,256), nn.ReLU(), nn.Linear(256,512), nn.ReLU(), nn.Linear(512,784) ) def forward(self,x): x = self.gnet(x) return x
=GAN_Train.py=訓練過程代碼
from GAN_Net import D_Net,G_Net
import torch.utils.data as Data
from imgdataset import DataSet
import torch
import torch.nn as nn
from torchvision.datasets import MNIST
from torchvision import transforms
from torch.utils import data
from torchvision.utils import save_image
import os
dataPath='dataset'
def loadDataset():
trainDataset = DataSet(dataPath,
"train-images-idx3-ubyte.gz",
"train-labels-idx1-ubyte.gz",
transform=transforms.ToTensor())
testDataset = DataSet(dataPath,
"t10k-images-idx3-ubyte.gz",
"t10k-labels-idx1-ubyte.gz",
transform=transforms.ToTensor())
return trainDataset, testDataset
def gnloader(loadbatsize):
trainDataset, testDataset=loadDataset()
# 訓練數據和測試數據的裝載
train_loader = Data.DataLoader(
dataset=trainDataset,
batch_size=loadbatsize,
shuffle=True
)
test_loader = Data.DataLoader(
dataset=testDataset,
batch_size=loadbatsize,
shuffle=True,
)
return train_loader,test_loader
class Trainer:
def __init__(self):
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.loss_fn = nn.BCELoss()
def train(self):
if not os.path.exists("./gan_img"):
os.mkdir("./gan_img")
if not os.path.exists("./gan_params"):
os.mkdir('./gan_params')
BATCH_SIZE = 100
NUM_EPOCHS = 150
INPUT_SIZE=128
train_loader, test_loader=gnloader(BATCH_SIZE)
d_net = D_Net().to(self.device)
g_net = G_Net(INPUT_SIZE).to(self.device)
d_opt = torch.optim.Adam(d_net.parameters())
g_opt = torch.optim.Adam(g_net.parameters())
for epochs in range(NUM_EPOCHS):
for i,(x,y) in enumerate(train_loader):
#訓練判別器
N = x.size(0)
real_img = x.to(self.device).reshape(N,-1)
real_label = torch.ones(N,1).to(self.device)
fake_label = torch.zeros(N,1).to(self.device)
real_out = d_net(real_img)
d_real_loss = self.loss_fn(real_out,real_label)
z = torch.randn(N,INPUT_SIZE).to(self.device)
fake_img = g_net(z)
fake_out = d_net(fake_img)
d_fake_loss = self.loss_fn(fake_out,fake_label)
d_loss = d_real_loss+d_fake_loss
d_opt.zero_grad()
d_loss.backward()
d_opt.step()
#訓練生成器
z = torch.randn(N,INPUT_SIZE).to(self.device)
fake_img = g_net(z)
fake_out = d_net(fake_img)
g_loss = self.loss_fn(fake_out,real_label)
g_opt.zero_grad()
g_loss.backward()
g_opt.step()
if i % 500 == 0:
print("Epoch:{}/{},d_loss:{:.3f},g_loss:{:.3f},"
"d_real:{:.3f},d_fake:{:.3f}".
format(epochs, NUM_EPOCHS, d_loss.item(), g_loss.item(),
real_out.data.mean(), fake_out.data.mean()))
real_image = real_img.cpu().data.reshape([-1, 1, 28, 28])
save_image(real_image, "./gan_img/real_img.jpg", nrow=10, normalize=True, scale_each=True)
fake_image = fake_img.cpu().data.reshape([-1, 1, 28, 28])
save_image(fake_image, "./gan_img/fake_img.jpg", nrow=10, normalize=True, scale_each=True)
torch.save(d_net.state_dict(), "gan_params/d_net.pth")
torch.save(g_net.state_dict(), "gan_params/g_net.pth")
if __name__ == '__main__':
t = Trainer()
t.train()
程序中每次迭代選擇100組數據,如果來自真實的樣本則以維度為[100,1]、值全為1的張量real_label 作為標簽輸入到判別器中;如果輸入來自生成器,則以[100,1]、值全為0的張量fake_label 作為標簽輸入到判別器中,通過優化過程提升判別器識別能力,這對應上述推導過程中max階段。隨后利用升級后的判別器訓練生成器,優化生成器參數從而不斷生成能以假亂真的圖片,這對應推導中min階段。訓練過程中只是簡單標注數據的真偽,沒有判斷每個真實圖片對應是哪個數字的圖片,可見GAN是一種無監督的機器學習結構,代碼生成后圖片如下:

二、DCGAN
上面GAN網絡生成器和判別器都是簡單全連接神經網絡,DCGAN的網絡部分為卷積網絡,具體來說DCGAN的判別器是一個卷積網絡,輸出仍然是一個0到1之間的實數,代表判別器判斷數據的真偽概率;而生成器是一個反卷積網絡輸出的是一個具體的數據,本篇中生成器生成的是一個28*28的單通道圖。判別器利用卷積網絡實現降維提取數據主要特征,而反卷積常稱作上采樣,是增維的過程。
在卷積神經網絡一章中詳細介紹過卷積網絡的訓練過程,設輸入尺寸為i,卷積核為k方陣,步長s,補padding層數為p,卷積之后的尺寸為:
⑤
公式⑤中[]代表取不大於該小數的整數,如[1.4]=1,[2.8]=2等,卷積輸出尺寸公式很好理解:

i+2p-k可以理解為上圖綠色右邊框'行程',s可以理解為速度,i+2p-k除以s為綠色右邊框走完全程一共所需要'時間',每一格時間代表輸出的一個信息,特殊一些,這個'時間'必須是整數。同時如果p=0代表valid模式,p>0代表full或same模式,幾種卷積模式如下圖:
a)valid模式,padding=0

b)full模式,開始時卷積核右下角與圖片左上角重疊,結束時卷積核左上角與圖片右下角重疊。

c)same模式,卷積核的中心始終和原圖的像素重合,如果步長為1,輸出尺寸等輸入圖片尺寸。

再來看反卷積,反卷積也稱轉置卷積,如果一個卷積操作將5*5圖片變為3*3圖片,那么反卷積可實現將3*3圖片恢復到5*5的圖片,也是說反卷積能實現卷積在尺寸上逆操作,需要強調的是反卷積只是在尺寸上還原,並不能實現將信息全部還原回去,反卷積后輸出尺寸可以通過公式⑤利用換元法推導出來,分幾種情況討論。
1、設原卷積過程步長為1,padding=0,卷積核大小為k,根據公式⑤,得卷積后輸出尺寸o:
⑥
對於反卷積而言,上式中o對應輸入尺寸,i對應反卷積的輸出,不妨設i'為反卷積的輸入尺寸,o'為反卷積輸出尺寸,顯然有:

上面方程組代入⑥有
(6.1)
為了能把上式寫成和卷積公式⑤一樣的形式,即
,(6.1)可變換為:

通過換元法將反卷積變成了卷積過程,同時這種情況下反卷積步長s',padding p',核大小k',輸出o'有以下對應關系:
表1
下圖是該情況下一個例子:

2、卷積過程步長s>1,padding層數 p>0,輸入尺寸i,且i+2p-k能被步長s整除
由於i+2p-k能被步長s整除,利用公式⑤計算卷積輸出尺寸時,依然能脫去取整符號[]:

同樣,利用代數分解法將上式變為公式⑤形式:

將上面推導結果對號入座,換算表如下:
表2
下面是情形二的一個例子:

當卷積過程步長s>1時,做反卷積時會在輸入圖像相鄰兩列插入s-1列0,這也是卷積步長大於1時反卷積的一個特點,通過插入0值列才能使得反卷積步長始終為1。
3、卷積過程步長s>1,padding層數 p>0,輸入尺寸i,且i+2p-k不能被步長s整除
此時i+2p-k不能被步長s整除,不妨設a為i+2p-k取余s的結果,即a=(i+2p-k) mod s,由整數取余性質可知,此時i+2p-k-a就可以被s整除了,引入a后即可脫去取整符號[]:

與情形二類似,利用多項式分解可得:

上式中k-p-1為反卷積padding層數,需要主要的是a的含義,a是在在輸入數據所有外層加k-p-1層padding后,再在最上面和最右面添加padding層數,在tensorflow和pytorch中叫做outpadding,下面給出一個該情形下的例子:

可以看到反卷積時現在外層加了p'=3-1-1=1層padding,而a=(6+2-3) mod 2=1,所以在圖像的上側和右側又加了1層padding,outpadding的引入是為了得到較為標准的輸出,舉一個例子,利用一個3*3的卷積核對輸入尺寸7*7圖像做步長2,padding為0的卷積,利用公式5可得到卷積的結果尺寸為[(7-3)/2]+1=3,而同樣方法對輸入尺寸8*8圖像做卷積輸出也是3:[(8-3)/2]+1=3,這樣一來對於反卷積而言,對一個3*3的圖像做反卷積會有兩個結果,這顯然不是我們希望發生的結果,有了outpadding后,反卷積的結果總是原卷積步長的整數倍,這就消除了歧義使反卷積的輸出尺寸統一。情形三換算表如下:
表3
下面是一組利用DCGAN實現繪制數字的代碼,其中圖片加載類imgdataset.py可復制之前的實例代碼。
=DCGAN_Net.py=生成器和判別器
import torch import torch.nn as nn class D_Net(nn.Module): def __init__(self): super().__init__() self.dnet = nn.Sequential( nn.Conv2d(1, 128, 5, 2, 2), nn.LeakyReLU(0.2), # 14*14 nn.Conv2d(128, 256, 5, 2, 2), nn.BatchNorm2d(256), # 7*7 nn.LeakyReLU(0.2), nn.Conv2d(256, 512, 5, 2, 1), # 3*3 nn.BatchNorm2d(512), nn.LeakyReLU(0.2), nn.Conv2d(512, 1, 3, 1, 0), # 1*1 nn.Sigmoid() ) def forward(self, x): x = self.dnet(x) return x class G_Net(nn.Module): def __init__(self): super().__init__() self.gnet = nn.Sequential( nn.ConvTranspose2d(128, 512, 3, 1, 0) # 3*3, nn.BatchNorm2d(512), nn.ReLU(), nn.ConvTranspose2d(512, 256, 5, 2, 1),# 7*7 nn.BatchNorm2d(256), nn.ReLU(), nn.ConvTranspose2d(256, 128, 5, 2, 2, 1),# 14*14 nn.BatchNorm2d(128), nn.ReLU(), nn.ConvTranspose2d(128, 1, 5, 2, 2, 1),# 28*28 nn.Tanh() ) def forward(self,x): x = self.gnet(x) return x
=DCGAN_Train.py=訓練DCGAN代碼
import torch
import torch.nn as nn
from torchvision.datasets import MNIST
from torch.utils import data
import os
from torchvision.utils import save_image
from torchvision import transforms
from DCGAN_Net import D_Net,G_Net
import torch.utils.data as Data
from imgdataset import DataSet
dataPath='dataset'
def loadDataset():
trainDataset = DataSet(dataPath,
"train-images-idx3-ubyte.gz",
"train-labels-idx1-ubyte.gz",
transform=transforms.ToTensor())
testDataset = DataSet(dataPath,
"t10k-images-idx3-ubyte.gz",
"t10k-labels-idx1-ubyte.gz",
transform=transforms.ToTensor())
return trainDataset, testDataset
def gnloader(loadbatsize):
trainDataset, testDataset=loadDataset()
# 訓練數據和測試數據的裝載
train_loader = Data.DataLoader(
dataset=trainDataset,
batch_size=loadbatsize,
shuffle=True
)
test_loader = Data.DataLoader(
dataset=testDataset,
batch_size=loadbatsize,
shuffle=True,
)
return trainDataset, testDataset,train_loader,test_loader
def loadMNIST(batch_size): # MNIST圖片的大小是28*28
return gnloader(batch_size)
class Trainer:
def __init__(self):
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.loss_fn = nn.BCELoss()
def train(self):
if not os.path.exists("./dcgan_img"):
os.mkdir("./dcgan_img")
if not os.path.exists("./dcgan_params"):
os.mkdir("./dcgan_params")
BATCH_SIZE = 100
NUM_EPOCHS = 150
trainset, testset, train_loader, test_loader = loadMNIST(BATCH_SIZE)
d_net = D_Net().to(self.device)
g_net = G_Net().to(self.device)
d_opt = torch.optim.Adam(d_net.parameters(),lr=0.001,betas=(0.5,0.999))
g_opt = torch.optim.Adam(g_net.parameters(),lr=0.001,betas=(0.5,0.999))
for epochs in range(NUM_EPOCHS):
for i,(x,y) in enumerate(train_loader):
N = x.size(0)
real_img = x.to(self.device)
fake_label = torch.zeros(N,1,1,1).to(self.device)
real_label = torch.ones(N,1,1,1).to(self.device)
real_out = d_net(real_img)
d_real_loss = self.loss_fn(real_out,real_label)
z = torch.randn(N,128,1,1).to(self.device)
fake_img = g_net(z)
fake_out = d_net(fake_img)
d_fake_loss = self.loss_fn(fake_out,fake_label)
d_loss = d_fake_loss+d_real_loss
d_opt.zero_grad()
d_loss.backward()
d_opt.step()
z = torch.randn(N,128,1,1).to(self.device)
fake_img = g_net(z)
fake_out = d_net(fake_img)
g_loss = self.loss_fn(fake_out,real_label)
g_opt.zero_grad()
g_loss.backward()
g_opt.step()
if i%50 == 0:
print("epochs:{}/{},d_loss:{:.3f},,g_loss:{:.3f},"
"d_real:{:.3f},d_fake:{:.3f}".format(
epochs,NUM_EPOCHS,d_loss.item(),g_loss.item(),
real_out.data.mean(), fake_out.data.mean()))
real_image = real_img.cpu().data
save_image(real_image, "./dcgan_img/epoh{0}-iteration{1}-real_img.jpg".
format(epochs ,i), nrow=10, normalize=True, scale_each=True)
fake_image = fake_img.cpu().data
save_image(fake_image, "./dcgan_img/epoh{0}-iteration{1}-fake_img.jpg".
format(epochs,i), nrow=10, normalize=True, scale_each=True)
if __name__ == '__main__':
t = Trainer()
t.train()
DCGAN一個難點在於參數的設定,上面的代碼如果不使用GPU,神經網絡訓練收斂很慢。DCGAN的訓練過程與GAN沒有區別,重點看DCGAN的生成器與判別器,判別器的輸入是28*28的圖片
nn.Conv2d(1, 128, 5, 2, 2)
卷積核大小為5,步長為2,padding層數為2,由公式⑤可得輸出為

按此規則向下分析,判別器最終輸出是一個1維實數,通過sigmod函數將此實數變為0到1之間小數。
生成器過程與判別器執行路徑倒序對應,代表着利用反卷積將一維數據逐步還原為28*28圖片大小的過程,生成器第一行代碼是一個反卷積:
nn.ConvTranspose2d(128, 512, 3, 1, 0)
這行代碼對應判別器代碼的最后一行,是核大小k=3,步長s=1,p=0的卷積過程:
nn.Conv2d(512, 1, 3, 1, 0)
通過換算表1可以得到反卷積輸出尺寸:

生成器第二次反卷積代碼:
nn.ConvTranspose2d(512, 256, 5, 2, 1),
對應於判別器卷積過程,這是輸入i=7,核大小k=5,步長為s=2,p=1的卷積過程:
nn.Conv2d(256, 512, 5, 2, 1)
步長大於1時,先計算i+2p-k是否能被步長s整除,i+2p-k=7+2-5=4,顯然能被步長2整除,反卷積輸出要使用換算表2
余下文章請轉至鏈接:對抗生成網絡
