MusicVAE는 recurrent VAE 기법을 활용한 음악 생성 모델입니다. 모델을 이해하기 위해 먼저 VAE와 베이즈 정리에 대해서 알아보도록 하겠습니다.
베이즈 정리란?
사전 확률(prior)이란 사건 A, B가 있을 때 사건 A를 기준으로 보면 사건 B가 발생하기 전에 가지고 있던 사건 A의 확률입니다. 만약 사건 B가 발생하면 이 정보를 반영하여 사건 A의 확률은 P(A|B)로 변하게 되고 이게 사후확률(posterior)입니다. 예를 들어보자면, 사건 B를 철수가 영희를 좋아한다로 정의하고 사건 A는 철수가 영희에게 초콜릿을 준다라고 할게요. 철수가 영희를 좋아할 확률을 0.5라고 하고 철수가 영희에게 초콜릿을 줄 확률은 0.4, 철수가 영희를 좋아할 때 초콜릿을 줄 확률을 확률은 0.2라고 정의해볼게요. 그러면 사전 확률은
P(B) = 0.5
가 되고 사후 확률 P(A|B) = 0.2
가 됩니다. 또 한가지 더 사전 확률 P(A)=0.4
이 되죠. - 철수가 영희를 좋아할 확률 P(B) = 0.5
- 철수가 영희에게 초콜릿을 줄 확률 P(A) = 0.4
- 철수가 영희를 좋아할 때 초콜릿을 줄 확률 P(A|B) = 0.2
여기서 한가지 더 나아가 베이즈 정리를 계산할 수 있어요. 베이즈 정리가 뭔지 위키백과에서 가져와봤습니다.
베이즈 정리는 두 확률 변수의 사전 확률과 사후 확률 사이의 관계를 나타내는 정리다.
위의 예시로 봤을 때 여기서 말하는 두 확률 변수는 철수가 영희를 좋아한다라는 변수와 철수가 영희에게 초콜릿을 준다로 얘기할 수 있습니다. 먼저 베이즈 정리 식을 볼까요?
이 식에서 저희가 모르는 건 초콜릿을 줬을 때 철수가 영희를 좋아할 확률을 의미하는
P(B|A)
밖에 없어요. 이 확률을 베이즈 정리를 이용해 구할 수 있는 것이죠. 식에 대입해볼까요?즉,
P(B|A)
는 위의 식을 통해 0.25를 얻을 수 있습니다. 즉, 철수가 영희에게 초콜릿을 줬을 때 철수가 영희를 좋아할 확률은 0.25라는 것이죠. 우리의 문제에 적용해본다면?
neural network에서 학습되는 파라미터 w와 우리가 input으로 넣는 데이터 D가 있을 때 p(w|D)는 우리가 data가 주어졌을 때 가장 재현 잘하는 w를 만들 확률 분포입니다. 좀 더 자세히 설명하자면 보통 딥러닝 학습에선 data가 주어졌을 때 확정된 w가 만들어지죠. 아래 그림처럼 확정된 w가 아닌 w의 확률 분포가 만들어지는 거예요.
데이터는 연속적이기 때문에 베이즈 정리의 식이 조금 변형됩니다. 위의 베이즈 정리 식과는 다르게 p(D)가 없어진 것을 볼 수 있어요. 여기서 한가지 문제가 있습니다. posterior p(D|w)는 쉽게 구할 수 있을 것 같지만 분모를 보시면 파라미터 w에 대해 적분하게 되는데 딥러닝 layer가 깊어질수록 갯수가 많아지고 모든 w에 대해 적분을 하는 것은 불가능합니다.
그래서 활용하는게 variational inference입니다. variational infernce를 한글로 번역하면 변분 추론으로 우리가 아는 함수 q를 정의하여 p(D|w)와 비슷한 분포를 가진 variational distibution을 만들자! 입니다. 변분이라는 말을 좀 더 자세히 보겠습니다. 변분이란 어떤 값이 최소 또는 최대가 되게 하는 조건을 찾는 방법입니다. 변분법에서 최소나 최대가 찾기 위해서 미분과 적분을 사용합니다. 다시 본론으로 돌아와서 설명하자면
p(w|D)
와 가장 비슷한 확률 분포를 가지는 q(w|θ)
를 만드는 θ를 정해주는 것입니다. 그렇다면 이 두 확률 분포가 비슷한지 어떻게 측정할 수 있을까요? 이 때 사용되는 이론이 KL-Divergece입니다. KL-Divergence를 설명하자면 두개의 분포가 비슷할수록 작은 값을 갖는 식입니다. KL Divergence에 대한 내용은 KL divergence 살펴보기 🔗를 참고하시면 좀 더 이해하기 편할거예요. Variational Auto Encoder(VAE)란?
위키피디아에 적혀있는 VAE 정의엔 다음과 같이 적혀있습니다.
Variational autoencoders are probabilistic generative models that require neural networks. The neural network components are typically referred to as the encoder and decoder for the first and second component respectively. The first neural network maps the input variable to a latent space that corresponds to the parameters of a variational distribution. In this way, the encoder can produce multiple different samples that all come from the same distribution. The decoder has the opposite function, which is to map from the latent space to the input space, in order to produce or generate data points.
번역기의 힘을 빌려보겠습니다. VAE는 신경망이 필요한 확률적 생성 모델입니다. 신경망의 구조는 인코더와 디코더로 이루어져 있으며, 인코더는 입력 변수를 변동 분포의 매개변수에 해당하는 latent space에 매핑합니다. 이러한 방식으로 인코더는 모두 동일한 분포에서 나오는 여러 다른 샘플을 생성할 수 있습니다. 디코더에는 데이터를 생성하기 위해 latent space에서 입력 space로 매핑하는 기능을 가지고 있습니다.
좀 더 쉽게 설명하자면 input X를 잘 설명하는 feature를 추출하여 latent vector z에 담고, 이 latent vector z를 통해 X와 비슷하면서 새로운 데이터를 생성하는 것을 목표로 합니다. 아래의 구조를 하나씩 살펴보겠습니다.
Encoder
먼저 x는 encoder를 통해 z를 만들기 위한 평균 μ와 표준편차 σ를 만들어냅니다. 아래 그림의 수식을 보시면 p가 아닌 q인데요. x가 주어졌을 때 z가 나올 확률 분포를 알면 좋겠지만 쉽지 않기 때문에 variational inference를 이용해 q라는 우리가 알고 있는 함수를 통해 μ와 σ를 만들어냅니다. 아래 코드를 보시면 우리가 알고 있는 함수는 relu를 포함한 2층 깊이의 linear입니다.
class VAE(nn.Module): def __init__(self, input_dim, hidden_dim, latent_dim): super(VAE, self).__init__() self.input_dim = input_dim self.hidden_dim = hidden_dim self.latent_dim = latent_dim # encode self.fc1 = nn.Linear(self.input_dim, self.hidden_dim) self.fc2 = nn.Linear(self.hidden_dim, self.latent_dim) def encode(self, x): hidden = F.relu(self.fc1(x)) mu = F.relu(self.fc2(hidden)) sigma = F.relu(self.fc2(hidden)) return mu, sigma
Latent vector z
z를 만들기 위해선 정규분포에서 샘플링하면 되지만 VAE에선 μ, σ, ε 3개의 분포를 입력으로 사용합니다. 정규분포를 샘플링하게 되면 back propagation이 불가하게 되기 때문에 reparametric trick을 사용하게 됩니다. reparametric trick은 back propation을 하기 위함과 동시에 noise를 sampling하여 매번 조금은 다른 데이터를 생성하기 위함입니다. 그림에서 보여드렸던 것과 같이 μ와 ε을 곱한 σ를 더해 z를 구합니다.
class VAE(nn.Module): def __init__(self, input_dim, hidden_dim, latent_dim): super(VAE, self).__init__() self.input_dim = input_dim self.hidden_dim = hidden_dim self.latent_dim = latent_dim def forward(self, x): mu, sigma = self.encode(x) z = mu + sigma * torch.randn(self.latent_dim)
Decoder
이제 decoder를 이용해 새로운 x를 생성합니다.
class VAE(nn.Module): def __init__(self, input_dim, hidden_dim, latent_dim): super(VAE, self).__init__() self.input_dim = input_dim self.hidden_dim = hidden_dim self.latent_dim = latent_dim # decode self.fc3 = nn.Linear(self.latent_dim, self.hidden_dim) self.fc4 = nn.Linear(self.hidden_dim, self.input_dim) def decode(self, z): hidden = F.relu(self.fc3(z)) output = self.fc4(hidden) return output def forward(self, x): reconstructed_z = self.decode(z) return reconstructed_z, mu, sigma
코드를 모두 합쳐면 아래와 같이 나타낼 수 있습니다.
class VAE(nn.Module): def __init__(self, input_dim, hidden_dim, latent_dim): super(VAE, self).__init__() self.input_dim = input_dim self.hidden_dim = hidden_dim self.latent_dim = latent_dim # encode self.fc1 = nn.Linear(self.input_dim, self.hidden_dim) self.fc2 = nn.Linear(self.hidden_dim, self.latent_dim) # decode self.fc3 = nn.Linear(self.latent_dim, self.hidden_dim) self.fc4 = nn.Linear(self.hidden_dim, self.input_dim) def encode(self, x): hidden = F.relu(self.fc1(x)) mu = F.relu(self.fc2(hidden)) sigma = F.relu(self.fc2(hidden)) return mu, sigma def decode(self, z): hidden = F.relu(self.fc3(z)) output = self.fc4(hidden) return output def forward(self, x): mu, sigma = self.encode(x) z = mu + sigma * torch.randn(self.latent_dim) reconstructed_z = self.decode(z) return reconstructed_z, mu, sigma