이번 글에서는 Oversampling기법과 Undersampling기법에 대해 다뤄보겠습니다.
Imbalanced data
이 게시글에선 Imbalaced data 상황에서 해결할 수 있는 Oversampling과 Undersampling의 개념에 대해서 다뤄보겠습니다.
우선, Oversampling과 Undersampling 기법이 왜 필요한지부터 생각해 볼 필요가 있습니다.
Classfication문제에서 majority class(다수 클래스) data가 minority class(소수 클래스) data의 수보다 훨씬 많은 경우, 모델은 new data(minority class data)에 대해서 주로 majority로 분류하게 되는 경향이 있습니다. 이렇게 되는 경우는 모델이 정확도를 최대화하려고 할 때 발생하게 됩니다.
아래 예시를 들어보겠습니다. 아래와 같이 불량 정상을 예측하는 모델이 있다고 가정하겠습니다.
예측
실제
불량
정상
불량
1
9
정상
11
979
위 표를 보면 이 모델의 정확도는 $\frac{1+979}{1+11+9+979}=\frac{980}{1000}$=0.98입니다. 이 모델은 좋은 모델이라고 할 수 있을까요? 사실 이 모델은 정확도는 되게 높지만, 좋은 모델이 아니라고 볼 수 있습니다. 만약 minority class의 예측을 중요시하게 되는(예를 들어 암과 정상 데이터) 데이터의 경우에는 전혀 도움이 되지 않는 모델입니다. 그래서 이런 Imbalacend data 문제를 해결하기 위해서 Oversampling과 Undersampling이 필요하게 됩니다. 이제 Oversampling과 Undersampling에 대해서 다뤄보겠습니다.
Oversampling
Oversampling은 minority class(소수 클래스)의 data를 복제하거나 증가시켜 majority class(다수 클래스)와의 균형을 맞추는 방법입니다. 이를 통해 모델이 minority class에 대해 더 나은 성능을 나타낼 수 있도록 합니다. Oversampling 기법은 Minority class의 data의 수가 현저히 적을 때 유용하게 사용될 수 있습니다. (위 방법의 경우에는 Minority class의 data를 무작위로 복제하여 샘플 수를 늘리는 방법)
장점(Pros)
소수 클래스 데이터를 효과적으로 늘려 모델이 소수 클래스에 더 민감하게 반응할 수 있도록 함(모두 다수 클래스로 예측하는 문제를 해결할 수 있음)
구현이 간단하고 빠름
단점(Cons)
데이터를 단순하게 복제하기 때문에 과적합(Overfitting)이 될 수 있음
Undersampling
Undersampling은 majority class(다수 클래스)의 data를 줄여 minority class(소수 클래스)와의 균형을 맞추는 방법입니다. 다수 클래스의 데이터가 많을 때 학습 데이터에서 다수 클래스의 샘플을 무작위로 선택해서 그 수를 줄이는 방법입니다.
장점(Pros)
간단하고 빠르며 데이터 셋의 크기가 줄어들어 학습 시간이 단축됩니다.
단점(Cons)
다수 클래스의 데이터를 랜덤으로 선택하여 개수를 줄였기 때문에 다수 클래스의 정보가 손실될 수 있습니다. 그로 인해 모델 성능도 저하가 될 수 있습니다.(모델이 다수 클래스의 중요한 패턴을 학습하지 못할 수 있음)
데이터가 불균형한 상황에서 데이터를 증강시키거나 줄여서 모델의 성능을 향상시킬 수 있도록 하는 Sampling 기법을 다뤄보았습니다. 이 기법들로 인해 불균형한 상황에서 조금 더 중요한 클래스에 대해서 초점을 맞출 수 있습니다.
Oversampling과 Undersampling에는 다양한 기법이 존재하기 때문에 다른 게시글에서 응용기법을 포스팅하도록 하겠습니다.
조건부 독립이 되려면 $p(y|x,z)=\frac{p(y|z)p(y|x)}{p(y)}$가 되어야 하지만, 두 식은 같지 않기 때문에 조건부 독립이 아니다!
정리하자면 다음 표와 같다.
Chain Structure Fork inverse Fork
$X\perp Z$
NO
NO
Yes
$X\perp Z|Y$
Yes
Yes
Yes
그럼 다음과 같은 예제를 풀어보자
Q : $W\perp X|Y?$
A : No!
Why?
표를 보면 위 그림은inverse Fork 형태에서 $X\perp Z|Y$형태와 같음
Q : $W\perp X|Z?$
A : NO!
Why?
표에서처럼 W와 X는 독립인 것 같지만, 사실 이 형태는 $X\perp Z|Y$형태와 같음. 왜냐하면 Z는 Y에 대한 정보를 받고 있기 때문에! 따라서 W와 X는 조건부 독립이 아니다!
이해가 잘 안된다면 아래 그림을 참고하자
Bayes ball이 굴러가는 경로라고 보면 된다. 경로가 끊이지 않는다면 독립이 아니라고 생각하면 된다.
왼쪽 그림은 독립 , 오른쪽 그림은 Z가 주어졌을 때 독립이 아님! Bayes ball이 W에서 X로 잘 굴러가기 때문!
Q : 이 그림에서 A가 주어졌을 때 C와 E는 독립인가?
A : NO!
Why?
우선 이 그림에서 Chain structure 형태가 보인다.( E → A → C) 이 형태를 보고 바로 독립이라고 생각할 수 있다.(경로가 끊겨있기 때문에)
하지만 이 그림에서 E → C로가는 경로가 한 가지 더 있다. 바로 C ← D → B →E 이렇게 말이다.
이 형태를 보면 Fork의 형태와 같다. Fork는 경로가 끊이지 않았기 때문에 C에서 E까지 굴러갈 수 있다. Bayes ball이 굴러간다는건 독립이 아닌 것이라는 것과 같음!! 그렇기 때문에 NO!
마지막으로 PC algorithm에 대해서 정리하고 마치겠습니다.
PC Algorithm
PC 알고리즘은 베이지안 네트워크의 구조를 학습하는데 사용됩니다. 이 알고리즘을 통해 변수 간 조건부 독립성을 추론하고, 이를 통해 인과관계를 유추합니다. PC 알고리즘의 Step은 다음과 같습니다.
아래 그래프의 관계를 추론하고 싶다고 하겠습니다.(True causal Graph)
1. 모든 변수들 사이 존재하는 Edge(선)를 그려줍니다.
2. 관계가 없는 즉, 독립 관계인 노드의 Edge를 제거해 줍니다.( True graph에서 보면 A와 B는 v-structure 구조를 가지기 때문에 독립인 것을 알 수 있습니다.)
위 그림에선 A와 B가 독립이 되어 두 개의 노드 사이의 Edge를 제거합니다. 또한 조건부 독립이라면 그때 또한 역시 Edges를 제거합니다. 이를 반복하여 Edges를 제거하게되면 아래 그림과 같이 됩니다. (예시 : Chain structure인 A→C→E에서 C가 주어지면 A와 E는 독립이므로 A와 E를 직접적으로 연결짓는 선은 제거)
3. A와 B가 독립이므로 A,B,C에서는 v-structure 구조를 가지고 있다고 생각할 수 있습니다.
4. 마지막으로 D와 E에도 방향을 그려줘야 하는데, 나머지 방향에서는 v-sturcture가 되지 않도록 방향을 설정해주어야 합니다. 결과는 다음과 같습니다.
Continuous columns은 multiple modes(여러개의 봉우리)를 가지고 있으며 Discrete columns은 각 카테고리 수가 불균형(암 환자 : 5%, 정상 환자 : 95%)하게 되어있으면 Deep neural network 모델은 모델링 하는데 어려움을 겪습니다.
저자는 CTGAN이라는 모델을 제안했으며 이 모델은 위에서 제시한 문제점을 해결하기 위해 Conditional Generator를 사용한다고 합니다. 모델이 어떤 구조인지 살펴보도록 하겠습니다.
Challenges with GANs in Tabular Data Generation Task
Table $T$(Tabular data)에 대한 column 정의는 다음과 같습니다.
Continuous columns : $\{C_1,...,C_{N_C}\}$
Discrete columns : $\{D_1,...,D_{N_d}\}$
Total columns : 총 $N_C$+$N_d$ 즉, N개의 컬럼을 갖고 있다고 보면 됩니다.
각각의 컬럼은 Random variable(확률변수)로 생각하고 각 컬럼은 unknown joint distribution $\mathbb{P}(C_{1:N_c},D_{1:N_d})$라고 합니다. 즉 각 컬럼 간 독립이 아니라 컬럼끼리 관계를 갖고 있다고 해석 할 수 있을 것 같네요
One row $\bold{r}j=\{c{1,j},...,c_{N_c,j},d_{1,j},...,d_{N_d,j}\},j\in\{1,...,n\}$
Table $T$를 $T_{train}$과 $T_{test}$로 나누고, $T_{train}$으로 G를 학습한 후에 G를 사용해 각 행을 독립적으로 생성한 집합을 $T_{syn}$이라 합니다.
저자는 2가지 측면으로 Generator의 효율성을 판단한다고 합니다!
Likelihood fitness : $T_{syn}$로 생성한 컬럼이 $T_{train}$와 같은 joint distribution 따르는지
Machine learning efficacy : $T_{train}$로 모델을 학습하여 $T_{test}$ 평가한 성능과 $T_{syn}$로 모델을 학습하여 $T_{test}$ 평가한 성능이 비슷한지?
기존의 GAN 모델을 사용해 Tabular data를 생성하면 문제가 있다고 했는데, 어떤 문제가 있는지 확인해 보겠습니다. 총 5가지 도전과제가 있습니다.
Mixed data types : 연속형, 이산형 columns을 동시에 생성하기 위해서는 Softmax, Tanh 함수를 적용해야 합니다. Softmax는 이산형 columns, Tanh는 연속형 columns를 처리하기 위한 함수입니다.
GAN의 경우 Mixed data를 처리할 때 최적화 문제가 발생한다고 합니다! GAN을 학습시킬 때, 이산형 데이터와 연속형 데이터를 동시에 다루기 위해 손실 함수를 적절히 조합하고 최적화해야 합니다. 이산형 데이터를 위한 손실은 일반적으로 분류 문제에 사용되는크로스 엔트로피같은 함수가 적합하고, 연속형 데이터에는평균 제곱 오차와 같은 함수가 적합합니다. 이 두 손실 함수를 적절히 조합하는 것은 도전적이라고 합니다.
Non-Gaussian distributions: 이미지 데이터의 경우 pixel들은 Gaussian과 유사한 분포를 따르기 때문에 [-1,1]의 범위로 normalizing할 수 있지만, Continuous 컬럼의 값들은 Gaussian 분포를 따르지 않아서 Tanh로 normalizing 시키면 vanishing gradient problem문제가 발생합니다.
왜 gradient problem문제가 발생하느냐? → 만약 3개의 mode를 가진다고 가정해봅시다. 각각의 mode는 $N(0,1)$, $N(5,1)$, $N(10,1)$의 분포를 갖는 데이터들이 존재한다고 하면 이 값들을 Tanh로 normalizing 시켜버리면 $N(0,1)$과 $N(10,1)$ 분포에 존재하는 데이터 들은 각각 -1과 1에 근사하는 값을 가집니다.
근데 Tanh함수는 아래와 같은 그림을 나타냅니다. 여기서 -1과 1에 근사하는 값의 기울기는 거의 0이 되겠죠. 그래서 Backpropagation과정에서 기울기가 소실된다고 말하는 것 같습니다. (기울기 소실이 발생하면, 네트워크의 특정 부분에서 가중치가 업데이트 되지 않거나 매우 느리게 학습되어, 전체적인 학습 과정의 효율성과 효과가 크게 저하됩니다.)Tanh 함수
Multimodal distributions : 여러개의 mode(봉우리)를 가지고 있어서 Kernel Density Estimation(KDE)로 mode를 추정합니다. 기존의 GAN은 이런 Multimodal distribution을 모델링하는데 어려움을 겪는다고 합니다.
그러면 GAN 모델은 Multimodal distribution을 모델링하는데 왜 어려움을 겪을까요? 모드 간 균형: GAN은 경향적으로 분포의 주된 모드에 초점을 맞추고, 덜 대표적인 모드는 무시할 수 있습니다. 이로 인해 데이터의 다양성과 복잡성을 완전히 포착하지 못할 수 있습니다.
Learning from sparse one-hot-encoded vectors : 새로운 샘플들을 생성하면 모델은 softmax를 사용하여 각 카테고리의 확률(e.g. [0.7,0.2,0.1])을 출력합니다. 하지만, 실제 데이터는 one-hot vector(e.g. [1,0,0])로 표현됩니다.
이게 무슨 문제가 되냐? 실제 데이터는 원-핫 벡터로 매우 희소한(0이 많음) 반면, 생성된 데이터는 확률 분포로 인해 상대적으로 덜 희소합니다. 이러한 차이는 판별자(discriminator)가 실제 데이터와 생성된 데이터를 구별하는 데 사용될 수 있습니다.
위의 예시의 경우 one-hot vector에서 1의 카테고리가 생성된 모델에서는 0.7의 확률을 가지니 ‘진짜’라고 판단을 해야하는데, 이런 특성을 보지 않고 그저 벡터의 희소성만 확인하여 생성된 데이터를 가짜라고 구별하게 됩니다. 이렇게 되면 [0.99,0.005,0.005]의 데이터여도 희소하지 않기 때문에 가짜라고 구별하게 돼서 GAN 학습 과정에서 문제가 발생할 수 있는 것 같습니다. D입장에서는 [0.99,0.005,0.005]도 가짜라 생각해 0을 출력하니 Maximize가 되지만, G입장에서 보면 E_{z\sim p(z)}[log(1-D(G(z)))]값이 E_{z\sim p(z)}[log(1-0)]=0이 되어 Minimize가 되지 않습니다. 즉, 제대로 학습이 안되는거죠.
Highly imbalanced categorical columns : 이산형 컬럼에서 category의 빈도가 불균형 하여 mode collapse(모드 붕괴)가 일어납니다. 즉, 생성자가 데이터의 다양성을 반영하지 못하고 주로 주요 카테고리만을 반복적으로 생성하는 현상입니다. 이는 GAN이 다양한 데이터 패턴을 학습하고 재현하는 데 실패하게 만듭니다.
그리고 minor category를 생성과정에서 누락해도 데이터의 분포의 변화는 거의 없어서 판별자가 이것을 감지하기는 어렵다고 합니다! 그렇게 되면 minor category 데이터는 거의 생성이 되지 않는 문제가 발생하여 전체 데이터의 다양성을 학습하지 못하고 major category의 데이터만 생성하게 되는 문제가 발생하는 것 같습니다.
Conditional Generator and Training-by-Sampling : Imbalanced(4,5) 문제 해결 (Discrete columns)
Mode-specific Normalization
Non-Gaussian, Multimodal 문제였던 것을 확인해 봅시다. $T$에서 Continuous 컬럼은 아래 그림처럼 여러개의 mode를 갖는다고 했습니다.
위 그림은 3개의 mode가 존재하니 3개의 sub distribution으로 나누고, 각각의 distribution에서의 평균 : $\eta_k$, Weight : $\mu_k$, standard deviation : $\phi_k$로 설정합니다.
$c_{i,j}$ : i 번째 컬럼에 해당하는 j번째 행 데이터로 각각 Gaussian mode에서 발생할 확률은 $\rho_k$입니다.
그림에선 $\rho_3$ 확률이 가장 높으므로($c_{i,j}$는 $\rho_3$의 분포에서 나왔을 것!) $\beta_{i,j}=[0,0,1]$로 표현하고 $\alpha_{i,j}$도 식에 대입합니다.
기존의 One row $\bold{r}_j$를 다시 재표현 하면 아래와 같습니다.
여기서 $d_{1,j}$ 이전의 부분들은 continuous columns이고, mode-specific Normalization을 통해 구할 수 있습니다.
$d_{1,j}$ 이후의 부분들은 discrete columns로 구성되었으며 one-hot encoding 되어있습니다.
Conditional Generator and Training-by-Sampling
기존의 GAN에서는 minor category를 고려하지 못하는 문제가 있었기 때문에 Conditional Generator를 도입합니다. Conditional Generator를 통해 특정 이산형 컬럼의 값에 따라 데이터 행을 생성할 수 있도록 하는 것입니다. CGAN과 유사한 방식이라고 생각하시면 됩니다! 그러면 이제 어떻게 Condition을 줄 것이냐? 바로 Training-by-Sampling 방법으로 Condition을 줄 것입니다! ( 이 방식으로 불균형 한 문제를 해결할 수 있습니다.)
아래는 Training-by-Sampling 방식입니다!
각각 단계를 자세히 보겠습니다.
$N_d$ 개의 discrete columns 중에 랜덤으로 한개의 컬럼을 선택 $i^*$
선택된 Discrete columns에 대해 PMF(확률질량함수) 구함
PMF를 따르는 확률 분포에 따라 하나의 카테고리를 선택한다. 이를 $k^$라고 합니다. 아래는 $k^$ 선택하는 과정입니다.(카테고리가 2개가 있다고 가정)
첫 번째 카테고리 빈도 : 100
두 번째 카테고리 빈도 : 10
첫번째 카테고리 확률 : 100/110 ~ 0.9
두번째 카테고리 확률 : 10/110 ~ 0.1
그대로 사용하는게 아니라 log 변환 수행
log(100) ~ 4.61, log(10) ~ 2.3
첫번째 카테고리 확률 : 4.61/6.91 ~ 0.667
두번째 카테고리 확률 : 2.3/6.91 ~ 0.333
log의 유무에 따라 뽑힐 확률이 달라지는게 보이시죠! log변환을 통해 minor category가 뽑힐 수 있는 확률을 늘려주었습니다!
Conditional vector 생성 만약 2개의 이산형 컬럼이 존재하고, 첫번째 컬럼에는 3개의 카테고리, 두번째 컬럼에는 2개의 카테고리가 있는데, 2번째 컬럼의 첫번째 카테고리가 선택되었다면 Conditional vector는 다음과 같습니다. Conditional vector : [0,0,0,1,0]
그리고 여기서 저희가 Generator loss를 추가해 주는데, conditional vector로 2번째 컬럼의 1번째 카테고리가 주어졌을 때 이 조건에 맞는 이산형 벡터를 생성해야하는데, 잘못된 벡터가 생성되었을 수도 있으니 그 손실을 감소시키기 위해 cross-entropy 손실을 추가합니다!
위 방법으로 기존의 문제였던 5(Highly imbalanced categorical columns)번 문제를 해결할 수 있었습니다.
그러면 이제 4번문제가 남아있는데, 기존의 GAN에서 Softmax 함수를 사용했다면, Gumbel-Softmax 함수를 사용해서 sparse한걸로 판별했던 문제를 해결할 수 있었습니다.
Gumbel-Softmax 내용은 Chat GPT를 사용하였습니다
Gumbel-Softmax는 각 범주에 대한 확률을 계산한 후, Gumbel 분포를 통해 샘플링하여 one-hot vector와 유사한 출력을 생성할 수 있게 합니다. 이를 통해 신경망은 연속적인 방식으로 역전파를 수행하면서도, 이산적인 범주형 데이터를 효과적으로 생성할 수 있습니다.
기존의 GAN의 도전과제들을 다 해결하였습니다. 이제 두 가지 평가지표로 성능이 확실한지 파악해보겠습니다.
Evaluation Metrics and Framework
두가지 평가지표
Likelihood fitness metric
Machine learning efficacy
Likelihood fitness metric
과정은 아래 사진과 같습니다.
Synthetic Data Generator: 이 생성기는 학습 데이터를 기반으로 합성 데이터를 생성합니다.
합성 데이터(Synthetic Data): Generator에 의해 생성된 데이터입니다. 이 데이터는 실제 데이터와 유사한 데이터 입니다
Likelihood $L_{syn}$: 합성 데이터의 likelihood를 계산하여, 이 데이터가 실제 분포를 얼마나 잘 따르는지 평가합니다.
Likelihood $L_{test}$: 테스트 데이터에 대한 likelihood를 계산하여, 합성데이터가 실제 데이터를 얼마나 잘 모델링하는지 평가합니다.
Machine learning efficacy
실제 데이터에서 효율 확인
이번에는 합성데이터를 이용해 Decision Tree, Linear SVM, MLP를 사용하여 학습한 후 Test data에 대해 예측을 수행하여 Accuracy와 F1, $R^2$ 확인!
Result
TVAE와 CTGAN에서 우수한 성능을 보이고 있음!
그리고 CTGAN에서는 Generator에서 input data가 아닌 noise를 사용하기 때문에 Privacy 문제에 유용하다고 합니다(TVAE보다)
GM Sim, BN Sim에서 Likelihood 값이 커야 두 분포가 유사하다고 볼 수 있습니다. CTGAN이나 TVAE가 역시 다른 모델에 비해 likelihood값이 대략적으로 큰 것을 볼 수 있습니다!
저번에 InforGAN에 대해 논문 리뷰를 해보았는데, 오늘은 MNIST 데이터셋에 대해서 InfoGAN에 대해 적용시켜 실제로 특징을 잘 학습하는지 확인해보도록 하겠습니다. (구글 코랩 기준으로 작성)
MNIST는 아래와 같은 아키텍쳐로 코드를 구현하셨습니다.
Import
from tqdm import tqdm
import time
import os
import numpy as np
import cv2
import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision.datasets as dset
import matplotlib.pyplot as plt
import torchvision.utils as vutils
import time
import torch.optim as optim
from PIL import Image
from torch.utils.data import Dataset,DataLoader
# Dictionary storing network parameters. 설정한 파라미터
params = {
'batch_size': 128,# Batch size.
'num_epochs': 30,# Number of epochs to train for.
'learning_rate': 2e-4,# Learning rate.
'beta1': 0.5,
'beta2': 0.999,
'save_epoch' : 25,# After how many epochs to save checkpoints and generate test output.
'dataset' : 'MNIST'}# Dataset to use. Choose from {MNIST, SVHN, CelebA, FashionMNIST}. CASE MUST MATCH EXACTLY!!!!!
데이터셋 불러오기
import torch
import torchvision.transforms as transforms
import torchvision.datasets as dsets
# Directory containing the data.
root = 'data/'
def get_data(dataset, batch_size):
# Get MNIST dataset.
if dataset == 'MNIST':
transform = transforms.Compose([
transforms.Resize(28),
transforms.CenterCrop(28),
transforms.ToTensor()])
dataset = dsets.MNIST(root+'mnist/', train='train',
download=True, transform=transform)
# Get FashionMNIST dataset.
elif dataset == 'FashionMNIST':
transform = transforms.Compose([
transforms.Resize(28),
transforms.CenterCrop(28),
transforms.ToTensor()])
dataset = dsets.FashionMNIST(root+'fashionmnist/', train='train',
download=True, transform=transform)
# Get CelebA dataset.
# MUST ALREADY BE DOWNLOADED IN THE APPROPRIATE DIRECTOR DEFINED BY ROOT PATH!
# Create dataloader.
dataloader = torch.utils.data.DataLoader(dataset,
batch_size=batch_size,
shuffle=True)
return dataloader
import torch
import torch.nn as nn
import numpy as np
def weights_init(m):
"""
Initialise weights of the model.
"""
if(type(m) == nn.ConvTranspose2d or type(m) == nn.Conv2d):
nn.init.normal_(m.weight.data, 0.0, 0.02)
elif(type(m) == nn.BatchNorm2d):
nn.init.normal_(m.weight.data, 1.0, 0.02)
nn.init.constant_(m.bias.data, 0)
class NormalNLLLoss:
"""
Calculate the negative log likelihood
of normal distribution.
This needs to be minimised.
Treating Q(cj | x) as a factored Gaussian.
"""
def __call__(self, x, mu, var):
logli = -0.5 * (var.mul(2 * np.pi) + 1e-6).log() - (x - mu).pow(2).div(var.mul(2.0) + 1e-6)
nll = -(logli.sum(1).mean())
return nll
def noise_sample(n_dis_c, dis_c_dim, n_con_c, n_z, batch_size, device):
"""
Sample random noise vector for training.
INPUT
--------
n_dis_c : Number of discrete latent code.
dis_c_dim : Dimension of discrete latent code.
n_con_c : Number of continuous latent code.
n_z : Dimension of iicompressible noise.
batch_size : Batch Size
device : GPU/CPU
"""
z = torch.randn(batch_size, n_z, 1, 1, device=device)
idx = np.zeros((n_dis_c, batch_size))
if(n_dis_c != 0):
dis_c = torch.zeros(batch_size, n_dis_c, dis_c_dim, device=device)
for i in range(n_dis_c):
idx[i] = np.random.randint(dis_c_dim, size=batch_size)
dis_c[torch.arange(0, batch_size), i, idx[i]] = 1.0
dis_c = dis_c.view(batch_size, -1, 1, 1)
if(n_con_c != 0):
# Random uniform between -1 and 1.
con_c = (torch.rand(batch_size, n_con_c, 1, 1, device=device) * 2 - 1)
noise = z
if(n_dis_c != 0):
noise = torch.cat((z, dis_c), dim=1)
if(n_con_c != 0):
noise = torch.cat((noise, con_c), dim=1)
return noise, idx
MNIST의 경우에 digit type을 나타내는 변수 10개와 rotaion, width를 나타내는 변수 2개에 62개의노이즈를 추가해서 총 72개의 z로 시작합니다.
Train
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.utils as vutils
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import time
import random
# Set random seed for reproducibility.
seed = 20240409
random.seed(seed)
torch.manual_seed(seed)
print("Random Seed: ", seed)
# Use GPU if available.
device = torch.device("cuda:0" if(torch.cuda.is_available()) else "cpu")
print(device, " will be used.\n")
dataloader = get_data(params['dataset'], params['batch_size'])
# Set appropriate hyperparameters depending on the dataset used.
# The values given in the InfoGAN paper are used.
# num_z : dimension of incompressible noise.
# num_dis_c : number of discrete latent code used.
# dis_c_dim : dimension of discrete latent code.
# num_con_c : number of continuous latent code used.
# num_z 62 -> 61, num_con_c 2 -> 3
if(params['dataset'] == 'MNIST'):
params['num_z'] = 62
params['num_dis_c'] = 1
params['dis_c_dim'] = 10
params['num_con_c'] = 2
elif(params['dataset'] == 'FashionMNIST'):
params['num_z'] = 62
params['num_dis_c'] = 1
params['dis_c_dim'] = 10
params['num_con_c'] = 2
# Plot the training images.
sample_batch = next(iter(dataloader))
plt.figure(figsize=(10, 10))
plt.axis("off")
plt.imshow(np.transpose(vutils.make_grid(
sample_batch[0].to(device)[ : 100], nrow=10, padding=2, normalize=True).cpu(), (1, 2, 0)))
plt.savefig('Training Images {}'.format(params['dataset']))
plt.close('all')
# Initialise the network.
netG = Generator().to(device)
netG.apply(weights_init)
print(netG)
discriminator = Discriminator().to(device)
discriminator.apply(weights_init)
print(discriminator)
netD = DHead().to(device)
netD.apply(weights_init)
print(netD)
netQ = QHead().to(device)
netQ.apply(weights_init)
print(netQ)
# Loss for discrimination between real and fake images.
criterionD = nn.BCELoss()
# Loss for discrete latent code.
criterionQ_dis = nn.CrossEntropyLoss()
# Loss for continuous latent code.
criterionQ_con = NormalNLLLoss()
# Adam optimiser is used.
optimD = optim.Adam([{'params': discriminator.parameters()}, {'params': netD.parameters()}], lr=params['learning_rate'], betas=(params['beta1'], params['beta2']))
optimG = optim.Adam([{'params': netG.parameters()}, {'params': netQ.parameters()}], lr=params['learning_rate'], betas=(params['beta1'], params['beta2']))
# Fixed Noise
z = torch.randn(100, params['num_z'], 1, 1, device=device)
fixed_noise = z
if(params['num_dis_c'] != 0):
idx = np.arange(params['dis_c_dim']).repeat(10)
dis_c = torch.zeros(100, params['num_dis_c'], params['dis_c_dim'], device=device)
for i in range(params['num_dis_c']):
dis_c[torch.arange(0, 100), i, idx] = 1.0
dis_c = dis_c.view(100, -1, 1, 1)
fixed_noise = torch.cat((fixed_noise, dis_c), dim=1)
if(params['num_con_c'] != 0):
# 회전, 너비 등을 더 자세히 보기위함
con_c = (torch.rand(100, params['num_con_c'], 1, 1, device=device) * 2 - 1)
fixed_noise = torch.cat((fixed_noise, con_c), dim=1)
real_label = 1
fake_label = 0
# List variables to store results pf training.
img_list = []
G_losses = []
D_losses = []
print("-"*25)
print("Starting Training Loop...\n")
print('Epochs: %d\nDataset: {}\nBatch Size: %d\nLength of Data Loader: %d'.format(params['dataset']) % (params['num_epochs'], params['batch_size'], len(dataloader)))
print("-"*25)
start_time = time.time()
iters = 0
for epoch in range(params['num_epochs']):
epoch_start_time = time.time()
for i, (data, _) in tqdm(enumerate(dataloader, 0)):
# Get batch size
b_size = data.size(0)
# Transfer data tensor to GPU/CPU (device)
real_data = data.to(device)
# Updating discriminator and DHead
optimD.zero_grad()
# Real data
label = torch.full((b_size, ), real_label, device=device)
# label type을 맞추기 위해 추가
label=label.to(torch.float32)
output1 = discriminator(real_data)
probs_real = netD(output1).view(-1)
loss_real = criterionD(probs_real, label)
# Calculate gradients.
loss_real.backward()
# Fake data
label.fill_(fake_label)
noise, idx = noise_sample(params['num_dis_c'], params['dis_c_dim'], params['num_con_c'], params['num_z'], b_size, device)
fake_data = netG(noise)
output2 = discriminator(fake_data.detach())
probs_fake = netD(output2).view(-1)
loss_fake = criterionD(probs_fake, label)
# Calculate gradients.
loss_fake.backward()
# Net Loss for the discriminator
D_loss = loss_real + loss_fake
# Update parameters
optimD.step()
# Updating Generator and QHead
optimG.zero_grad()
# Fake data treated as real.
output = discriminator(fake_data)
label.fill_(real_label)
probs_fake = netD(output).view(-1)
gen_loss = criterionD(probs_fake, label)
q_logits, q_mu, q_var = netQ(output)
target = torch.LongTensor(idx).to(device)
# Calculating loss for discrete latent code.
dis_loss = 0
for j in range(params['num_dis_c']):
dis_loss += criterionQ_dis(q_logits[:, j*10 : j*10 + 10], target[j])
# Calculating loss for continuous latent code.
con_loss = 0
if (params['num_con_c'] != 0):
con_loss = criterionQ_con(noise[:, params['num_z']+ params['num_dis_c']*params['dis_c_dim'] : ].view(-1, params['num_con_c']), q_mu, q_var)*0.1
# Net loss for generator.
G_loss = gen_loss + dis_loss + con_loss
# Calculate gradients.
G_loss.backward()
# Update parameters.
optimG.step()
# Check progress of training.
if i != 0 and i%100 == 0:
print('[%d/%d][%d/%d]\tLoss_D: %.4f\tLoss_G: %.4f'
% (epoch+1, params['num_epochs'], i, len(dataloader),
D_loss.item(), G_loss.item()))
# Save the losses for plotting.
G_losses.append(G_loss.item())
D_losses.append(D_loss.item())
iters += 1
epoch_time = time.time() - epoch_start_time
print("Time taken for Epoch %d: %.2fs" %(epoch + 1, epoch_time))
# Generate image after each epoch to check performance of the generator. Used for creating animated gif later.
with torch.no_grad():
gen_data = netG(fixed_noise).detach().cpu()
img_list.append(vutils.make_grid(gen_data, nrow=10, padding=2, normalize=True))
# Generate image to check performance of generator.
if((epoch+1) == 1 or (epoch+1) == params['num_epochs']/2) or epoch%5==0:
with torch.no_grad():
gen_data = netG(fixed_noise).detach().cpu()
plt.figure(figsize=(10, 10))
plt.axis("off")
plt.imshow(np.transpose(vutils.make_grid(gen_data, nrow=10, padding=2, normalize=True), (1,2,0)))
plt.savefig("Epoch_%d {}".format(params['dataset']) %(epoch+1))
plt.close('all')
# Save network weights.
if (epoch+1) % params['save_epoch'] == 0:
torch.save({
'netG' : netG.state_dict(),
'discriminator' : discriminator.state_dict(),
'netD' : netD.state_dict(),
'netQ' : netQ.state_dict(),
'optimD' : optimD.state_dict(),
'optimG' : optimG.state_dict(),
'params' : params
}, 'InfoGAN/model_epoch_%d_{}'.format(params['dataset']) %(epoch+1))
training_time = time.time() - start_time
print("-"*50)
print('Training finished!\nTotal Time for Training: %.2fm' %(training_time / 60))
print("-"*50)
# Generate image to check performance of trained generator.
with torch.no_grad():
gen_data = netG(fixed_noise).detach().cpu()
plt.figure(figsize=(10, 10))
plt.axis("off")
plt.imshow(np.transpose(vutils.make_grid(gen_data, nrow=10, padding=2, normalize=True), (1,2,0)))
plt.savefig("Epoch_%d_{}".format(params['dataset']) %(params['num_epochs']))
# Save network weights.
torch.save({
'netG' : netG.state_dict(),
'discriminator' : discriminator.state_dict(),
'netD' : netD.state_dict(),
'netQ' : netQ.state_dict(),
'optimD' : optimD.state_dict(),
'optimG' : optimG.state_dict(),
'params' : params
}, 'InfoGAN/model_final_{}'.format(params['dataset']))
# Plot the training losses.
plt.figure(figsize=(10,5))
plt.title("Generator and Discriminator Loss During Training")
plt.plot(G_losses,label="G")
plt.plot(D_losses,label="D")
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.savefig("Loss Curve {}".format(params['dataset']))
# Animation showing the improvements of the generator.
fig = plt.figure(figsize=(10,10))
plt.axis("off")
ims = [[plt.imshow(np.transpose(i,(1,2,0)), animated=True)] for i in img_list]
anim = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=1000, blit=True)
anim.save('infoGAN_{}.gif'.format(params['dataset']), dpi=80, writer='imagemagick')
plt.show()
이렇게 돌리면 오류가 나는 부분이 있는데, torch.save쪽에서 오류가 납니다. 저장할때 MNIST data에 대해서 수행했다면 ‘InfoGAN/model_final_MNIST’ 에 저장이 됩니다. 즉, InfoGAN 파일에 model_final_MNIST로 저장이 되는데 저희는 코랩에서 아무것도 건드리지 않았기 때문에 InfoGAN 파일이 없죠. 그래서 직접 만들어야 합니다. 만들면 이 오류는 없어지게 됩니다!
InfoGAN 파일을 위 처럼 만드셨다면 문제없이 실행됩니다.
또한 label=label.to(torch.float32) 부분은 label의 type이 torch.long 형태에서 ‘loss_real = criterionD(probs_real, label)’ 이부분에서 오류가 납니다.(probs_real은 float형태기 때문에) 따라서 probs_real과 type을 동일하게 하기 위해 float으로 변경하였습니다.
마지막으로 if((epoch+1) == 1) or epoch%5==0: 이 부분은 epoch이 5의 배수만큼 돌았을 때 사진을 출력하도록 변경하였습니다.
이렇게 설정하고 나서 분석을 수행한 결과를 보여드리겠습니다.
Epochs 100번 iteration
아무런 정보도 없었는데, 그래도 잘 분류하네요!
이제 숫자 말고 $c_2$, $c_3$(Rotation,Width)을 uniform 분포에서 변경할수록 어떻게 변화하는지 살펴보도록 하겠습니다.
Feature
import argparse
import torch
import torchvision.utils as vutils
import numpy as np
import matplotlib.pyplot as plt
parser = argparse.ArgumentParser()
parser.add_argument('-load_path', required=True, help='Checkpoint to load path from')
args = parser.parse_args(['-load_path', 'InfoGAN/model_final_MNIST'])
# from models.mnist_model import Generator
# Load the checkpoint file
state_dict = torch.load(args.load_path)
# Set the device to run on: GPU or CPU.
device = torch.device("cuda:0" if(torch.cuda.is_available()) else "cpu")
# Get the 'params' dictionary from the loaded state_dict.
params = state_dict['params']
# Create the generator network.
netG = Generator().to(device)
# Load the trained generator weights.
netG.load_state_dict(state_dict['netG'])
print(netG)
c = np.linspace(-2, 2, 10).reshape(1, -1)
c = np.repeat(c, 10, 0).reshape(-1, 1)
c = torch.from_numpy(c).float().to(device)
c = c.view(-1, 1, 1, 1)
zeros = torch.zeros(100, 1, 1, 1, device=device)
# Continuous latent code.
c2 = torch.cat((c, zeros), dim=1)
c3 = torch.cat((zeros, c), dim=1)
# c4 = torch.cat((zeros, c), dim=1)
idx = np.arange(10).repeat(10)
dis_c = torch.zeros(100, 10, 1, 1, device=device)
dis_c[torch.arange(0, 100), idx] = 1.0
# Discrete latent code.
c1 = dis_c.view(100, -1, 1, 1)
z = torch.randn(100, 62, 1, 1, device=device)
# To see variation along c2 (Horizontally) and c1 (Vertically)
noise1 = torch.cat((z, c1, c2), dim=1)
# To see variation along c3 (Horizontally) and c1 (Vertically)
noise2 = torch.cat((z, c1, c3), dim=1)
# # To see variation along c4 (Horizontally) and c1 (Vertically)
# noise3 = torch.cat((z, c1, c4), dim=1)
# Generate image.
with torch.no_grad():
generated_img1 = netG(noise1).detach().cpu()
# Display the generated image.
fig = plt.figure(figsize=(10, 10))
plt.axis("off")
plt.imshow(np.transpose(vutils.make_grid(generated_img1, nrow=10, padding=2, normalize=True), (1,2,0)))
plt.show()
# Generate image.
with torch.no_grad():
generated_img2 = netG(noise2).detach().cpu()
# Display the generated image.
fig = plt.figure(figsize=(10, 10))
plt.axis("off")
plt.imshow(np.transpose(vutils.make_grid(generated_img2, nrow=10, padding=2, normalize=True), (1,2,0)))
plt.show()
Rotation(-1~1)
Width(-1~1)
결과를 보시면 오른쪽에서 왼쪽으로 갈수록 Rotaiton, Width의 특징을 학습하고 있다고 볼 수 있을 것 같습니다!
Rotation의 경우에는 조금씩 기울어지고있으며, Width의 경우에는 너비가 조금씩 커지는 것을 확인할 수 있었습니다!
논문에서와는 달리 뚜렷한 결과를 보이지 않고, 두 변수간에 섞임이 조금 있어보이는 것 같습니다. Width에서도 약간 기울어지며 생성이 되는듯한 모습을 보이고, Rotation에서도 약간 너비가 커져가는 것을 보이는 듯 합니다!
InfoGAN이란 기존의 GAN 모델에서 정보이론(information-theoretic)의 개념을 추가하여 Disentangled representation을 학습할 수 있도록 하는 모델입니다.
Disentangled representation이란?
데이터의 특징(feature)이나 변수가 서로 분리되어 표현된다는 것을 의미합니다.
사람의 얼굴 이미지를 다룬다고 할때 사람의 표정, 눈 색상, 헤어스타일, 선글라스 유무, 숫자 이미지를 다룬다고 할때 숫자의 크기, 두께(thickness), 각도(angle) 등의 특징이 분리되어 표현된다면 disentangled representation이라고 합니다.
분리된 feature를 통해 사람의 얼굴 이미지를 생성
기존의 GAN 모델에서는 Input data와 유사한 데이터를 만드는 것에 목적을 두었다면, InfoGAN은 유사한 데이터를 만들면서 데이터의 특징을 잘 학습하는데 중점을 둡니다.
잘 학습되게 된다면, 숫자 데이터에서 두께, 각도 등을 다르게 생성할 수 있게 된다는 큰 장점이 있습니다.
위 그림을 보면 (a),(c),(d)의 경우 InfoGAN의 결과이고, (b)는 Original GAN의 결과입니다. 변수에 약간의 값을 변경해서 넣어주게 되면 회전하는 것과 넓이가 커지는 것을 볼 수 있습니다. (위 그림에선 c2는 회전의 특징을 학습한 변수이며 c3는 넓이의 특징을 학습한 변수라고 생각할 수 있습니다.)
2. Abstract
‘learn disentangled representations’ 위에서 말했듯이 정보이론의 개념을 도입해 데이터의 특징을 잘 학습할 수 있도록 학습하는 것입니다.
maximizes the mutual information mutual information term을 maximize하는 것을 목표로 합니다. (Mutual information은 상호정보라 불리고 아래에서 자세하게 설명하겠습니다.)
lower bound of the mutual information 직접적인 mutual information term을 구할 수 없으므로 lower bound를 구해서 그 lower bound를 maximize하는 것을 목표로 합니다.
learns interpretable representations 실험결과에서 InfoGAN은 해석가능한 representation을 학습했다고 말하고 있습니다.
3. Background: Generative Adversarial Networks
GAN(Generative Adversarial Networks) 모델의 목적은 생성된 데이터 분포 $P_{G}(x)$ 가 실제 데이터 분포인 $P_{data}(x)$와 유사하게 학습하도록 하는 모델입니다.
그러기 위해서는 생성자(Generator)와 판별기(Discriminator)를 학습하게 되는데 생성자의 경우 noise를 입력으로 받아 생성자(G)를 통해 이미지를 생성하고, 판별기(D)는 입력으로 받은 데이터가 실제 데이터인지, G가 생성해낸 이미지인지 판별하게 됩니다.
생성자(G)의 입장에서는 실제 데이터와 아주 유사하게 만들어야 하고, 판별기(D)는 실제 데이터인지, 생성자(G)가 만든 데이터인지 구분을 잘 할 수 있도록 설계되어 있다고 생각하시면 됩니다! 이 내용을 식으로 쓰게되면 아래와 같이 표현할 수 있습니다.
GAN의 목적함수
이제 G와 D입장에서 minimization, maximizatinon하는 과정을 보겠습니다.
Discriminator : D의 입장에서는 실제 데이터를 1로 출력하고 생성된 데이터는 0이라고 출력하길 원합니다!
위 그림처럼 되면 maximization이 되겠지요!
Generator : G의 입장에서는 판별기(D)가 1로 출력해주길 원합니다!
첫 번째 항에는 G가 없으니 두 번째 항만 신경쓰면 됩니다! minimization해야하니 log0 에 수렴해야 가장 작은 값을 얻을 수 있겠죠!
4. Mutual Information for Inducing Latent Codes
GAN에서 noise vector(z)를 입력으로 받고 G가 데이터를 생성했었습니다. 하지만 이때 z에게 아무런 제약을 주지 않았습니다. 그렇기 때문에 Generator는 매우 꼬여있다(entangled way)고 볼 수 있습니다. 여기서 꼬여있다는 것은 z의 차원들이 data에서 의미를 가지는 feature와 대응되지 않는다고 볼 수 있습니다!
아래 사진처럼 얼굴 이미지를 생성할 때처럼 z의 차원들이 의미(특징)를 가지고 있지 않다는 것으로 보면 될 것 같습니다!
분리된 feature를 통해 사람의 얼굴 이미지를 생성
MNIST data의 경우만 봐도 0~9 숫자를 나타내는 digit type, 회전을 나타내는 Rotation, 너비를 나타내는 Width, 두께를 나타내는 thickness 등의 특징을 가지고 있음을 알 수 있습니다.
이런 특징에 대한 정보를 가지고서 이미지를 생성하면 원하는 데이터를 생성할 수 있지 않을까? 라는 생각에서 GAN의 확장모델인 InfoGAN이 만들어진 것입니다!
InfoGAN에서는 noise vector(z)를 두개의 파트로 나누어서 사용합니다.
압축할 수 없는 noise z 이 z는 생성된 데이터에 무작위성이나 변동성을 주입하기 위해 사용된다고 합니다. 데이터의 구조를 설명하진 않지만, 생성된 샘플의 다양성을 증가시키는 역할을 합니다.
데이터분포에서 특징을 가지는 latent code “c” 얼굴이미지를 생성한다고 할 때 표정, 눈 색상 등 의미를 가지는 feature를 c라고 생각하시면 됩니다. 또한 latent code c는 factored distribution을 가정합니다. factored distribution은 변수들과 독립성을 가정하고 확률분포로 표현합니다. $P(c_1,c_2,...,c_L)=\Pi_{i=1}^LP(c_i)$
저희는 이제 noise vector를 두 파트로 나누었으니 Generator에서는 z와 c를 입력으로 받아 생성해야겠죠! G(z) → G(z,c)가 됩니다!
🤔 However, in standard GAN, the generator is free to ignore the additional latent code c by finding a solution satisfying $P_G(x|c)=P_G(x)$
기존의 GAN의 모델에서는 아무런 제약을 주지 않아 $P_G(x|c)=P_G(x)$를 만족하는 solution을 찾기 때문에 c가 무시되게 됩니다.
이렇게 되는 것을 피하기 위해 우리는 추가로 제약을 걸어줘야 합니다! 그래서 저자는 information -theoretic regularization을 제안하였고, latent code c와 G(z,c)간의 상호정보량이(mutual information) 높아야 한다고 말하고 있습니다. 따라서 $I(c;G(z,c))$이 값이 높기를 원한다는 겁니다.(제가 초반에 정보이론개념을 추가할거라고 말한 부분입니다!)
정보이론에서 X와 Y 사이의 상호정보량(mutual information)은 $I(X;Y)$로 표현되고 Y 변수를 통해 X에 대해 얻어진 “정보량”이라고 하고 식은 아래와 같이 쓰일 수 있습니다.
동물의 특징이 주어졌을 때 동물을 예측하는것과 특징이 주어지지 않았을 때 동물을 예측하는 것 중 어느것이 더 ‘잘’ 예측할 수 있을까요? → 당연히 정보가 주어졌을 때 예측하기 쉽겠죠!(불확실성이 더 감소!) 이렇게 정보가 주어졌을 때 얼마나 더 잘 예측할 수 있는가를 불확실성의 감소량이라고 볼 수 있습니다!
만약 X와 Y가 독립이면
$I(X;Y)=\Sigma_{x\in X,y\in Y}P_{X,Y}(x,y)log(\frac{P_{X,Y}(x,y)}{P_{X}(x)P_{Y}(y)})$ 이 부분에서 $P_{X,Y}(x,y)=P_X(x)P_Y(y)$로 쓸 수 있기 때문에 $I(X;Y)=\Sigma_{x\in X,y\in Y}P_{X,Y}(x,y)log(1)$ =0이 되는 것을 알 수 있습니다!
직관적으로 해석해보면 동전을 던져서 나오는 결과를 생각해본다고 해보겠습니다.
첫 번째 동전을 던져서 앞면이 나왔다고 두 번째 동전이 앞면이 나올 확률이 바뀔까요? 아닙니다. 그대로 $\frac{1}{2}$인 것을 알 수 있죠. 즉, 두 번째 결과는 첫 번째 결과를 통해 얻어진 정보가 없다고 볼 수 있죠! 그렇기에 상호정보량은 0이라고 할 수 있습니다.
다시 돌아와서, 저희는 $P_G(x)$에서 뽑힌 x가 주어졌을 때, $P_G(c|x)$가 small entropy를 갖기를 원합니다!(small entropy는 정보의 불확실성이 적다는 것을 의미한다고 생각하시면 됩니다.)
그래서 기존 GAN의 목적함수에서 information-regularized term인 $I(c;G(z,c))$을 추가하여 이 값이 최대가 되도록 합니다! 식을 쓰면 아래와 같습니다.
GAN에서는 G를 minimization하도록 만들었으니 $I(c;G(z,c))$을 maximization하는 것 대신 -$I(c;G(z,c))$를 minimization하도록 term을 추가한 것입니다! ($\lambda$는 hyperparameter 입니다.)
5. Variational Mutual Information Maximization
이제 우리는 $I(c;G(z,c))$ term을 maximization[ -$I(c;G(z,c))$를 minimization하는 것과 같아서 maximization관점으로 보겠습니다.]해야합니다. 하지만, 우리는 이 식을 통해서 직접적으로 maximization할 수 없습니다. 왜그럴까요? 우선 $I(c;G(z,c))$ 식을 전개해 보겠습니다.
식이 되게 복잡해 보입니다… 차근차근 하나씩 살펴보죠!! 형광색으로 칠한 부분 먼저 보겠습니다.
$I(X;Y)$에서 {X ← c, Y ← G(z,c)}대입하시면 $I(c;G(z,c))$ 이렇게 위 식처럼 나오게 됩니다.
위와 같은 식($I(c;G(z,c))$)을 maximization하기 위해서는 $P(c|x)$의 분포에서 뽑은 샘플 $c'$이 필요합니다. 즉, 우리는 데이터가 주어졌을 때 그 데이터의 특징(헤어스타일, 표정 등)을 알고싶은 것입니다! 하지만 우리는 알지 못하기 때문에 auxiliary distribution(보조분포) Q를 사용하여 근사하려 합니다. 보조분포는 보통 우리가 흔히 알고있는 Gaussian distribution으로 설정합니다! 그렇게 $Q(c|x)$를 설정하게되면, $P(c|x)$의 분포와 $Q(c|x)$의 분포간 차이가 있겠죠?? 그래서 분포들 간 거리를 측정하는 지표로 KL divergence라는 함수로 거리를 측정합니다.
위의 식을 그대로 갖고와서 log에음수를 취해서 분모 분자를 바꿨습니다. 편의를 위해 $\frac{Q(c'|x)}{P(c'|x)}=Z$라고 설정하겠습니다.
$\mathbb{E}\left[-log(Z)\right]$로 표현이 됩니다. 그런데 이 식을 자세히 보면 $-log(x)$와 $\mathbb{E}$의 형태로 이루어진 것을 볼 수 있습니다. $-log(x)$는 convex function이기 때문에 Jensen’s inequality를 적용할 수 있죠!
마지막으로 $D_{KL}(P(|x)||Q(|x))$ 식은 항상 0보다 크기때문에 이 식을 삭제하면 저 부등호가 성립한다는 것도 알 수 있습니다! 결국 하고싶은건 $I(c;G(z,c))$을 maximization을 하고싶은데, $P(c|x)$를 알지 못하니까 보조분포인 $Q(c|x)$를 이용해서 lower bound를 만들고 저 lower bound를 maximization 하고자 하는 것 입니다! 이것을 variational mutual information maximization라 합니다.
하지만 여기서 또 한가지 문제점이 있죠. 저희는 $P(c|x)$를 모르기 때문에 $Q(c|x)$를 사용한다고 했는데, 마지막에 구한식을 보면 $c'$을 $P(c|x)$분포에서 뽑고있습니다.
저자는 이 Lemma를 사용하여 값 교묘하게 변경하였습니다!
🔑 Lemma 5.1 For random variables X,Y and function $f(x,y)$under suitable regularity conditions: $\mathbb{E}{x\sim X,y\sim Y|x}\left[f(x,y)\right]=\mathbb{E}{x\sim X,y\sim Y|x,x'\sim X|y}\left[f(x',y)\right]$.
약간의 트릭을 사용하여 이렇게 변환이 가능하다고 하네요! 이 자료는 고려대학교 임성빈 교수님께서 2가지 방식으로 증명해주신 자료입니다.
첫 번째 증명
2번째 줄에서 3번째줄에서 x→ x’ 으로 rename했는데, 확률변수를 바꾼게 아니라 적분할 때 변수 표기를 바꾼거에 불과하다고 합니다. 그래서 성립하는 것을 볼 수 있죠.
2번째 증명에서는 Law of total expectation(이중 기대값) 정리를 사용하여 증명하셨습니다.
Law of total expectation 증명을 먼저 보시고 아래의 사진을 참고하시면 됩니다!
$I(c;G(z,c))$을 maximization하는 것 대신에 lower bound인 $L_1(G,Q)$을 maximization하자!!
따라서 infoGAN의 최종 목적식은 다음과 같습니다. ($\lambda$는 hyperparameter입니다.)
6. Experiments
실험을 통해 저자는 2가지를 달성하고자 합니다.
첫 번째 : 실제로 mutual information이 maximization이 되는지
두 번째 : InfoGAN이 구분되고 해석 가능한 representation을 학습하는지(사람의 표정, 헤어스타일 등의 특징을 잘 학습했는지)
6-1. Mutual Information Maximization
latent codes c와 G(z,c)간의 mutual information을 평가하기 위해서 MNIST 데이터셋을 사용했습니다. $c \sim Cat(K=10,p=0.1)$의 분포로 설정하고나서 Lower bound를 각 iteration(반복)마다 $H(c)$값을 기록했습니다. $H(10) \approx 2.30$으로 빠르게 maximization되는 것을 확인할 수 있습니다.
Cat(카테고리 분포)의 pmf를 나타내면 다음과 같습니다.
Lower bound L1 over training iteration
6-2. Disentabgled Representation
MNIST 데이터셋에서 Disentabgled Representation을 잘 학습했는지 확인하기 위해 latent code $c_1,c_2,c_3$를 추가했는데 $c_1 \sim Cat(K=10,p=0.1)$이며 $c_2,c_3$변수는 연속형 변수로 $Unif(-1,1)$을 사용하였습니다.
$c_1$ 변수의 경우 label에 대한 정보도 없이 0~9까지의 숫자를 잘 생성해낼 수 있는 것을 볼 수 있었습니다.
$c_2,c_3$ 변수의 경우 $c_2$는 숫자의 rotation(회전)에 대한 변수이고 $c_3$는 숫자의 Width(너비)에 대한 변수인것을 확인할 수 있습니다. 논문에서는 $Unif(-2,2)$를 사용해서 결과를 보여주고 있습니다.(더 극명한 결과를 보여주기 위함입니다.) latent code가 이런 특징들을 잘 포착한 걸로 보아 Disentabgled Representation을 잘 학습했다고 볼 수 있습니다.
즉, InfoGAN을 통해서 Mutual Information Maximization, Disentabgled Representation을 모두 달성했다고 볼 수 있습니다!
7. Conclusion
이 논문은 “Information Maximizing Generative Adversarial Networks”(InfoGAN) 이라고 불리는representation 학습 알고리즘을 소개했습니다. supervision을 요구로 하는 이전의 접근방법들과 다르게, InfoGAN은 비지도학습으로 해석과 분리가능한 representation을 학습하였다는 것입니다. 또한 GAN과 연산시간이 거의 비슷하다고 합니다!