用pytorch编写的DCGAN生成动漫角色的头像 | 您所在的位置:网站首页 › 办公卡通头像 › 用pytorch编写的DCGAN生成动漫角色的头像 |
我以前听说过DCGAN,并且对此感兴趣。我最近开始学习pytorch,所以我想写DCGAN作为练习。 已经有很多使用DCGAN生成动漫角色头像的示例,但是我找不到用pytorch编写的示例,因此我决定自己编写。 首先,结果示例
基本上,我是用pytorch编写的,是指我之前编写的诸如mattya和rezoolab之类的代码。 用于参考的代码 https://qiita.com/rezoolab/items/5cc96b6d31153e0c86bc https://qiita.com/mattya/items/e5bfe5e04b9d2f0bbd47 https://qiita.com/triwave33/items/35b4adc9f5b41c5e8141 https://github.com/carpedm20/DCGAN-tensorflow https://github.com/chainer/chainer/tree/master/examples/dcgan https://github.com/pytorch/examples/tree/master/dcgan 在上面的链接中详细解释了该理论,因此在这里我将写有关如何用pytorch编写和结果可视化的文章。 实施理念 图像像素可以是64、80、96 可以在CPU和GPU上运行 每次训练后输出样本结果 始终保存参数,以便以后继续训练 保存优化器参数首先,我想知道被16整除的像素数量是否适用于任何大图像,而当我尝试以112px进行尝试时,我无法生成任何图片,这是一个很大的错误。毕竟64px,80px和96px似乎是最好的。但是,96px很容易失败,结果似乎不及80px。 生成器和鉴别器模型 生成器和鉴别器由这些层组成。 卷积卷积层 卷积反卷积层 林全连接层 BatchNorm批次正则化层 ReLU 吕鲁 h 乙状结肠这样从上到下排列。 生成器 鉴别器 Conv和ConvT之后的数字是:输入大小,输出大小,过滤器大小,步幅,填充 LIN之后的数字是:输入大小,输出大小 BatchNorm之后的数字是:输入和输出大小 这是使用80像素大小的图像时的模型。使用64像素和96像素时,结构相同,但是数字略有不同。 使用的模型图片 数据来自https://konachan.net和https://safebooru.org上的图像,并使用lbpcascade-animeface(https://github.com/nagadomi/lbpcascade_animeface)使用cv2进行人脸检测。 实作 tomodcgan.py 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191# -*- coding: UTF-8 -*- import numpy as np from skimage.io import imsave,imread import torch import time,sys,os,re from glob import glob from random import shuffle nn = torch.nn relu = nn.ReLU(True) lrelu = nn.LeakyReLU(0.2,True) sigmoid = nn.Sigmoid() tanh = nn.Tanh() entropy = nn.BCELoss() # 損失を計算する関数 # それぞれの層の定義 class Conv(nn.Conv2d): def __init__(self,*arg,**kw): if('bias' not in kw): kw['bias'] = False # ここでバイアスは使用しない super(Conv,self).__init__(*arg,**kw) self.weight.data.normal_(0,0.02) # 初期値の設定 class ConvT(nn.ConvTranspose2d): def __init__(self,*arg,**kw): if('bias' not in kw): kw['bias'] = False super(ConvT,self).__init__(*arg,**kw) self.weight.data.normal_(0,0.02) class Lin(nn.Linear): def __init__(self,*arg,**kw): if('bias' not in kw): kw['bias'] = False super(Lin,self).__init__(*arg,**kw) self.weight.data.normal_(0,0.02) class BN(nn.BatchNorm2d): def __init__(self,*arg,**kw): super(BN,self).__init__(*arg,**kw) self.weight.data.normal_(1,0.02) self.bias.data.fill_(0) # テンソルを変形するためのモジュール class Henkei(nn.Module): def __init__(self,*k): super(Henkei,self).__init__() self.k = k def forward(self,x): return x.view(x.size()[0],*self.k) # ジェネレータの定義 class Genera(nn.Sequential): def __init__(self,nz=100,c=3,d=64,p=4,kn=4,st=2,pd=1): d = [d*2**i for i in range(4)] super(Genera,self).__init__( Henkei(nz,1,1), ConvT(nz,d[3],p,1,0), BN(d[3]), relu, ConvT(d[3],d[2],kn,st,pd), BN(d[2]), relu, ConvT(d[2],d[1],kn,st,pd), BN(d[1]), relu, ConvT(d[1],d[0],kn,st,pd), BN(d[0]), relu, ConvT(d[0],c,kn,st,pd), tanh) # ディスクリミネータの定義 class Discrim(nn.Sequential): def __init__(self,c=3,d=64,p=4,kn=4,st=2,pd=1): d = [d*2**i for i in range(4)] super(Discrim,self).__init__( Conv(c,d[0],kn,st,pd), lrelu, Conv(d[0],d[1],kn,st,pd), BN(d[1]), lrelu, Conv(d[1],d[2],kn,st,pd), BN(d[2]), lrelu, Conv(d[2],d[3],kn,st,pd), BN(d[3]), lrelu, Henkei(d[3]*p*p), Lin(d[3]*p*p,1), Henkei(), nn.Sigmoid()) # DCGANの定義 class DCGAN: def __init__(self,nz,kk,eta=0.002,gpu=0): assert(kk%16==0) super(DCGAN,self).__init__() self.nz = nz # zの数 self.kk = kk # 画像のサイズ self.g = Genera(nz,p=int(kk/16)) self.g.opt = torch.optim.Adam(self.g.parameters(),lr=eta,betas=(0.5,0.999)) self.d = Discrim(p=int(kk/16)) self.d.opt = torch.optim.Adam(self.d.parameters(),lr=eta,betas=(0.5,0.999)) if(gpu): self.g.cuda() self.d.cuda() self.dev = torch.device('cuda') else: self.dev = torch.device('cpu') # 学習 def gakushuu(self,X,kurikaeshi=200,n_batch=64): if(not os.path.exists(outfolder)): os.mkdir(outfolder) param = sorted(glob(os.path.join(outfolder,'tomopara*.pkl'))) # 前に保存されたパラメータを探して、見つかったら読み出す if(param): param = param[-1] state_dict = torch.load(param) self.g.load_state_dict(state_dict[0]) self.d.load_state_dict(state_dict[1]) self.g.opt.load_state_dict(state_dict[2]) self.d.opt.load_state_dict(state_dict[3]) z0 = torch.load(os.path.join(outfolder,'z0.pkl')) hajime = int(re.search(r'(\d+)\.pkl',param)[1]) else: hajime = 0 z0 = (torch.rand(n_batch,self.nz)*2-1).to(self.dev) torch.save(z0,os.path.join(outfolder,'z0.pkl')) n = len(X) t0 = time.time() v0 = torch.zeros(n_batch).to(self.dev) v1 = torch.ones(n_batch).to(self.dev) for j in range(hajime,hajime+kurikaeshi): sentaku = np.random.permutation(n) # ミニバッチのランダム選択 for i in range(0,n,n_batch): X_mohan = X[sentaku[i:i+n_batch]] X_mohan = [imread(img) for img in X_mohan] # 模範の画像を読み出す X_mohan = np.stack(X_mohan) X_mohan = X_mohan/127.5-1. X_mohan = X_mohan.transpose(0,3,1,2) X_mohan = torch.FloatTensor(X_mohan).to(self.dev) # テンソルに変換 nx = len(X_mohan) z = (torch.rand(nx,self.nz)*2-1).to(self.dev) X_seisei = self.g(z) # ジェネレーターで画像を生成する hyouka_seisei = self.d(X_seisei) # ディスクリミネータで生成された画像を評価する hyouka_mohan = self.d(X_mohan) # 模範の画像も評価する # 損失を計算して逆伝播 self.d.opt.zero_grad() loss_d_mohan = entropy(hyouka_mohan,v1[:nx]) loss_d_mohan.backward() loss_d_seisei = entropy(hyouka_seisei,v0[:nx]) loss_d_seisei.backward(retain_graph=True) # 後でまた逆伝播を再利用するためにretain_graphは必須 self.d.opt.step() self.g.opt.zero_grad() loss_g = entropy(hyouka_seisei,v1[:nx]) loss_g.backward() # retain_graphをしておいていなかったらここでエラーが出る self.g.opt.step() # サンプル画像を生成して書き込む if(i+n_batch>=n or i/n_batch%100==99): gazou = np.hstack(np.hstack(self.g(z0).data.cpu().numpy().transpose(0,2,3,1).reshape(8,8,self.kk,self.kk,3)))/2.+0.5 if(i/n_batch%100==99): namae = os.path.join(outfolder,'pts%03d_%06d.jpg'%(j,i+nx)) else: namae = os.path.join(outfolder,'pts%03d.jpg'%(j+1)) imsave(namae,gazou) print('%d/%d 第%d回 %.3f分過ぎた'%(i+nx,n,j+1,(time.time()-t0)/60)) # 毎回進度発表 state_dict = [self.g.state_dict(),self.d.state_dict(),self.g.opt.state_dict(),self.d.opt.state_dict()] torch.save(state_dict,os.path.join(outfolder,'tomopara%03d.pkl'%(j+1))) # パラメータを保存 if(__name__=='__main__'): nz = 100 # zの数 lr = 0.0002 # 学習率 n_batch = 64 # ミニバッチの数 kurikaeshi = 151 # 何回繰り返す pixel = 80 # 画像の大きさ gpu = 1 # GPUを使うかどうか datafolder = 'imgdata' # 模範の画像のフォルダ outfolder = 'dcganimg' # 生成の画像とパラメータを保存するフォルダ X = glob(os.path.join(datafolder,'*.jpg')) shuffle(X) X = np.array(X) dcgan = DCGAN(nz,pixel,lr,gpu) dcgan.gakushuu(X,kurikaeshi,n_batch) # 学習開始哪里要小心 鉴别器对生成图像的评估将用于生成器和鉴别器的反向传播,因此,如果不对.backward()添加keep_graph = True,则会出现错误。 使用pytorch的torch.nn.Sequential很方便,因为您可以通过安排模块来生存,而不必像keras一样往前写,但是从卷积层到完全连接层时,需要转换张量。 pytorch不会自动转换,也没有模块可以转换它,因此您必须自己编写它。 似乎设置参数的初始值出乎意料是必要的。在pytorch中,默认初始值设置不太适合DCGAN,因此我将继承Conv2d,ConvTranspose2d和BatchNorm2d的类,并自行更改初始值设置。 由于生成器的输出使用tanh,它将是-1到1,但是如果要将其输出到图像,则需要将其转换为0到1。 输入到鉴别器时,模型图像以及生成器生成的图像必须转换为-1到1。结果 我尝试了64px,80px和96px,但是在这里我将宣布80px的结果 根据训练数据,结果将有所不同,但首先,使用可从konachan进行检测的117870个面部图像的结果如下
这似乎不是一个很好的结果。通过自动检测脸部创建的图像似乎包含约4%的无关图像。我本人试图擦除不相关的图像,但是当我期望处理所有这些图像可能需要几天的时间,因为有太多图像时,我放弃了它,并按原样用于学习。结果差可能也归因于不良样品的存在。 以下是我第一次学习使用所有图像时的结果。我仍然不成熟,无法清晰地画画,但是我认为这也令人惊讶。
之后,我使用safebooru的图像进行了尝试。由于safebooru具有更多图像,因此这次我只取出了全部为19008的蓝色和红色头发。 这个数字很小,但是不良样本很少。这要归功于safebooru精心制作的标签。 这是每个阶段输出的图像。 1/3次 2/3次 1次 1 1/3倍 1 2/3次 2次 3次 5次 7次 10次 20次 30次 50次 151次 起初变化很快,但是经过一段时间的学习,变化已经逐渐。似乎学得越多越好。相反,如果您学得太多,变异似乎就会消失。这就是为什么在151张图像中,有多少张脸变得相同。 我试图制作一段学习阶段的视频,但由于它很大,所以将其放在pixiv上 |
CopyRight 2018-2019 实验室设备网 版权所有 |