ICLR'2017的投稿里,有一篇很有意思但被拒掉的投稿《Sampling Generative Networks》 by Tom White(https://openreview.net/forum?id=SypU81Ole)。文章比较松散地讲了一些在latent space挺有用的采样和可视化技巧,其中一个重要的点是指出在GAN的latent space中,比起常用的线性插值,沿着两个采样点之间的“弧”进行插值是更合理的办法。实现的方法就是图形学里的 Slerp(spherical linear interpolation)在高维空间中的延伸:
形象理解并不难,以 wiki 上的图为例:
要求的是图中 P 点,在 Wiki 图的基础上我加了 A、B 和 O 三个点,O 就是原点。所以 P 其实就是 。先考虑求 ,第一步引入和 垂直的 ,也就是图中蓝色的箭头。那么 和 的比值就等于他们分别投影到蓝色向量上的部分的比值,也就是蓝色箭头两侧的橙色线段比红色线段。这个比值正是 ,于是 就是 。很显然,同样的方法也可以用来求 ,然后代入 ,并令 ,就得到了 Slerp 的公式。注意虽然推导的时候用的虽然是 和 在同一个(超)球面上,但是实际用的时候不同长度的 和 之间利用 Slerp 也是可以很自然的插值的,得到的向量长度介于二者之间且单调(非线性)增减。ICLR 的 Review 中也讨论到了这个问题。
使用Slerp比起纯线性插值的好处在哪里呢?作者原文这样解释:
Frequently linear interpolation is used, which is easily understood and implemented. But this is often inappropriate as the latent spaces of most generative models are high dimensional (> 50 dimensions) with a Gaussian or uniform prior. In such a space, linear interpolation traverses locations that are extremely unlikely given the prior. As a concrete example, consider a 100 dimensional space with the Gaussian prior µ=0, σ=1. Here all random vectors will generally a length very close to 10 (standard deviation < 1). However, linearly interpolating between any two will usually result in a「tent-pole」 effect as the magnitude of the vector decreases from roughly 10 to 7 at the midpoint, which is over 4 standard deviations away from the expected length.
左边是在 latent space 里随机采样的样本到中心距离的分布,右边是原点在随机采样的两个样本所在直线上的投影点到中心距离的分布,也就是线性插值中到中心最近点的距离的分布。可以看到随机采样并进行线性插值的办法还真的是容易路过样本几乎不可能出现的区域(距原点距离 5~7.5)。可是《Sampling Generative Networks》(https://openreview.net/forum?id=SypU81Ole)被拒的 comment 里有一句:「neither the reviewers nor I were convinced that spherical interpolation makes more sense than linear interpolation」。就这一点来说,感觉 Tom White 有些冤枉,虽然确实不是什么眼前一亮的大改进,但是有理有据。那为什么 reviewer 们没觉得比线性插值好多少呢?原因可能就是:
经过一番分析,得到了一个好像也没什么用的结论。再回来看最初的问题,线性插值会路过低概率区域(虽然并没有什么影响),Slerp 比线性插值也没什么视觉上的本质提高,那么有没有什么更优雅地行走在 latent space 的方法呢?我觉得是:Great Circle。比起 Slerp,Greate Circle 通常要经过多 3 倍的距离,虽然这和 Slerp 其实也没什么本质区别,但是感觉上要更屌,而且沿着 great circle 走起点和终点是同一个点,这感觉更屌。
产生 great circle 路径比 Slerp 要简单得多:1)根据所使用分布产生一个超球面半径r(按前面讨论,Gaussian 的话就是 chi 分布,或者 Gaussian 近似);2)产生一个随机向量 u 和一个与 u 垂直的随机向量 v ,然后把 u 和 v 所在平面作为 great circle 所在平面;3)u 和 v 等效于一个坐标系的两轴,所以 great circle 上任一点就用在 u 和 v 上的投影表示就可以,最后在乘上 r 就得到了行走在 great circle 上的采样。代码如下:
from __future__ import print_function import argparse import os import numpy from scipy.stats import chi import torch.utils.data from torch.autograd import Variable from networks import NetG from PIL import Image
parser = argparse.ArgumentParser() parser.add_argument('--nz', type=int, default=100, help='size of the latent z vector') parser.add_argument('--niter', type=int, default=10, help='how many paths') parser.add_argument('--n_steps', type=int, default=23, help='steps to walk') parser.add_argument('--ngf', type=int, default=64) parser.add_argument('--ngpu', type=int, default=1, help='number of GPUs to use') parser.add_argument('--netG', default='netG_epoch_49.pth', help="trained params for G")
forj in range(opt.niter): # step 1 r = chi.rvs(df=100)
# step 2 u = numpy.random.normal(0, 1, nz) w = numpy.random.normal(0, 1, nz) u /= numpy.linalg.norm(u) w /= numpy.linalg.norm(w)
v = w - numpy.dot(u, w) * u v /= numpy.linalg.norm(v)
ndimgs = [] for i in range(opt.n_steps): t = float(i) / float(opt.n_steps) # step 3 z = numpy.cos(t * 2 * numpy.pi) * u + numpy.sin(t * 2 * numpy.pi) * v z *= r