其实网上很多博主写了这块的内容,但是我感觉那些博主的代码有些过时了,怎么说呢,模型这块我没办法改,但是在训练阶段,我总感觉他们的代码有些怪异,最开始我按照自己的习惯去更新梯度这些,但是梯度的变化总感觉很奇怪,鉴别器都快到0了,然后就开始怀疑自己,不停的找原因,但是找来找去,原理就是这样,没有错呀,然后又改变我的训练阶段的函数,梯度变化就感觉比较正常了。为什么会这样呢,纠结了很久,后面想了很久,我也是在仔细观察那些博主给的梯度曲线之后,发现其实他们的梯度最初的时候其实也非常不稳定,更新器的损失函数也会很大,然后就不管了,直接让代码跑一段时间再说,还好,再跑了一段时间之后,生成器生成的照片开始有人脸的趋势了。这也说明,我的代码并没有错,完全符合要求。现在再次记录一下,毕竟我自己也弄了这么久,留个纪念。
首先,关于dcgan的原理,我就不再说了,大家自行的去看其它博主的文章,多年前的技术了,资料还是很完善。我直接给你们看论文中给的模型框架吧:
我最初是完全按照上面这张图来构件网络的,但是可能是由于自己的理解不到位,我将100z理解为一个【100】的向量,然后通过全连接生成[1024x4x4]的向量,然后进行维度变化为[1024, 4,4],然后模型按照上面搭建完后就开始训练了,但是训练的效果很奇怪,最后看了看pytorch官方给的dcgan模型,我才发现,整个dcgan的过程,没有一个fc全连接层,生成器的输入数据的维度是[batchsize, 100, 1,1 ],我看了看论文,百思不得其解为什么要这么做,难道真的是我对论文的理解不到位造成的?不管了,先借鉴pytorch官方给的模型方案进行搭建吧。搭建的过程,我还发现一个不一样的地方,对于参数,我们这里要自己进行初始化,而且对于bias我们还要设置为false,代表梯度更新的时候不更新这个参数,由于我没有深入研究gan以及自己的统计学方面的数学知识基本为空,所以也不是很理解为什么要这样做,先就这样吧,试一试官方的模型框架,代码如下:
'''this code was desinged by nike hu'''
import torch
import torch.nn as nn
device = torch.device('cuda:0')
# 鉴定器网络
class Discrimite(nn.Module):
def __init__(self):
super(Discrimite, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=4, stride=2, padding=1, bias=False), # 这里false就是bias不进行梯度更新
nn.LeakyReLU(0.2),
nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(128),
nn.LeakyReLU(0.2),
nn.Conv2d(128, 256, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(256),
nn.LeakyReLU(0.2),
nn.Conv2d(256, 512, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.2),
nn.Conv2d(512, 1, kernel_size=4, stride=1, bias=False),
nn.Sigmoid()
)
self.weight_init()
def forward(self, x):
x = self.conv(x)
return x
# 初始化参数
def weight_init(self):
for m in self.conv.modules():
if isinstance(m, nn.ConvTranspose2d): # 判断一个变量是否是某个类型可以用isinstance()判断
nn.init.normal_(m.weight.data, 0, 0.02)
elif isinstance(m, nn.BatchNorm2d):
nn.init.normal_(m.weight.data, 0, 0.02) # 初始化为正太分布,torch.nn.init.uniform_(tensor, a=0, b=1)是均匀分布
nn.init.constant_(m.bias.data, 0) # 初始化为常数
class Generate(nn.Module):
def __init__(self):
super(Generate, self).__init__()
self.conv = nn.Sequential(
nn.ConvTranspose2d(100, 512, kernel_size=4, stride=1, bias=False),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.ConvTranspose2d(512, 256, kernel_size=4, stride=2, padding=1,bias=False),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.ConvTranspose2d(256, 128, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.ConvTranspose2d(64, 3, kernel_size=4, stride=2, padding=1, bias=False),
nn.Tanh()
)
self.weight_init()
def forward(self, x):
x = self.conv(x)
return x
# 初始化参数
def weight_init(self):
for m in self.conv.modules():
if isinstance(m, nn.ConvTranspose2d): # 判断一个变量是否是某个类型可以用isinstance()判断
nn.init.normal_(m.weight.data, 0, 0.02)
elif isinstance(m, nn.BatchNorm2d):
nn.init.normal_(m.weight.data, 0, 0.02)
nn.init.constant_(m.bias.data, 0) # 将bias的值设置为0
if __name__ == '__main__':
# x = torch.rand(64, 100, 1, 1)
# x = x.cuda()
# net = Generate()
# net.to(device)
# x = net(x)
# print(x.shape)
x = torch.randn(16, 3, 64, 64)
x = x.to(device)
net = Discrimite()
net.to(device)
x = net(x)
print(x.shape)
网络框架搭建好了之后,我们就可以开始进行训练代码的设计了,代码如下:
'''this code was desinged by nike hu'''
import torch
from torch.utils.data import DataLoader
from torchvision import transforms
from face_model import Discrimite, Generate # 这里face_model是搭建模型的py文件的名字
import visdom
from torchvision.datasets import ImageFolder
from torch import nn, autograd
from torch.autograd import Variable
batchsize = 64
def getData():
trainData = ImageFolder('F:/code/DataSet/data/focusight1_round1_train_part1/OK_Images', transform=transforms.Compose([
transforms.Resize((64, 64)),
transforms.CenterCrop(64),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
])) # 使用transform.Compose(),里面要加上[],否则会报错,而且无法迭代数据,这里是加载训练图片路径的
trainLoader = DataLoader(trainData, batch_size=batchsize, shuffle=True, drop_last=True)
return trainLoader
# 这里的函数是根据wgan的原理设计的,目的是让鉴别器的梯度变化在1附近
def gradient_penalty(D, xr, xf):
batch = xr.size(0)
t = torch.rand(batch, 1, 1, 1).cuda()
t = t.expand_as(xr)
mid = t * xr + (1 - t) * xf # 线性差值
mid.requires_grad_() # 设置上面的线性差值需要求导
pred = D(mid)
grads = autograd.grad(outputs=pred, inputs=mid,
grad_outputs=torch.ones_like(pred), create_graph=True,
retain_graph=True, only_inputs=True)[0]
# 如果输入x,输出是y,则求y关于x的导数(梯度):
gp = torch.pow(grads.norm(2, dim=1) - 1, 2).mean() # 二范数和1的差值的平方的均值
return gp
def trainModel():
torch.manual_seed(23) # 随机种子设置
generate_net = Generate()
print(generate_net)
discrimi_net = Discrimite()
print(discrimi_net)
device = torch.device('cuda:0')
generate_net = generate_net.to(device)
discrimi_net = discrimi_net.to(device)
generate_optimer = torch.optim.Adam(generate_net.parameters(), lr=0.0002, betas=(0.5, 0.9))
discrimi_optimer = torch.optim.Adam(discrimi_net.parameters(), lr=0.0002, betas=(0.5, 0.9))
trainLoader = getData()
viz = visdom.Visdom()
critimer = nn.BCELoss()
viz.line([[0, 0]], [0], win='loss', opts=dict(title='loss', legend=['D', 'G']))
epoch = 0
for i in range(10000):
print('------------------------------第', i, '次的函数统计----------------------------------------------------')
for really_x, _ in trainLoader:
for _ in range(1): # 这里可以设置为先训练鉴别器多次然后再训练生成器,只需要把1改一下,然后把下面的一行代码恢复
# really_x = next(iter(trainLoader))[0]
really_x = really_x.to(device)
batchs = really_x.size(0)
pred_r = discrimi_net(really_x).view(batchs, -1) # 生成器生成的数据,最后维度是[batch, 1]
real_label = Variable(torch.ones((batchs, 1)), requires_grad=False)
fake_label = Variable(torch.zeros((batchs, 1)), requires_grad=False)
real_label = real_label.to(device)
fake_label = fake_label.to(device)
loss_r = critimer(pred_r, real_label)
fake_x = torch.randn((batchs, 100, 1, 1))
fake_x = fake_x.to(device) # 放到gpu上
fake_x = generate_net(fake_x).detach()
pred_f = discrimi_net(fake_x).view(batchs, -1) # 生成的照片还要进行判别
loss_f = critimer(pred_f, fake_label)
gp = gradient_penalty(discrimi_net, really_x, fake_x.detach())
loff_D = loss_r + loss_f + 0.2*gp # 这里的0.2是一个可以变化的参数,数值不一样,可能最后效果不一样
discrimi_optimer.zero_grad()
loff_D.backward()
discrimi_optimer.step()
# 接下来是生成器的loss
fake_x1 = torch.randn((batchs, 100, 1, 1))
fake_x1 = fake_x1.to(device)
fake_image2 = generate_net(fake_x1)
fake_data2 = discrimi_net(fake_image2).view(batchs, -1)
real_label = Variable(torch.ones((batchs, 1)), requires_grad=False)
real_label = real_label.to(device)
generate_losses = critimer(fake_data2, real_label)
generate_optimer.zero_grad()
generate_losses.backward()
generate_optimer.step()
print('第', i, '个', '生成器的loss->', generate_losses.item(), '判断器的loss->', loff_D.item())
viz.images(fake_image2, nrow=8, win='x', opts=dict(title='x'))
viz.line([[loff_D.item(), generate_losses.item()]], [epoch], win='loss', update='append')
epoch += 1
# torch.save(generate_net, '/content/drive/My Drive/model/generate.pkl')
# torch.save(discrimi_net, '/content/drive/My Drive/model/discrimi.pkl')
if __name__ == '__main__':
trainModel()
我相信能看这篇文章到最后的人,代码能力也不差,所以就不再解释代码了,我个人觉得,如果你想用我的代码去训练看看效果,也知道修改哪些地方,基本就是训练图像的路径改一下就行,但是要注意一下ImageFolder这个接口要求的目录结构,我这不好画图,你们大家自己百度一下就行,然后就是如果训练效果你觉得不满意,可以试着调一下超参数。然后就是pytorch给的代码中,关于鉴别器的损失函数,是没有加上wgan那部分的,按照pytorch官方代码,上面一行代码要如下改变:
loff_D = loss_r + loss_f + 0.2*gp 要变成 loff_D = loss_r + loss_f
大家如果觉得效果还不好,也可以试着按照pytorch官方代码去掉gp看看效果。
然后,我们看看训练效果:
这是我用rtx2060训练了大概两个小时后的效果,接下来看看我在谷歌的colaboratory上训练了十个小时的效果吧:
感觉效果好了一些,但是没有我想象的那么好,难道是由于训练时长还不够或者上面的gp要去掉再试试?算了算了,不折腾了,大家有兴趣的,可以去试一试。
说到这了,还是忍不住要吐槽一下百度的aistudio,这平台显卡是v100,显卡比谷歌的colaboratory更牛逼(colaboratory有时候还会分配到k80这种已经过时的显卡,目前这种k80显卡也就1000多,而v100显卡要七万左右),操作方式百度也更人性化,但是,重点来了,我在aistudio上如果使用非飞浆的框架,比如我使用pytorch,那么我过不了多久就会被断掉这个项目。感觉百度在强制性的推广自己的框架。推广自己的框架这没错,但是这种强制性的推广,感觉就很烦了,只能去谷歌上面弄了。
github链接,我将代码放到上面了
2020 6.16