Wasserstein Divergence for GANs
Wasserstein Divergence for GANs
參考自:
为什么还要有WGAN-div?
尽管看起来WGAN-gp已无可挑剔,但应该看出来到底它还是有缺陷的。回看之前的WGAN-gp,你会发现它的梯度非常不稳定,总是上下大幅度波动。
我们来回顾一下WGAN和WGAN-GP对权重矩阵的处理:
WGAN采用的措施是裁剪权重,即将每一个权重都裁剪到的范围内,其中是自由设定的常数。但这个做法是非常朴素的做法,而且经过实验发现表现的性能并不太令人满意,所以现在也基本上不用了。
WGAN-GP采用的措施是梯度惩罚,通过给判别器梯度增加惩罚来限制判别器的学习能力,避免判别器训练过强的情况。由于可以由保证,因此提出了相应的损失函数:
但我们要求在每一处都成立,因此真实分布应当是全空间的均匀分布,但这很难做到。不过作者也采取了一种做法:在真假样本之间随机插值来惩罚。这样就可以保证真假样本之间的过渡区域满足Lipschitz约束。这种思路非常直接,但它是一种经验方案,并没有更完备的理论支撑。而WGAN-div论文指出这一公式不总是有梯度。「具体情况可以直接去论文内翻看,日后我理解明白了再在这里写出来。」
在这样的情况下,W散度就被提了出来,可以去掉Lipschitz约束条件,还保留了Wasserstein距离的良好性质。
另外,值得一提的是WGAN衍生版本中,WGAN-div的数学理论支撑是最完备的。论文很值得一读,如果有时间,最好推导一下作者的数学论证。
WGAN-div的理论依据是什么?
作者提出了一种方案,将判别器的表达式推导出来:
接着推导了生成器的Loss表达式:
目标函数也随之而确定下来:
其中。
- 是个对称的散度。散度的意思是:且,它跟“距离”的差别是它不一定满足三角不等式,也有叫做“半度量”、“半距离”的。是一个散度,这已经非常棒了,因为我们大多数GAN都只是在优化某个散度而已。散度意味着当我们最小化它时,我们真正是在缩小两个分布的距离。
- 的最优解跟距离有一定的联系。判别器公式就是一个特殊的。这说明当我们最大化得到之后,可以去掉梯度项,通过最小化生成器损失函数来训练生成器。这也表明以为目标,性质跟距离类似,不会有梯度消失的问题。
- 作者证明了不总是一个散度。当时就是WGAN-GP的梯度惩罚,作者说它不是一个散度。不是散度意味着WGAN-GP在训练判别器的时候,并非总是会在拉大两个分布的距离(鉴别者在偷懒,没有好好提升自己的鉴别技能),从而使得训练生成器时回传的梯度不准。
GAN-div将如何实现?
我们再回顾一下判别器和生成器的损失函数:
前面的代码和Original GAN一样,几无差别,而在绘制图像这个函数的后面需要再接着写:
def weights_init(m):
if isinstance(m, nn.Linear):
# m.weight.data.normal_(0.0, 0.02)
nn.init.kaiming_normal_(m.weight)
m.bias.data.fill_(0)
这个用来初始化网络结构的权重矩阵。
接着这里我反复琢磨、理解论文,然后写出了这个惩罚项:
def gradient_penalty(D, x_r, x_f, p):
t = torch.rand(batchsz, 1).cuda()
t = t.expand_as(x_r)
x_hat = (1 - t) * x_r + t * x_f
x_hat.requires_grad_(True)
pred = D(x_hat)
grads = autograd.grad(outputs=pred,
inputs=x_hat,
grad_outputs=torch.ones_like(pred),
create_graph=True,
retain_graph=True,
only_inputs=True)[0]
gp = torch.pow(torch.square(grads), p/2)
return gp.mean()
接着在main主函数里,我做的改动还是蛮大的,不过也是严格按照论文所说去做,实现的代码如下:
def main():
k = 2
p = 6
torch.manual_seed(23)
np.random.seed(23)
data_iter = data_generator()
x = next(data_iter)
G = Generator().cuda()
G.apply(weights_init)
D = Discriminator().cuda()
D.apply(weights_init)
optim_G = optim.Adam(G.parameters(), lr=5e-6, betas=(0.5, 0.9))
optim_D = optim.Adam(D.parameters(), lr=5e-6, betas=(0.5, 0.9))
print('batch:', next(data_iter).shape)
viz.line([[0, 0]], [0], win='loss', opts=dict(title='loss', legend=['D', 'G']))
for epoch in range(50000):
for _ in range(15):
x_r = next(data_iter)
x_r = torch.from_numpy(x_r).cuda()
pred_r = D(x_r)
loss_r = pred_r.mean()
z = torch.randn(batchsz, 2).cuda()
x_f = G(z).detach()
pred_f = D(x_f)
loss_f = pred_f.mean()
gp = gradient_penalty(D, x_r, x_f, p)
loss_D = loss_r - loss_f + k * gp # 论文给出的公式如此
optim_D.zero_grad()
loss_D.backward()
optim_D.step()
z = torch.randn(batchsz, 2).cuda()
x_fake = G(z)
pred_fake = D(x_fake)
loss_G = pred_fake.mean() # 论文给出的公式亦如此
optim_G.zero_grad()
loss_G.backward()
optim_G.step()
if epoch % 10 == 0:
viz.line([[loss_D.item(), loss_G.item()]], [epoch], win='loss', update='append')
generate_image(D, G, x_r, epoch)
print(loss_D.item(), loss_G.item())
最后再直接执行主函数调用一下就可以了:
if __name__ == '__main__':
main()
最终的实现效果如图:
“睿智”作者的话(不重要)
这花了我三四天的功夫,终于是改完了这一堆bug,都不知道经历了多少辛酸,真是一言难尽……不过这是我尝试复现并获得成功的第一篇论文,心里还是蛮自豪的,之前遭的罪吃的苦也就不算什么了!只希望往后继续学习的话会顺利一些吧!