深度學習框架PyTorch一書的學習-第七章-生成對抗網絡(GAN)


參考: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為:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM