GAN 理论推导及代码实现
生成对抗网络(Generative Adversarial Network,简称GAN)是无监督学习的一种方法,通过让两个神经网络相互博弈的方式进行学习。该方法由Ian J.Goodfellow等人于2014年提出。一经提出就在无监督学习领域掀起了一股热潮,自2014年来与GAN相关论文数目每年呈指数性增长,并且目前已经有许多研究者利用GAN在某些特定领域取得了不错的进展。2018年图灵奖得主同时也是深度学习领域的著名学者Yann Lecun多次给予了GAN高度评价。
基于Maximum Likelihood Estimation的生成
在GAN之前,大部分使用监督训练的方式通过Maximum Likelihood Estimation(极大似然估计)来估计 p d a t a p_{data} pdata分布,具体做法如下:
给定一个真实数据分布 p d a t a p_{data} pdata ,我们不知道其具体形式,但是可以从其中进行采样;
假设一个由参数 θ \theta θ控制的分布 p G ( x ; θ ) p_G(x;\theta) pG(x;θ),我们想要使 p G p_G pG和 p d a t a p_{data} pdata 尽可能的接近;
从 p d a t a p_data pdata中采样 { x 1 , x 2 , x 3 . . . x m } \{x^1, x^2, x^3...x^m\} {x1,x2,x3...xm};
计算通过generator生成这批采样数据的似然: L = ∏ m i = 1 p G ( x i ; θ ) L = \prod_{m}^{i=1}p_G(x^i;\theta) L=∏mi=1pG(xi;θ) ;
寻找 θ ∗ \theta* θ∗最大化该似然;
通过下述理论推导,我们可以得到上述MLE的过程本质上是在最小化
p
d
a
t
a
p_{data}
pdata和
p
G
p_G
pG之间的KL Divergence:
θ
⋆
=
arg
max
θ
∏
i
=
1
m
p
G
(
x
i
;
θ
)
=
arg
max
θ
log
∏
i
=
1
m
p
G
(
x
i
;
θ
)
=
arg
max
θ
∑
i
=
1
m
log
p
G
(
x
i
;
θ
)
≈
arg
max
θ
E
x
∼
p
data
[
log
p
G
(
x
;
θ
)
]
⇒
arg
max
θ
∫
x
p
data
(
x
)
log
p
G
(
x
;
θ
)
d
x
−
arg
max
θ
∫
x
p
data
(
x
)
log
p
data
(
x
)
d
x
=
arg
min
θ
K
L
(
p
data
(
x
)
∥
p
G
(
x
)
)
\begin{aligned} \theta^{\star} & =\arg \max _\theta \prod_{i=1}^m p_G\left(x^i ; \theta\right) \\ & =\arg \max _\theta \log \prod_{i=1}^m p_G\left(x^i ; \theta\right) \\ & =\arg \max _\theta \sum_{i=1}^m \log p_G\left(x^i ; \theta\right) \\ & \approx \arg \max _\theta E_{x \sim p_{\text {data }}}\left[\log p_G(x ; \theta)\right] \\ & \Rightarrow \arg \max _\theta \int_x p_{\text {data }}(x) \log p_G(x ; \theta) d x-\arg \max _\theta \int_x p_{\text {data }}(x) \log p_{\text {data }}(x) d x \\ & =\arg \min _\theta K L\left(p_{\text {data }}(x) \| p_G(x)\right) \end{aligned}
θ⋆=argθmaxi=1∏mpG(xi;θ)=argθmaxlogi=1∏mpG(xi;θ)=argθmaxi=1∑mlogpG(xi;θ)≈argθmaxEx∼pdata [logpG(x;θ)]⇒argθmax∫xpdata (x)logpG(x;θ)dx−argθmax∫xpdata (x)logpdata (x)dx=argθminKL(pdata (x)∥pG(x))
基于Adversarial Net的生成
假设generator
G
G
G是一个neural network,它定义了
p
G
p_G
pG的概率分布。输入noise input vector
z
z
z ,通过G生成
x
^
\hat{x}
x^ ,很多这样的生成数据就构成了
p
G
(
x
)
p_G(x)
pG(x),然后我们需要使
p
G
(
x
)
p_G(x)
pG(x) 与
p
d
a
t
a
(
x
)
p_{data}(x)
pdata(x) 尽可能接近。如下图表示:
因此,我们通过下式寻找最优:
G
:
G
⋆
=
arg
min
C
D
i
v
(
p
G
,
p
d
a
t
a
)
G: G^{\star}=\arg \min _{C} Div(p_G, p_{data})
G:G⋆=argCminDiv(pG,pdata)
在 GAN 原论文中,有一个思想和其它很多方法都不同,即生成器 G 不需要满足可逆条件。Scott Rome 认为这一点非常重要,因为实践中 G 就是不可逆的。而很多证明笔记都忽略了这一点,他们在证明时错误地使用了积分换元公式,而积分换元却又恰好基于 G 的可逆条件。Scott 认为证明只能基于以下等式的成立性:
E
z
∼
p
z
(
z
)
log
(
1
−
D
(
G
(
z
)
)
)
=
E
x
∼
p
G
(
x
)
log
(
1
−
D
(
x
)
)
E_{z \sim p_z(z)} \log (1-D(G(z)))=E_{x \sim p_G(x)} \log (1-D(x))
Ez∼pz(z)log(1−D(G(z)))=Ex∼pG(x)log(1−D(x))
该等式来源于测度论中的 Radon-Nikodym 定理,它展示在原论文的命题 1 中,并且表达为以下等式:
∫
x
p
data
(
x
)
log
D
(
x
)
d
x
+
∫
z
p
(
z
)
log
(
1
−
D
(
G
(
z
)
)
)
d
z
=
∫
x
p
data
(
x
)
log
D
(
x
)
+
p
G
(
x
)
log
(
1
−
D
(
x
)
)
d
x
\begin{gathered} \int_x p_{\text {data }}(x) \log D(x) \mathrm{d} x+\int_z p(z) \log (1-D(G(z))) \mathrm{d} z \\ \quad=\int_x p_{\text {data }}(x) \log D(x)+p_G(x) \log (1-D(x)) \mathrm{d} x \end{gathered}
∫xpdata (x)logD(x)dx+∫zp(z)log(1−D(G(z)))dz=∫xpdata (x)logD(x)+pG(x)log(1−D(x))dx
我们看到该讲义使用了积分换元公式,但进行积分换元就必须计算 G ( − 1 ) G^{(-1)} G(−1),而 G 的逆却并没有假定为存在。并且在神经网络的实践中,它也并不存在。可能这个方法在机器学习和统计学文献中太常见了,因此我们忽略了它。
最优判别器
在极小极大博弈的第一步中,给定生成器 G,最大化 V(D,G) 而得出最优判别器 D。其中,最大化 V(D,G) 评估了
P
G
P_G
PG 和
p
d
a
t
a
p_{data}
pdata 之间的差异或距离。因为在原论文中价值函数可写为在 x 上的积分,即将数学期望展开为积分形式:
V
(
G
,
D
)
=
∫
x
p
p
data
(
x
)
log
(
D
(
x
)
)
+
p
g
(
x
)
log
(
1
−
D
(
x
)
)
d
x
V(G, D)=\int_x p_{ p_{\text {data }}}(\boldsymbol{x}) \log (D(\boldsymbol{x}))+p_g(\boldsymbol{x}) \log (1-D(\boldsymbol{x})) d x
V(G,D)=∫xppdata (x)log(D(x))+pg(x)log(1−D(x))dx
其实求积分的最大值可以转化为求被积函数的最大值。而求被积函数的最大值是为了求得最优判别器 D,因此不涉及判别器的项都可以看作为常数项。如下所示,
p
G
p_G
pG和
P
d
a
t
a
P_{data}
Pdata 都为标量,因此被积函数可表示为
a
∗
D
(
x
)
+
b
∗
l
o
g
(
1
−
D
(
x
)
)
a*D(x)+b*log(1-D(x))
a∗D(x)+b∗log(1−D(x))。
P
data
(
x
)
log
D
(
x
)
+
P
G
(
x
)
log
(
1
−
D
(
x
)
)
P_{\text {data }}(x) \log D(x)+P_G(x) \log (1-D(x))
Pdata (x)logD(x)+PG(x)log(1−D(x))
若令判别器
D
(
x
)
D(x)
D(x) 等于
y
y
y,那么被积函数可以写为:
f
(
y
)
=
a
log
y
+
b
log
(
1
−
y
)
f(y)=a \log y+b \log (1-y)
f(y)=alogy+blog(1−y)
为了找到最优的极值点,如果 a+b≠0,我们可以用以下一阶导求解:
f
′
(
y
)
=
0
⇒
a
y
−
b
1
−
y
=
0
⇒
y
=
a
a
+
b
f^{\prime}(y)=0 \Rightarrow \frac{a}{y}-\frac{b}{1-y}=0 \Rightarrow y=\frac{a}{a + b}
f′(y)=0⇒ya−1−yb=0⇒y=a+ba
如果我们继续求表达式
f
(
y
)
f(y)
f(y) 在驻点的二阶导:
f
′
′
(
a
a
+
b
)
=
−
a
(
a
a
+
b
)
2
−
b
1
−
(
a
a
+
b
)
2
<
0
f^{\prime \prime}\left(\frac{a}{a+b}\right)=-\frac{a}{\left(\frac{a}{a+b}\right)^2}-\frac{b}{1-\left(\frac{a}{a+b}\right)^2}<0
f′′(a+ba)=−(a+ba)2a−1−(a+ba)2b<0
其中 a , b ∈ ( 0 , 1 ) a,b \in(0,1) a,b∈(0,1)。因为一阶导等于零、二阶导小于零,所以我们知道 a / ( a + b ) a/(a+b) a/(a+b)为极大值。若将 a = p d a t a a=p_{data} a=pdata 、 b = P G ( x ) b=P_G(x) b=PG(x)代入该极值,那么最优判别器 D ( x ) = P d a t a ( x ) / ( P d a t a ( x ) + P G ( x ) ) D(x)=P_{data}(x)/(P_{data}(x)+P_G(x)) D(x)=Pdata(x)/(Pdata(x)+PG(x))。
最后我们可以将价值函数表达式写为:
V ( G , D ) = ∫ x p data ( x ) log D ( x ) + p G ( x ) log ( 1 − D ( x ) ) d x ≤ ∫ x max y p data ( x ) log y + p G ( x ) log ( 1 − y ) d x \begin{aligned} & V(G, D)=\int_x p_{\text {data }}(x) \log D(x)+p_G(x) \log (1-D(x)) \mathrm{d} x \\ & \quad \leq \int_x \max _y p_{\text {data }}(x) \log y+p_G(x) \log (1-y) \mathrm{d} x \end{aligned} V(G,D)=∫xpdata (x)logD(x)+pG(x)log(1−D(x))dx≤∫xymaxpdata (x)logy+pG(x)log(1−y)dx
如果我们令 D ( x ) = P d a t a / ( P d a t a + P G ) D(x)=P_{data}/(P_{data}+P_G) D(x)=Pdata/(Pdata+PG),那么我们就可以令价值函数 V(G,D) 取极大值。因为 f ( y ) f(y) f(y) 在定义域内有唯一的极大值,最优 D D D 也是唯一的,并且没有其它的 D D D 能实现极大值。
其实该最优的 D D D 在实践中并不是可计算的,但在数学上十分重要。我们并不知道先验的 P d a t a P_{data} Pdata,所以我们在训练中永远不会用到它。另一方面,它的存在令我们可以证明最优的 G G G 是存在的,并且在训练中我们只需要逼近 D D D。
最优生成器
当然 GAN 过程的目标是令
P
G
=
P
d
a
t
a
P_G=P_{data}
PG=Pdata。这对最优的
D
D
D 意味着什么呢?我们可以将这一等式代入
D
G
∗
D_G*
DG∗的表达式中:
D
G
∗
=
p
data
p
data
+
p
G
=
1
2
D_G^*=\frac{p_{\text {data }}}{p_{\text {data }}+p_G}=\frac{1}{2}
DG∗=pdata +pGpdata =21
这意味着判别器已经完全困惑了,它完全分辨不出 P d a t a P_{data} Pdata 和 P G P_G PG 的区别,即判断样本来自 P d a t a P_{data} Pdata和 P G P_G PG 的概率都为 1/2。基于这一观点,GAN 作者证明了 G 就是极小极大博弈的解。该定理如下:
「当且仅当 P G = P d a t a P_G=P_{data} PG=Pdata,训练标准 C ( G ) = m a x V ( G , D ) C(G)=maxV(G,D) C(G)=maxV(G,D)的全局最小点可以达到。」
以上定理即极大极小博弈的第二步,求令 V(G,D) 最小的生成器 G(其中 G代表最优的判别器)。之所以当 P G ( x ) = P d a t a P_G(x)=P_{data} PG(x)=Pdata可以令价值函数最小化,是因为这时候两个分布的 JS 散度 J S D ( P d a t a ∣ ∣ P G ( x ) ) JSD(P_{data}||P_G(x)) JSD(Pdata∣∣PG(x))等于零,这一过程的详细解释如下。
原论文中的这一定理是「当且仅当」声明,所以我们需要从两个方向证明。首先我们先从反向逼近并证明 C(G) 的取值,然后再利用由反向获得的新知识从正向证明。设
P
d
a
t
a
=
P
G
P_{data}=P_G
Pdata=PG(反向指预先知道最优条件并做推导),我们可以反向推出:
V
(
G
,
D
G
∗
)
=
∫
x
p
data
(
x
)
log
1
2
+
p
G
(
x
)
log
(
1
−
1
2
)
d
x
V\left(G, D_G^*\right)=\int_x p_{\text {data }}(x) \log \frac{1}{2}+p_G(x) \log \left(1-\frac{1}{2}\right) \mathrm{d} x
V(G,DG∗)=∫xpdata (x)log21+pG(x)log(1−21)dx
and
V
(
G
,
D
G
∗
)
=
−
log
2
∫
p
G
(
x
)
d
x
−
log
2
∫
p
data
(
x
)
d
x
=
−
2
log
2
=
−
log
4
V\left(G, D_G^*\right)=-\log 2 \int p_G(x) \mathrm{d} x-\log 2 \int p_{\text {data }}(x) \mathrm{d} x=-2 \log 2=-\log 4
V(G,DG∗)=−log2∫pG(x)dx−log2∫pdata (x)dx=−2log2=−log4
该值是全局最小值的候选,因为它只有在
P
d
a
t
a
=
P
G
P_{data}=P_G
Pdata=PG 的时候才出现。我们现在需要从正向证明这一个值常常为最小值,也就是同时满足「当」和「仅当」的条件。现在放弃
P
d
a
t
a
=
P
G
P_{data}=P_G
Pdata=PG的假设,对任意一个
G
G
G,我们可以将上一步求出的最优判别器
D
∗
D*
D∗ 代入到 中:
C
(
G
)
=
∫
x
p
data
(
x
)
log
(
p
data
(
x
)
p
G
(
x
)
+
p
data
(
x
)
)
+
p
G
(
x
)
log
(
p
G
(
x
)
p
G
(
x
)
+
p
data
(
x
)
)
d
x
C(G)=\int_x p_{\text {data }}(x) \log \left(\frac{p_{\text {data }}(x)}{p_G(x)+p_{\text {data }}(x)}\right)+p_G(x) \log \left(\frac{p_G(x)}{p_G(x)+p_{\text {data }}(x)}\right) \mathrm{d} x
C(G)=∫xpdata (x)log(pG(x)+pdata (x)pdata (x))+pG(x)log(pG(x)+pdata (x)pG(x))dx
因为已知 -log4 为全局最小候选值,所以我们希望构造某个值以使方程式中出现 log2。因此我们可以在每个积分中加上或减去 log2,并乘上概率密度。这是一个十分常见并且不会改变等式的数学证明技巧,因为本质上我们只是在方程加上了 0。
C
(
G
)
=
∫
x
(
log
2
−
log
2
)
p
data
(
x
)
+
p
data
(
x
)
log
(
p
data
(
x
)
p
G
(
x
)
+
p
data
(
x
)
)
+
(
log
2
−
log
2
)
p
G
(
x
)
+
p
G
(
x
)
log
(
p
G
(
x
)
p
G
(
x
)
+
p
d
a
t
a
(
x
)
)
d
x
\begin{aligned} C(G) & =\int_x(\log 2-\log 2) p_{\text {data }}(x)+p_{\text {data }}(x) \log \left(\frac{p_{\text {data }}(x)}{p_G(x)+p_{\text {data }}(x)}\right) \\ & +(\log 2-\log 2) p_G(x)+p_G(x) \log \left(\frac{p_G(x)}{p_G(x)+p_{data}(x)}\right) \mathrm{d} x \end{aligned}
C(G)=∫x(log2−log2)pdata (x)+pdata (x)log(pG(x)+pdata (x)pdata (x))+(log2−log2)pG(x)+pG(x)log(pG(x)+pdata(x)pG(x))dx
采用该技巧主要是希望能够构建成含 log2 和
J
S
JS
JS 散度的形式,上式化简后可以得到以下表达式:
C
(
G
)
=
−
log
2
∫
x
p
G
(
x
)
+
p
data
(
x
)
d
x
+
∫
x
p
data
(
x
)
(
log
2
+
log
(
p
data
(
x
)
p
G
(
x
)
+
p
data
(
x
)
)
)
+
p
G
(
x
)
(
log
2
+
log
(
p
G
(
x
)
p
G
(
x
)
+
p
datad
(
x
)
)
)
d
x
\begin{gathered} C(G)=-\log 2 \int_x p_G(x)+p_{\text {data }}(x) \mathrm{d} x \\ +\int_x p_{\text {data }}(x)\left(\log 2+\log \left(\frac{p_{\text {data }}(x)}{p_G(x)+p_{\text {data }}(x)}\right)\right) \\ +p_G(x)\left(\log 2+\log \left(\frac{p_G(x)}{p_G(x)+p_{\text {datad }}(x)}\right)\right) \mathrm{d} x \end{gathered}
C(G)=−log2∫xpG(x)+pdata (x)dx+∫xpdata (x)(log2+log(pG(x)+pdata (x)pdata (x)))+pG(x)(log2+log(pG(x)+pdatad (x)pG(x)))dx
因为概率密度的定义,
P
G
P_G
PG和
P
d
a
t
a
P_{data}
Pdata在它们积分域上的积分等于 1,即:
此外,根据对数的定义,我们有:
log
2
+
log
(
p
data
(
x
)
p
G
(
x
)
+
p
data
(
x
)
)
=
log
(
2
p
data
(
x
)
p
G
(
x
)
+
p
data
(
x
)
)
=
log
(
p
data
(
x
)
(
p
G
(
x
)
+
p
data
(
x
)
)
/
2
)
\begin{gathered} \log 2+\log \left(\frac{p_{\text {data }}(x)}{p_G(x)+p_{\text {data }}(x)}\right)=\log \left(2 \frac{p_{\text {data }}(x)}{p_G(x)+p_{\text {data }}(x)}\right) \\ =\log \left(\frac{p_{\text {data }}(x)}{\left(p_G(x)+p_{\text {data }}(x)\right) / 2}\right) \end{gathered}
log2+log(pG(x)+pdata (x)pdata (x))=log(2pG(x)+pdata (x)pdata (x))=log((pG(x)+pdata (x))/2pdata (x))
因此代入该等式,我们可以写为:
C
(
G
)
=
−
log
4
+
∫
x
p
data
(
x
)
log
(
p
data
(
x
)
(
p
G
(
x
)
+
p
data
(
x
)
)
/
2
)
d
x
+
∫
x
p
G
(
x
)
log
(
p
G
(
x
)
(
p
G
(
x
)
+
p
data
(
x
)
)
/
2
)
d
x
.
\begin{aligned} C(G)= & -\log 4+\int_x p_{\text {data }}(x) \log \left(\frac{p_{\text {data }}(x)}{\left(p_G(x)+p_{\text {data }}(x)\right) / 2}\right) \mathrm{d} x \\ & +\int_x p_G(x) \log \left(\frac{p_G(x)}{\left(p_G(x)+p_{\text {data }}(x)\right) / 2}\right) \mathrm{d} x . \end{aligned}
C(G)=−log4+∫xpdata (x)log((pG(x)+pdata (x))/2pdata (x))dx+∫xpG(x)log((pG(x)+pdata (x))/2pG(x))dx.
现在,如果读者阅读了前文的 KL 散度(Kullback-Leibler divergence),那么我们就会发现每一个积分正好就是它。具体来说:
C
(
G
)
=
−
log
4
+
K
L
(
p
data
∣
p
data
+
p
G
2
)
+
K
L
(
p
G
∣
p
data
+
p
G
2
)
C(G)=-\log 4+K L\left(p_{\text {data }} \mid \frac{p_{\text {data }}+p_G}{2}\right)+K L\left(p_G \mid \frac{p_{\text {data }}+p_G}{2}\right)
C(G)=−log4+KL(pdata ∣2pdata +pG)+KL(pG∣2pdata +pG)
KL 散是非负的,所以我们马上就能看出来 -log4 为 C ( G ) C(G) C(G) 的全局最小值。
如果我们进一步证明只有一个 G G G 能达到这一个值,因为 P d a t a = P G P_{data}=P_G Pdata=PG将会成为令 C ( G ) = − l o g 4 C(G)=-log4 C(G)=−log4的唯一点,所以整个证明就能完成了。
从前文可知 KL 散度是非对称的,所以 C(G) 中的 K L ( P d a t a ∣ ∣ ( P d a t a + P G ) / 2 ) KL(P_{data}||(P_{data}+P_G)/2) KL(Pdata∣∣(Pdata+PG)/2)左右两项是不能交换的,但如果同时加上另一项 K L ( P G ∣ ∣ ( P d a t a + P G ) / 2 ) KL(P_G||(P_{data}+P_G)/2) KL(PG∣∣(Pdata+PG)/2),它们的和就能变成对称项。这两项 KL 散度的和即可以表示为 JS 散度(Jenson-Shannon divergence):
J S D ( P ∥ Q ) = 1 2 D ( P ∥ M ) + 1 2 D ( Q ∥ M ) M = 1 2 ( P + Q ) \begin{array}{r} \mathrm{JSD}(P \| Q)=\frac{1}{2} D(P \| M)+\frac{1}{2} D(Q \| M) \\ M=\frac{1}{2}(P+Q) \end{array} JSD(P∥Q)=21D(P∥M)+21D(Q∥M)M=21(P+Q)
假设存在两个分布 P 和 Q,且这两个分布的平均分布 M = ( P + G ) / 2 M=(P+G)/2 M=(P+G)/2那么这两个分布之间的 JS 散度为 P 与 M 之间的 KL 散度加上 Q 与 M 之间的 KL 散度再除以 2。
JS 散度的取值为 0 到 log2。若两个分布完全没有交集,那么 JS 散度取最大值 log2;若两个分布完全一样,那么 JS 散度取最小值 0。
因此 C(G) 可以根据 JS 散度的定义改写为:
C
(
G
)
=
−
log
4
+
2
⋅
J
S
D
(
p
data
∣
p
G
)
C(G)=-\log 4+2 \cdot J S D\left(p_{\text {data }}| p_G\right)
C(G)=−log4+2⋅JSD(pdata ∣pG)
这一散度其实就是 Jenson-Shannon 距离度量的平方。根据它的属性:当 时 P d a t a = P G P_{data}=P_G Pdata=PG, J S D ( P d a t a ∣ ∣ P G ) JSD(P_{data}||P_G) JSD(Pdata∣∣PG)为 0。综上所述,生成分布当且仅当等于真实数据分布式时,我们可以取得最优生成器。
收敛
现在,该论文的主要部分已经得到了证明:即P_G=P_{data}为 maxV(G,D) 的最优点。此外,原论文还有额外的证明白表示:给定足够的训练数据和正确的环境,训练过程将收敛到最优 G,我们并不详细讨论这一块。
重述训练过程
下面是推导的最后一步,我们会重述整个参数优化过程,并简要介绍实际训练中涉及的各个过程。
参数优化过程
若我们需要寻找最优的生成器,那么给定一个判别器 D,我们可以将 maxV(G,D) 看作训练生成器的损失函数 L(G)。既然设定了损失函数,那么我们就能使用 SGD、Adam 等优化算法更新生成器 G 的参数,梯度下降的参数优化过程如下:
θ
G
←
θ
G
−
η
∂
L
(
G
)
/
∂
θ
G
\theta_G \leftarrow \theta_G-\eta \partial L(G) / \partial \theta_G
θG←θG−η∂L(G)/∂θG
其中求 L(G) 对
θ
G
\theta_G
θG 的偏导数涉及到求 max{V(G,D)} 的偏导数,这种对 max 函数求微分的方式是存在且可用的。
现在给定一个初始
G
0
G_0
G0 ,我们需要找到令
V
(
G
0
,
D
)
V(G_0, D)
V(G0,D) 最大的
D
0
∗
D_0*
D0∗,因此判别器更新的过程也就可以看作损失函数为
−
V
(
G
,
D
)
-V(G,D)
−V(G,D) 的训练过程。并且由前面的推导可知,V(G,D) 实际上与分布
P
d
a
t
a
P_{data}
Pdata和
P
G
P_G
PG之间的 JS 散度只差了一个常数项。因此这样一个循环对抗的过程就能表述为:
实际训练过程
根据前面价值函数 V(G,D) 的定义,我们需要求两个数学期望,即 E[log(D(x))] 和 E[log(1-D(G(z)))],其中 x 服从真实数据分布,z 服从初始化分布。但在实践中,我们是没有办法利用积分求这两个数学期望的,所以一般我们能从无穷的真实数据和无穷的生成器中做采样以逼近真实的数学期望。
若现在给定生成器 G,并希望计算 maxV(G,D) 以求得判别器 D,那么我们首先需要从
P
d
a
t
a
(
x
)
P_{data}(x)
Pdata(x)采样 m 个样本
{
x
1
,
x
2
,
x
3
,
.
.
.
x
m
}
\{x^1, x^2,x^3,...x^m\}
{x1,x2,x3,...xm},从生成器
P
G
(
x
)
P_G(x)
PG(x)采样
m
m
m 个样本。
{
x
~
1
,
x
~
2
,
…
,
x
~
m
}
\left\{\tilde{x}^1, \tilde{x}^2, \ldots, \tilde{x}^m\right\}
{x~1,x~2,…,x~m}
因此最大化价值函数 V(G,D) 就可以使用以下表达式近似替代:
Maximize
V
~
=
1
m
∑
i
=
1
m
log
D
(
x
i
)
+
1
m
∑
i
=
1
m
log
(
1
−
D
(
x
~
i
)
)
\text { Maximize } \tilde{V}=\frac{1}{m} \sum_{i=1}^m \log D\left(x^i\right)+\frac{1}{m} \sum_{i=1}^m \log \left(1-D\left(\tilde{x}^i\right)\right)
Maximize V~=m1i=1∑mlogD(xi)+m1i=1∑mlog(1−D(x~i))
若我们需要计算上述的极大化过程,可以采用等价形式的训练方法。若我们有一个二元分类器 D(参数为
θ
\theta
θ ),当然该分类器可以是深度神经网络,那么极大化过程的输出就为该分类器 D(x)。现在我们从
P
d
a
t
a
(
x
)
P_{data}(x)
Pdata(x)抽取样本作为正样本,从P_G(x)抽取样本作为负样本,同时将逼近负 V(G,D) 的函数作为损失函数,因此我们就将其表述为一个标准的二元分类器的训练过程:
在实践中,我们必须使用迭代和数值计算的方法实现极小极大化博弈过程。在训练的内部循环中完整地优化 D 在计算上是不允许的,并且有限的数据集也会导致过拟合。因此我们可以在 k 个优化 D 的步骤和一个优化 G 的步骤间交替进行。那么我们只需慢慢地更新 G,D 就会一直处于最优解的附近,这种策略类似于 SML/PCD 训练的方式。
综上,我们可以描述整个训练过程,对于每一次迭代:
- 从真实数据分布 P d a t a P_{data} Pdata抽取 m m m 个样本
- 从先验分布 P p r i o r ( z ) P_prior(z) Pprior(z) 抽取 m m m 个噪声样本
- 将噪声样本投入 G 而生成数据
{ x ~ 1 , x ~ 2 , … , x ~ m } , x ~ i = G ( z i ) \left\{\tilde{x}^1, \tilde{x}^2, \ldots, \tilde{x}^m\right\}, \tilde{x}^i=G\left(z^i\right) {x~1,x~2,…,x~m},x~i=G(zi)
,通过最大化 V 的近似而更新判别器参数 θ \theta θ,即极大化
V ~ = 1 m ∑ i = 1 m log D ( x i ) + 1 m ∑ i = 1 m log ( 1 − D ( x ~ i ) ) \tilde{V}=\frac{1}{m} \sum_{i=1}^m \log D\left(x^i\right)+\frac{1}{m} \sum_{i=1}^m \log \left(1-D\left(\tilde{x}^i\right)\right) V~=m1i=1∑mlogD(xi)+m1i=1∑mlog(1−D(x~i))
,且判别器参数的更新迭代式为
θ d ← θ d + η ∇ V ~ ( θ d ) \theta_d \leftarrow \theta_d+\eta \nabla \tilde{V}\left(\theta_d\right) θd←θd+η∇V~(θd)
以上是学习判别器 D 的过程。因为学习 D 的过程是计算 JS 散度的过程,并且我们希望能最大化价值函数,所以该步骤会重复 k 次。
- 从先验分布 P p r i o r ( z ) P_{prior}(z) Pprior(z)中抽取另外 m m m 个噪声样本 z 1 , z 2 , z 3 . . . z m z^1,z^2,z^3...z^m z1,z2,z3...zm
- 通过极小化
V
~
\tilde{V}
V~ 而更新生成器参数
θ
g
\theta_g
θg,即极大化
V ~ = 1 m ∑ i = 1 m log ( 1 − D ( G ( z i ) ) ) \tilde{V}=\frac{1}{m} \sum_{i=1}^m \log \left(1-D\left(G\left(z^i\right)\right)\right) V~=m1i=1∑mlog(1−D(G(zi)))
,且生成器参数的更新迭代式为
θ g ← θ g − η ∇ V ~ ( θ g ) \theta_g \leftarrow \theta_g-\eta \nabla \tilde{V}\left(\theta_g\right) θg←θg−η∇V~(θg)
以上是学习生成器参数的过程,这一过程在一次迭代中只会进行一次,因此可以避免更新太多而令 JS 散度上升。
代码
import torch
import torch.nn as nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import numpy as np
import torch.nn.functional as F
import torchvision
from torch.autograd import Variable
from torchvision.utils import save_image
train_data = datasets.MNIST("./datasets/",
download=True,
train=True,
transform=transforms.Compose([
transforms.ToTensor(), transforms.Normalize([0.5], [0.5])
]))
test_data = datasets.MNIST('./datasets/',
train=False,
download=True,
transform=transforms.Compose([
transforms.ToTensor(), transforms.Normalize([0.5], [0.5])
]))
train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
test_loader = DataLoader(test_data, batch_size=64, shuffle=False)
## ##### 定义判别器 Discriminator ######
## 将图片28x28展开成784,然后通过多层感知器,中间经过斜率设置为0.2的LeakyReLU激活函数,
## 最后接sigmoid激活函数得到一个0到1之间的概率进行二分类
class Discriminator(nn.Module):
def __init__(self):
super().__init__()
self.discriminator = nn.Sequential(
nn.Linear(784, 512), ## 输入特征数为784,输出为512
nn.LeakyReLU(0.2, inplace=True), ## 进行非线性映射
nn.Linear(512, 256), ## 输入特征数为512,输出为256
nn.LeakyReLU(0.2, inplace=True), ## 进行非线性映射
nn.Linear(256, 1), ## 输入特征数为256,输出为1
nn.Sigmoid(), ## sigmoid是一个激活函数,二分类问题中可将实数映射到[0, 1],作为概率值, 多分类用softmax函数
)
def forward(self,
x:torch.Tensor):
"""_summary_
Args:
x (torch.Tensor):
Returns:
_type_:
"""
x = x.flatten(1)
return self.discriminator(x)
class Generator(nn.Module):
def __init__(self):
super().__init__()
def block(in_feat, out_feat, normalize=True):
layer = [nn.Linear(in_feat, out_feat)] ## 线性变换将输入映射到out维
if normalize:
layer.append(nn.BatchNorm1d(out_feat, 0.8)) ## 正则化
layer.append(nn.LeakyReLU(0.2, inplace=True)) ## 非线性激活函数
return layer
self.generator = nn.Sequential(
*block(100, 128,normalize=False), ## 线性变化将输入映射 100 to 128, 正则化, LeakyReLU
*block(128, 256), ## 线性变化将输入映射 128 to 256, 正则化, LeakyReLU
*block(256, 512), ## 线性变化将输入映射 256 to 512, 正则化, LeakyReLU
*block(512, 1024), ## 线性变化将输入映射 512 to 1024, 正则化, LeakyReLU
nn.Linear(1024, 784), ## 线性变化将输入映射 1024 to 784
nn.Tanh() # ## 将(784)的数据每一个都映射到[-1, 1]之间
)
def forward(self, z):
imgs = self.generator(z) ## 噪声数据通过生成器模型
imgs = imgs.view(imgs.size(0), 1, 28, 28) ## reshape成(64, 1, 28, 28)
return imgs ## 输出为64张大小为(1, 28, 28)的图像
## 参数
epcohs = 10
device = torch.device("cuda")
## 首先需要定义loss的度量方式 (二分类的交叉熵)
criterion = torch.nn.BCELoss()
## 创建生成器,判别器对象
generator = Generator()
discriminator = Discriminator()
## 其次定义 优化函数,优化函数的学习率为0.0003
## betas:用于计算梯度以及梯度平方的运行平均值的系数
optimizer_G = torch.optim.Adam(generator.parameters(), lr=0.003, betas=(0.5, 0.999))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=0.003, betas=(0.5, 0.999))
## 都在cuda模式中运行
generator = generator.to(device)
discriminator = discriminator.to(device)
criterion = criterion.to(device)
## 训练
for epoch in range(epcohs):
d_epoch_loss = 0
g_epoch_loss = 0
count = len(train_loader) #返回批次数
for i, (x, y) in enumerate(train_loader):
bz = x.shape[0]
x = x.to(device)
## ---------------------
## Train Discriminator
## 分为两部分:1、真的图像判别为真;2、假的图像判别为假
## ---------------------
## 计算真实图片的损失
real_out = discriminator(x)
d_real_loss = criterion(real_out, torch.ones_like(real_out))
## 计算假的图片的损失
## detach(): 从当前计算图中分离下来避免梯度传到G,因为G不用更新
random_noise = torch.randn(bz,100,device=device)
gen_img = generator(random_noise)
fake_output = discriminator(gen_img.detach())
d_fake_loss = criterion(fake_output,torch.zeros_like(fake_output))
d_loss = d_real_loss + d_fake_loss ## 损失包括判真损失和判假损失
optimizer_D.zero_grad() ## 在反向传播之前,先将梯度归0
d_loss.backward() ## 将误差反向传播
optimizer_D.step() ## 更新参数
d_epoch_loss += d_loss.item()
## -----------------
## Train Generator
## 原理:目的是希望生成的假的图片被判别器判断为真的图片,
## 在此过程中,将判别器固定,将假的图片传入判别器的结果与真实的label对应,
## 反向传播更新的参数是生成网络里面的参数,
## 这样可以通过更新生成网络里面的参数,来训练网络,使得生成的图片让判别器以为是真的, 这样就达到了对抗的目的
## -----------------
random_noise = torch.randn(bz,100,device=device)
fake_img = generator(random_noise) ## 随机噪声输入到生成器中,得到一副假的图片
output = discriminator(fake_img) ## 经过判别器得到的结果
## 损失函数和优化
g_loss = criterion(output, torch.ones_like(output)) ## 得到的假的图片与真实的图片的label的loss
optimizer_G.zero_grad() ## 梯度归0
g_loss.backward() ## 进行反向传播
optimizer_G.step() ## step()一般用在反向传播后面,用于更新生成网络的参数
g_epoch_loss += g_loss.item()
## 打印训练过程中的日志
## item():取出单元素张量的元素值并返回该值,保持原元素类型不变
if (i + 1) % 100 == 0:
print(
"[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f] [D real: %f] [D fake: %f]"
% (epoch, 10, i, len(train_data), d_loss.item(), g_loss.item(), real_out.data.mean(), fake_output.data.mean())
)
# ## 保存训练过程中的图像
if i % 500 == 0:
batches_done = "_"+str(epoch)+"_"+ str(i)+"_"
save_image(fake_img.data[:25], "./results/gan/images/%s.png" % batches_done, nrow=5, normalize=True)
print("d_epoch_loss:", d_epoch_loss/count)
print("g_epoch_loss:", g_epoch_loss/count)
## 保存模型
torch.save(generator.state_dict(), './results/gan/generator.pth')
torch.save(discriminator.state_dict(), './results/gan/discriminator.pth')
参考
https://www.jianshu.com/p/058fd15cfa52