大家好,欢迎来到专栏《百战GAN》,在这个专栏里,我们会进行GAN相关项目的核心思想讲解,代码的详解,模型的训练和测试等内容。
背景要求:会使用Python和Pytorch
附带资料:参考论文和项目,视频讲解
1 项目背景
作为一门新兴的技术,GAN在很多人脸图像任务中有着广泛的应用,下面列举了一些典型的方向。
在阅读接下来的内容之前,请大家务必回顾之前StyleGAN人脸生成的内容,没有这一部分内容基础,无法学习接下来的内容。
【百战GAN】StyleGAN原理详解与人脸图像生成代码实战
【项目实战课】基于Pytorch的StyleGAN v1人脸图像生成实战
2 原理简介
(1) 如何获得真实人脸的潜在编码向量,它对应StyleGAN中的映射 络(mapping network)的输入Z或者输出W。
(2) 如何通过修改Z或者W向量,控制生成人脸图像的高层语义属性。
当前对真实人脸编码向量的求取基本上基于两种思路,一种是学习一个编码器来实现映射,一种是直接对向量进行优化求解。
2.1 基于编码器的求解
基于编码器的求解框架如下图所示,由两部分模块组成。
Encoder表示需要训练的编码器,Decoder表示已经训练完的生成模型,如StyleGAN的生成器部分。真实图像输入编码器得到Z或者W,再输入生成器得到生成的人脸,完成人脸图像的重建。
通过直接学习一个编码器,可以不需要对每一张图都进行优化,实现一次训练,对任意图像都能提取潜在编码向量,但是也容易在训练数据集上发生过拟合。
2.2 基于优化求解的方法
基于优化求解的方法包括以下几步:
(1) 给定图片I,以及预训练好的生成器G。
(2) 初始化潜在编码向量,如W,其初始值可以使用计算得到的统计平均值。
(3) 根据优化目标进行反复迭代,直到达到预设的终止条件。
优化目标的常见形式为:
其中Lpercept为特征空间中的感知损失距离,是很通用的问题,具体形式不再赘述。lamda用于平衡感知损失和MSE损失之间的权重比。可以使用梯度下降算法进行求解。
基于优化求解方法的优点是精度较高,但是优化速度慢,而且对每一个图片都必须进行优化迭代。
3.1 人脸重建
接下来我们来看人脸重建的求解,核心代码如下:
if __name__ == "__main__": ## 预训练模型权重 parser.add_argument( "--ckpt", type=str, required=True, help="path to the model checkpoint" ) ## 输出图像尺寸 parser.add_argument( "--size", type=int, default=256, help="output image sizes of the generator" ) ## 学习率参数 parser.add_argument( "--lr_rampup", type=float, default=0.05, help="duration of the learning rate warmup", ) parser.add_argument( "--lr_rampdown", type=float, default=0.25, help="duration of the learning rate decay", ) parser.add_argument("--lr", type=float, default=0.1, help="learning rate") ## 噪声相关参数,噪声水平,噪声衰减范围,噪声正则化 parser.add_argument( "--noise", type=float, default=0.05, help="strength of the noise level" ) parser.add_argument( "--noise_ramp", type=float, default=1.0, help="duration of the noise level decay", ) parser.add_argument( "--noise_regularize", type=float, default=10000, help="weight of the noise regularization", ) ## MSE损失 parser.add_argument("--mse", type=float, default=0.5, help="weight of the mse loss") ## 迭代次数 parser.add_argument("--step", type=int, default=1000, help="optimize iterations") ## 重建图像 parser.add_argument( "--files", type=str, help="path to image files to be projected" ) ## 重建结果 parser.add_argument( "--results", type=str, help="path to results files to be stored" )## 计算学习率def get_lr(t, initial_lr, rampdown=0.25, rampup=0.05): lr_ramp = min(1, (1 - t) / rampdown) lr_ramp = 0.5 - 0.5 * math.cos(lr_ramp * math.pi)lr_ramp = lr_ramp * min(1, t / rampup)return initial_lr * lr_ramp## 合并latent向量和噪声def latent_noise(latent, strength):noise = torch.randn_like(latent) * strength return latent + noise## 生成图片def make_image(tensor): return ( tensor.detach() .clamp_(min=-1, max=1) .add(1) .div_(2) .mul(255) .type(torch.uint8) .permute(0, 2, 3, 1) .to("cpu") .numpy() )## 生成与图像大小相等的噪声def make_noise(device,size): noises = [] step = int(math.log(size, 2)) - 2 for i in range(step + 1): size = 4 * 2 ** i noises.append(torch.randn(1, 1, size, size, device=device))return noises## 噪声归一化def noise_normalize_(noises): for noise in noises: mean = noise.mean() std = noise.std() noise.data.add_(-mean).div_(std)args = parser.parse_args() device = "cpu" ## 计算latent向量的平均次数 n_mean_latent = 10000 ## 获得用于计算损失的最小图像尺寸 resize = min(args.size, 256) ## 预处理函数 transform = transforms.Compose( [ transforms.Resize(resize), transforms.CenterCrop(resize), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]), ] ) ## 投影的人脸图片,将图片处理成一个batch imgs = [] imgfiles = os.listdir(args.files) for imgfile in imgfiles: img = transform(Image.open(os.path.join(args.files,imgfile)).convert("RGB")) imgs.append(img) imgs = torch.stack(imgs, 0).to(device) ## 载入模型 netG = StyledGenerator(512,8) netG.load_state_dict(torch.load(args.ckpt,map_location=device)["g_running"], strict=False) netG.eval() netG = netG.to(device) step = int(math.log(args.size, 2)) - 2 with torch.no_grad(): noise_sample = torch.randn(n_mean_latent, 512, device=device) latent_out = netG.style(noise_sample) ##输入噪声向量Z,输出latent向量W=latent_out latent_mean = latent_out.mean(0) latent_std = ((latent_out - latent_mean).pow(2).sum() / n_mean_latent) ** 0.5 ## 感知损失计算 percept = lpips.PerceptualLoss( model="net-lin", net="vgg", use_gpu=device.startswith("cuda") ) ## 构建噪声输入 noises_single = make_noise(device,args.size) noises = [] for noise in noises_single: noises.append(noise.repeat(imgs.shape[0], 1, 1, 1).normal_()) ## 初始化Z向量 latent_in = latent_mean.detach().clone().unsqueeze(0).repeat(imgs.shape[0], 1) latent_in.requires_grad = True for noise in noises: noise.requires_grad = True optimizer = optim.Adam([latent_in] + noises, lr=args.lr) pbar = tqdm(range(args.step)) ## 优化学习Z向量 for i in pbar: t = i / args.step ## t的范围是(0,1) lr = get_lr(t, args.lr) optimizer.param_groups[0]["lr"] = lr## 噪声衰减 noise_strength = latent_std * args.noise * max(0, 1 - t / args.noise_ramp) ** 2 latent_n = latent_noise(latent_in, noise_strength.item()) latent_n.to(device) img_gen = netG([latent_n], noise=noises, step=step) ## 生成的图片 batch, channel, height, width = img_gen.shape## 在不超过256的分辨率上计算损失 if height > 256: factor = height // 256 img_gen = img_gen.reshape( batch, channel, height // factor, factor, width // factor, factor ) img_gen = img_gen.mean([3, 5]) p_loss = percept(img_gen, imgs).sum() ## 感知损失 n_loss = noise_regularize(noises) ## 噪声损失 mse_loss = F.mse_loss(img_gen, imgs) ## MSE损失 loss = p_loss + args.noise_regularize * n_loss + args.mse * mse_loss optimizer.zero_grad() loss.backward() optimizer.step() noise_normalize_(noises) pbar.set_description( ( f"perceptual: {p_loss.item():.4f}; noise regularize: {n_loss.item():.4f};" f" mse: {mse_loss.item():.4f}; loss: {loss.item():.4f}; lr: {lr:.4f}" ) ) ## 重新生成高分辨率图片 img_gen = netG([latent_in], noise=noises,step=step) img_ar = make_image(img_gen) result_file = {} for i, input_name in enumerate(imgfiles): noise_single = [] for noise in noises: noise_single.append(noise[i : i + 1]) print("i="+str(i)+"; len of imgs:"+str(len(img_gen))) result_file[input_name] = { "img": img_gen[i], "latent": latent_in[i], "noise": noise_single, } img_name = os.path.join(args.results,input_name) pil_img = Image.fromarray(img_ar[i]) pil_img.save(img_name) ##存储图片 np.save(os.path.join(args.results,input_name.split('.')[0]+'.npy'),latent_in[i].detach().numpy())##存储latent向量
在上面的代码中,latent_in就是要优化学习的潜在变量,当使用netG([latent_n], noise=noises, step=step)的调用方式时,latent_n是映射 络(mapping network)的向量Z,它由latent_in和随着迭代不断衰减的噪声向量组成,此时没有输入平均风格向量。
根据之前我们对生成器模型结构的解读,此时只有latent_n会影响生成的风格,因此为基于向量Z的重建方法。
当然我们也可以通过将latent_n设为平均风格向量,将混合权重style_weight设置为0,这样也只有latent_n会影响生成的风格,并且它会作为生成 络(s
声明:本站部分文章内容及图片转载于互联 、内容不代表本站观点,如有内容涉及侵权,请您立即联系本站处理,非常感谢!