昨天說到GAN是依靠生成器與辨識器不斷交互訓練的方式來產生圖片,所以我們可以建立一個CNN模型稍加修更一下,就能夠建構一個GAN的神經網路,這種網路的名稱叫做DCGAN(Deep Convolutional Generative Adversarial Networks)。
今天的目錄如下:
由於pixiv中有許多不同的畫風,要讓機器學習沒有統一性的資料,訓練時間就會相當的久,甚至無法訓練成功,所以這次我使用了這篇文章的方式,利用opencv擷取角色的臉部,來減少一些無意義的圖像,這方式與我們在臉部辨識時的作法相同,只需要更換XML檔案。
首先我們先到這裡來下載XML檔案,之後利用OPENCV來建立角色頭像資料集,這邊在前面有說過怎麼做了就直接丟程式碼與註解快速帶過
import cv2
import os
#動漫人臉檢測
cascade = cv2.CascadeClassifier('lbpcascade_animeface.xml')
#找到檔案名稱
for i in os.listdir('data'):
#讀取
image = cv2.imread('data/'+i, cv2.IMREAD_COLOR)
#轉成灰階
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.equalizeHist(gray)
#使用辨識器
faces = cascade.detectMultiScale(gray,scaleFactor = 1.1,minNeighbors = 5,minSize = (32, 32))
#有東西才執行
if len(faces) > 0:
#檢測不只會有一張人臉
for cnt,(x, y, w, h) in enumerate(faces):
face = image[y: y+h, x:x+w, :]
#我們要輸入的圖片大小(Lento採用的是96*96)
face = cv2.resize(face,(96,96))
#儲存
cv2.imwrite(f"faces/{i}_{cnt}.jpg",face)
首先我們今天的資料夾結構是這個樣子
main.py
├─holo(資料夾)
│ └─train(訓練圖片)
├─model(資料夾)
└─pic(輸出影像資料夾)
之後我們開始導入函式庫與建立資料集
導入函式庫
import os
import torchvision as tv
import torch as t
import torch.nn as nn
from tqdm import tqdm
建立資料集
transforms = tv.transforms.Compose([
tv.transforms.Resize(96),
tv.transforms.CenterCrop(96),
tv.transforms.ToTensor(),
tv.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
dataset = tv.datasets.ImageFolder('holo', transform = transforms)
dataloader = t.utils.data.DataLoader(dataset,batch_size = 128, shuffle=True,num_workers = 0,drop_last=True)
接下來為了要判別圖片是生成器創作的還是pixiv爬蟲取得的,所以在DGCNN中是使用變種的CNN的方式來辨別圖像,首先先移除全連結層,再來maxpooling層都更換成BatchNorm2d(將圖片歸一化),因為我們不需要強化特徵,而是保有圖片本身,在這邊為了方便創建網路可以使用Sequential來快速創建
ndf = 64
self.main = nn.Sequential(
# 3 x 96 x 96
nn.Conv2d(3, ndf, 5, 3, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# (ndf) x 32 x 32
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
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
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
nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
建立生成器前,我們要知道資料是有label還是沒有label的,例如我們想生成特定髮色與眼睛顏色,那就必須在建構資料集時定義每個圖像的髮色與眼睛顏色,但這個工程非常的浩大。所以今天只用一種比較簡單的方式,就是直接產生一個隨機的數值當作我們的輸入,這樣子就能夠產生圖片了。
train_noises = t.randn(128, 100, 1, 1).cuda()
在判別器我們是使用CNN將圖片從(batch_size,3,96,96)慢慢的變小變成(batch_size,1, 1, 1),所以我們要在生成器做一個逆向的動作,將一個(batch_size,輸入資料,1,1)放大到(3x96x96)。
剛剛的判別器輸入=3 輸出=64,之後以倍數增長,一直到輸出變成64x8時才會停止。所以生成器的輸入需要從64x8開始,以倍數遞減。
ngf = 64
self.main = nn.Sequential(
nn.ConvTranspose2d(100, ngf * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
# (ngf*8) x 4 x 4
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
# (ngf*4) x 8 x 8
nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
# (ngf*2) x 16 x 16
nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(True),
# (ngf) x 32 x 32
nn.ConvTranspose2d(ngf, 3, 5, 3, 1, bias=False),
nn.Tanh()
# 3 x 96 x 96
)
GAN的訓練可以說是最重要的事情,在這裡我們要經過無數的測試,查看最適合這張圖片的loss值(或動態調整),每個人控制的方法可能會不太一樣可能是調整學習率,或是控制訓練次數。但在本質上只有一個,就是控制好生成器與辨識器的Loss值(通常其中一個上升另一個就會下降)。
不過在訓練前我們先定義一下、真圖片標籤、假圖標籤、與我們的輸入(noize)
#訓練生成器與辨識器的label 結果為128個1(希望生成器的結果是1)
fake_labels = t.ones(128).cuda()
#訓練辨識器的label 結果為0
true_labels = t.zeros(128).cuda()
#亂數產生訓練noize
train_noises = t.randn(128, 100, 1, 1).cuda()
接下來定義loss function與優化器、學習率。在這裡使用的loss function是BCELoss,因為BCELoss的輸出會包含所有輸入分類的loss值(保有更多的資料)。
model_G = Generator().cuda()
model_D = Discriminator().cuda()
criterion = t.nn.BCELoss().cuda()
optimizer_g = t.optim.Adam(model_G.parameters(),1e-4)
optimizer_d = t.optim.Adam(model_D.parameters(),1e-5)
之後就來看一下GAN該怎麼定義訓練方式吧,首先是判別器的訓練,需要判別一次真圖片與假圖片當作一個結果,這裡比較需要注意事情,是我們在訓練判別器時,需要使用生成器產生圖片,但在做這個動作時,生成器多做了一次計算,所以我們要避免這個問題,我們可以使用model.eval()或是detach()的方式來解決。
##真實圖片訓練方式
#判別器梯度歸0
optimizer_d.zero_grad()
#將真實圖片交給判別器判斷
output = model_D(real_img)
#利用計算真圖片loss
r_loss_d = criterion(output, true_labels)
#反向傳播
r_loss_d.backward()
#禁止生成器反向傳播(因為我們在訓練的是判別器而不是生成器)
fake_img = model_G(train_noises).detach()
#利用生成器產生的圖片判別結果
output = model_D(fake_img)
#計算假圖片loss
f_loss_d = criterion(output, fake_labels)
#反向傳播
f_loss_d.backward()
#這時才將兩個loss傳給優化器運算
optimizer_d.step()
all_loss_d+=f_loss_d.item()+r_loss_d.item()
生成器的訓練方式就與之前相同了,同樣的需要禁止判別器反向傳播
#生成器梯度歸0
optimizer_g.zero_grad()
#創造假圖片
fake_img = model_G(train_noises)
#交給判別器判別
output = model_D(fake_img)
#計算loss(這裡要判斷是true因為我們希望生成器是生成真的圖片)
loss_g = criterion(output, true_labels)
loss_g.backward()
#傳送給優化器
optimizer_g.step()
all_loss_g+=loss_g.item()
之後我們可以控通過控制 cnt的次數來調整兩者之間的loss值,就可以了
for epoch in range(20000):
all_loss_d = 0
all_loss_g = 0
tq = tqdm(dataloader)
for cnt, (img, _) in enumerate(tq,1):
real_img = img.cuda()
if cnt%1 ==0:
optimizer_d.zero_grad()
output = model_D(real_img)
r_loss_d = criterion(output, true_labels)
r_loss_d.backward()
fake_img = model_G(train_noises).detach()
output = model_D(fake_img)
f_loss_d = criterion(output, fake_labels)
f_loss_d.backward()
optimizer_d.step()
all_loss_d+=f_loss_d.item()+r_loss_d.item()
if cnt % 2 == 0:
optimizer_g.zero_grad()
fake_img = model_G(train_noises)
output = model_D(fake_img)
loss_g = criterion(output, true_labels)
loss_g.backward()
optimizer_g.step()
all_loss_g+=loss_g.item()
tq.set_description(f'Train Epoch {epoch}')
tq.set_postfix({'D_Loss':float(all_loss_d/cnt),'G_loss':float(all_loss_g/cnt*5)})
fix_fake_imgs = model_G(train_noises).detach()
tv.utils.save_image(fix_fake_imgs,f'pic/{epoch}.jpg')
t.save(model_D.state_dict(), f'model/model_D_{epoch}.pth')
t.save(model_G.state_dict(), f'model/model_G_{epoch}.pth')
接下來看看我使用2000多張的hololive二創圖片訓練1600多次出來的結果
可以看到人物的輪廓與色彩都已經出來了,以一個2000多張的人臉照片來說,我認為效果還算不錯,而且我並沒有手動處理任何的圖像資料,導致訓練樣本裡面有根本不是人臉的圖片,這樣子也影響了些訓練效果,若要更好的效果可以增加圖片量與手動過濾一些圖片。
本來是想把結果跑完,但是電腦已經快要撐不住了...
import os
import torchvision as tv
import torch as t
import torch.nn as nn
from tqdm import tqdm
class Discriminator(nn.Module):
def __init__(self):
super().__init__()
ndf = 64
self.main = nn.Sequential(
# 3 x 96 x 96
nn.Conv2d(3, ndf, 5, 3, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# (ndf) x 32 x 32
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
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
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
nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
def forward(self, x):
x = self.main(x)
x = x.view(-1)
return x
class Generator(nn.Module):
def __init__(self):
super().__init__()
ngf = 64
self.main = nn.Sequential(
nn.ConvTranspose2d(100, ngf * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(True),
# (ngf*8) x 4 x 4
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(True),
# (ngf*4) x 8 x 8
nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(True),
# (ngf*2) x 16 x 16
nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(True),
# 上(ngf) x 32 x 32
nn.ConvTranspose2d(ngf, 3, 5, 3, 1, bias=False),
nn.Tanh()
# 3 x 96 x 96
)
def forward(self, x):
x = self.main(x)
return x
transforms = tv.transforms.Compose([
tv.transforms.Resize(96),
tv.transforms.CenterCrop(96),
tv.transforms.ToTensor(),
tv.transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
dataset = tv.datasets.ImageFolder('holo', transform = transforms)
dataloader = t.utils.data.DataLoader(dataset,batch_size = 128, shuffle=True,num_workers = 0,drop_last=True)
model_G = Generator().cuda()
model_D = Discriminator().cuda()
optimizer_g = t.optim.Adam(model_G.parameters(), 1e-4)
optimizer_d = t.optim.Adam(model_D.parameters(), 1e-5)
criterion = t.nn.BCELoss().cuda()
fake_labels = t.ones(128).cuda()
true_labels = t.zeros(128).cuda()
test_noises = t.randn(128, 100, 1, 1).cuda()
train_noises = t.randn(128, 100, 1, 1).cuda()
for epoch in range(20000):
all_loss_d = 0
all_loss_g = 0
tq = tqdm(dataloader)
for cnt, (img, _) in enumerate(tq,1):
real_img = img.cuda()
if cnt % 1 ==0:
optimizer_d.zero_grad()
output = model_D(real_img)
r_loss_d = criterion(output, true_labels)
r_loss_d.backward()
fake_img = model_G(train_noises).detach()
output = model_D(fake_img)
f_loss_d = criterion(output, fake_labels)
f_loss_d.backward()
optimizer_d.step()
all_loss_d+=f_loss_d.item()+r_loss_d.item()
if cnt % 2 == 0:
optimizer_g.zero_grad()
fake_img = model_G(train_noises)
output = model_D(fake_img)
loss_g = criterion(output, true_labels)
loss_g.backward()
optimizer_g.step()
all_loss_g+=loss_g.item()
tq.set_description(f'Train Epoch {epoch}')
tq.set_postfix({'D_Loss':float(all_loss_d/cnt),'G_loss':float(all_loss_g/cnt*2)})
fix_fake_imgs = model_G(train_noises).detach()
tv.utils.save_image(fix_fake_imgs,f'pic/{epoch}.jpg')
if epoch %10==0:
t.save(model_D.state_dict(), f'model/model_D_{epoch}.pth')
t.save(model_G.state_dict(), f'model/model_G_{epoch}.pth'
課程中的程式碼都能從我的github專案中看到
https://github.com/AUSTIN2526/learn-AI-in-30-days