朴素GAN
朴素GAN的思想比较单纯,生成器负责生成假的数据。然后判别器负责鉴别这些数据。在计算LOSS的时候就是计算BCE LOSS,看一看代码就非常的清楚了。朴素GAN在计算损失函数的时候的计算依据是真的数据label就是1,假的数据label就是0。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 for d_index in range (d_steps): d_real_data = Variable(d_sampler(d_input_size)) d_real_decision = D(preprocess(d_real_data)) d_real_error = criterion(d_real_decision, Variable(torch.ones([1 ]))) d_real_error.backward() d_gen_input = Variable(gi_sampler(minibatch_size, g_input_size)) d_fake_data = G(d_gen_input).detach() d_fake_decision = D(preprocess(d_fake_data.t())) d_fake_error = criterion(d_fake_decision, Variable(torch.zeros([1 ]))) d_fake_error.backward() d_optimizer.step() for g_index in range (g_steps): G.zero_grad() gen_input = Variable(gi_sampler(minibatch_size, g_input_size)) g_fake_data = G(gen_input) dg_fake_decision = D(preprocess(g_fake_data.t())) g_error = criterion(dg_fake_decision, Variable(torch.ones([1 ]))) g_error.backward() g_optimizer.step() ge = extract(g_error)[0 ]
WGAN
WGAN有以下的优点,只能说太妙了!
彻底解决GAN训练不稳定的问题,不再需要小心平衡生成器和判别器的训练程度
基本解决了collapse mode的问题,确保了生成样本的多样性
训练过程中终于有一个像交叉熵、准确率这样的数值来指示训练的进程,这个数值越小代表GAN训练得越好,代表生成器产生的图像质量越高(如题图所示)
以上一切好处不需要精心设计的网络架构,最简单的多层全连接网络就可以做到
WGAN的改进只有如下几点:
判别器最后一层去掉sigmoid
生成器和判别器的loss不取log
每次更新判别器的参数之后把它们的绝对值截断到不超过一个固定常数c
不要用基于动量的优化算法(包括momentum和Adam),推荐RMSProp,SGD也行
前两点的其实就是不再使用JS散度,而第三点用于工程实训上保证判别器的目标函数平滑。最后一点其实是在实验中发现的,当使用Adam之类涉及动量的梯度下降算法时,判别器的损失可能会出现大幅度抖动的现象,而使用RMSProb或SGD算法后,这个问题就不会出现。 ·
更多的内容可以参考:令人拍案叫绝的Wasserstein GAN
WGAN损失函数部分代码
Code from: https://github.com/martinarjovsky/WassersteinGAN/blob/master/main.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 while i < len (dataloader): for p in netD.parameters(): p.requires_grad = True if gen_iterations < 25 or gen_iterations % 500 == 0 : Diters = 100 else : Diters = opt.Diters j = 0 while j < Diters and i < len (dataloader): j += 1 data = data_iter.next () i += 1 real_cpu, _ = data netD.zero_grad() batch_size = real_cpu.size(0 ) if opt.cuda: real_cpu = real_cpu.cuda() input .resize_as_(real_cpu).copy_(real_cpu) inputv = Variable(input ) errD_real = netD(inputv) errD_real.backward(one) noise.resize_(opt.batchSize, nz, 1 , 1 ).normal_(0 , 1 ) noisev = Variable(noise, volatile = True ) fake = Variable(netG(noisev).data) inputv = fake errD_fake = netD(inputv) errD_fake.backward(mone) errD = errD_real - errD_fake optimizerD.step() for p in netD.parameters(): p.data.clamp_(opt.clamp_lower, opt.clamp_upper) for p in netD.parameters(): p.requires_grad = False netG.zero_grad() noise.resize_(opt.batchSize, nz, 1 , 1 ).normal_(0 , 1 ) noisev = Variable(noise) fake = netG(noisev) errG = netD(fake) errG.backward(one) optimizerG.step() gen_iterations += 1
WGAN-GP损失函数部分代码
Code from:https://github.com/caogang/wgan-gp/blob/master/gan_mnist.py
计算gradient_penalty部分的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def calc_gradient_penalty (netD, real_data, fake_data ): alpha = torch.rand(BATCH_SIZE, 1 ) alpha = alpha.expand(real_data.size()) alpha = alpha.cuda(gpu) if use_cuda else alpha interpolates = alpha * real_data + ((1 - alpha) * fake_data) if use_cuda: interpolates = interpolates.cuda(gpu) interpolates = autograd.Variable(interpolates, requires_grad=True ) disc_interpolates = netD(interpolates) gradients = autograd.grad(outputs=disc_interpolates, inputs=interpolates, grad_outputs=torch.ones(disc_interpolates.size()).cuda(gpu) if use_cuda else torch.ones( disc_interpolates.size()), create_graph=True , retain_graph=True , only_inputs=True )[0 ] gradient_penalty = ((gradients.norm(2 , dim=1 ) - 1 ) ** 2 ).mean() * LAMBDA return gradient_penalty
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 for p in netD.parameters(): p.requires_grad = True for iter_d in xrange(CRITIC_ITERS): _data = data.next () real_data = torch.Tensor(_data) if use_cuda: real_data = real_data.cuda(gpu) real_data_v = autograd.Variable(real_data) netD.zero_grad() D_real = netD(real_data_v) D_real = D_real.mean() D_real.backward(mone) noise = torch.randn(BATCH_SIZE, 128 ) if use_cuda: noise = noise.cuda(gpu) noisev = autograd.Variable(noise, volatile=True ) fake = autograd.Variable(netG(noisev).data) inputv = fake D_fake = netD(inputv) D_fake = D_fake.mean() D_fake.backward(one) gradient_penalty = calc_gradient_penalty(netD, real_data_v.data, fake.data) gradient_penalty.backward() D_cost = D_fake - D_real + gradient_penalty Wasserstein_D = D_real - D_fake optimizerD.step() for p in netD.parameters(): p.requires_grad = False netG.zero_grad() noise = torch.randn(BATCH_SIZE, 128 ) if use_cuda: noise = noise.cuda(gpu) noisev = autograd.Variable(noise) fake = netG(noisev) G = netD(fake) G = G.mean() G.backward(mone) G_cost = -G optimizerG.step()
SN-GAN
WGAN-GP使用graident penalty的方法来限制判别器,但是这种方法只能对生成数据分布与真实数据分布之间的分布空间的数据做梯度惩罚,无法对整个空间的数据都做惩罚,这会导致随着训练的进行,生成数据分布与真实数据分布之间的空间会逐渐变化,从而导致graident penalty正则化方法不稳定,在实验中,当我们使用一个比较大的学习率去训练WGAN-GP的时候,WGAN-GP的表现并不稳定。而且因为WGAN-GP涉及比较多的运算,所以训练的过程也比较耗时。
SN-GAN提出用Spectral Normalization方法来让判别器D满足Lipschitz约束。简单的说,SN-GAN只需要改变判别器权值的最大奇异值。
奇异值是矩阵里面的概念,一般通过奇异值分解定理求得。设A为mxn阶矩阵,q=min(m,n),AxA的q个非负特征值的算术平方根叫做A的奇异值。
L约束
所以,大多数时候我们都希望模型对输入扰动是不敏感的,这通常能提高模型的泛化性能。也就是说,我们希望 很小时:
也尽可能地小。当然,“尽可能”究竟是怎样,谁也说不准。于是Lipschitz提出了一个更具体的约束,那就是存在某个常数CC(它只与参数有关,与输入无关),使得下式恒成立
换言之,在这里我们认为满足L约束的模型才是一个好模型并且对于具体的模型,我们希望估算出C(w)的表达式,并且希望C(w)越小越好,越小意味着它对输入扰动越不敏感,泛化性越好。
经过一番数学推导我们可以得到F函数是C的一个具体值:
为了让一个模型更好的服从Lipschitz约束,即让模型更加的平滑,就应当最小化参数C。我们可以将 作为一个惩罚项带入普通监督模型的损失函数中,以此来让模型更加平滑。
将 代入上式。
这其实就是一个 正则项。从而可以的出来一个结论,即一个神经网络模型添加了 正则项之后,模型的泛华能力以及抗干扰能力会更强,这样符合常识,前面的内容就是从数据的角度证明了这个常识背后的机理。
SN-GAN loss
SN-GAN中使用了一个叫做Spectral Normalization的方法非常简单。就是判别器的所有权重都进行除以谱范数的操作即 , 这样做之所以有效的原因和F函数是一样的。
我们知道传统的GAN如果不加上Lipschitz约束,判别器就会被无线优化,导致判别器与生成器能力之间失衡,造成GAN难以训练,而WGAN,WGAN-GP都通过不同的方式让GAN的判别器服从Lipschitz约束,但是都有各自的问题。其中WGAN-GP梯度惩罚的方式可以满足比较多的情况,但是训练比较慢,随着训练的进行,梯度会出现波动。还有一个值得关注的问题,就是对于类别数据训练,WGAN-GP得不到比较理想的效果,这是因为梯度惩罚方式只针对生成数据分布于真实数据分布之间的空间分布中的数据进行梯度政法,无视其他空间。这种方式使得 它难以处理多类别数据,多类别数据在空间分布中是多样的,因此WGAN0-GP就不知道到底把哪里作为惩罚空间。从而得不到比较好的效果。
对SN-GAN而言,它将谱正则化的思想运用到GAN中,从而提出了谱归一化,通过谱归一化的方式让GAN满足1-Lipschitz约束。
你可以通过公式证明 是严格服从1-Lipschitz约束的。
在训练的过程中,因为直接计算谱范数 是比较耗时的,为了让模型训练的时候速度比较快,就需要使用一个技巧。power iteration方法通过迭代计算的思想可以比较快速地计算出谱范数的近似值。
所谓的power iteration就是通过下面的迭代格式进行迭代计算。
若干次迭代后,就可以得到谱范数的近似值。
部分关键代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def _l2normalize (v, eps=1e-12 ): return v / (torch.norm(v) + eps) def max_singular_value (W, u=None , Ip=1 ): """ power iteration for weight parameter """ if not Ip >= 1 : raise ValueError("Power iteration should be a positive integer" ) if u is None : u = torch.FloatTensor(1 , W.size(0 )).normal_(0 , 1 ).cuda() _u = u for _ in range (Ip): _v = _l2normalize(torch.matmul(_u, W.data), eps=1e-12 ) _u = _l2normalize(torch.matmul(_v, torch.transpose(W.data, 0 , 1 )), eps=1e-12 ) sigma = torch.sum (F.linear(_u, torch.transpose(W.data, 0 , 1 )) * _v) return sigma, _u
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class SNConv2d (conv._ConvNd ): def __init__ (self, in_channels, out_channels, kernel_size, stride=1 , padding=0 , dilation=1 , groups=1 , bias=True ): """ 在计算卷积的时候对权重添加惩罚项 """ kernel_size = _pair(kernel_size) stride = _pair(stride) padding = _pair(padding) dilation = _pair(dilation) super (SNConv2d, self).__init__( in_channels, out_channels, kernel_size, stride, padding, dilation, False , _pair(0 ), groups, bias) self.register_buffer('u' , torch.Tensor(1 , out_channels).normal_()) @property def W_ (self ): w_mat = self.weight.view(self.weight.size(0 ), -1 ) sigma, _u = max_singular_value(w_mat, self.u) self.u.copy_(_u) return self.weight / sigma def forward (self, input ): return F.conv2d(input , self.W_, self.bias, self.stride, self.padding, self.dilation, self.groups)