下载地址:https://gitee.com/yang_guo123/dcgan-pytorch
数据集下载地址:链接: https://pan.baidu.com/s/1PcZ4TLyqcN52096eIfbmCA 密码: auc5
生成对抗网络(GAN)是由Ian Goodfellow在2014年提出的一种生成网络,它由生成器(Generator)与判别器(Discriminator)构成。
生成器:负责伪造假图片,试图以假乱真。
判别器:负责区分图片,希望能从图片中区分真实图片与假图片
在GAN的训练过程中,生成器通过凭空捏造出一幅图片,令判别器进行打分,并通过判别器的分数进行梯度更新,而判别器可以看到真实图片与生成器的伪造图片,通过区分伪造图片与真实图片进行梯度更新。
使用通俗的话来讲,生成器好比制作假冒伪劣手机的奸商,而判别器好比鉴定专家。在一开始,奸商与鉴定专家都是新手,奸商制作的山寨手机都是随机的形状,而鉴定专家也是随机判断手机的好坏;突然有一天奸商了解到手机是长方形的,由此制作的手机能够轻松骗过鉴定专家,这样一来,鉴定专家损失惨重;由于时间的积累,鉴定专家有了一定的经验,能够判定山寨手机的一些特征,则奸商损失惨重,由此这样多轮的较量,鉴定专家能够精确的判定手机是否为赝品,而奸商也可以制作可以以假乱真的赝品手机。
在传统的GAN中,通常使用全连接神经网络,并且其难以训练,于是,在2016年Alec Radford等人提出了DCGAN(论文详见:https://arxiv.org/pdf/1511.06434.pdf),通过改进CNN的模型,将深度卷积神经网络引入GAN,并且获得了很好的效果。
由于本文主要为实践方面,理论层面就不再赘述,有兴趣的网友可以自行下载论文阅读。
本文所采用的数据集为动漫头像数据集,图像大小为96x96,由下图所示:
DCGAN的生成器器由全卷积神经网络构成,首先在网络中输入一个长度为100的随机向量,通过多次的反卷积运算,并使用tanh激活函数输出,将随机向量变为长宽为64x64三通道图像,具体结构由下图所示:
有图可见,在每一层反卷积时,图像的通道数都会缩小一半,并在最后一层将通道数目调整为3通道,输出一张RGB图像,构建的代码由下文所示:
class Generator(nn.Module):
def __init__(self, nz, nc, ngf):
super(Generator, self).__init__()
self.net = Sequential(
nn.ConvTranspose2d(nz, ngf * 8, 4, 1, 0, bias=False),
nn.BatchNorm2d(ngf * 8),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 4),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf * 2),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ngf),
nn.ReLU(inplace=True),
nn.ConvTranspose2d(ngf, nc, 4, 2, 1, bias=False),
nn.Tanh()
)
def forward(self, x):
return self.net(x)
DCGAN有趣的地方在于判别器的网络结构与生成器正好是对称的,唯一有所不同的是其激活函数换成了LeakyRelu,并且输出端使用的激活函数为sigmoid,下面是判别器的代码实现:
class Discriminator(nn.Module):
def __init__(self, nc, ndf):
super(Discriminator, self).__init__()
self.net = Sequential(
nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf, ndf*2, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf*2),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf*2, ndf*4, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf*4),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf*4, ndf*8, 4, 2, 1, bias=False),
nn.BatchNorm2d(ndf*8),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(ndf*8, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
def forward(self, x):
return self.net(x)
在训练的开始,使用pytorch自带的Detaset进行数据集的预处理与读取:
class DCGAN_dataloader(Dataset):
def __init__(self, csv_path, transforms=None):
super(DCGAN_dataloader, self).__init__()
self.transforms = transforms
self.data = pd.read_csv(csv_path)
def __getitem__(self, item):
img_path = self.data["img"][item]
img = Image.open(img_path).convert("RGB")
img = self.transforms(img)
return img
def __len__(self):
return len(self.data["img"])
在这里,本文使用了一些数据增广的方法,例如随机翻转:
def transform():
return transforms.Compose([
transforms.Resize((64, 64)),
transforms.RandomHorizontalFlip(0.3),
transforms.ToTensor(),
transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
])
最后通过DataLoader传入网络:
# 读取数据集
dataset = DCGAN_dataloader("./dataset/data.csv", transforms=transform())
data = DataLoader(
dataset,
batch_size=bach_size,
shuffle=True,
drop_last=True
)
在DCGAN的论文中,使用了Adam优化器,并且将β1设置为0.5(Adam的具体实现请看吴恩达深度学习教程):
# 设置生成器和判别器的优化器为Adam
optim_D = Adam(
self.Discriminator.parameters(),
lr=self.lr_d,
betas=[0.5, 0.999]
)
optim_G = Adam(
self.Generator.parameters(),
lr=self.lr_g,
betas=[0.5, 0.999]
)
在这里将生成器与判别器的损失函数设为二值交叉熵损失:
# 设置损失函数为二值交叉熵损失
loss_func = BCELoss()
接下来就到了正式的训练过程,由于比较复杂,就分开来讲了。
首先读取数据集,并且将真实数据喂入判别器,并将标签设置为1,令判别器通过观测真实数据进行梯度更新:
img = Variable(img)
if self.device == "cuda":
img = img.cuda()
# 将数据喂入Discriminator
self.Discriminator.zero_grad()
label = torch.ones((bach_size,), device=self.device)
out = self.Discriminator(img).view(-1)
errD_real = loss_func(out, label)
errD_real.backward()
然后通过生成器生成一个批次的假数据并喂入判别器,并将标签设置为0,令判别器知道这次喂入的数据为假数据:
# 使用Generator生成假数据喂入Discriminator进行判别
noise = torch.randn(bach_size, self.nz, 1, 1, device=self.device)
fake = self.Generator(noise)
label.fill_(0)
out = self.Discriminator(fake.detach()).view(-1)
errD_fake = loss_func(out, label)
errD_fake.backward()
D_G_z1 = out.mean().item()
errD = errD_fake + errD_real
loss_d += errD.item()
optim_D.step()
最后通过生成器生成一个批次的假数据并让判别器判断,通过设置标签为1,使用蒙骗过判别器的数据进行梯度更新:
# 通过Discriminator返回的数值更新Generator
self.Generator.zero_grad()
label.fill_(1)
out = self.Discriminator(fake).view(-1)
errG = loss_func(out, label)
errG.backward()
loss_g += errG.item()
optim_G.step()
以下为全部代码:
def train(self, epoch=20, bach_size=64):
"""
进行训练
:param epoch: epoch数值
:param bach_size: batch的大小
:return: None
"""
# 读取数据集
dataset = DCGAN_dataloader("./dataset/data.csv", transforms=transform())
data = DataLoader(
dataset,
batch_size=bach_size,
shuffle=True,
drop_last=True
)
# 设置生成器和判别器的优化器为Adam
optim_D = Adam(
self.Discriminator.parameters(),
lr=self.lr_d,
betas=[0.5, 0.999]
)
optim_G = Adam(
self.Generator.parameters(),
lr=self.lr_g,
betas=[0.5, 0.999]
)
# 设置损失函数为二值交叉熵损失
loss_func = BCELoss()
loss_g_log = []
loss_d_log = []
loss_d = 0
loss_g = 0
print("#" * 10 + " 开始训练 " + "#" * 10)
# 开始训练
with tqdm(total=epoch, desc="训练进度", postfix=dict, mininterval=0.3) as pbar:
for i_epoch in range(epoch):
loss_d = 0
loss_g = 0
for i, img in enumerate(data):
img = Variable(img)
if self.device == "cuda":
img = img.cuda()
# 将数据喂入Discriminator
self.Discriminator.zero_grad()
label = torch.ones((bach_size,), device=self.device)
out = self.Discriminator(img).view(-1)
errD_real = loss_func(out, label)
errD_real.backward()
D_x = out.mean().item()
# 使用Generator生成假数据喂入Discriminator进行判别
noise = torch.randn(bach_size, self.nz, 1, 1, device=self.device)
fake = self.Generator(noise)
label.fill_(0)
out = self.Discriminator(fake.detach()).view(-1)
errD_fake = loss_func(out, label)
errD_fake.backward()
D_G_z1 = out.mean().item()
errD = errD_fake + errD_real
loss_d += errD.item()
optim_D.step()
# 通过Discriminator返回的数值更新Generator
self.Generator.zero_grad()
label.fill_(1)
out = self.Discriminator(fake).view(-1)
errG = loss_func(out, label)
errG.backward()
loss_g += errG.item()
optim_G.step()
if i % 5 == 0:
pbar.set_postfix(**{'loss_D': "{:.4f}".format(loss_d / (i + 1)),
'loss_G': "{:.4f}".format(loss_g / (i + 1)),
'D(x)': "{:.4f}".format(D_x),
'D(G(z))': "{:.4f}".format(D_G_z1)
})
loss_g_log.append(loss_g / (i + 1))
loss_d_log.append(loss_d / (i + 1))
with torch.no_grad():
img = self.Generator(torch.randn(20, self.nz, 1, 1, device=self.device)).cpu()
img = (img + 1) * 127
os.mkdir("./log/img_log/epoch{}".format(i_epoch))
for j in range(20):
img_ = np.array(img[j]).transpose((1, 2, 0))
cv2.imwrite("./log/img_log/epoch{}/img{}.jpg".format(i_epoch, j), img_)
pbar.update(1)
torch.save(self.Generator.state_dict(),
"./log/model_g/epoch{}_loss{}.pth".format(i_epoch, loss_g / len(loss_g_log)))
torch.save(self.Discriminator.state_dict(),
"./log/model_d/epoch{}_loss{}.pth".format(i_epoch, loss_d / len(loss_d_log)))
# 清空缓存
torch.cuda.empty_cache()
# 保存loss日志文件
loss_log = pd.DataFrame()
loss_log["g_loss"] = loss_g_log
loss_log["d_loss"] = loss_d_log
loss_log.to_csv("./log/loss/loss.csv")
在训练好GAN模型以后,需要生成图片时,只需调用生成器网络,并通过喂入一组随机数进行生成图片。值得注意的一点在于,由于生成器的最后一层通过tanh输出,输出的数据分布为[-1,1],而正常的图片像素值在0~255之间,所以要转换一下输出图片的分布:
def generate(self, path="./img/generate.jpg"):
"""
生成图片
:param path: 生成图片的路径
:return: None
"""
param = torch.load(self.model_path)
self.Generator.load_state_dict(param)
with torch.no_grad():
img = self.Generator(torch.randn(1, self.nz, 1, 1, device=self.device))
img = np.array(img[0].cpu()).transpose((1, 2, 0))
img = (img + 1) * 127
cv2.imshow("generate", img.astype(np.uint8))
cv2.waitKey(0)
cv2.imwrite(path, img)
以下为训练过程中生成的结果
epoch0:
epoch4:
可见,随着迭代次数的增加,生成的图像的质量越来越好了。