자 다시 GAN 포스팅을 작성할 시간이네요.

아 GAN은 쓸게 많아서 너무 괴로워요... PS쪽이 확실히 더 작성하기 편하네요.

익숙한 개념이라서 그런걸까요?

오늘 알아볼 내용은 GAN의 증명과 한계 그리고 해결법에 관한 내용입니다. 오늘 내용은 WGAN을 들어가기 앞서 필요한 GAN에 대한 증명과 한계들을 다뤄볼 예정입니다.

수학적인 내용을 최소한으로 준비했지만, 도저히 못덜어낸 수학들이 좀 많이 남았습니다.

작성한 글에 오류가 있다면 코멘트 달아주세요! 그럼 바로 시작하겠습니다.

복습

역시 복습을 안하고 갈수는 없겠죠?

복습에서는 이전 포스팅의 내용을 요약하도록 하겠습니다.

 

[논문 정리] GAN에 대해서 알아보자 (1) - 개념, 알고리즘, 소스코드 분석

GAN에 대해서 알아보자 - 개념, 알고리즘, 소스코드 최근에 GAN에 관해서 이것저것 많이 찾아볼 기회가 있어서 이것을 정리하고 블로깅하고자 합니다. 이번 블로깅에서는 최대한 수학적인 내용없

lifeignite.tistory.com

GAN의 Component

GAN은 두가지 component로 구성되어 있습니다. Generator와 Discriminator입니다. Generator는 실제 데이터와 유사한 분포의 데이터를 생성하기 위해 노력하는 Component죠. Discriminator는 Generator가 만든 가짜 데이터와 실제 Data를 구별해주는 Component였습니다. Discriminator로 머신러닝 네트워크를 사용하는 이유는 generator가 생성하는 분포와 실제 데이터 분포를 수식화 하기 힘든 매우 어려운 분포이기 때문입니다.

GAN의 Sampling

그리고, GAN은 latent space에서 latent vector z를 sampling합니다. 초기의 latent space는 아무런 의미를 가지지 않기 때문에 여기서 sampling된 latent vector z도 아무런 의미를 가지지 않습니다만, 학습을 통해서 latent vector와 특정 데이터를 연결하게 되면서 latent space가 의미를 가지도록 바꿔줍니다. 마치 사람들이 아무의미없는 것들에 의미를 부여하는것처럼 네트워크도 경험적으로 latent space에 의미를 부여하기 시작할겁니다. ㅎㅎ

GAN의 학습 Algorithm

GAN의 학습은 2가지 step으로 이루어졌었죠. 첫번째 스텝에서는 Generator를 고정시키고, Discriminator를 학습시킵니다. 두번째 스텝에서는 Discriminator를 고정시키고, Generator를 학습시킵니다. 그리고 고정시킴에 따라 Loss Function이 일부 변경되었었습니다.

전체를 요약해보자면

GAN의 전체 구조를 요약해보자면 GAN은 latent space에서 sampling 한 z로부터 Generator는 어떠한 데이터 분포를 만듭니다. 그리고 이렇게 생성한 Generator의 분포와 실제 데이터 분포를 Discriminator로부터 비교하게 함으로서 G가 잘생성하면 D를 혼내주고, D가 잘 구분하면 G를 혼내주는 방법으로 학습을 수행합니다.

아래와같은 구조를 가지고 있죠.

GAN과 JS Divergence

직관적으로 GAN이 왜 동작하는지는 쉽게 이해가 됩니다. 그런데 직관적으로만 ㅇㅋ 하고 가면 조금 찝찝하죠. 그래서 GAN이 동작하는 이유에 대해서 증명을 하나하고 넘어가겠습니다.

이 내용은 이 후 WGAN으로 넘어가기 위해서 반드시 필요하다고 생각해서 넣었는데, 생략하시고 싶으시면 바로 WGAN으로 넘어가시면 됩니다.

한줄 요약

최초 제안된 GAN의 Loss function은 JSD를 계산하게 되는데, JSD는 두 분포가 겹치지 않았을 때, 상수 값을 가져서 gradient 계산으로 optimal을 찾는 것이 쉽지 않다.

GAN의 동작 조건

GAN의 목표는 실제 데이터 분포인 $P_{data}$ 와 Generator 가 만든 가짜 데이터 분포인 $P_{g}$ 가 같아지는 것을 목표로 합니다.

즉 GAN의 Loss Function인 아래 식이 최적일 때, $P_{data}=P_g$ 임을 만족해야한다는 뜻이죠.

$$\min_{G} \max_{D} V(G, D)=\mathbb{E}_{x \sim P_{data}(x)}[\log D(x)]+\mathbb{E}_{z \sim P_{z} (z)}[\log (1-D(G(z))]$$

일단, 최적의 G에 대해서 생각할 거니까, G를 고정한 것과 같죠? 저번 시간에 봤던 식이 튀어나올 겁니다.

$$\max_{D} V(G, D)=\mathbb{E}_{x \sim P_{data}(x)}[\log D(x)]+\mathbb{E}_{x \sim P_g (x)}[\log (1-D(x)]$$

 


Term 정리

자 변수가 너무 많으니까 좀 정리해볼께요.

  • $x$: 이미지 ( high-dimensional vector)
  • $G$: generator 함수입니다.
  • $D$: discriminator 함수입니다.
  • $P_{data}$: 실제 데이터 셋의 분포입니다.
  • $P_g$: generator가 생성한 가짜 데이터의 분포입니다.
  • $\mathbb{E}$: 기대값이에요. 의외로 이 기호를 잘모르시는 분들이 많더라구요.
  • 기대값은 $p(x)$의 확률로 나타나는 어떤 함수 $f(x)$의 평균으로써 $$\mathbb{E}_{x \sim p}[f] = \int p(x)f(x)dx$$로 계산될 수 있습니다.

 


Loss Function 이 최적일 때의 D

자 변수를 정리해놓고 봤는데, 저희의 목적을 잊지 말죠.

저희가 확인하고 싶은 것은 이 Loss Function이 최적일 때, $P_{data}=P_g$ 가 되는지 확인하는 것이에요.

그럼, Loss Function이 최적일 때는 어떨까요? 바로 Loss Function $V$ 를 최대화하는 것입니다.

Loss Function을 최대화하는 최적의 (optimal) $D$를 $D^*$ 라고 할께요 . 이를 수식으로 나타내면 아래와 같은 거에요.

$$D^*=arg\underset {D}{\max}V(D,G)$$

그리고 우리의 Loss Function $V$의 식이 아래와 같았습니다.

$$\max_{D} V(G, D)=\mathbb{E}_{x \sim P_{data}(x)}[\log D(x)]+\mathbb{E}_{x \sim P_g (x)}[\log (1-D(x)]$$

$\mathbb{E}$ 를 풀어볼게요. 기대값은 $\mathbb{E}_{x \sim p}[f] = \int p(x)f(x)dx$ 로 풀리니까,

$$\begin{aligned}V &=\mathbb{E}_{x \sim P_{ data }}[\log D(x)]+\mathbb{E}_{x \sim P_{g}}[\log (1-D(x))] \\&=\int_{x} P_{data }(x) \log D(x) d x+\int_{x} P_{g}(x) \log (1-D(x)) d x \\&=\int_x\left[P_{\text {data }}(x) \log D(x)+P_{g}(x) \log (1-D(x))\right] d x\end{aligned}$$

단순하게 식에 대입하고, 적분 기호안쪽으로 모아둔 것 밖에 없는 식입니다! 정말 단순한 식이에요.

자 이제 저 식에서 $a=P_{data}$ 이고, $b=P_G(x)$ 라고 했을 때 위식을 정리해보면,

$$f(x) = alogx+blog(1-x)$$

꼴이 되게 됩니다. 이 함수를 그래프로 그려보시면, 위로 볼록한 모양이 되게 되는데, 0과 1 사이에서 미분 가능합니다. 저희는 이 식의 최대값을 구하고 싶은 거니까 이 식을 미분해볼께요.

그러면 아래와 같이 미분이 가능합니다. (고등학교 로그 미분 입니다!)

$$\begin{aligned} \frac{f(x)}{dx} = \frac{a}{x} - \frac{b}{1-x}&=0 \\ a(x-1)+bx &=0\\ (b+a)x-a &=0 \\ \therefore x =\frac{a}{a+b} \\ \end{aligned}$$

즉, $x$가 $\frac{a}{a+b}$ 일 때, 최대 값을 가진다는 뜻이죠.

최적의 D에 다시 $a=P_{data}$ 이고, $b=P_G(x)$ 를 넣고 정리해보자면,

$$optimal \space D^*= \frac{P_{data}(x)}{P_{data}(x)+P_G(x)}$$

이 되게 됩니다.

D가 최적일 때, Loss Function의 의미는?

그럼 $D^*$ 일 때, 식은 어떻게 될까요?

한번 $a=P_{data}$ 이고, $b=P_G(x)$ , $D^*=\frac{a}{a+b}$ 로 넣고 정리 해볼께요.

$$\begin{aligned} &\max _{D} V(G, D)=V\left(G, D^{*}\right) \\ &=\int_x\left[P_{\text {data }}(x) \log D^*(x)+P_{G}(x) \log (1-D^*(x))\right] dx\\&=\int_x[\text{log}(a\frac{a}{a+b}) + \text{log}(b(1-\frac{a}{a+b}))]dx \\&=\int_x[\text{log}(\frac{1}{2}a\frac{a}{(a+b)/2}) + \text{log}(\frac{1}{2}b\frac{b}{(a+b)/2})]dx\\&=-2\text{log}2 + \int_x\text{log}a\frac{a}{(a+b)/2}dx + \int_x\text{log}b\frac{b}{(a+b)/2}dx \end{aligned}$$

자! 여기서 KLD와 JSD가 등장하게 됩니다. KLD에 대해서는 아래 포스팅을 참고하세요!

 

Entropy, Cross Entropy, KL-Divergence에 대해 알아보자

Entropy 엔트로피는 머신러닝에서 정보량을 나타내는 방법으로 사용된다. 정보의 량이라는게 되게 추상적으로 들리는데, 생각해보면 되게 간단한 개념이다. ML상에서 굉장히 많이 사용되는 개념

lifeignite.tistory.com

어쨌든 KL Divergence는 아래와 같이 정의됩니다.

$$KL(P \parallel Q)= \sum_xP(x)log\frac{P(x)}{Q(x)}$$

이식을 사용해서 풀어보자면,

$$\max _{D} V(G, D)=V\left(G, D^{*}\right) \\ =-2\text{log}2 + \int_x\text{log}a\frac{a}{(a+b)/2}dx + \int_x\text{log}b\frac{b}{(a+b)/2}dx\\ =-2 \log 2+\int_{x} P_{\text {data }}(x) \log \frac{P_{\text {data }}(x)}{\left(P_{\text {data }}(x)+P_{G}(x)\right) / 2} d x \\ +\int_{x} P_{G}(x) \log \frac{P_{G}(x)}{\left(P_{\text {data }}(x)+P_{G}(x)\right) / 2} d x \\ =-2 \log 2+\mathrm{KL}\left(\mathrm{P}_{\text {data }} \| \frac{\mathrm{P}_{\text {data }}+\mathrm{P}_{\mathrm{G}}}{2}\right)+\mathrm{KL}\left(\mathrm{P}_{\mathrm{G}} \| \frac{\mathrm{P}_{\text {data }}+\mathrm{P}_{\mathrm{G}}}{2}\right)$$

그리고, Jensen-Shannon Divergence(JSD)는 아래와 같이 정의가 됩니다.

$$\begin{aligned}\operatorname{JSD}(P \| Q)=\frac{1}{2} KL(P \| M) &+\frac{1}{2} KL(Q \| M) \\M &=\frac{1}{2}(P+Q)\end{aligned}$$

JSD는 KL의 개량형 버전이라고 생각하셔도되는데, KL이 대칭성이 성립하지 않아 거리로 사용할 수 없던 것을 대칭적으로 만들어줘 거리로 사용할 수 있게 됩니다.

모양이 JSD 처럼 변했죠? 그럼 아래와 같이 정리 할수 있게 됩니다.

$$\max _{D} V(G, D)=V\left(G, D^{*}\right)\\=-2 \log 2+\mathrm{KL}\left(\mathrm{P}_{\text {data }} \| \frac{\mathrm{P}_{\text {data }}+\mathrm{P}_{\mathrm{G}}}{2}\right)+\mathrm{KL}\left(\mathrm{P}_{\mathrm{G}} \| \frac{\mathrm{P}_{\text {data }}+\mathrm{P}_{\mathrm{G}}}{2}\right) \\=-2 \log 2+2 J S D\left(P_{\text {data }} \| P_{G}\right)$$

오우 정신 사납습니다. 결론은 저희의 Loss function은 JSD로 귀결되게 됩니다.

그리고 JSD는 두 분포사이의 거리로 사용할 수 있음으로, 따라서 위의 수식을 최소로 만드는 것은 두 분포사이의 거리를 최소로 만드는 것이란 거죠!

WGAN의 등장

자 이제부터 쉬워져요. 왜냐면 여기서 수학을 다뺏거든요.

한숨돌리시고 갑시다!

JS Divergence는 학습에 적합하지 않다!

WGAN에서 주장하길 좋은친구아저씨가 제안한 loss의 JSD는 학습에 적합하지 않다는 것이었습니다. 이걸 이해하기 위해서는 SUPP를 이해하셔야하는데, 간단히 설명할께요.

지지 집합 SUPP

SUPP는 support라고 읽는데, 한국어로는 지지집합이라고 합니다. 멋진 위키를 참고하자면

수학에서, 함수의 지지집합(支持集合, 영어: support 서포트[*]) 또는 받침은 그 함수가 0이 아닌 점들의 집합의 폐포이다.

X가 위상 공간이고, ${\displaystyle f\colon X\to \mathbb {R} }$이 함수라고 하자. 그렇다면 ${\displaystyle f}$의 지지집합 ${\displaystyle \operatorname {supp} f}$는 다음과 같다.
${\displaystyle \operatorname {supp} f=\operatorname {cl} {x\in X\colon f(x)\neq 0}}$
여기서 ${\displaystyle \operatorname {cl} }$ 폐포 연산자다.

인데, 사실 어려운말 다 쳐내고, 이해하시면 좋은 것 단하나 바로 0이 아닌 점들의 집합 입니다. 콤펙트하고 머시기하고 유계 집합 머시기 이런것들이 있는데 사실 저도 잘모릅니다. 수학과가아니라서요. 개념상 0이 아닌 점들의 집합으로 이해하고 넘어가셔도 큰문제가 없습니다.

그림으로 표현해보겠습니다. 두 분포 $\mathbb{P}_r$ 과 $\mathbb{P}_g$ 가 있다고 하겠습니다. 그 분포가 아래와 같이 있다고 가정해볼께요.

여기서 $SUPP \space \space \mathbb{P}_r$ 는 B 공간이 되고, $SUPP \space \space \mathbb{P}_g$ 는 A공간이 된다는 겁니다. 그야말로 0이 아닌 점들의 집합입니다.

이 때, $\mathbb{P}_r(A)=0, \mathbb{P}_r (B)=1$ 이 될 것입니다. 반대로, $\mathbb{P}_g(A)=1, \mathbb{P}_g (B)=0$ 이 되겠죠?

분포의 거리 측정

자 그럼 조금더 직관적인 설명을 위해서, 두 확률 변수를 2차원상의 분포라고 가정해서 생각해보겠습니다.

두 분포가 겹치지 않는다는 사실은 명확하죠. 서로 $0$ 과 $\theta$ 사이에서만 움직이니까요.

여기서 $\theta$ 는 다양한 값을 가질 수 있습니다.

하지만, $\theta \neq0$ 인 경우를 제외한 어떤 경우에도 두 분포가 겹칠 수는 없습니다. 이 경우 두 분포 중 하나가 0이 아닌 값을 가질 때는 다른 분포에서는 무조건 0인 값을 가지게 될 것입니다.

식으로 나타내면

$$\left\{\begin{array}{ll}P_{0}(x) \neq 0 & \Rightarrow P_{\theta}(x)=0 \\P_{\theta}(x) \neq 0 & \Rightarrow P_{0}(x)=0\end{array}\right.$$

이 떄 이상적으로 분포의 거리를 측정하는 지표라면 이 때 두 분포사의 거리는 어떻게 측정되는 것이 좋을까요?

직관적으로 두 분포사이의 최단거리(직선거리) $\theta$ 에 따른 값을 가지는 것이 적합할 것입니다. 예를들면 $\text{distance}(\mathbb{P}_0,\mathbb{P}_\theta)=\theta$ 와 같이 나타낼 수 있으면 좋겠네요.

KL의 경우

자, 그럼 KL divergence는 어떻게 두 분포사이의 거리를 측정할까요?

$$\mathrm{KL}\left(\mathbb{P}_{\theta} \| \mathbb{P}_{0}\right)=\int_{\left\{x: P_{\theta}(x) \neq 0\right\}} \log \left(\frac{P_{\theta}(x)}{P_{0}(x)}\right) P_{\theta}(x) \mu(d x)$$

$P_\theta>0$ 인 곳에서, $P_{0}(x)=0$ 이 되니까 아래와 같이 쓸수 있겠네요.

$$\log \left(\frac{P_{\theta}(x)}{P_{0}(x)}\right)=\infty$$

그럼 KL은 아래와 같이 계산됩니다.

$$\mathrm{KL}\left(\mathbb{P}_{\theta} \| \mathbb{P}_{0}\right)=\int \infty \cdot P_{\theta}(x) \mu(d x)=\infty$$

즉, $KL$ 값은 무한대의 값을 가지게 됩니다.

$\theta$ 가 0 이아닌 어떤 값이든, 모든 상황에서 무한대의 값을 가지게 되죠.

$$\mathrm{KL}\left(\mathbb{P}_{0}\|  \mathbb{P}_{\theta} \right)=\mathrm{KL}\left(\mathbb{P}_{0} \| \mathbb{P}_{\infty}\right)=\left\{\begin{array}{ll}\infty & : \theta \neq 0 \\0 & : \theta=0\end{array}\right.$$

JSD의 경우

이 경우는 매우 간단합니다. 한번 보시죠.

$$\begin{aligned}\mathrm{KL}\left(\mathbb{P}_{0} \| \mathbb{P}_{m}\right)=\int_{P_{0} \neq 0} \log \left(\frac{P_{0}(x)}{P_{0}(x) / 2}\right) P_{0}(x) \mu(d x)=\log 2 \\\mathrm{KL}\left(\mathbb{P}_{\theta} \| \mathbb{P}_{m}\right)=\int_{P_{\theta} \neq 0} \log \left(\frac{P_{\theta}(x)}{P_{\theta}(x) / 2}\right) P_{\theta}(x) \mu(d x)=\log 2\end{aligned}$$

그러므로 $\theta\neq0$ 이면 JS는 $\text{log}2$ 의 값을 가지게 됩니다. 따라서

$$\operatorname{JS}\left(\mathbb{P}_{\theta}, \mathbb{P}_{0}\right)=\left\{\begin{array}{ll}\log 2 & : \theta \neq 0 \\0 & : \theta=0\end{array}\right.$$

가 된다는 것이죠. 따라서 마찬가지로 $\theta$ 의 값과 상관없이 상항 같은 상수값을 나타내게됩니다.

GAN에서의 의미

이는 GAN을 학습하는데 큰 문제가 됩니다. Loss Function은 가까우면 가깝다고, 멀면 멀다고 명확히 말을 해주어야 이에 따른 gradient를 계산할 수 있습니다. 하지만 두 분포의 SUPP이 겹치지 않는다면 '두 분포가 완전히 다르다.' 라는 정보만 줄 뿐 어떻게 가깝게 만들지에 대한 정보 즉 gradient를 계산할 수 없다는 뜻입니다.

즉, D가 너무 깐깐하게 두 분포를 판단해서, 만약 두 분포가 겹치지 않는다면 두분포를 어떻게 가깝게 만들지에 대한 gradient를 계산할 수 없게 된다는 거죠.

이는 사실 일만적으로 이미지 생성과 같은 높은 dimension의 문제를 푸는 GAN에서는 이러한 분포가 겹치지 않는 문제가 더 심하게 발상할 것이기 때문에 GAN의 학습 성능이 떨어지는 등 꽤 크리티컬한 문제로 다가올 것이었습니다.

이 문제를 해결하러 왔다.

이 문제를 해결하기 위해서 2가지 방법을 소개해드리도록 하겠습니다.

  • 분포를 건드리는 방법
  • 분포의 거리 측정 방식을 개선하는 방법

당연히 우리가 앞으로 할 짓은 후자입니다만, 전자 쪽은 매우 간단하고 직관적으로 이해가 쉬우니까 한번 보고 가실께요.

노이즈를 통한 해결

Support가 겹치지 않는 것이 문제였습니다. 때문에 기존의 이미지에 노이즈 n을 추가해주면서 아래 그림처럼 두 분포의 Support영역을 넓혀 겹칠수 있도록 만들어주는 것입니다. 이렇게 두 분포를 겹치게 만들어주면 JSD를 사용해서 문제를 해결할 수 있습니다.

하지만 이러한 해결방법은 생성된 이미지가 굉장히 흐릿하게 나오는 등 문제가 발생하는 등 성능이 좋지 않게 나왔다고 합니다.

JSD가 아닌 다른 형태의 거리 측정 방식 사용

JSD는 두 분포가 겹치지 않을 때 상수가 나와 gradient를 계산하기 힘든 문제가 있었습니다.

따라서 저자는 분포가 겹치지 않아도 두 분포의 거리를 측정할 수 있는 방법인 Earth Mover Distance를 사용할 것을 제안합니다. 그리고 이제부터가 WGAN의 시작이죠.

WGAN은 다음 포스팅에서 다루도록 하겠습니다. 길이 너무 길어져서 여기서 한번 끊고 가도록 하겠습니다.

Reference

GAN에 대해서 알아보자 - 개념, 알고리즘, 소스코드

최근에 GAN에 관해서 이것저것 많이 찾아볼 기회가 있어서 이것을 정리하고 블로깅하고자 합니다. 이번 블로깅에서는 최대한 수학적인 내용없이 GAN에 대해서 깊게 이해해보고자하는 시간을 가지려고 합니다. 사실 제가 수학을 잘하지도 못하구요...

틀린 내용이 있을 수 있으니 말씀 주시고, 이제 시작해봅니다 ^ㅇ^

오늘의 내용은 크게 3개로 나누어져 있습니다.

  • Generative Model에 관한 내용
  • GAN의 개념
  • GAN의 학습 알고리즘

Introduction

GAN은 일단 생성 모델입니다. 특히 초기 GAN의 연구에서는 랜덤하게 그럴듯한 이미지를 생성하는 것부터 시작했죠. 머신러닝이 생성을한다... 딱 생각했을 때부터 쉽게 상상히 안가는 일입니다. 그만큼 풀고자하는 문제가 어렵다고 볼 수 있습니다.

파인만 아조씨는 What I cannot create, I do not understand. 라는 말을 했습니다. 대우명제로 보자면, 내가 이해했다면 생성할 수 있다고 볼수 있겠죠? ㅎㅎ 아무튼 실제로 그렇게 잘 되지는 않지만, 그만큼 생성한다는 것 자체가 쉽지 않은 영역이라는 것을 말씀드리고 싶었습니다.

무엇인가를 생성한다는 것은 일반적으로 Unsupervised Learning (UL)에 속합니다. 특히 GAN처럼 실제로 있는 것을 생성하는 것이 아니라, 랜덤하게 그럴 듯한 것들을 만드는 것은 그야말로 답이 없는 문제를 푸는 것과 같습니다. 답이 있는 Supervised Learning (SL)에 비해서 조금 더 생각하기 어려운 문제를 풀게 되는 것이죠.

머신 러닝 계열에서 이런 문제를 풀수 있는 대표적인 솔루션으로 크게 3가지를 들 수 있습니다.

  • Boltzmann machine
  • Auto-encoder (AE)
  • Generative Adversarial Network (GAN)

Auto-encoder

모두 다 어려운 영역입니다만, 직관적으로 이해하기 쉬운 AE의 개념에대해서 먼저 간단히 설명하고 넘어가겠습니다. 그림부터 보고 넘어가시죠.

image from https://blog.keras.io/building-autoencoders-in-keras.html

AE는 어려운 UL 문제를 SL 문제로 바꿔서 푼 것이라고 이해하시면 쉽습니다. 데이터 그 자체를 label로 활용한 것이죠.

  • Original Input = $x$
  • Encoder = $f$
  • Compressed representation (latent space) = $z$
  • Decoder = $g$

라고 했을 때, 일반적으로 Auto encoder는 다음과 같이 표현할 수 있습니다.

$$z=f(x), x=g(x) \rightarrow x=g(f(x))$$

즉, Encoder에게는 이미지를 잘 압축하는 방법을, 디코더에게는 잘압축된 표현 방식 (latent space)를 보고 원래 이미지를 재생성해내는 방식을 배우게 하는 것으로 동작하게 됩니다.

Generative Adversarial Network

그렇다면 GAN은 어떨까요? GAN의 기본 구조는 Auto Encder의 뒷부분을 때놓았다고 생각하셔도 좋습니다. 바로 아래와 같이요!

어떤 임의의 표현 $z$ 로부터 이미지를 생성할 수 있는 Decoder, 다른말로 Generator라고 하죠. 이 Generator로부터 이미지를 생성해냅니다.

이 구조만으로는 사실 학습이 불가능하겠죠. 원래 데이터의 분포를 알 수가 없을 테니까요. 여기서 등장하는 것이 바로 Discriminator입니다.

image from https://blog.keras.io/building-autoencoders-in-keras.html

Discriminator는 한국어로는 구별자정도로 해석될 수 있을 것 같습니다. 이 Discriminator는 실제로 구별을 하는 친구이기 때문에 이런 이름이 붙었죠. 정확히 말씀드리자면, 실제 데이터 (Real Data)로부터 데이터를 몇개 뽑고, Generator가 생성한 데이터 몇개를 또 뽑습니다. 그리고 Discriminator에게 이 두개를 섞어서 주죠. Discriminator의 역할은 주어진 데이터가 실제 데이터인지 우리가 생성한데이터인지를 구별시킵니다.

Generator의 주된 목표는 Discriminator가 구분하지 못할 정도로 실제 데이터와 유사한 데이터를 만드는 것입니다. Discriminator의 주된 목표는 Generator가 만든 것과 실제 데이터를 잘 구분해 내는 것이죠.

제설명이 부족한것 같아 GAN의 저자이신 우리의 좋은 동료.. Good fellow 아저씨께서 드신 비유를 한번 살펴 보도록 하겠습니다. 바로, 지폐위조범과 경찰입니다.

image from https://www.slideshare.net/thinkingfactory/pr12-intro-to-gans-jaejun-yoo

가짜 돈을 찍어내는 지폐위조범과, 진짜 돈과 가짜돈을 구별해야하는 경찰의 역활을 생각해볼까요?

  1. 지폐위조범이 가짜돈을 찍어 냅니다.
  2. 그럼 경찰이 이것을 보고 가짜(Fake)인지 진짜(Real)인지 구별합니다. 처음 찍어낸 돈의 품질이 좋지 않을 테니 쉽게 구분할수 있겠군요.
  3. 이를 보고 지폐 위조범은 더 돈을 정교하게 찍어낼 것입니다.
  4. 그러면 또 그것을 보고 경찰이 구별할 것입니다. 조금은더 정교해질 것이니 경찰도 최대한 가짜를 잘 구별하고자 노력할 것입니다.

이것을 계속 반복하다보면 경찰이 지폐위조범이 만든 돈을 잘 구분하지 못하는 순간이 올 것입니다. 즉 진짜 데이터와 가짜 데이터가 구분이 안가는 순간이 오는 것이죠.

그렇습니다! 이제 우리는 드디어 가짜 지폐를 잘 만들어내는 생성자를 가진 것입니다!

이것이 바로 직관적으로 GAN이 동작하는 이유입니다.

GAN의 개념

자 여기까지가 abstract 였고, 쬐끔 더 디테일하게 이해해 보도록 할까요?

Generator

자 세상의 모든 것으로 변할 수 있는 것들이 모여있는 공간을 생각해봅시다. 우리는 이러한 공간을 latent space 라고 부릅니다. 한국어로는 잠재 공간으로 불리고 있습니다. 모든 것으로도 변할 수 있는 것들이 모여 있으니까, 우리가 가지고 있는 실제 데이터 셋으로도 변할 수 있겠죠?

어떻게 이렇게 변화시킬지는 모르겠지만, 어떤 신적인 존재가 그렇게 변화시킨다고 해보도록 할께요. 그림으로 그려보자면 다음과 같은 것입니다.

하지만 신은 언제나 저희 곁에 있지만 실제로 보이지 않습니다 😢 어쩔수가 없군요. 이 신의 역할을 대체할 가짜 신을 만들어 보겠습니다. 바로 저희의 생성자 Generator $G$ 입니다.

자 문제가 있군요. 우리가 $G$ 를 가짜신으로 만드려면 저 신이 어떻게 latent를 우리가 원하는 데이터로 바꾸는지알아야 하는데, latent의 정체도, GOD의 정체도 모릅니다. 😖

하지만, 저희는 그 결과물을 알고 있습니다. 그럼 이렇게 뽑은 결과물들로부터 신이 어떻게 저렇게 멋들어진 데이터를 뽑아낼 수 있었는지에 대해서 추론해볼 수 있을 것입니다.

이런 방법을 바로 sampling 이라고 합니다. 여러번 sampling 함으로서 원래 분포가 어떤지 추론해보는 것이죠.

자 이렇게 뽑아 놓고 보니 서로 생긴 분포들이 매우 다르게 생겼다는 것을 알수 있습니다. 이런 우리의 가짜신 $G$ 가 영 신노릇을 잘 못하는 것 같네요. 🤢

어쩔 수 없습니다. 그럼 이 두 분포를 최대한 똑같게 만들 수 밖에요.

이 두 분포를 똑같이 만든다면, 우리의 $G$ 는 신처럼 실제 데이터를 생성해 낼 수 있을 것입니다!

Sampling

사실 제가 GAN을 이해하는데 있어서 이 Sampling이 잘 와닿지 않았습니다. 야매로 공부해서그런지 이런쪽 머리가 잘 굴러가지가 않네요. 슬픈일입니다.

GAN에서 sampling을 수행할 때, latent space에서 Normal Distribution으로부터 벡터를 몇개 뽑아와 이를 generator에게 던져줍니다.

네? Normal Distribution으로 부터 뽑는다구요?... 라는 생각을 하게 됩니다. 실제 GAN의 코드를 보면 정말로 random을 호출해서 normal distribution으로 값 N개를 뽑게 됩니다. 어떻게 이런 것이 가능한 걸까요?

아래와 같이 이해하시면 그래도 납득이 가게 됩니다. (제 사견입니다.)

저희가 학습 시키는 Generator는 이 latent space로부터 생성된 latent vector를 던져 주면, 어떤한 이미지(어떤 종류라도 상관없지만요)를 생성하도록 교육 됩니다.

사실 초기에는 latent space는 아무런 의미를 가지지 않는 공간이죠. 즉, 정말로 Normal distribution를 따르는 랜덤한 공간입니다. 하지만 학습을 진행하다보면, Generator가 latent space에서 sampling한 latent vector와 특정 이미지들을 연결시키기 시작합니다. 아 latent vector의 첫번째 원소는 이런의미였겠군, 두번째 원소는 저런 의미였겠군 하구요.

실제로 저희가 Generator에게 latent space를 던져주면서 이런 데이터를 만들어야 해! 하고 계속 훈육(?)을 시키다 보면, Generator로는 정말 완전히 자기맘대로 랜덤한 데이터를 마음대로 뱉는 것이 아니라 latent vector에 따라 다른 이미지를 생성하게 됩니다.

사실 사람도, 아무의미없는 것을 몇번 보면 거기에 의미를 부여하곤 하잖아요? 사람들의 징크스 같은 것들처럼요. 인문학적 겜성 1그램 추가하자면, 되게 사람같은 네트워크들이네요. 😉

Discriminator

자 그럼 어떻게 이 두개의 분포를 똑같이 만들어줄수있을까요? 사실 두 분포는 간단한 숫자나 수식이 아닙니다. 매우 복잡하고, 어떻게 생겼는지조차 그리기 어려운 그런 어려운 분포를 따르고 있겠죠 😢 따라서 if문 한줄이나 가벼운 수식 하나로 쉽게 분류할 수는 없을 것 입니다.

그래서 도입한게 바로 구별자 Discriminator 입니다. Discriminator는 두개의 서로 다른 분포가 얼마나 다른지 학습하고, 그 두개의 분포를 명확히 구분할 수 있는 어떤 종류의 함수라도 올수 있습니다! 하지만, 저희는 마법의 머신러닝이 있으니까요. 이 머신러닝 네트워크를 활용할 것입니다.

Discriminator의 주요한 목적은 두개의 분포를 명확히 구분하는 것입니다. 그 구분한 만큼 Generator에게 벌을 줘야하기 때문이죠. 이미 매우 오래전부터 이렇게 두개의 분포를 명확히 구분시킬 수 있는 유명한 Loss가 있죠. 바로 Binary Cross Entropy Loss 입니다. tensorflow나 pytorch에도 이미 구현되어 있는 loss죠? Cross Entropy의 수식은 아래와 같습니다.

$$BCELoss(P,Q)=-(P(x)\cdot\text{log }Q(x) + (1-P(x)) \cdot \text{log }(1 - Q(x)))$$

Cross Entropy에 대해서 조금 더 알고 싶다면 아래 포스팅을 참고해주세요!

 

 

 

Entropy, Cross Entropy, KL-Divergence에 대해 알아보자

Entropy 엔트로피는 머신러닝에서 정보량을 나타내는 방법으로 사용된다. 정보의 량이라는게 되게 추상적으로 들리는데, 생각해보면 되게 간단한 개념이다. ML상에서 굉장히 많이 사용되는 개념

lifeignite.tistory.com

GAN의 Loss는 위의 Loss를 조금 수정해서 사용합니다. 현재는 CE를 이용해서 두 분포의 차이를 구할 수 있고, 그 분포차이를 바탕으로 Generator를 혼내주면서 $G$의 학습을 돕는다고 생각하시면 좋습니다.

그래도, Loss Function이 어떻게 생겼는지는 구경하고 가야겠죠?

GAN의 Loss를 보고 minmax problem이라는 말들을 많이 합니다. 아이 수식자체에 minmax가 들어가 있으니까요. 이 부분을 자세히 집고 넘어가면 너무 글이 길어지고 어려워질 것 같아서 이쯤에서 다음 포스팅으로 넘기도록 하겠습니다.쬐금 귀찮은 증명들이 사아알짝 들어가 있거든요. 이 Loss들을 조금 자세히 이해하시려면 Cross Entropy (CE)에 대해서 조금 자세한 이해를 필요로 합니다!

학습 알고리즘

아래 직접 작성해서 colab으로 실행할 수 있도록 코드를 공개해 두었습니다. 같이 보시면서 코드의 흐름을 파악하시면 좋습니다.

 

choiking10/ML-tutorial

하나하나 구현을 하는게 아니라 한두가지의 시나리오를 가지고, 여러가지 ML 모델을 적용 혹은 개선시키는 작업을 수행해봐야겠다. - choiking10/ML-tutorial

github.com

소스코드를 보고 있다고 가정하고 글을 작성하도록 할께요.

Overview

자 이제 복잡한 내용은 조금 치우고, 어떻게 학습이 이루어지는지 확인하도록 하죠.

Loss에서 Generator의 역할을 $D$ 가 구별할 수 없을 정도로 데이터를 잘 만드는 것입니다. 그리고 Discriminator의 역할은 $G$ 가 만든 것을 완벽히 가짜라고 구별해 내는 것이죠. 이렇게 GAN은 $G$ 가 잘만들면 $D$ 가 혼나고, $D$ 가 잘구별하면 $G$ 가 혼나는 구조로 동작하게 됩니다. 하지만 $G$ 랑 $D$를 모두 생각하기 매우 버겁지 않나요? 사실 이 둘을 모두 한꺼번에 학습시키란 쉽지 않습니다.

그래서 실제 학습을 시킬 때는 두가지 step을 반복하면서 학습을 수행하게 됩니다. 서로 서로가 고정되어있다는 가정하게 업데이트를 진행하게 되는데, 그에 따라서 Loss Function이 살짝 바뀌게 되는 것도 구경하게 되실 겁니다.

어떻게 따로 따로 업데이트할까요? 간단하게 코드 레벨로 보시고 가실께요.

D = Discriminator(image_size, hidden_size).to(device)
G = Generator(latent_size, hidden_size, image_size).to(device)

criterion = nn.BCELoss()
d_optimizer = torch.optim.Adam(D.parameters(), lr=0.0002)
g_optimizer = torch.optim.Adam(G.parameters(), lr=0.0002)

모델이 두개니까, 각 모델의 파라미터를 뽑은 후 서로 다른 optimizer로 업데이트를 수행하는 모습입니다. 어려운 코드는 아니에요!

optimizer의 종류에 대해서는 다음에 한번더 다뤄보도록 할께요!

Step 1. Fix G, Update D

첫번째는 $G$를 멈춰 놓고 $D$를 업데이트 하는 것입니다.

엇...? G를 멈췄다구요?

그러면 G는 변하지 않는다고 생각해도 되겠군요. 그럼 Loss가 이렇게 변할 수 있습니다.

G는 상수니까 골치 아픈 G들을 치환해버렸습니다.

z로부터 뽑는게 아니라 이제 G로부터 뽑는걸로 간단하게 생각해서 뽑으면 되겠군요.

사실 이부분은 소스코드에 영향을 주지 않지만, Loss Function이 이렇게 변할 수 있다는 사실을 이해하고 넘어가면 좋습니다.

소스코드로도 보고 가실께요.

# ---- D ----
z = rand_z()

# real
real_outputs = D(images)
d_loss_real = binary_cross_entropy(real_outputs, real_label)
real_score = real_outputs

# fake
fake_images = G(z)
fake_outputs = D(fake_images)
d_loss_fake = binary_cross_entropy(fake_outputs, fake_label)
fake_score = fake_outputs

# loss
d_loss = d_loss_real + d_loss_fake

# backprop
reset_grad()
d_loss.backward()
d_optimizer.step()

제가 위에서 데이터를 섞어서 D에게 보여준다고 했었죠?

사실 D 입장에서는 섞여서 들어오는지 한꺼번에 들어오는지 번갈아가면서 들어오는지 이런 들어오는 것에 대한 패턴을 알기 어려울 것입니다.

weight를 업데이트하지 않았거든요 아직! 업데이트 하기전까지 넣어준건 한꺼번에 넣어준 것이라고 봐도 무방합니다. (라고 이해했습니다 헷)

# real
real_outputs = D(images)
d_loss_real = binary_cross_entropy(real_outputs, real_label)
real_score = real_outputs

저 images 변수에는 실제 데이터셋으로부터 뽑은 이미지들이 들어있습니다.

실제 데이터셋에서 뽑아서 D가 실제 데이터가 실제 데이터라고 (1이라고) 판단했는지 검사합니다. BCE loss가 쓰인 것을 볼 수 있군요. real_outputs에는 모두 1로된 데이터가 들어가 있습니다. (GT라고 생각하셔도 될것같네요!)

z = rand_z()

# real
# ...

# fake
fake_images = G(z)
fake_outputs = D(fake_images)
d_loss_fake = binary_cross_entropy(fake_outputs, fake_label)
fake_score = fake_outputs

latent space에서 latent vector z를 뽑고 G에게 넘겨줍니다. 그럼 G는 fake images들을 생성할 것이고, 이를 D에 넘겨주죠. 수식으로 보자면 $D(G(z))$ 에 해당하겠군요. BCE loss가 똑같이 쓰인 것을 볼 수 있군요. fake_outputs에는 모두 0로된 데이터가 들어가 있습니다.

# loss
d_loss = d_loss_real + d_loss_fake

# backprop
reset_grad()
d_loss.backward()
d_optimizer.step()

전체 Loss는 fake일때와 real일때의 loss값을 d_loss에 더해주고 backward를 수행해줍니다. 아직 모델의 파라미터에는 업데이트가 이루어지지 않았습니다!

그리고 d_optimizer.step() 을 함으로서 discriminator의 파라미터만 업데이트가 되었군요.

Step 2. Fix D, Update G

자 마찬가지로 D를 멈춰두고 G를 업데이트 해 봅시다. Step 1처럼 Loss 에 변화가 있을 거에요.

뭔가 커다란 것이 없어 졌습니다. 바로 $\mathbb{E}{x \sim P{\text {data }}(x)}[\log D(x)]$ 자체가 날아가버렸네요. Gradient를 계산하는 행위는 미분과정을 통해 이루어집니다. 그리고 고등학교 때 달달외웠던 미분의 가장 기본적인 성질은 상수는 날아가버린다는 거죠.

그리고 우리는 D를 고정했고, G가 없는 모든 항들은 상수 취급됩니다. 그러니까 굳이 계산할 필요가 없다는 거죠! 코드를 Step1 보다는 조금더 간단하게 작성할 수 있겠네요.

# ---- G ----
z = rand_z()
fake_images = G(z)
outputs = D(fake_images)
g_loss = binary_cross_entropy(outputs, real_label)

reset_grad()
g_loss.backward()
g_optimizer.step()

첫번째 항이 날아가버렸으므로, real data 자체를 D에 넣을 필요가 없어집니다. 그 외에는 Step 1과 같게 되겠습니다.

역시 눈에 띄는건 g_optimizer를 통해서 generator의 gradient만 업데이트 해주었다는 사실입니다.

마무리

한줄 정리

latent space에서 sampling 한 z로부터 Generator는 어떠한 데이터 분포를 만듭니다. 그리고 이렇게 생성한 Generator의 분포와 실제 데이터 분포를 Discriminator로부터 비교하게 함으로서 G가 잘생성하면 D를 혼내주고, D가 잘 구분하면 G를 혼내주는 방법으로 학습을 수행합니다.

사족

휴, 드디어 벼루고 벼뤘던 GAN의 시작을 끊게되었네요.

GAN은 크게 4부작으로 작성될 예정입니다.

다음 글 링크는 아래에 있습니다. 

 

 

[논문정리] GAN에 대해서 알아보자 (2) - 증명과 한계 그리고 WGAN의 등장

자 다시 GAN 포스팅을 작성할 시간이네요. 아 GAN은 쓸게 많아서 너무 괴로워요... PS쪽이 확실히 더 작성하기 편하네요. 익숙한 개념이라서 그런걸까요? 오늘 알아볼 내용은 GAN의 증명과 한계 그리

lifeignite.tistory.com

하나하나가 정말 작성하기 매우매우 귀찮군요.

다음부터는 주제를 좀 쓰기 쉬운걸로 작성해야겠어요. ㅎㅎ...

Reference

노션으로 보시면 더 편할 수 있습니다.

www.notion.so/AdaIN-Arbitrary-Style-Transfer-in-Real-Time-With-Adaptive-Instance-Normalization-9fe8b5fb60154380b6fbe3147e0afe9e

 

[논문 정리] AdaIN (Arbitrary Style Transfer in Real-Time With Adaptive Instance Normalization)

어떤 사람이 이 글을 보는게 좋은가?

www.notion.so

어떤 사람이 이 글을 보는게 좋은가?

  • 기본적인 ML지식을 갖추고 있으신 분
  • Style Transfer에 관심이 있으나, 이제 막 시작해서 알고싶으신 분
  • AdaIN의 수식에 대해서 궁금하신 분
  • AdaIN 소스코드의 간단한 리뷰가 궁금하신 분

요약

최초의 Neural Style Transfer를 제안한 Gatys의 Style Transfer 방식은 다양한 Style을 Arbitrary하게(그 때 그 때 새로운 스타일을) 적용할 수 있는 반면에, 굉장히 느린 속도로 style transfer를 수행한다는 단점이 있었다.

이를 극복하기 위해 Feed-forward 방식으로 Style Transfer를 수행하는 방식들이 제안되었는데, 이들은 Gatys 방식의 비해서 빠른속도의 Style Transfer가 가능했으나, 한정적인 몇가지 미리 학습된 Style 에대해서만 Style Transfer가 가능했다.

이에 비해 AdaIN 방식은 빠른속도로 추론이 가능하면서 동시에 Arbitrary하게 새로운 스타일을 적용할 수 있는 방식이다.

위 표의 Method를 설명해보자면 Gatys - 최초 논문, Ulyanov - Instance Normalization(IN), Dumoulin - Conditional Instance Normalization(CIN), our - Adaptive Instance Normalization (AdaIN) 이다.

실험 결과를 보면 Gatys는 많은 스타일에 대해서 적용할 수 있지만, 속도가 굉장히 느린 것을 확인할 수 있다. 이에 비하여 Feed-forward방식의 IN 과 CIN은 빠른속도의 추론이 가능했지만, 스타일이 한정적이다. AdaIN은 이러한 방식들과는 차별화되게 빠른 속도의 추론이 가능하면서 동시에 무한한 스타일을 생성해 낼 수 있다는 장점을 가지고 있다.

Architecture

AdaIN의 네트워크 구조는 아래와 같으며, 인상 깊은 점은 녹색의 VGG의 pre-trained 모델을 통해서 Encoding을 수행하며, 이 encoder를 feature를 인코딩할 때, 그리고 Loss Function을 구할 때 사용한다는 것이다. 즉 Encoder는 학습 시키지 않는다는 점이 포인트다.

그러므로, 자연스럽게 이 네트워크 상에서 학습 시키는 것은 Decoder 뿐이며, 저자들의 표현을 빌리자면 이 Decoder는 AdaIN으로 생성된 feature들이 decoder를 통해서 image space로 invert 하는 법을 학습한다. 아직 설명하지 않았지만, AdaIN 내에서는 learnable parameter가 없다.

AdaIN Layer

그렇다면 AdaIN은 어떻게 생겼는가? 이를 알기 앞서서 Style Transfer의 개념에 대해서 간단하게 알고 있어야 한다. Style Transfer는 특정 이미지에서 Style을 뽑고, 다른 이미지에서 Contents를 뽑아서 이를 합성한다.

Style은 직관적으로 와닿는데 Contents는 무엇인지 잘 이해가 안갈 것이다. 간단하게 어떤 형태라고 생각하면 좋다. 나무의자가 있다면, 의자가 Contents, 나무가 Style이 될 것이다.

AdaIN에서는 Style과 Contents에 대한 정보를 VGG Encoder를 통해서 추출할 수 있다고 주장한다 사실 이러한 아이디어는 초기 연구인 Gatys 때부터 사용해왔었다.

아주 조금만 더 깊게 들어가서...초기 연구인 Gatys 방식에서는 VGG에서 나온 Feature들에 Gram Matrix를 사용해서 Style을 표현(representation) 있다는 것을 실험적으로 보였다. 이러한 Gram Matrix가 대표적인 feature space 상의 statistics를 추출해내는 방법인데, 이 이후 많은 연구가 이루어지면서 feature space상의 여러 statistics가 Style을 표현하는데 유용하다는 것이 실험적으로 많이 밝혀졌다.

statistics? 잘모르겠다고 생각한다면 이 논문에서는 그냥 평균(mean)과 분산(variance)라고 생각해도 무방하다. AdaIN은 feature space 상의 평균과 분산이 Style에 영향을 끼친다면, 이들을 뽑아서 즉석으로 교환해주는 방식을 택한 것이다. 식을 보자면 아래와 같다.

$$\operatorname{AdaIN}(x, y)=\sigma(y)\left(\frac{x-\mu(x)}{\sigma(x)}\right)+\mu(y)$$

이 식에서 $\mu$ 함수는 평균을 구하는 함수이고, $\sigma$ 함수는 표준 편차를 구하는 함수이다. 자주 사용되는 term이다.

Style Transfer의 경우, 내가 원하는 Contents를 담고 있는 이미지의 feature $x$ 에서, 이미지의 스타일을 빼주고, 내가 입히고 싶은 Style을 더해주는 방식으로 수행된다.

그런데, 위에서도 말했 듯 스타일은 feature 상에서의 statistics로 표현된다고 말했는데 그게 바로 평균과 분산이다. 따라서 식에서 보자면 $\left(\frac{x-\mu(x)}{\sigma(x)}\right)$ 는 Contents 이미지에서 Contents 이미지의 스타일을 빼준 것이고, $\sigma(y)\left(\frac{x-\mu(x)}{\sigma(x)}\right)+\mu(y)$ 는 이미지 y의 스타일을 입혀준 것이다. 이해가 됐으려나...

그리고 했깔릴 수 있는 부분인데, 이건 전부 Feature space상에서 이루어진다는 것이다! 그냥 이미지상에서 이루어지는 것이 아니다.

Formulation

노테이션을 정리해보자면

  • $T$= Style Transfer Network (인코더-AdaIN-Decoder)
  • $f$= Encoder (pre-trained VGG-19의 앞부분 (~relu4_1)
  • $g$= Decoder (학습시켜야하는 디코터)

라고 할 때, AdaIN layer를 통해서 생성되는 feature $t$ 는 아래와 같이 나타낼 수 있다.

$$t=\operatorname{AdaIN}(f(c), f(s))$$

랜덤하게 초기화된 디코더 $g$는 $t$를 image space로 보내는 방법을 트레인하며 스타일이 입혀진 이미지 $T(c,s)$를 생성한다.

$$T(c, s)=g(t)$$

Architecture Detail

  • checker-board effect를 감소시키기 위하여 decoder의 pooling layer를 nearest up-sampling 방식으로 교체.
  • $f$와 $g$에서 모두 reflection padding을 사용했다.
  • decoder에서 normalization 방식을 고르는 것이 중요했는데, 결론은 no normalization이 제일 좋았다.
  • 전처리: 두 이미지를 aspect ratio를 유지한체 512로 사이즈를 키웠고, 여기서 256 by 256로 crop한다. 우리의 네트워크는 fully convolutional 이기 때문에, 어떤 사이즈의 이미지가 온다고해도 적용 가능하다.

Loss

Loss는 다른 네트워크와 유사하게 다음과같은 로스를 사용한다.

$$\mathcal{L}=\mathcal{L}_{c}+\lambda \mathcal{L}_{s}$$

content loss는 target feature와 output image의 feature의 Euclidean distance를 구했다. 일반적으로 사용되는 content image의 feature response를 사용하는 대신에 AdaIN output $t$를 content target으로 삼았다. 이게 조금더 빨리 convergence가 이루어진다.

$$\mathcal{L}_{c}=\parallel f(g(t))-t \parallel_{2}$$

그냥 전체 아키텍처 오버뷰의 보라색 화살표의 식이다. $t$를 디코더에 넣고 다시 인코더에 넣은 후에 그 두개의 차이를 비교하는 것을 Contents Loss로 삼겠다는 간단한 식이다.

AdaIN 레이어는 오직 style features의 mean과 standard deviation를 transfer하기 때문에 style loss는 이러한 statistics를 match시켜야한다. 따라서 아래와 같은 스타일 로스를 사용하는데,

$$\begin{array}{r}\mathcal{L}_{s}=\sum_{i=1}^{L}\left\|\mu\left(\phi_{i}(g(t))\right)-\mu\left(\phi_{i}(s)\right)\right\|_{2}+ \\\sum_{i=1}^{L}\left\|\sigma\left(\phi_{i}(g(t))\right)-\sigma\left(\phi_{i}(s)\right)\right\|_{2}\end{array}$$

$\phi_i$.는 VGG-19의 i번째 레이어이다. 스타일로스에서 사용한 레이어는 relu1 1, relu2 1, relu3 1, relu4 1 이다. 이 역시 간단하게 설명하자면, 원래 스타일 $s$ 를 인코더에 넣었을 때의 $i$ 번째 feature $\phi_i(s)$ 의 평균과 $t$ 를 디코더에 넣고 이를 다시 encoder에 넣었을 때의 $i$번째 Feature $\phi_i(g(t))$ 의 평균($\mu$)과 표준편차($\sigma$)를 최소화 시키는 방법으로 스타일 로스를 구한것이다.

Code

사실 이렇게 길게 설명했는데, 코드로는 아래와같이 씸플하다.

def adaptive_instance_normalization(content_feat, style_feat):
    assert (content_feat.size()[:2] == style_feat.size()[:2])
    size = content_feat.size()
    style_mean, style_std = calc_mean_std(style_feat)
    content_mean, content_std = calc_mean_std(content_feat)

    normalized_feat = (content_feat - content_mean.expand(
        size)) / content_std.expand(size)
    return normalized_feat * style_std.expand(size) + style_mean.expand(size)

AdaIN의 코드는 그냥 content_feature와 style feature를 수식 그대로 적용해 반환할 뿐인 함수다.

def forward(self, content, style, alpha=1.0):
        assert 0 <= alpha <= 1
        style_feats = self.encode_with_intermediate(style)
        content_feat = self.encode(content)
        t = adain(content_feat, style_feats[-1])
        t = alpha * t + (1 - alpha) * content_feat

        g_t = self.decoder(t)
        g_t_feats = self.encode_with_intermediate(g_t)

        loss_c = self.calc_content_loss(g_t_feats[-1], t)
        loss_s = self.calc_style_loss(g_t_feats[0], style_feats[0])
        for i in range(1, 4):
            loss_s += self.calc_style_loss(g_t_feats[i], style_feats[i])
        return loss_c, loss_s

encode_with_intermediate 함수는 중간중간 레이어를 추출해서 적용하는 함수고, encode 함수는 그야말로 vgg를 relu4_1까지 통화시킨 후 feature map을 반환하는 함수다.

주목할만한 점은 adain에 style_feature의 마지막 것만 들어가는건데, 생각해보면 당연하다. shape이 마지막껏만 content feature와 맞기 때문이다. 그리고 이래도 style의 mean과 variance를 adaIN을 통해 transfer할 수 있다.

그리고 생성된 $t$ 를 디코더를 통해 이미지로 만들고, 이를 다시 encoder에 집어넣어서 contents loss와 style loss를 구해서 반환해준다.

논문의 전체 번역본과 소스코드 주소는 아래와 같다.

[번역] Arbitrary Style Transfer in Real-Time With Adaptive Instance Normalization

naoto0804/pytorch-AdaIN

 

논문 정리본은 아래에서 확인하실 수있습니다. 본 포스팅은 단순 번역으로 직역한 내용들을 담았습니다.

 

 

[논문 정리] AdaIN을 제대로 이해해보자

노션으로 보시면 더 편할 수 있습니다. www.notion.so/AdaIN-Arbitrary-Style-Transfer-in-Real-Time-With-Adaptive-Instance-Normalization-9fe8b5fb60154380b6fbe3147e0afe9e [논문 정리] AdaIN (Arbitrary Styl..

lifeignite.tistory.com

Notion으로 보시면 더 편합니다.

www.notion.so/Arbitrary-Style-Transfer-in-Real-Time-With-Adaptive-Instance-Normalization-5f8d9aba82ea4a83a797a48371f6cecb

 

Arbitrary Style Transfer in Real-Time With Adaptive Instance Normalization

Abstract

www.notion.so

 

Abstract

Gatys는 content image를 다른 이미지의 스타일로 rendering하는 알고리즘을 개발했다. 그리고 이를 style transfer라고 부른다. 하지만, gatys의 framework는 느린 반복적 최적화 과정을 요구하기 때문에 현실적인 적용이 힘들었다. 이에 FFNN을 통해서 빠른 속도로 이에 근접한 성능을 내는 Neural style transfer 알고리즘이 제안되었다. 하지만, 이러한 speed의 증가는 style의 종류를 한정시키고, 각각의 새로운 스타일에 독립적으로 adapt 시키기 어려웠다. 이에 우리는 간단하지만, 효율적인 독립적인 실시간 style transfer를 제안한다. 우리 방식의 key는 AdaIN layer인데, 이 adaIN 레이어는 contents feature의 mean과 variance를 style features와 align한다. 이 방식은 매우 빠른 속도의 추론을 가능하게하며, 동시에 pre-defined된 style-set의 제약을 없앴다. 추가적으로 우리의 접근법은 content-style trade-off, style interpolation, color & spatial controls 등의 유연한 user control을 하나의 FFNN을 통해 가능하게 한다.

Introduction

이미지는 style과 contents로 어느정도 분리할 수 있다. 때문에 이미지의 contents를 유지한체 style을 바꿀수 있는데, 이를 우린 style transfer라고 부른다.

대충 gatys 방식의 한계 - 느림

대충 기존 FFNN 방식의 한계 - 스타일이 한정됨

우리의 접근방식은 새로운 스타일은 실시간으로 독립적인 transfer를 수행할 수 있다. gatys 방식(최적화기반)의 유연성과 feed-forward 방식 (FFNN 방식)와 유사한 속도를 결합해서. 우리의 방식은 Instance Normalization (IN) 방식에서 movitation을 받았는데, IN방식은 NN style transfer상에서 놀랍도록 효율적이다.

instance noramlization 의 성공을 설명하기 위해서, 우리는 새로운 해석을 제안하는데, 그것은IN이 feature statistics를 정규화함으로서 style normalization을 수행한다는 것이다. 이는 feature statistics를 정규화 하는것이 style information을 유도할 수 있다는 기존의 연구들로 비롯된 것이다. 이러한 우리의 해석에 motivation을 얻어서, 우리는 IN을 간단하게 확장한 AdaIN을 제안한다.

AdaIN은 Contents input과 Style input이 주어졌을 때, 간단하게 content input의 mean과 variance를 style input의 mean과 variance와 match되도록 조정한다. 전체적인 실험해서, Through experiments, we find AdaIN effectively combines the content of the former and the style latter by transferring feature statistics. 디코더 네트워크는 AdaIN output을 image space로 inverting 함으로서 마지막 stylized image를 생성하는 법을 배운다.

우리의 방식은 input을 독립적인 새로운 스타일로 변환하는 유연성을 희생하지 않고도 Gatys방식보다 3배이상 빠르다. 그리고 유저컨트롤이가능하다.

Related Work

Style Transfer

초창기의 style transfer

  • Style transfer는 non-photo-realistic rendering으로부터 비롯되었으며, 이것은 texture synthesis와 transfer와 연관이 깊다.
    • 이러한 이전 접근 방식에는 linear filter response상에서의 histogram matching방식이나 non-parametric sampling등이 포함된다.
    • 이러한 방식들은 일반적으로 low-level statistics에 의존하며, 자주 semantic structure를 잡아내는데 실패했다.

feature statistics를 이요한 style transfer

  • 하지만 Gatys는 DNN의 Convolutional layers 상에서 feature statistics를 매칭함으로서 굉장히 인상깊은 style transfer 결과를 최초로 발표했다.
  • 최근에는 몇가지 개선점들이 발표되었는데,
    • Li와 wand는 local patterns를 찾기 위한 (enforce) deep feature space상에서의 MRF framework를 제안했다.
    • gatys는 color preservation, spatial location, scale of style transfer를 조절할 수 있는 새로운 방법을 제안했다.
      • Controlling Perceptual Factors in Neural Style Transfer - CVPR 2017 인데 별로 주목은 못받은듯?
    • Ruder는 시간상의 제약을 도입함으로서 video style transfer의 퀄리티를 향상시킬 수 있는 방법을 제안했다.
  • 초창기의 방식 (gatys 2016)은 느려서 On-device processing 은 실용적인 사용이 어려웠다. 공통적인 제 2의 방식은 optimization process를 FFNN으로 대체하는 것인데, 이는 Gatys와 같은 object function을 최소화 하는 방식이다. 이러한 FFNN방식은 최적화 방식 보다 훨신 빠른 속도를 보여줘 이를 real-time application에서 사용 가능한 정도 수준까지 끌어올렸다.
    • Wang et al. 은 multi-resolution architecture를 제안했다.
    • Ulyanov는 IN이다. (생성된 smaple의 quality와 diversity를 개선시키는 방법을 제안)
  • 하지만 이러한 Style Transfer방식은 각각의 network들이 특정 fixed style에 tied되어있다는 한계점을 가지고 있었다.
  • 이를 해결하기 위해(to address this problem), Dumoulin et al은 CIN을 제안해서 하나의 네트워크로 여러개의 스타일을 제공할 수 있게 되었찌만 여전히 32개라는 encoding된 몇몇개만 제공 가능했기 때문에 한계점이있다.
  • 매우 최근에는 Chen and Schmidt 이 FF방식의 arbitrary style transfer를 제안했는데, 이는 style swap layer를 이용한 것이다. content와 style 이미지의 feature activation이 주어졌을 때, style swap layer는 content feature를 가장 가깝게 매칭된 style feature로 patch-by-patch방식으로 대체한다. 하지만 이방식은 굉장히 높은 computational bottleneck을만든다. 거의 512 x 512 인풋 이미지에 대하여 style swap을 수행하는데만 95%에 이르는 계산을 사용한다. 우리의 AdaIN은 이방식에 비해서 거의 수배에서 수십배 빠르다.

Style transfer의 Loss Function

  • Gatys는 feature상에서 Gram matrix를 이용하여 second-order statistics를 매칭시키는 Loss를 사용하였다.
  • 다른 효과적인 Loss function들도 많이 제안되었는데, MRF loss, adversarial loss, histogram loss, CORAL loss, MMD loss, distance between channel-wise mean and variance. 등이 있다.
  • 뭐 근데 이런애들은 결국 하나의 공통적인 목표가 있는데 그건 바로 style image와 synthesized image상의 어떤 feature statistics를 match시키는 것을 목표로한다.

GAN

  • GAN도 cross-domain image generation을 통해서 style transfer를수행할 수 있다.

3. Background

3.1 Batch Normalization

$$\mathrm{BN}(x)=\gamma\left(\frac{x-\mu(x)}{\sigma(x)}\right)+\beta$$

$$\mu_{c}(x)=\frac{1}{N H W} \sum_{n=1}^{N} \sum_{h=1}^{H} \sum_{w=1}^{W} x_{n c h w}$$

$$\sigma_{c}(x)=\sqrt{\frac{1}{N H W} \sum_{n=1}^{N} \sum_{h=1}^{H} \sum_{w=1}^{W}\left(x_{n c h w}-\mu_{c}(x)\right)^{2}+\epsilon}$$

Batch Normalization은 아래 포스팅에서 자세히 다룹니다.

lifeignite.tistory.com/47?category=460775

 

Batch Normalization을 알아보자

notion으로 보면 더 편합니다. www.notion.so/Batch-Normalization-0649da054353471397e97296d6564298 Batch Normalization Summary www.notion.so 목차 Summary Introduction Background Normalization Covariate..

lifeignite.tistory.com

3.2 Instance Normalization

기존의 feed-forward stylization method에서는 각각의 Convolutional layer상에서 BN layer가 포함되어 있다. 놀랍게도 Ulyanov 아저씨가 BN을 IN으로 바꾸기만 했서 높은 성능 향상을 달성했다.

$$\mathrm{IN}(x)=\gamma\left(\frac{x-\mu(x)}{\sigma(x)}\right)+\beta$$

IN은 다음과 같이 생겼는데, BN과 마찬가지로 $\gamma,\beta$는 학습되어야 하는 파라미터다. 식자체는 BN과 똑같지만 다른점은 바로 group by c 뿐만 아니라 group by (b, c)해서 정규화 한다는 것이다.

바로 아래처럼!

$$\mu_{n c}(x)=\frac{1}{H W} \sum_{h=1}^{H} \sum_{w=1}^{W} x_{n c h w}$$

$$\sigma_{n c}(x)=\sqrt{\frac{1}{H W} \sum_{h=1}^{H} \sum_{w=1}^{W}\left(x_{n c h w}-\mu_{n c}(x)\right)^{2}+\epsilon}$$

BN은 $\sigma_n$이었는데 IN은 $\sigma_{nc}$다. 또한 BN과의 가장 큰 차이점은 바로 BN은 batch와 inference시에 사용하는 statistics가 다르지만, IN은 바뀌지 않는다.

맨날보는 그림

 

 

3.3 Conditional Instance Normalization

affine parameter인 $\gamma,\beta$ 하나만 학습시키는대신에, 각각의 style s에 따라서 서로다른 파라미터인 $\gamma^s,\beta^s$를 학습시키는 CIN이 제안되었다(Dumoulin 아조씨).

$$\operatorname{CIN}(x ; s)=\gamma^{s}\left(\frac{x-\mu(x)}{\sigma(x)}\right)+\beta^{s}$$

트레이닝시에 style image를 각각의 index s로 묶고, 랜덤하게 선택해서 style s에 따라서 파라미터를 학습시켰다. (set S는 실험에서 32개로) Content image는 CIN layer상에서 $\gamma^s$와 $\beta^s$를 사용하는 style transfer network에 의해서 처리된다. 놀랍게도, 네트워크는 같은 convolutional parameters를 사용하며 다른 affine parameters를 IN layer에서 사용함으로서 완벽하게 다른 스타일을 생성해낼 수 있었다.

이는 기존의 CIN이 없는 일반적인 네트워크를 사용할 경우와 비교해서 CIN 레이어는 추가적으로 2FS개의 파라미터를 사용한다. 여기서 F: 총 feature map의 개수며, S: 총 스타일의 개수이다. 따라서 style 의 개수가 증가할 때 선형적으로 파라미터의 개수가 증가하며, 스타일의 개수가 큰 모델의 경우 굉장히 challenging하게 된다. 또한, 이러한 방식은 독립적인 새로운 스타일에 대해서 트레이닝 없이 적용할 수 없다.

4. Interpreting Instance Normalization

비록 IN의 큰 성과에도 불구하고, 왜 style transfer가 잘 작동하는지에 관해서는 여전히 알수 없다. IN의 성공은 content image의 contrast에 invariance하게 IN 동작하기 때문이라고 말한다. 하지만 IN은 feature space에서 존재하고, 그러므로 이것은 픽셀 상에서 간단한 contrast normalization보다는 큰 영향을 줄 수 있을 것이다. 아마도 더 놀라운 것은 IN의 affine parameters가 output images의 style을 완벽하게 바꿀 수 있다는 사실이다.

DNN의 feature statistics는 이미지의 style을 capture할 수 있다고 알려져 있다. Gatys도 second-order statistics를 optimization의 objective로 삼았고 Li et al.도 다른 statistics를 매칭하는 방식을 보여줬다. 이러한 관찰에 모티브를 받아서, 우리는 instance normalization이 mean과 variance라고 이름붙여진 feature statistics를 normalizing함으로서 style normalization의 형태를 수행한다고 주장한다. 비록 DNN은 image descriptor로서 사용되지만, 우리는 generator network의 feature statistics가 생성된 이미지의 style을 컨트롤한다고 믿는다.

우리는 IN 모델과 BN모델을 각각 학습시켜서 style loss를 비교해봤다. 우리는 이 때 (a) 원본 이미지, (b) contrast normalized image, (c) pre-trained 모델을 활용하여 style normalized를 수행한 image에 대해서 학습을 수행했다. 놀랍게도 IN에 의한 개선이 contrast에 대해서 정규화한 이미지에 대해서 꽤 차이가 났따. 하지만 style을 normalized한 실험에서는 IN과 BN의 차이가 크게 나지 않았다. 우리의 결과는 IN이 style normalization의 한종류로 동작한다고 주장한다.

BN는 single image의 feature statistics가 아니라 sample의 배치의 feature statistics를 정규화 하기 때문에, 직관적으로 배치상의 서로 다른 스타일들을 하나의 스타일 근처로 정규화 시킨다고 이해할 수 있다. 이는 성능을 하락시킬 수 있다.

이러한 이해는 여러 연구들에 공통적으로 적용시킬 수 있으며 CIN도 마찬가지로 쉽게 이해할 수 있다.

5. Adaptive Instance Normalization

만약IN이 인풋을 하나의 단일 스타일, 특히 affine parameters에 의해 구체화되는 하나의 스타일로 정규화 한다면, adaptive affine transformations를 활용하여 독립적으로 주어진 스타일을 적용하는 것도 가능하지 않을까?

이에 따라 우리는 IN을 간단하게 확장한 Adaptive InstnaceNormalization (AdaIN)을 제안한다. AdaIN은 Content input x 와 style input y를 받았을 때, 간단하게 channel-wise mean and varinece of x를 channel-wise mean and varinece of y로 alighn한다. BN, IN, CIN과달리 AdaIN은 Affine parameter가 없다! 대신 affine parameter를 adaptively 계산한다.

$$\operatorname{AdaIN}(x, y)=\sigma(y)\left(\frac{x-\mu(x)}{\sigma(x)}\right)+\mu(y)$$

여기서 우리는 간단하게 content input을 $\sigma(y)$와 $\mu(y)$로 조정한다. IN과 유사하게, 이러한 statistics는 spatial locations을 across하여 계산된다.

직관적으로, 우리가 feature channel을 특정한 스타일의 brushstrokes 을 detect하자고 하자. 이러한 stock는 이 feature를 위한 high average activation 를 생성할 것이다. AdaIN에 의해서 생성된 output은 contents image의 spatial structure를 유지하면서 이 feature를 위한 같은 high average activation를 가지게 될 것이다. 이러한 brushstrokes feature는 Fedd-forward decoder를 통해 iamge space로 inverted될 것이다. 이러한 feature channel의 variance는 더 미묘한 style imnformation을 encode할 수 있을 것이고, 이것은 또한 AdaIN output으로 전달되고, 최종적인 output image에 전달될 것이다.

정리하자면, AdaIN은 channel-wise mean and variance라는 feature statistics를 transfering함으로서 feature space상에서 style trasnfer를 수행한다.

 

style transfer algorithm의 overview. 우리는 style, contents image를 인코딩하기위해 fixed VGG-19네트워크의 첫 몇개의 레이어를 사용하였다. AdaIN layer는 style trasnfer를 feature space 상에서 수행한다. decoder는 adaIN output을 image spaces상으로 invert하는 방법을 learning한다. 우리는 같은 VGG encoder를사용해 content loss와 style loss를 구했다.

6. Experimental Setup

6.1 Architecture

우리의 style transfer network T는 content image c와 arbitrary style image s를 input으로 받아서, content image에 style image를 합성한다. 우리는 간단한 encoder-decoder architecture를 사용했으며, encode $f$는pre-trained VGG-19의 앞 몇개 부분(upto relu4_1)까지 사용했다. feature 이미지 상에서 content와 style image를 인코딩한 후에 우리는 두 인코딩 된 feature map을 AdaIN 레이어에게 주고, mean과 variance를 맞춘다.

$$t=\operatorname{AdaIN}(f(c), f(s))$$

랜덤하게 초기화된 디코더 $g$는 $t$를 image space로 보내는 방법을 트레인하며 스타일이 입혀진 이미지 $T(c,s)$를 생성한다.

$$T(c, s)=g(t)$$

Architecture Detail

  • checker-board effect를 감소시키기 위하여 encoder의 pooling layer를 nearest up-sampling 방식으로 교체.
  • $f$와 $g$에서 모두 reflection padding을 사용했다.
    • IN레이어는 각각의 셈플들을 하나의 스타일로 정규화하고 BN은 셈플들의 배치를 하나의 스타일로 centered시킨다. 두 normalization방식은 모두 원하지 않는방식인데, 왜냐하면 우리는 decoder가 굉장히 다른 스타일로 이미지를 생성하기를 위하기 때문이다.
    • 따라서 우리는 normalization을 사용하지로 결정했고, IN/BN이 모두 정말로 performance를 떨어트리는지 보여주겠다.
    • decoder에서 normalization방식을 고르는 것이 중요했는데, 결론은 no normalization이 제일 좋았다.
  • MS-COCO를 컨텐츠 이미지로, WIkiArt 데이터셋을 스타일이미지로 나머지는 baseline network 셋팅을 따라했다. (어 키페이퍼가 patch-by-patch 였네?...) 대충 8만개쯤 된다.
  • adam optimizer사용 batch size는 8로 content-style image pair로
  • 전처리: 두 이미지를 aspect ratio를 유지한체 512로 사이즈를 키웠고, 여기서 256 by 256로 crop한다. 우리의 네트워크는 fully convolutional 이기 때문에, 어떤 사이즈의 이미지가 온다고해도 적용 가능하다. 

Loss

우리는 다른 네트워크와 유사하게 다음과같은 로스를 사용한다.

$$\mathcal{L}=\mathcal{L}{c}+\lambda \mathcal{L}{s}$$

content loss는 target feature와 output image의 feature의 Euclidean distance를 구했다. 우리는 일반적으로 사용되는 content image의 feature response를 사용하는 대신에 AdaIN output $t$.를 content target으로 삼았다. 이게 조금더 빨리 convergence가 이루어졌기 때문이다.

$$\mathcal{L}{c}=|f(g(t))-t|{2}$$

우리의 AdaIN 레이어는 오직 style features의 mean과 standard deviation를 transfer하기 때문에 우리의 style loss는 오직 이러한 statistics를 match시켜야한다. 우리는 gram matrix loss가 유사한 결과물을 생성한다는 사실을 알았지만, 우리는 IN statistics를 match 시켰는데, 왜냐하면 이게 개념상 더 깔끔하기 대문이다. 이 스타일 로스는 Li et al.에 의해서 발견된 스타일로스이다.

$$\begin{array}{r}\mathcal{L}{s}=\sum_{i=1}^{L}\left|\mu\left(\phi_{i}(g(t))\right)-\mu\left(\phi_{i}(s)\right)\right|_{2}+ \sum_{i=1}^{L}\left|\sigma\left(\phi_{i}(g(t))\right)-\sigma\left(\phi_{i}(s)\right)\right|_{2}\end{array}$$

$\phi_i$.는 VGG-19의 i번째 레이어이다. 스타일로스에서 사용한 레이어는 relu1 1, relu2 1, relu3 1, relu4 1 이다.

Result

7.1. Comparison with other methods

Speed

Titan X GPU를 썼을때 저정도 속도가나온다고하네요

Quality

 

7.2. Additional experiments

필요한것들은 위에 적어놧음

7.3. Runtime controls

Figure 9. 이 좀 재밌는 부분인데, Gatys가 2017년도에 color control이 가능한 style transfer를 제안했음. content image의 color를 유지하기위해서 첫번째로 style image의 color distributiond을 content image의 color distribution을 맞춰 준다음 style transfer를 수행한다는 방법임. (color distribution을 맞춰준다는건 color histogram을 맞춰준다는 건가... )

Reference

Arbitrary Style Transfer in Real-time with Adaptive Instance Normalization

 

혹시 수식 깨진 부분 있으면 말씀주세요 (notion에서 옮기다보니 수식이 종종 깨집니다.)

+ Recent posts