這是一個github的教程须喂,我用自己的理解復(fù)述一遍趟妥,方面記憶
一庇勃、關(guān)于生成對抗網(wǎng)絡(luò)的第一篇論文是Generative Adversarial Networks,這是2014年發(fā)表的一篇論文
二倒彰、隨機產(chǎn)生噪聲數(shù)據(jù)(至于為什么要使用噪聲作為輸入數(shù)據(jù)可以參考這里對抗生成網(wǎng)絡(luò)GAN為什么輸入隨機噪聲?), 通過生成器變成一個假數(shù)據(jù)(Fake Data)一般來說是圖片莱睁。
三待讳、生成器的目標(biāo)是盡力生成和目標(biāo)數(shù)據(jù)分布一致的假數(shù)據(jù), 而判別模型目標(biāo)是分辨真數(shù)據(jù)和假數(shù)據(jù), 抽象來說可以看下圖:
假設(shè)數(shù)據(jù)是一維數(shù)據(jù),如圖所示仰剿,上方的圖形(綠線创淡,虛線)表示數(shù)據(jù)的密度分布。綠線表示真實數(shù)據(jù)分布酥馍,大虛線表示GAN生成數(shù)據(jù)的分布辩昆,而小虛線表示一個判別函數(shù)(你可以近似看成Sigmoid)。
(a)圖中由于沒有迭代旨袒,導(dǎo)致判別函數(shù)只能大致判斷真數(shù)據(jù)和假數(shù)據(jù)汁针。
(b)圖中經(jīng)過訓(xùn)練,可以看出判別函數(shù)已經(jīng)可以大致判斷出真數(shù)據(jù)和假數(shù)據(jù)分布(你可以理解為>0.5表示假數(shù)據(jù),<0.5表示真數(shù)據(jù))砚尽。
(c)圖中施无,經(jīng)過不斷學(xué)習(xí),GAN生成的數(shù)據(jù)和真實分布已經(jīng)很接近了,這是判別函數(shù)已經(jīng)很難區(qū)分真實數(shù)據(jù)和假數(shù)據(jù)分布了必孤。
(d)最終猾骡,真數(shù)據(jù)和假數(shù)據(jù)分布一致,判別函數(shù)無法判斷(恒為0.5)敷搪,這是GAN生成的數(shù)據(jù)和真實數(shù)據(jù)分布已經(jīng)一致兴想,可以達(dá)到以假亂真。
1. 加載必要的包
這里使用的pytorch==1.3.0, torchvision==0.4.1
import os
import numpy as np
import torchvision.transforms as transforms # 對數(shù)據(jù)進(jìn)行轉(zhuǎn)化赡勘,歸一化等操作
from torchvision.utils import save_image
from torch.utils.data import DataLoader # 加載數(shù)據(jù)嫂便,變成一個類似迭代器的東西
from torchvision import datasets # 再帶的數(shù)據(jù)數(shù)據(jù)集,一般都是常用的數(shù)據(jù)集
import torch.nn as nn
import torch.nn.functional as F
import torch
2. 參數(shù)預(yù)設(shè)
這里使用一個字典opt保存所需變量
opt = {}
opt['n_epochs'] = 200 # 迭代次數(shù)
opt['batch_size'] = 64 # 每個batch的樣本數(shù)
opt['lr'] = 0.0002 # 優(yōu)化器adam的學(xué)習(xí)率
opt['b1'] = 0.5 # 優(yōu)化器adam的梯度的一階動量衰減 momentum
opt['b2'] = 0.999 # 優(yōu)化器adam的梯度的二階動量衰減 momentum
opt['latent_dim'] = 100 # latent(潛)空間的維數(shù), 可以理解為噪聲數(shù)據(jù)的維度
opt['img_size'] = 28 # 輸入數(shù)據(jù)是一個1*28*28的灰度圖片
opt['channels'] = 1 # RGB通道個數(shù),這里是1個通道的灰度圖
opt['sample_interval'] = 400 # 圖像采樣間隔(做記錄)
# 輸入圖片大小 1*28*28
img_shape = (opt['channels'], opt['img_size'], opt['img_size'])
# 如果GPU可以使用, 則先在這里立個flag
cuda = True if torch.cuda.is_available() else False
print(cuda)
# GPU可以使用的話使用GPU的FloatTensor
Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor
3.加載數(shù)據(jù)
這里使用torch自帶的手寫體數(shù)據(jù)集
# 輸出圖片(GAN生成假圖片文件夾)
os.makedirs("images", exist_ok=True)
# 訓(xùn)練數(shù)據(jù)文件夾
os.makedirs("mnist", exist_ok=True)
# 下載圖片數(shù)據(jù)
dataloader = torch.utils.data.DataLoader(
datasets.MNIST(
"mnist",
train=True,
download=True,
transform=transforms.Compose(
[transforms.Resize(opt['img_size']), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]
),
),
batch_size=opt['batch_size'], # 每次取出數(shù)據(jù)量
shuffle=True,
)
4.定義模型
# 生成器模型
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
# 簡化代碼,將Linear闸与,BatchNorm1d毙替, LeakyReLU裝在一起
def block(in_feat, out_feat, normalize=True):
layers = [nn.Linear(in_feat, out_feat)]
if normalize:
layers.append(nn.BatchNorm1d(out_feat, 0.8))
layers.append(nn.LeakyReLU(0.2, inplace=True)) # inplace一個原地操作
# 是對于Conv2d這樣的上層網(wǎng)絡(luò)傳遞下來的tensor直接進(jìn)行修改,好處就是可以節(jié)省運算內(nèi)存践樱,不用多儲存變量y
return layers
# 這里前面加*相當(dāng)于在Sequential中extend
self.model = nn.Sequential(
*block(opt['latent_dim'], 128, normalize=False),
*block(128,256),
*block(256,512),
*block(512, 1024),
nn.Linear(1024, int(np.prod(img_shape))), # np.prod攤開
nn.Tanh()
)
def forward(self, data):
img = self.model(data)
img = img.view(img.size(0), *img_shape) # 因為輸出數(shù)據(jù)應(yīng)該為一張圖片厂画,所以需要將Reshae變?yōu)閳D片(圖片數(shù), channel,長拷邢, 寬)
return img
# 定義判別模型
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.model = nn.Sequential(nn.Linear(int(np.prod(img_shape)), 512), # np.prod將圖片展開成一維向量
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 256),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(256, 1),
nn.Sigmoid(),
)
def forward(self, img):
img_flat = img.view(img.size(0), -1) # img.size(0)表示每個batch圖片數(shù)量
# 拉成 圖片1 維度相乘 1*28*28
# 圖片n 維度相乘
validity = self.model(img_flat)
return validity
需要說明的是block前面加*表示把block中的層安順序平鋪開,等價于下面的代碼:
self.model = nn.Sequential(
*block(opt['latent_dim'], 128),
*block(128,256),)
# 等價于下面的代碼
self.model = nn.Sequential(
# 這是一個block
nn.Linear(in_feat, 128)
layers.append(nn.BatchNorm1d(out_feat, 0.8))
layers.append(nn.LeakyReLU(0.2, inplace=True))
# 這是第二個block
nn.Linear(128, 256)
layers.append(nn.BatchNorm1d(256, 0.8))
layers.append(nn.LeakyReLU(0.2, inplace=True))
)
4.損失函數(shù)和優(yōu)化器
# 判別器損失函數(shù)
adversarial_loss = torch.nn.BCELoss()
# 初始化生成器和判別器
generator = Generator()
discriminator = Discriminator()
# 如果CPU可以用
if cuda:
generator.cuda()
discriminator.cuda()
adversarial_loss.cuda()
# 優(yōu)化器
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt['lr'], betas=(opt['b1'], opt['b2']))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt['lr'], betas=(opt['b1'], opt['b2']))
到此為止基本的模型定義就算完成了,我們現(xiàn)在有數(shù)據(jù)集dataloader(一個迭代器)跋理,generator生成器對象蔑匣, discriminator判別器對象鸦致,下面我們將訓(xùn)練模型
5.訓(xùn)練模型
# 訓(xùn)練模型
for epoch in range(opt['n_epochs']):
for i, (imgs, _) in enumerate(dataloader): # 這里不需要類標(biāo)簽
# Variable和Tensor在新版本中已經(jīng)合并
valid = Tensor(imgs.size(0), 1).fill_(1.0) # 真實圖片類標(biāo)簽設(shè)為1.0
valid.requires_grad=False # 不能更新梯度
fake = Tensor(imgs.size(0), 1).fill_(0.0) # 假圖片類標(biāo)簽設(shè)為0.0
fake.requires_grad=False
# 將數(shù)據(jù)轉(zhuǎn)成cuda的tensor,加速
# imgs.type() = 'torch.FloatTensor'
# real_imgs.type() = 'torch.cuda.FloatTensor'
real_imgs = imgs.type(Tensor)
optimizer_G.zero_grad() # 生成模型的優(yōu)化器梯度清零
# 訓(xùn)練生成器
# 產(chǎn)生噪聲(輸入)數(shù)據(jù) 64張100維噪聲
noise = Tensor(np.random.normal(0, 1, (imgs.shape[0], opt['latent_dim'])))
# 生成假圖片
fake_img = generator(noise)
# 更新生層模型
g_loss = adversarial_loss(discriminator(fake_img), valid)
g_loss.backward() # 返回梯度
optimizer_G.step() # 更新權(quán)重
optimizer_D.zero_grad() # 判別模型的優(yōu)化器梯度清零
# 訓(xùn)練判別器
real_loss = adversarial_loss(discriminator(real_imgs), valid) # 真實圖片的損失
# detach返回一個新的張量,它與當(dāng)前圖形分離抛人。fake_img結(jié)果永遠(yuǎn)不需要梯度
fake_loss = adversarial_loss(discriminator(fake_img.detach()), fake)
d_loss = (real_loss + fake_loss) / 2 # 去一個均值作為損失
d_loss.backward()
optimizer_D.step()
# 打印損失
if i%500 == 0:
print(
"[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]"
% (epoch, opt['n_epochs'], i, len(dataloader), d_loss.item(),
g_loss.item())
)
# 選擇前25個圖片保存
batches_done = epoch * len(dataloader) + i
if batches_done % opt['sample_interval'] == 0:
save_image(fake_img.data[:25], "images/%d.png" % batches_done,
nrow=5, normalize=True)
下面是訓(xùn)練第0次(epoch * len(dataloader) + i),10000次和100000次的結(jié)果,可以看出若持續(xù)訓(xùn)練脐瑰,結(jié)果會更加逼近真實數(shù)據(jù)
下面給出完整的代碼
import os
import math
import numpy as np
import argparse
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.nn.functional as F
import torch
opt = {}
opt['n_epochs'] = 200 # 迭代次數(shù)
opt['batch_size'] = 64
opt['lr'] = 0.0002 # adam: 學(xué)習(xí)率
opt['b1'] = 0.5 # adam: 梯度的一階動量衰減 momentum
opt['b2'] = 0.999 # adam: 梯度的一階動量衰減 momentum
opt['latent_dim'] = 100 # latent空間的維數(shù)
opt['img_size'] = 28
opt['channels'] = 1
opt['sample_interval'] = 400 # 圖像采樣間隔(做記錄)
# 輸入圖片大小
img_shape = (opt['channels'], opt['img_size'], opt['img_size'])
cuda = True if torch.cuda.is_available() else False
print(cuda)
os.makedirs("images", exist_ok=True)
os.makedirs("mnist", exist_ok=True)
# 下載圖片
dataloader = torch.utils.data.DataLoader(
datasets.MNIST(
"mnist",
train=True,
download=True,
transform=transforms.Compose(
[transforms.Resize(opt['img_size']), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]
),
),
batch_size=opt['batch_size'],
shuffle=True,
)
# 如果CPU可以用
if cuda:
generator.cuda()
discriminator.cuda()
adversarial_loss.cuda()
Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor
# 生成器模型
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
# 整個作為一層
def block(in_feat, out_feat, normalize=True):
layers = [nn.Linear(in_feat, out_feat)]
if normalize:
layers.append(nn.BatchNorm1d(out_feat, 0.8))
layers.append(nn.LeakyReLU(0.2, inplace=True)) # inplace一個原地操作
# 是對于Conv2d這樣的上層網(wǎng)絡(luò)傳遞下來的tensor直接進(jìn)行修改妖枚,好處就是可以節(jié)省運算內(nèi)存,不用多儲存變量y
return layers
# 這里前面加*相當(dāng)于在Sequential中extend
self.model = nn.Sequential(
*block(opt['latent_dim'], 128, normalize=False),
*block(128,256),
*block(256,512),
*block(512, 1024),
nn.Linear(1024, int(np.prod(img_shape))), # np.prod攤開
nn.Tanh()
)
def forward(self, data):
img = self.model(data)
img = img.view(img.size(0), *img_shape) # 將平鋪的數(shù)據(jù)變?yōu)閳D片(圖片數(shù), 長苍在, 寬绝页,channel)
return img
# 定義判別函數(shù)
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.model = nn.Sequential(nn.Linear(int(np.prod(img_shape)), 512),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 256),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(256, 1),
nn.Sigmoid(),
)
def forward(self, img):
img_flat = img.view(img.size(0), -1)
# 拉成 圖片1 維度相乘
# 圖片n 維度相乘
validity = self.model(img_flat)
return validity
# 判別器損失函數(shù)
adversarial_loss = torch.nn.BCELoss()
# 初始化生成器和判別器
generator = Generator()
discriminator = Discriminator()
# 優(yōu)化器
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt['lr'], betas=(opt['b1'], opt['b2']))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt['lr'], betas=(opt['b1'], opt['b2']))
# 訓(xùn)練模型
for epoch in range(opt['n_epochs']):
for i, (imgs, _) in enumerate(dataloader):
valid = Tensor(imgs.size(0), 1).fill_(1.0) # 真實圖片類標(biāo)簽
valid.requires_grad=False
fake = Tensor(imgs.size(0), 1).fill_(0.0) # 假圖片類標(biāo)簽
fake.requires_grad=False
# 將數(shù)據(jù)轉(zhuǎn)成cuda的tensor
real_imgs = imgs.type(Tensor)
# 訓(xùn)練生成器
optimizer_G.zero_grad() # 生成器梯度清零
# 產(chǎn)生噪聲數(shù)據(jù) 64張100維噪聲
noise = Tensor(np.random.normal(0, 1, (imgs.shape[0], opt['latent_dim'])))
# 生成噪聲圖片
fake_img = generator(noise)
# 更新生層器
g_loss = adversarial_loss(discriminator(fake_img), valid)
g_loss.backward()
optimizer_G.step()
# 訓(xùn)練判別器
optimizer_D.zero_grad()
real_loss = adversarial_loss(discriminator(real_imgs), valid)
# detach返回一個新的張量,它與當(dāng)前圖形分離寂恬。結(jié)果永遠(yuǎn)不需要梯度
fake_loss = adversarial_loss(discriminator(fake_img.detach()), fake)
d_loss = (real_loss + fake_loss) / 2
d_loss.backward()
optimizer_D.step()
if i%500 == 0:
print(
"[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]"
% (epoch, opt['n_epochs'], i, len(dataloader), d_loss.item(), g_loss.item())
)
batches_done = epoch * len(dataloader) + i
if batches_done % opt['sample_interval'] == 0:
save_image(fake_img.data[:25], "images/%d.png" % batches_done, nrow=5, normalize=True)