參考:https://github.com/chenyuntc/pytorch-book/tree/v1.0/chapter7-GAN生成動漫頭像
GAN解決了非監督學習中的著名問題:給定一批樣本,訓練一個系統能夠生成類似的新樣本
生成對抗網絡的網絡結構如下圖所示:
- 生成器(generator):輸入一個隨機噪聲,生成一張圖片
- 判別器(discriminator):判斷輸入的圖片是真圖片還是假圖片
訓練判別器D時,需要利用生成器G生成的假圖片和來自現實世界的真圖片;訓練生成器時,只需要使用噪聲生成假圖片
判別器用來評估生成的假圖片的質量,促使生成器相應地調整參數
生成器的目標是盡可能地生成以假亂真的圖片,讓判別器以為這是真的圖片;判別器的目標是將生成器生成的圖片和真實世界的圖片區分開
可以看出這兩者的目標相反,在訓練過程中相互對抗,這也是它被稱為生成對抗網絡的原因
一開始,生成器和判別器的水平都很差,因為兩者都是隨機初始化的。訓練的步驟分兩步交替進行:
- 第一步是訓練判別器D(只修改判別器的參數,固定生成器),目標是把真圖片和假圖片區分開
- 第二步是訓練生成器(只修改生成器的參數,固定判別器),為的是生成的假圖片能夠被判別器判別為真圖片
這兩步交替進行,為的是生成的假圖片能夠被判別為真圖片
1.網絡結構的設計
判別器的目標是判斷輸入的圖片是真圖片還是假圖片,所以可以被看作是二分類網絡
生成器的目標是從噪聲中生成一張彩色圖片
這里我們采用的是廣泛使用的DCGAN(Deep Convolutional Generative Adversarial Networks)結構,即采用全卷積網絡,如圖所示:
網絡的輸入是一個100維的噪聲,輸出是一個3*64*64的圖片
這里的輸入可以看成是一個100*1*1的圖片,通過上卷積慢慢增大為4*4、8*8、16*16、32*32和64*64。當上卷集的stride=2時,輸出會上采樣到輸入的兩倍
這種上采樣的做法可以理解為圖片的信息保存於100個向量之中,然后神經網絡會根據這100個向量描述的信息,前幾步的上采樣先勾勒出輪廓、色調等基礎信息,后幾步上采樣慢慢完善細節。網絡越深,細節越詳細
在DCGAN中,判別器的結構和生成器對稱:生成器中采用上采樣的卷積,判別器中就采用下采樣的卷積。
生成器是根據噪聲輸出一張64*64*3的圖片,而判別器則是根據輸出的64*64*3的圖片輸出圖片屬於正負樣本的分數(即概率)
2.用GAN生成動漫頭像
從https://pan.baidu.com/s/1eSifHcA 提取碼:g5qa下載數據(275M,約5萬多張圖片)
把所有圖片保存於data/face/目錄下,形如:
data/ └── faces/ ├── 0000fdee4208b8b7e12074c920bc6166-0.jpg ├── 0001a0fca4e9d2193afea712421693be-0.jpg ├── 0001d9ed32d932d298e1ff9cc5b7a2ab-0.jpg ├── 0001d9ed32d932d298e1ff9cc5b7a2ab-1.jpg ├── 00028d3882ec183e0f55ff29827527d3-0.jpg ├── 00028d3882ec183e0f55ff29827527d3-1.jpg ├── 000333906d04217408bb0d501f298448-0.jpg ├── 0005027ac1dcc32835a37be806f226cb-0.jpg
即data目錄下只有一個文件夾,文件夾中有所有的圖片
注意這里圖片的分辨率是3*96*96,而不是3*64*64,所以需要相應地調整網絡結構,使生成圖像的尺寸為96
1)實驗的代碼結構
checkpoints/ #無代碼,用來保存訓練好的模型 imgs/ #無代碼,用來保存生成的圖片 data/ #無代碼,用來保存訓練所需的圖片 main.py #訓練和生成代碼 model.py #模型定義代碼 visualize.py #可視化工具visdom的封裝代碼 requirements.txt #程序中用到的第三方庫 README.MD #說明文檔
1》model.py
定義生成器和判別器
判別器:
class NetD(nn.Module): """ 判別器定義 """ def __init__(self, opt): super(NetD, self).__init__() ndf = opt.ndf #判別器channel值 self.main = nn.Sequential( # 輸入 3 x 96 x 96 # kernel_size = 5,stride = 3, padding =1 # 按式子計算 floor((96 + 2*1 - 1*(5-1) - 1)/3 + 1) = 32 # 是same卷積,96/32 = stride = 3 nn.Conv2d(3, ndf, 5, 3, 1, bias=False), nn.LeakyReLU(0.2, inplace=True), # 輸出 (ndf) x 32 x 32 #kernel_size = 4,stride = 2, padding =1 #按式子計算 floor((32 + 2*1 - 1*(4-1) - 1)/2 + 1) = 16 #是same卷積,32/16 = stride = 2 nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 2), nn.LeakyReLU(0.2, inplace=True), # 輸出 (ndf*2) x 16 x 16 #kernel_size = 4,stride = 2, padding =1 #按式子計算 floor((16 + 2*1 - 1*(4-1) - 1)/2 + 1) = 8 #是same卷積,16/8 = stride = 2 nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 4), nn.LeakyReLU(0.2, inplace=True), # 輸出 (ndf*4) x 8 x 8 #kernel_size = 4,stride = 2, padding =1 #按式子計算 floor((8 + 2*1 - 1*(4-1) - 1)/2 + 1) = 4 #是same卷積,8/4 = stride = 2 nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf * 8), nn.LeakyReLU(0.2, inplace=True), # 輸出 (ndf*8) x 4 x 4 #kernel_size = 4,stride = 1, padding =0 #按式子計算 floor((4 + 2*0 - 1*(4-1) - 1)/1 + 1) = 1 nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False), #輸出為1*1*1 nn.Sigmoid() # 返回[0,1]的值,輸出一個數(作為概率值) ) def forward(self, input): return self.main(input).view(-1) #輸出從1*1*1變為1,得到生成器生成假圖片的分數,分數高則像真圖片
生成器:
class NetG(nn.Module): """ 生成器定義 """ def __init__(self, opt): super(NetG, self).__init__() ngf = opt.ngf # 生成器feature map數channnel,默認為64 self.main = nn.Sequential( # 輸入是一個nz維度(默認為100)的噪聲,我們可以認為它是一個1*1*nz的feature map # kernel_size = 4,stride = 1, padding =0 # 根據計算式子 (1-1)*1 - 2*0 + 4 + 0 = 4 nn.ConvTranspose2d(opt.nz, ngf * 8, 4, 1, 0, bias=False), nn.BatchNorm2d(ngf * 8), nn.ReLU(True), # 上一步的輸出形狀:(ngf*8) x 4 x 4 #kernel_size = 4,stride = 2, padding =1 #根據計算式子 (4-1)*2 - 2*1 + 4 + 0 = 8 nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 4), nn.ReLU(True), # 上一步的輸出形狀: (ngf*4) x 8 x 8 #kernel_size = 4,stride = 2, padding =1 #根據計算式子 (8-1)*2 - 2*1 + 4 + 0 = 16 nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf * 2), nn.ReLU(True), # 上一步的輸出形狀: (ngf*2) x 16 x 16 #kernel_size = 4,stride = 2, padding =1 #根據計算式子 (16-1)*2 - 2*1 + 4 + 0 = 32 nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf), nn.ReLU(True), # 上一步的輸出形狀:(ngf) x 32 x 32 # kernel_size = 5,stride = 3, padding =1 #根據計算式子 (32-1)*3 - 2*1 + 5 + 0 = 96 nn.ConvTranspose2d(ngf, 3, 5, 3, 1, bias=False), nn.Tanh() # 輸出范圍 -1~1 故而采用Tanh # 輸出形狀:3 x 96 x 96 ) def forward(self, input): return self.main(input)
可以看出判別器和生成器的網絡結構幾乎是對稱的,從卷積核大小kernel_size到padding、stride等設置,幾乎是一模一樣。例如生成器的最后一個卷積層的尺度是(5,3,1),判別器的第一個卷積層的尺度也是(5,3,1)
再這里可見生成器的激活函數使用的是ReLU(),而判別器使用的是LeakyReLU,二者並沒有本質的區別,這里的選擇不同更多是經驗總結導致的
每一個樣本經過判別器后,輸出一個0~1的數,表示這個樣本是真圖片的概率
2》main.py
配置參數信息:
class Config(object): data_path = 'data/' # 數據集存放路徑 num_workers = 4 # 多進程加載數據所用的進程數 image_size = 96 # 圖片尺寸 batch_size = 256 max_epoch = 200 lr1 = 2e-4 # 生成器的學習率 lr2 = 2e-4 # 判別器的學習率 beta1 = 0.5 # Adam優化器的beta1參數 gpu = True # 是否使用GPU nz = 100 # 噪聲維度 ngf = 64 # 生成器feature map數 ndf = 64 # 判別器feature map數 save_path = 'imgs/' # 生成圖片保存路徑 vis = True # 是否使用visdom可視化 env = 'GAN' # visdom的env plot_every = 20 # 每間隔20 batch,visdom畫圖一次 debug_file = '/tmp/debuggan' # 存在該文件則進入debug模式 d_every = 1 # 每1個batch訓練一次判別器 g_every = 5 # 每5個batch訓練一次生成器 save_every = 10 # 沒10個epoch保存一次模型 netd_path = None # 'checkpoints/netd_.pth' #預訓練模型 netg_path = None # 'checkpoints/netg_211.pth' # 只測試不訓練 gen_img = 'result.png' # 從512張生成的圖片中保存最好的64張 gen_num = 64 gen_search_num = 512 gen_mean = 0 # 噪聲的均值 gen_std = 1 # 噪聲的方差 opt = Config()
這些是模型的默認參數,還可以利用Fire等工具通過命令行傳入,覆蓋默認值。可以用opt.attr的方式來指定使用的參數
這里的參數設置大多是照搬DCGAN論文的默認值,作者經過大量的實驗,發現這些參數能夠更快地訓練出一個不錯的模型
數據處理:
使用torchvision.ImageFolder函數讀取data/faces中的圖片,不必自己寫Dataset
數據讀取和加載的代碼為:
# 數據 transforms = tv.transforms.Compose([ tv.transforms.Resize(opt.image_size), #重新設置圖片大小,opt.image_size默認值為96 tv.transforms.CenterCrop(opt.image_size), #從中心截取大小為opt.image_size的圖片 tv.transforms.ToTensor(), #轉為Tensor格式,並將值取在[0,1]中 tv.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) #標准化,得到在[-1,1]的值 ]) dataset = tv.datasets.ImageFolder(opt.data_path, transform=transforms) #從data中讀取圖片,圖片類別會設置為文件夾名faces dataloader = t.utils.data.DataLoader(dataset, #然后對得到的圖片進行批處理,默認一批為256張圖,使用4個進程讀取數據 batch_size=opt.batch_size, shuffle=True, num_workers=opt.num_workers, drop_last=True )
定義變量:模型,優化器,噪聲
# 網絡,netg為生成器,netd為判別器 netg, netd = NetG(opt), NetD(opt) # 把所有的張量加載到CPU中 map_location = lambda storage, loc: storage # 把所有的張量加載到GPU 1中 #torch.load('tensors.pt', map_location=lambda storage, loc: storage.cuda(1)) #也可以寫成: #device = torch.device('cpu') #netd.load_state_dict(t.load(opt.netd_path, map_location=device)) #或: #netd.load_state_dict(t.load(opt.netd_path)) #netd.to(device) if opt.netd_path: #是否指定訓練好的預訓練模型,加載模型參數 netd.load_state_dict(t.load(opt.netd_path, map_location=map_location)) if opt.netg_path: netg.load_state_dict(t.load(opt.netg_path, map_location=map_location)) netd.to(device) netg.to(device) # 定義優化器和損失,學習率都默認為2e-4,beta1默認為0.5 optimizer_g = t.optim.Adam(netg.parameters(), opt.lr1, betas=(opt.beta1, 0.999)) optimizer_d = t.optim.Adam(netd.parameters(), opt.lr2, betas=(opt.beta1, 0.999)) criterion = t.nn.BCELoss().to(device) # 真圖片label為1,假圖片label為0 # noises為生成網絡的輸入 true_labels = t.ones(opt.batch_size).to(device) fake_labels = t.zeros(opt.batch_size).to(device) fix_noises = t.randn(opt.batch_size, opt.nz, 1, 1).to(device)#opt.nz為噪聲維度,默認為100 noises = t.randn(opt.batch_size, opt.nz, 1, 1).to(device) #AverageValueMeter測量並返回添加到其中的任何數字集合的平均值和標准差, #對度量一組示例的平均損失是有用的。 errord_meter = AverageValueMeter() errorg_meter = AverageValueMeter()
再加載預訓練模型時,最好指定map_location。因為如果程序之前在GPU上運行,那么模型就會被存為torch.cuda.Tensor,這樣加載時會默認將數據加載至顯存。如果運行該程序的計算機中沒有GPU,加載就會報錯,故通過指定map_location將Tensor默認加載入內存(CPU),待有需要再移至顯存
訓練網絡:
1)訓練判別器
- 先固定生成器
- 對於真圖片,判別器的輸出概率值盡可能接近1
- 對於生成器生成的假圖片,判別器盡可能輸出0
2)訓練生成器
- 固定判別器
- 生成器生成圖片,盡可能使生成的圖片讓判別器輸出為1
3)返回第一步,循環交替進行
epochs = range(opt.max_epoch) for epoch in iter(epochs): for ii, (img, _) in tqdm.tqdm(enumerate(dataloader)): real_img = img.to(device) if ii % opt.d_every == 0: # 訓練判別器 # 每d_every=1(默認)個batch訓練一次判別器 optimizer_d.zero_grad() ## 盡可能的把真圖片判別為正確 output = netd(real_img) error_d_real = criterion(output, true_labels) error_d_real.backward() ## 盡可能把假圖片判別為錯誤 #更新noises中的data值 noises.data.copy_(t.randn(opt.batch_size, opt.nz, 1, 1)) fake_img = netg(noises).detach() # 根據噪聲生成假圖 output = netd(fake_img) error_d_fake = criterion(output, fake_labels) error_d_fake.backward() optimizer_d.step() error_d = error_d_fake + error_d_real errord_meter.add(error_d.item()) if ii % opt.g_every == 0: # 訓練生成器 # 每g_every=5個batch訓練一次生成器 optimizer_g.zero_grad() #更新noises中的data值 noises.data.copy_(t.randn(opt.batch_size, opt.nz, 1, 1)) fake_img = netg(noises) output = netd(fake_img) error_g = criterion(output, true_labels) error_g.backward() optimizer_g.step() errorg_meter.add(error_g.item())
注意:
訓練生成器時,無須調整判別器的參數;訓練判別器時,無須調整生成器的參數
在訓練判別器時,需要對生成器生成的圖片用detach()操作進行計算圖截斷,避免反向傳播將梯度傳到生成器中。因為在訓練判別器時,我們不需要訓練生成器,也就不需要生成器的梯度
在訓練判別器時,需要反向傳播兩次,一次是希望把真圖片判定為1,一次是希望把假圖片判定為0.也可以將這兩者的數據放到一個batch中,進行一次前向傳播和反向傳播即可。但是人們發現,分兩次的方法更好
對於假圖片,在訓練判別器時,我們希望判別器輸出為0;而在訓練生成器時,我們希望判別器輸出為1,這樣實現判別器和生成器互相對抗提升
可視化:
接下來就是一些可視化代碼的實現。每次可視化時使用的噪音都是固定的fix_noises,因為這樣便於我們比較對於相同的輸入,可見生成器生成的圖片是如何一步步提升的
因為對輸出的圖片進行了歸一化處理,值在(-1,1),所以在輸出時需要將其還原會原來的scale,值在(0,1),方法就是圖片的值*mean + std
# 每間隔20 batch,visdom畫圖一次 if opt.vis and ii % opt.plot_every == opt.plot_every - 1: ## 可視化 ## 存在該文件則進入debug模式 if os.path.exists(opt.debug_file): ipdb.set_trace() fix_fake_imgs = netg(fix_noises) vis.images(fix_fake_imgs.detach().cpu().numpy()[:64] * 0.5 + 0.5, win='fixfake') vis.images(real_img.data.cpu().numpy()[:64] * 0.5 + 0.5, win='real') vis.plot('errord', errord_meter.value()[0]) vis.plot('errorg', errorg_meter.value()[0])
保存模型:
# 每10個epoch保存一次模型 if (epoch+1) % opt.save_every == 0: # 保存模型、圖片 tv.utils.save_image(fix_fake_imgs.data[:64], '%s/%s.png' % (opt.save_path, epoch), normalize=True, range=(-1, 1)) t.save(netd.state_dict(), 'checkpoints/netd_%s.pth' % epoch) t.save(netg.state_dict(), 'checkpoints/netg_%s.pth' % epoch) errord_meter.reset()#重置,清空里面的值 errorg_meter.reset()
驗證:
使用訓練好的模型進行驗證
@t.no_grad() def generate(**kwargs):#進行驗證 """ 隨機生成動漫頭像,並根據netd的分數選擇較好的 """ for k_, v_ in kwargs.items(): setattr(opt, k_, v_) device=t.device('cuda') if opt.gpu else t.device('cpu') netg, netd = NetG(opt).eval(), NetD(opt).eval() noises = t.randn(opt.gen_search_num, opt.nz, 1, 1).normal_(opt.gen_mean, opt.gen_std) noises = noises.to(device) map_location = lambda storage, loc: storage netd.load_state_dict(t.load(opt.netd_path, map_location=map_location)) netg.load_state_dict(t.load(opt.netg_path, map_location=map_location)) netd.to(device) netg.to(device) # 生成圖片,並計算圖片在判別器的分數 fake_img = netg(noises) scores = netd(fake_img).detach() # 挑選最好的某幾張,默認opt.gen_num=64張,並得到其索引 indexs = scores.topk(opt.gen_num)[1] result = [] for ii in indexs: result.append(fake_img.data[ii]) # 保存圖片 tv.utils.save_image(t.stack(result), opt.gen_img, normalize=True, range=(-1, 1))
2)開始訓練
使用gpu,並且visdom實現可視化
python main.py train --gpu=True --vis=True
進行了200次迭代,生成的圖片存儲在imgs文件夾中,第一次10輪迭代后生成的結果為:
20次迭代后的結果為:
一直到200次迭代的結果為,多訓練幾輪可能效果會更好:
在該基礎上又訓練了200輪:
python main.py train --netd-path=checkpoints/netd_199.pth --netg-path=checkpoints/netg_199.pth
得到的結果是:
3)驗證
使用最后一次迭代的到的訓練網絡進行驗證,生成器網絡為--netd-path=checkpoints/netd_199.pth,判別器網絡為--netg-path=checkpoints/netg_199.pth,會輸出結果最好的64張圖,並存儲在本地,命名為result.png:
(deeplearning) userdeMBP:DCGAN user$ python main.py generate --gpu=False --vis=False --netd-path=checkpoints/netd_199.pth --netg-path=checkpoints/netg_199.pth
得到的result.png為: