携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第22天,点击查看活动详情
DCGAN 生成人脸照片
前言
数据集:Large-scale CelebFaces Attributes (CelebA) Dataset
CelebA是大规模面部属性数据集,包含20万张名人照片,每个图片有40个属性,数据集中的图像姿势和背景多样。
- 10,177 个人
- 202,599张脸
- 每张图片有5个特征点坐标和40个二元特征

可以从官方的 Google Drive 进行下载即可。
生成器结构图如下:
判别器结构图如下:
文件说明
| data目录 | 存放原始数据与预处理后切分为训练集、验证机、测试集的数据 |
|---|---|
| log目录 | 训练过程中使用tensorboardX保存的指标数值,如损失、精确度等 |
| model_save目录 | 存放不同训练阶段的模型,最后找出个最优的用于测试集 |
| config.py | 保存超参数 |
| dataset_face.py | Banknote数据类,用于训练时获取数据 |
| inference.py | 挑选模型在测试集上运行 |
| generator.py | 生成器模型 |
| discriminator.py | 判别器模型 |
| trainer.py | 模型训练代码 |
| data目录 | 存放数据集 |
| utils.py | 工具类文件 |
数据加载器
我们会用到 torchvision 。torchvision 是独立于 pytorch 的关于图像操作的一些方便工具库。torchvision 的详细介绍在:pypi.org/project/tor…
- vision.datasets : 几个常用视觉数据集,可以下载和加载,这里主要的高级用法就是可以看源码如何自己写自己的Dataset的子类
- vision.models : 流行的模型,例如 AlexNet, VGG, ResNet 和 Densenet 以及 与训练好的参数。
- vision.transforms : 常用的图像操作,例如:随机切割,旋转,数据类型转换,图像到tensor ,numpy 数组到tensor , tensor 到 图像等。
- vision.utils : 用于把形似 (3 x H x W) 的张量保存到硬盘中,给一个mini-batch的图像可以产生一个图像格网。
主要运用到 ImageFolder,是一个通用的数据加载器,图像应该按照以下方式放置:
root/dog/xxx.png
root/dog/xxy.png
root/dog/xxz.png
root/cat/123.png
root/cat/nsdf3.png
root/cat/asd932_.png
datasets.ImageFolder(root="root folder path", [transform, target_transform])
注:
root的地址应该是根目录,例如root/
ImageFolder有以下成员:
self.classes- 类别名列表self.class_to_idx- 类别名到标签,例如 “狗”-->[1,0,0]self.imgs- 一个包括 (image path, class-index) 元组的列表。
# 读取图片文件
data_face = TD.ImageFolder(root=HP.data_root,
# 图片处理
transform=T.Compose([
T.Resize(HP.image_size), # 64x64x3 设置图片大小
T.CenterCrop(HP.image_size), # 剪裁图片,取中间
T.ToTensor(), # to [0, 1] # 归一化
T.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # can't apply ImageNet statistic # 标准化
]),
)
接下来设置数据加载器,配置相关超参数。
face_loader = DataLoader(data_face,
batch_size=HP.batch_size,
shuffle=True,
num_workers=HP.n_workers)
生成器
根据上诉图片进行配置:
首先第一个层是 projection_layer ,主要目的:shape 的转换,输出向量做一下压缩,从而能把高纬度的信息降维。
self.projection_layer = nn.Linear(HP.z_dim, 4*4*1024)
接下来是四个卷积,主要采用转置卷积(一种特殊的卷积,先padding来扩大图像尺寸,紧接着跟正向卷积一样,旋转卷积核180度,再进行卷积计算。),它的卷积核,步幅,填充相关参数配置主要实现 2 倍上采样。反卷积计算公式如下:
self.generator = nn.Sequential(
# TransposeConv layer: 1
nn.ConvTranspose2d(in_channels=1024, # [N, 512, 8, 8]
out_channels=512,
kernel_size=(4, 4),
stride=(2, 2),
padding=(1, 1),
bias=False),
nn.BatchNorm2d(512),
nn.ReLU(),
# TransposeConv layer: 2
nn.ConvTranspose2d(in_channels=512, # [N, 256, 16, 16]
out_channels=256,
kernel_size=(4, 4),
stride=(2, 2),
padding=(1, 1),
bias=False),
nn.BatchNorm2d(256),
nn.ReLU(),
# TransposeConv layer: 3
nn.ConvTranspose2d(in_channels=256, # [N, 128, 32, 32]
out_channels=128,
kernel_size=(4, 4),
stride=(2, 2),
padding=(1, 1),
bias=False),
nn.BatchNorm2d(128),
nn.ReLU(),
# TransposeConv layer: final
nn.ConvTranspose2d(in_channels=128, # [N, 3, 64, 64]
out_channels=HP.data_channels, # output channel: 3 (RGB)
kernel_size=(4, 4),
stride=(2, 2),
padding=(1, 1),
bias=False),
nn.Tanh() # [0, 1] Relu [0, inf]
)
开始写前向传播函数,输入形状是 [N,100],第一层 projection_layer 的形状是 [N, 4*4*1024],接下来是卷积层输入,所以需要对第一层输出进行处理,改成 NCHW 这样的形式,也就是 [N, 1024, 4, 4]。
def forward(self, latent_z): # latent space (Ramdon Input / Noise) : [N, 100]
z = self.projection_layer(latent_z) # [N, 4*4*1024]
z_projected = z.view(-1, 1024, 4, 4) # [N, 1024, 4, 4]: NCHW
return self.generator(z_projected)
对卷积层和 BatchNorm 进行参数初始化,方便模型进行约束。GAN 模型对参数比较敏感。
@staticmethod
def weights_init(layer):
layer_class_name = layer.__class__.__name__
if 'Conv' in layer_class_name:
nn.init.normal_(layer.weight.data, 0.0, 0.02)
elif 'BatchNorm' in layer_class_name:
nn.init.normal_(layer.weight.data, 1.0, 0.02)
nn.init.normal_(layer.bias.data, 0.)
判别器
前面是 4 层卷积,它的卷积核,步幅,填充相关参数配置主要实现形状大小减半,后面接着使用了
BatchNorm,使用 LeakyReLU 作为激活函数。
self.discriminator = nn.Sequential( # 1. shape transform 2. use conv layer as "feature extraction"
# conv layer : 1
nn.Conv2d(in_channels=HP.data_channels, # [N, 64, 32, 32]
out_channels=64,
kernel_size=(3, 3),
stride=(2, 2),
padding=(1, 1),
bias=False),
nn.LeakyReLU(0.2),
# conv layer : 2
nn.Conv2d(in_channels=64, # [N, 128, 16, 16]
out_channels=128,
kernel_size=(3, 3),
stride=(2, 2),
padding=(1, 1),
bias=False),
nn.BatchNorm2d(128),
nn.LeakyReLU(0.2),
# conv layer : 3
nn.Conv2d(in_channels=128, # [N, 256, 8, 8]
out_channels=256,
kernel_size=(3, 3),
stride=(2, 2),
padding=(1, 1),
bias=False),
nn.BatchNorm2d(256),
nn.LeakyReLU(0.2),
# conv layer : 4
nn.Conv2d(in_channels=256, # [N, 512, 4, 4]
out_channels=512,
kernel_size=(3, 3),
stride=(2, 2),
padding=(1, 1),
bias=False),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.2),
)
卷积层之后的结果,输入分类器里面,Sigmoid 作为激活函数,得到一个概率。
self.linear = nn.Linear(512*4*4, 1)
self.out_ac = nn.Sigmoid()
开始写前向传播,输入图像的形状是 [N, 3, 64, 64],经过卷积层之后,输出形状是 [N, 512, 4, 4],接下来是进入分类器,我们需要对输出结果进行处理,将其拉平,拉成一个二维的。
def forward(self, image):
out_d = self.discriminator(image) # image [N, 3, 64, 64] -> [N, 256, 2, 2]
out_d = out_d.view(-1, 256*2*2) # tensor flatten
return self.out_ac(self.linear(out_d))
训练
整个实现代码过程,主要是实现论文中这个过程。
首先,我们需要保证代码的可复现性,需要对 random 设置种子。
torch.random.manual_seed(HP.seed)
torch.cuda.manual_seed(HP.seed)
random.seed(HP.seed)
np.random.seed(HP.seed)
准备工作,获取模型,设置模型初始化,设置 损失函数,设置优化器。
# model init
G = Generator() # new a generator model instance
G.apply(G.weights_init) # apply weight init for G
D = Discriminator() # new a discriminator model instance
D.apply(D.weights_init) # apply weight init for G
G.to(HP.device)
D.to(HP.device)
# loss criterion
criterion = nn.BCELoss() # binary classification loss
# optimizer
optimizer_g = optim.Adam(G.parameters(), lr=HP.init_lr, betas=(HP.beta, 0.999))
optimizer_d = optim.Adam(D.parameters(), lr=HP.init_lr, betas=(HP.beta, 0.999))
第一步,完成以下公式的实现:
公式前半部分:拿到输入 ,送入模型 D 中,获取结果,设置真实照片(正样本)的 label 值,送入损失函数中。
labels_gt = torch.full(size=(b_size, ), fill_value=0.9, dtype=torch.float, device=HP.device) # batch 批次的设置 label
predict_labels_gt = D(batch.to(HP.device)).squeeze() # [64, 1] -> [64,]
loss_d_of_gt = criterion(predict_labels_gt, labels_gt)
公式后半部分:需要随机生成噪声 ,送入 D(G(z)) 模型中,获取结果,接下来设置假照片(负样本)的 label 值,都送入损失函数中。
labels_fake = torch.full(size=(b_size, ), fill_value=0.1, dtype=torch.float, device=HP.device)# 设置负样本label
latent_z = torch.randn(size=(b_size, HP.z_dim), device=HP.device) # 噪声
predict_labels_fake = D(G(latent_z)).squeeze() # [64, 1] - > [64,]
loss_d_of_fake = criterion(predict_labels_fake, labels_fake)
两个损失值相加,开始梯度更新。
loss_D = loss_d_of_gt + loss_d_of_fake # add the two parts
loss_D.backward()
optimizer_d.step()
第二步:完成以下公式实现:
该公式的实现过程与上面是一致的,都是设置噪声,送入模型,设置 label 值,送入损失函数中,开始梯度更新。但是这里不同的是 label 值的设置,第二步是为了训练 G 模型,我们希望生成器生成的图片可以骗过判别器的,所以 D 模型输出的值越大越好,所以我们比较的 label 值应该设置成正样本。
latent_z = torch.randn(size=(b_size, HP.z_dim), device=HP.device)
labels_for_g = torch.full(size=(b_size, ), fill_value=0.9, dtype=torch.float, device=HP.device) # 正样本label值
predict_labels_from_g = D(G(latent_z)).squeeze() # [N, ]
loss_G = criterion(predict_labels_from_g, labels_for_g
loss_G.backward()
optimizer_g.step()
注:正样本 label 值设置为 0.9,负样本 label 值设置为 0.1。
目的:
- 一定程度上,可以缓解模型过于武断的问题,也有一定的抗噪能力
- 弥补了简单分类中监督信号不足(信息熵比较少)的问题,增加了信息量;
- 提供了训练数据中类别之间的关系(数据增强);
- 可能增强了模型泛化能力
接下来,保存生成器和判别器模型数据和生成器生成的照片。
if not step % HP.verbose_step:
with torch.no_grad():
fake_image_dev = G(fixed_latent_z)
logger.add_image('Generator Faces', invTrans(vutils.make_grid(fake_image_dev.detach().cpu(), nrow=8)), step)
if not step % HP.save_step: # save G and D
model_path = 'model_g_%d_%d.pth' % (epoch, step)
save_checkpoint(G, epoch,optimizer_g, os.path.join('model_save', model_path))
model_path = 'model_d_%d_%d.pth' % (epoch, step)
save_checkpoint(D, epoch, optimizer_d, os.path.join('model_save', model_path))
到这里训练代码基本结束了,这里没有给出完整的代码,主要是理解,可以自己尝试。