基于DCGAN的动漫头像生成
数据
- 数据集:动漫图库爬虫获得,经过数据清洗,裁剪得到动漫头像。分辨率为3 * 96 * 96,共5万多张动漫头像的图片,从知乎用户何之源处下载。
- 生成器:输入为随机噪声,输出为归一化到[-1,1]之间的原图大小。
- 判别器:输入图片,输出为图片为真实的概率,范围为[0,1]。
模型
DCGAN目前是GAN在实际工程实践中被采用最多的衍生网络,为了提高图像生成质量,增强其稳定性,许多研究学者尝试进行优化,并提出了四点设计原则:
(1)卷积层代替池化层
池化操作会使卷积核在缩小的特征图上覆盖了更大的图像视野,但是对网络性能的优化效果较小,使用卷积层代替池化层,让网络自动选择筛去不必要信息,学习上采样和下采样过程,提高计算机运算能力。
(2)去掉全连接层
全连接层一般添加在网络的末层,用于将图像特征进行连接,可以减少特征信息的损失,但是由于其参数过多,会产生过拟合、计算速度降低等问题。由于面部图像特征提取的感受野范围较小,不需要提取全图特征,所以为了避免上述问题,本项目中网络模型去掉了全连接层。
(3)批量归一化
本课题中的生成器和判别器都是五层神经网络,每一层输入的数据的复杂度都会逐层递增,使输出数据的分布发生变化,对网络参数的初始化和BP算法的性能产生影响。将数据进行批量归一化(Bach Normalization,BN),可以使输出的数据服从某个固定数据的分布,把数据特征转换为相同尺度,从而加速神经网络的收敛速度。
(4)激活函数
激活函数(Activation Function)具有连续可导的特性,可以使神经网络进行非线性变化,通过对数值优化来学习网络参数,提升网络的扩展性。本课题的生成器和判别器均为五层网络模型,计算量较大,每一层的激活函数选择需要满足高计算效率和训练稳定两点,其导函数的值域分布合理。
生成器
DCGAN生成器模型如下图,共五层,本项目中生成器输出通道数ngf默认为64,所以其中的通道数都减半,其他一样。从输入的100维的随机噪声,不断上采样反卷积操作,最终得到生成的假图片。
判别器
生成器整体框架逆过来,其中反卷积变为卷积,卷积核大小,步长等设置一样,除最后一层外ReLU激活函数变为LeakyReLU,不断下采样,最后通过sigmoid函数输出真实样本概率值,也就是一个二分类网络。
损失函数
BCELoss是CrossEntropyLoss的一个特例,用于计算输入 input 和标签 label 之间的二值交叉熵损失值。
由于生成网络和判别网络的输出层的激活函数分别为Than函数和Sigmoid函数,两者都是S型函数,其函数特性会导致反向传播算法收敛速度降低,使用BCELoss函数后,解决了因sigmoid函数导致的梯度消失问题。
criterion = t.nn.BCELoss().to(device)
# 训练判别器,分开训练
## 尽可能的把真图片判别为正确
error_d_real = criterion(output, true_labels)
error_d_real.backward()
## 尽可能把假图片判别为错误
error_d_fake = criterion(output, fake_labels)
error_d_fake.backward()
# 训练生成器
error_g = criterion(output, true_labels)
优化器
选用Adam优化程序调整超参数,它结合了 AdaGrad 和 RMSProp 算法最优的性能,不仅可以计算每个参数的自适应学习率,还可以通过训练数据的不断迭代使网络权重自动更新,相较于其他几种算法而言Adam算法实现简单、对计算机资源占用率较低,收敛速度也更快。
实验
从https://pan.baidu.com/s/1eSifHcA 提取码:g5qa下载数据(275M,约5万多张图片),把所有图片保存于data/face/目录下,这是因为用了默认的ImageFolder读取数据集,标签为faces,不需要重写datasets类。
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
训练过程
(1)训练判别器
- 先固定生成器
- 对于真图片,判别器的输出概率值尽可能接近1
- 对于生成器生成的假图片,判别器尽可能输出0
(2)训练生成器
- 固定判别器
- 生成器生成图片,尽可能使生成的图片让判别器输出为1
(3)返回第一步,循环交替进行
本次训练过程,每1个batch训练一次判别器, 每5个batch训练一次生成器,可以尝试改变训练比例,改变两者的学习率实验,观察哪种效果最好。
在训练判别器时,需要对生成器生成的图片用detach()操作进行计算图截断,避免反向传播将梯度传到生成器中。因为在训练判别器时,我们不需要训练生成器,也就不需要生成器的梯度。
在训练判别器时,需要反向传播两次,一次是希望把真图片判定为1,一次是希望把假图片判定为0.也可以将这两者的数据放到一个batch中,进行一次前向传播和反向传播即可。但是研究发现,分两次的方法更好。
对于假图片,在训练判别器时,希望判别器输出为0;而在训练生成器时,希望判别器输出为1,这样实现判别器和生成器互相对抗提升。
测试
python main.py generate --gpu --vis False --netd-path checkpoints/netd_199.pth --netg-path checkpoints/netg_199.pth --gen-img result.png --gen-num 64
使用最后一次迭代的到的训练网络进行验证,生成器网络为netd_199.pth,判别器网络为netg_199.pth,从生成的512张图中,根据判别器中输出的值,选择结果最好的64张图,并存储在本地,命名为result.png:
结果分析
生成器和判别器损失函数变化如下,可以看到训练过程还是不稳定。
问题及改进:
-
样本数据有些比较模糊,检查图像样本库,在样本数量足够的情况下,检查样本中是否存在非动漫图像,动漫风格是否类似,样本的表情、发色等面部属性是否足够丰富。
-
模型训练不稳定,将训练次数比例和学习率结合,动态调整。判别器训练效果太好,会导致生成器反向调整参数,生成一些已经被识别为“真”的样本,特殊情况下,还输出许多面部特征畸变的图像,导致样本缺乏多样性和准确性。
完整代码
main.py用于训练和测试
# coding:utf8
import os
import ipdb
import torch as t
import torchvision as tv
import tqdm
from model import NetG, NetD
from torchnet.meter import AverageValueMeter
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()
def train(**kwargs):
for k_, v_ in kwargs.items():
setattr(opt, k_, v_)
device=t.device('cuda') if opt.gpu else t.device('cpu')
if opt.vis:
from visualize import Visualizer
vis = Visualizer(opt.env)
# 数据
transforms = tv.transforms.Compose([
tv.transforms.Resize(opt.image_size),
tv.transforms.CenterCrop(opt.image_size),
tv.transforms.ToTensor(