iT邦幫忙

2022 iThome 鐵人賽

DAY 15
0
AI & Data

新手也能懂得AI-深入淺出的AI課程系列 第 15

【day15】預測Hololive七期生的樣貌-生成式對抗網路(Generative Adversarial Network)(下)

  • 分享至 

  • xImage
  •  

預測Hololive七期生的樣貌

昨天說到GAN是依靠生成器與辨識器不斷交互訓練的方式來產生圖片,所以我們可以建立一個CNN模型稍加修更一下,就能夠建構一個GAN的神經網路,這種網路的名稱叫做DCGAN(Deep Convolutional Generative Adversarial Networks)。

今天的目錄如下:

  • 1.利用opencv辨識臉部
  • 2.建立初始環境
  • 3.建立判別器(Discriminator)
  • 4.建立生成器(Generator)
  • 5.訓練模型

利用opencv辨識臉部

由於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)

建立判別器(Discriminator)

接下來為了要判別圖片是生成器創作的還是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()  
        )

建立生成器(Generator)

建立生成器前,我們要知道資料是有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多次出來的結果
https://ithelp.ithome.com.tw/upload/images/20220919/20152236pBxX42V0Eh.jpg
可以看到人物的輪廓與色彩都已經出來了,以一個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


上一篇
【day14】預測Hololive七期生的樣貌-生成式對抗網路(Generative Adversarial Network)(上)
下一篇
【day16】NLP的首選模型Transformer介紹
系列文
新手也能懂得AI-深入淺出的AI課程30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言