GAN

 

前置知识

  1. restricted Boltzmann machines (RBMs)
  2. deep Boltzmann machines (DBMs)
  3. Markov chain Monte Carlo (MCMC) methods
  4. Deep belief networks (DBNs)
  5. normalized/unnormalized
  6. partition function
  7. directed layer/undirected layer
  8. approximate
  9. layer-wise/piece-wise/element-wise
  10. bound the log-likelihood
  11. score matching
  12. noise-contrastive estimation (NCE)
  13. tractable/intractable
  14. probability density
  15. denoising auto-encoders
  16. generative stochastic network (GSN)
  17. unbounded activation
  18. auto-encoding variational Bayes
  19. stochastic backpropagation
  20. multilayer perceptrons
  21. differentiable function
  22. value function
  23. SML/PCD
  24. KL散度/JS散度

理论分析

解决什么问题?

学习 probability distribution 的一种方法是学习它的 density 。但是 low dimensional manifolds 的 distribution 的 density 经常是不存在的。此时就不能用 极大似然估计最小化KL散度 求 density。【AB,2017】。典型的修正方法是,对 distribution 添加 noise 。而 noise 会降低采样质量。

另一种方法是,用先验概率 $p(z)$ 定义随机变量 $\mathtt{Z}$ ,参数化函数 $g_\theta$ 使它映射到 $\mathtt{X}$ ,$\mathtt{X}$ 的 distribution 是 $\mathbb{P}\theta$ ,$\mathtt{X}$ 的 density 不一定存在,但是可以通过改变 $g\theta$ 来控制 $\mathtt{X}$ ,换句话说,可以用 $p(z)$ 和 $g_\theta$ 来定义 $\mathtt{X}$ 。Variational Auto-Encoder (VAE) 和 Generative Adversarial Network (GAN) 是这种方法的两个例子。

data generating distribution : $p_{data}(\vec x)$
generator’s distribution over data : $p_{g}(\vec x)$ input noise variables : $p_{z}(\vec z)$
a mapping from z to data space : $G(\vec z;\theta_g)$
the probability that x come from data rather than G : $D(\vec x;\theta_d)$

训练 $G$ 最小化 $min(\mathbb{E}{z~p(z)}[log(1-D(G(\vec z)))])$ 或 $min(\mathbb{E}{z~p(z)}[-logD(G(\vec z))])$ ;同时,训练 $D$ 最大化正确地分辨数据来自 data 还是 $G$ ,最大化 ${\mathbb{E}\vec x}~{p_g(\vec x)}[logD(\vec x)]+{\mathbb{E}\vec z}~{p_z(\vec z)}[log(1-D(G(\vec z))]$ ;最终得到 $p_g=p_{data}$ 的全局最优解。

当 $G$ 固定时,最优的 $D$ (表示为$D^*G$)是$\frac{p{data}(x)}{p_{data}(x)+p_g(x)}$

证明:

  1. 展开[式1]。
  2. 因为 $\vec x=G(\vec z)$ ,所以:${\mathbb{E}\vec z}~{p_z(\vec z)}[log(1-D(G(\vec z)))]$ = ${\mathbb{E}\vec x}~{p_g(\vec x)}[log(1-D(\vec x))]$ 。得到[式3]
  3. 因为函数$alog(y)+blog(1-y)$在 $\frac{a}{a+b}$ 处取得最大值,所以:得证。

把 $D^*_G=$[式2]代入[式1],得到 $C(G)$=[式4]。

当且仅当 $p_g=p_{data}$ 时,$C(G)=-log4$

证明:

  1. 当 $p_g=p_{data}$ 时,$D^*_G=1/2$,代入[式4],得到 $C(G)=-log4$。

$-log4$是$C(G)$的最小值

证明:

  1. 当 $p_g=p_{data}$ 时,有 ${\mathbb{E}\vec x}~{p_{data}}[-log2]+{\mathbb{E}\vec x}~{p_{g}}[-log2]=-log4$ 。
  2. $C(G)$=[式4] 减去上式,得[式5]。
  3. KL散度 换成 JS散度 ,得[式6]。
  4. 因为 JS散度 恒非负,所以:$C(G)$ 的最小值为$-log4$ 。

[算法1]的收敛性

如果:

  1. $G$ 和 $D$ 有足够的 capacity
  2. 在[算法1]的每次迭代中,对给定的 $G$ , $D$ 都能达到最优;
  3. 不断地更新 $p_g$ 以最小化 ${\mathbb{E}\vec x}~{p_{data}}[logD^_G(\vec x)]+{\mathbb{E}_\vec x}~_{p_{g}}[log(1-D^_G(\vec x)]$;

那么:

$p_g$ 收敛于 $p_{data}$。

证明:

  1. 考虑[式3](表示为 $U(p_g,D)$ ),它对 $p_g$ 是凸的;
  2. 如果,那么;
  3. 因为 $sup_DU(p_g,D)$ 对 $p_g$ 是凸的,所以:得证。

网络结构

Generator

数据集的图片的 shape=[1, 28, 28],1个 Batch 有64张图片,所以1个 Batch 的 shape=[64, 1, 28, 28]。

输入数据(Sample noise)的 shape=[100],这个是自由设定的。所以1个 Batch 的 shape=[64, 100]。

4个隐藏层,每1层由 全连接层 BN层 ReLU激活函数 组成;输出层由 全连接层 tanh激活函数 组成。

输入层的节点有 100 个,与输入数据的 shape相同;隐藏层的节点数量为:128、256、512、1024;输出层的节点有 $1\times28\times28=784$ ,是数据集的图片的 shape 连乘。

输出数据时会调用 torch.Tensor.view(*shape) 改变 shape ,1个 Batch 的 shape=[64, 1, 28, 28]。

损失函数使用 二值交叉熵(Binary Cross Entropy, BCE)

优化器使用 Adam

Discriminator

输入数据是 Generator的输出数据数据集中的图片 ,会先把 shape 改为 [1, 28, 28],1个 Batch 的 shape=[64, 1, 28, 28]。

2个隐藏层,每1层由 全连接层 ReLU激活函数 组成;输出层由 全连接层 Sigmoid激活函数 组成。

输入层的节点有 784 个;隐藏层的节点数量为:512、256;输出层的节点有 1 个。

输出数据是 1 个表示概率的标量。

损失函数使用 BCE

优化器使用 Adam

代码实现

运行报错 OMP: Error #15

如果Python是基本于Conda安装的,则Conda上的numpy包中的mkl很容易与系统内库发生冲突,可选择update numpy package in Conda或者设置为系统库。把numpy的版本降为1.16.5就行了。

line 43 :

layers = [nn.Linear(in_feat, out_feat)]

torch.nn.Linear(in_features, out_features, bias=True)

对输入数据施加 线性变换 $y=xA^T+b$,相当于 全连接层全连接层 的输入数据和输出数据都是 二维张量(2D) ,shape=[batch_size, size],所以in_featuresout_features 都是 标量

in_features

输入数据的 size。

out_features

输出数据的 size ,也是 全连接层 的神经元数量。

bias

为 True 时,计算 $b$ ;否则不计算。

line 45 :

layers.append(nn.BatchNorm1d(out_feat, 0.8))

torch.nn.BatchNorm1d(num_features, eps=1e-0.5, momentum=0.1, affine=True, track_running_stats=True)

2D3D 输入数据施加 Batch Normalization

num_features

3D 的数据是 (N, C, L),N是 batch size,C是 channel,L是数据的 size,num_features应取C。2D 的数据是 (N, L),N是 batch size,L是数据的 size,等价于每条数据只有1个 channel,num_features应取L。

eps

对输入数据进行 归一化 时加在分母上,防止除零。

momentum

更新 全局均值方差 时使用该值进行平滑。

affine

为 True 时,学习 $\gamma$ 和 $\beta$ ,变量名是 weight 和 bias;否则不学习。

track_running_stats

为 True 时,BN层会统计 全局均值方差 ;否则不统计。

Internal Covariate Shift

训练深度网络的时候经常发生训练困难的问题,因为,每一次参数更新后,上一层网络的输出数据经过这一层网络计算后,数据的分布会发生变化,为下一层网络的学习带来困难,此现象称为 Internal Covariate Shift 。另外,它还会导致 梯度消失 问题。

Covariate Shift

Internal Covariate Shift 发生在神经网络内部,Covariate Shift 发生在输入数据上。由于训练数据和测试数据的分布有差异,会给网络的泛化和训练带来影响,解决的办法是 归一化白化

Batch Normalization

在训练过程中,在每一层的激活函数之前都把输入数据强制拉回到 正态分布 ,以使网络的每一层的输入保持相同的分布。 \(y=\frac{x-\mathbb{E}[x]}{\sqrt{\mathtt{Var}[x]+\epsilon}}*\gamma+\beta\)

line 38 :

class Generator(nn.Module):

自定义网络需要继承 torch.nn.Module 。重写 __init__(self) ,构造各个层;重写 forward(*input) ,调用各个层。

add_module(name, module)

add_module(‘conv1’, nn.Conv2d(1, 20, 5)) 等价于 self.conv1 = nn.Conv2d(1, 20, 5) 。

apply(fn)

每个 subModule 和它自己都施加一下这个函数。常用于初始化 parameter

cpu()

把所有 parameterbuffer 移动到 CPU。

cuda(device=None)

把所有 parameterbuffer 移动到 GPU。需要在构造 optimizer 之前调用。device 是设备ID。

zero_grad(set_to_none=False)

把所有 moduleparametergradient 设置为 0 。

line 46 :

layers.append(nn.LeakyReLU(0.2, inplace=True))

Torch.nn.LeakyReLU(negative_slope=0.01, inplace=False)

激活函数 \(LeakyReLU(x)=max(0, x)+negative\_slope*min(0, x)\)

line 49 :

self.model = nn.Sequential(
            *block(opt.latent_dim, 128, normalize=False),
            *block(128, 256),
            *block(256, 512),
            *block(512, 1024),
            nn.Linear(1024, int(np.prod(img_shape))),
            nn.Tanh()
        )

torch.nn.Sequential(*args)

序列容器,继承自 Module 。可以传入多个 Module ,或者包含多个 ModuleOrderedDict ,返回 1 个 Sequential

numpy.prod(a, axis=None, dtype=None, out=None, keepdims=, initial=, where=)

torch.nn.Tanh()

激活函数 \(Tanh(x)=tanh(x)=\frac{exp(x)-exp(-x)}{exp(x)+exp(-x)}\)

line 58 :

def forward(self, z):
	img = self.model(z)
	img = img.view(img.size(0), *img_shape)
	return img

Module()

把实例当作函数调用,相当于调用实例的 __call__() 。这里就相当于调用模型的 forward(*input)

torch.Tensor.view(*shape)

返回改变了 shapetensor ,与原 tensor 共享内存。当某一维是 -1 时会自动计算它的大小。

line 68 :

self.model = nn.Sequential(
            nn.Linear(int(np.prod(img_shape)), 512),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(256, 1),
            nn.Sigmoid(),
        )

torch.nn.Sigmoid()

激活函数 \(Sigmoid(x)=\sigma(x)=\frac{1}{1+exp(-x)}\)

line 85 :

adversarial_loss = torch.nn.BCELoss()

torch.nn.BCELoss(weight=None, size_average=None, reduce=None, reduction=’mean’)

使用 二值交叉熵(Binary Cross Entropy) 作为损失函数。Batch size 为 N 时的计算公式为: \(l(x, y)=L=\{l_1, ..., l_N\}^T,\quad l_n=-w_n[y_n\cdot \log{x_n}+(1-y_n)\cdot \log{(1-x_n)}]\) 如果 reduction 为 ‘mean’ ,则 $l(x, y)=mean(L)$ ;如果 reduction 为 ‘sum’ ,则 $l(x, y)=sum(L)$ 。

y 必须是 0~1 的值。

line 98 :

dataloader = torch.utils.data.DataLoader(
    datasets.MNIST(
        "../../data/mnist",
        train=True,
        download=False,
        transform=transforms.Compose(
            [transforms.Resize(opt.img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]
        ),
    ),
    batch_size=opt.batch_size,
    shuffle=True,
)

torchvision.transforms 模块

用于对图像进行预处理。

transforms.Compose(*transforms)

把多个transform操作连起来。

transforms.Resize(*size)

修改图片的 size 。

transforms.ToTensor()

PIL图像numpy.ndarray 转换为 tensor ,shape 从 [H, W, C] 变为 [C, H, W] ,并把数值做 归一化 处理。

transforms.Normalize(mean, std, inplace=False)

标准化 处理。mean 和 std 都是 list,list 的 size 应该等于 channel 的数量。

line 112 :

optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))

Training :

# 创建 BCE计算器
adversarial_loss = torch.nn.BCELoss()
# 创建 generator
generator = Generator()
# 创建 discriminator
discriminator = Discriminator()

# 获取数据

# 创建 generator 的优化器
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
# 创建 discriminator 的优化器
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))

# 迭代epoch
for epoch in range(opt.n_epochs):
		# 迭代batch
    for i, (imgs, _) in enumerate(dataloader):

        # 创建 shape=[64, 1] 的 tensor,数值都初始化为 1.0,并用它创建 Variable,用于计算 loss
        valid = Variable(Tensor(imgs.size(0), 1).fill_(1.0), requires_grad=False)
        # 创建 shape=[64, 1] 的 tensor,数值都初始化为 0.0,并用它创建 Variable,用于计算 loss
        fake = Variable(Tensor(imgs.size(0), 1).fill_(0.0), requires_grad=False)

        # 把图片强转为 tensor,shape=[64, 1, 28, 28],并用它创建 Variable
        real_imgs = Variable(imgs.type(Tensor))
        
        # ---------------------
        #  Train Generator
        # ---------------------
        
        # 优化器的数值都初始化为 0
        optimizer_G.zero_grad()

        # 从(0, 1)正态分布中采样出 generator 的输入数据
        z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim))))

        # forward:用 generator 生成图片
        gen_imgs = generator(z)

        # 计算 discriminator 判断 生成图片 为真的 loss
        g_loss = adversarial_loss(discriminator(gen_imgs), valid)

				# backward:计算 generator 的 parsmeters 的梯度
        g_loss.backward()
        # 更新 generator 的 parsmeters
        optimizer_G.step()

        # ---------------------
        #  Train Discriminator
        # ---------------------

        optimizer_D.zero_grad()

        # 计算 discriminator 判断 真图片 为真的 loss
        real_loss = adversarial_loss(discriminator(real_imgs), valid)
        # 计算 discriminator 判断 生成图片 为假的 loss
        fake_loss = adversarial_loss(discriminator(gen_imgs.detach()), fake)
        # discriminator 的判断能力取两者平均数
        d_loss = (real_loss + fake_loss) / 2

				# backward:计算 discriminator 的 parsmeters 的梯度
        d_loss.backward()
        # 更新 discriminator 的 parsmeters
        optimizer_D.step()
				
				# 保存 生成图片
        save_image(...)