나의 작은 valley
[Generation] 생성 모델의 변천 본문
들어가며
역대급으로 긴 글이 되지 않을까. 쓰기 전부터 두려움이 든다. 모든 모델들이 하나같이 어렵고 딥하지만 이야기의 결론은 확산모델이 생성 모델로서 가장 좋다이다. 즉 그 부분만 읽어도 된다 .... 그래도 다 읽어.
오토 인코더
: 입력 이미지의 패턴을 학습하여 데이터를 재건한다. 이때 PCA(차원 축소) 기법이 사용된다.
즉, 오토 인코더의 핵심은 이미지의 특징을 추출하는 것이다.
-디노이징 오토 인코더
: 입력 데이터에 Noise를 섞거나 encoder에 dropout을 걸어주는 오토 인코더이다.
-> 이미지가 지저분하기에 특징이 더 주목하는 형질이 있다. 안개 낀 상황에서 사물을 볼 때 더욱 특징에 주목하기 마련이다.
활용)
1. 이미지 특징 추출기 2.이상치(불량품) 찾기 - anomly detection
오토 인코더 구현
encoder
"""
인코더
arguments:
latent_dim: 잠재 벡터의 차원
"""
class Encoder(nn.Module):
def __init__(self, latent_dim: int=20):
super().__init__()
self.fc1 = nn.Linear(784, 256)
self.fc2 = nn.Linear(256, 128)
self.fc_mean = nn.Linear(128, latent_dim)
def forward(self, x: torch.Tensor):
x = x.view(x.size(0), -1)
x = F.leaky_relu(self.fc1(x), negative_slope=0.2)
x = F.leaky_relu(self.fc2(x), negative_slope=0.2)
z = F.leaky_relu(self.fc_mean(x))
return z
Decoder
"""
디코더
arguments:
latent_dim: 잠재 벡터의 차원
"""
class Decoder(nn.Module):
def __init__(self, latent_dim: int=20):
super().__init__()
self.fc1 = nn.Linear(latent_dim, 128)
self.fc2 = nn.Linear(128, 256)
self.fc3 = nn.Linear(256, 784)
def forward(self, z: torch.Tensor):
z = F.leaky_relu(self.fc1(z), negative_slope=0.2)
z = F.leaky_relu(self.fc2(z), negative_slope=0.2)
x_hat = F.sigmoid(self.fc3(z))
x_hat = x_hat.view(x_hat.size(0), 1, 28, 28)
return x_hat
AutoEncoder
class Autoencoder(nn.Module):
def __init__(self, encoder=Encoder, decoder=Decoder, latent_dim: int=20):
# nn.Module의 __init__ 함수를 먼저 호출해줍니다
super().__init__()
# 인코더와 디코더 클래스를 설정합니다.
self.encoder = encoder(latent_dim=latent_dim)
self.decoder = decoder(latent_dim=latent_dim)
self.latent_dim = latent_dim
def forward(self, x: torch.Tensor):
z = self.encode(x)
x_hat = self.decode(z)
return z, x_hat
def encode(self, x: torch.Tensor):
return self.encoder(x)
def decode(self, z: torch.Tensor):
return self.decoder(z)
AutoEncoder 학습
- hyper parms
# 학습률을 설정합니다. Adam 최적화기를 사용할 예정이므로, Adam의 기본값인 0.001을 사용합니다.
# 너무 크거나 작은 학습률이 아니라면, 자유롭게 변경 가능합니다.
learning_rate = 0.001
# Mini-batch Gradient Descent를 위한 배치 사이즈를 설정합니다.
batch_size = 100
# 학습할 에포크 수를 설정합니다.
# 너무 많거나 적은 횟수가 아니라면, 자유롭게 변경 가능합니다.
epochs = 10
# 오토 인코더의 잠재 변수 차원을 설정합니다.
# 차원을 줄이거나 늘리면서 변하는 결과물을 확인해보는 것도 많은 공부가 됩니다.
latent_dim = 20
- 손실 함수와 옵티마이저
autoencoder = Autoencoder(latent_dim=latent_dim).to(device)
# 손실 함수로 평균 제곱 오차를 활용합니다.
recon_loss = nn.MSELoss(reduction='sum')
# 옵티마이저로 Adam 최적화기를 사용합니다.
# 최적화 대상으로는 오토 인코더 클래스의 파라미터들을 포함합니다.
optimizer = optim.Adam(params=list(autoencoder.parameters()), lr=learning_rate)
- train
# 훈련 데이터와 검증 데이터에 대한 손실을 저장합니다.
history = dict(
train_loss=[],
valid_loss=[]
)
print(f"Training start with {epochs} epochs.")
for epoch in range(1, 1 + epochs):
# epoch 단위의 손실 함수의 누적합
train_epoch_loss = 0
train_size = 0
# 오토 인코더 모델을 훈련 모드로 변환합니다.
autoencoder.train()
for i, batch in enumerate(train_loader):
image, label = batch
# 훈련할 이미지를 기기로 전달합니다.
image = image.to(device)
# 배치 크기를 더해줍니다.
train_size += label.size(0)
# 데이터를 잠재 벡터로 축소합니다.
latent = autoencoder.encode(image)
# 잠재 벡터를 데이터로 복원합니다.
reconstruction_image = autoencoder.decode(latent)
optimizer.zero_grad()
# 원본 이미지와 복원된 이미지를 이용해 손실 함수를 계산합니다.
train_loss = recon_loss(image, reconstruction_image)
train_loss.backward()
optimizer.step()
train_epoch_loss += train_loss
test_epoch_loss = 0
test_size = 0
# 오토 인코더 모델을 평가 모드로 전환합니다.
autoencoder.eval()
# 추론만 진행할 것이므로 기울기를 필요로 하지 않습니다.
with torch.no_grad():
for i, batch in enumerate(test_loader):
image, label = batch
image = image.to(device)
test_size += label.size(0)
latent = autoencoder.encode(image)
reconstruction_image = autoencoder.decode(latent)
test_loss = recon_loss(image, reconstruction_image)
test_epoch_loss += test_loss
train_epoch_loss /= train_size
test_epoch_loss /= test_size
# 에포크 단위의 손실 정보를 저장합니다.
history["train_loss"].append(train_epoch_loss.item())
history["valid_loss"].append(test_epoch_loss.item())
변분 오토 인코더(Variational Autoencoders, VAE)
: 개념 설명은 ... 아래의 영상으로 대체한다.
https://www.youtube.com/watch?v=cOkoeIfR724
VAE 구현
- 샘플 데이터셋 출력
from torchvision import utils # 시각화를 위한 라이브러리
import matplotlib # 시각화를 위한 라이브러리
from matplotlib import pyplot as plt # 시각화를 위한 라이브러리
import numpy as np #numpy 데이터 형식을 다루기 위한 라이브러리
"""
64개 이미지 그려보기
1. train_dataset[idx][0]: 해당 idx의 이미지 (28x28 tensor)
2. torch.cat: 64개의 tensor를 합쳐 64x1x28x28 크기의 tensor로 만듦
3. torchvision.utils.make_grid: 1x296x296 크기의 np.ndarray로 바꿔줌 (normalize=True: 이미지를 [0,1] 범위로 바꿔줌)
4. plt.imshow 함수는 입력으로 WxHxC 순서의 데이터를 받으므로 296x296x1 크기로 transpose
"""
plt.axis('off')
plt.title("Training Images")
plt.imshow(np.transpose(utils.make_grid(torch.cat([train_dataset[i][0].unsqueeze(0) for i in range(64)], dim=0), normalize=True), (1,2,0)))
"""
train_dataset의 i번쨰 샘플의 입력 값은 (이미지,label)의 형태로 존재한다.
이를 unsqueeze(0)를 한다는 말은 차원을 하나 추가한다는 의미이고 (1,C,H,W)의 형태가 된다.
0부터 64까지 반복하는데 이는 64개의 이미지를 시각화하려는 의도이다.
0~1사이로 이미지를 정규화(normalize)하고 make_grid 매써드를 통해 여러 이미지 텐서를 하나의 그리드 이미지로 만든다.
imshow() 매써드는 np 형태의 데이터를 입력으로 받기 때문에 torch를 np 형태로 변환을 하여야한다.
이때 차원 규약이 numpy와 pytorch가 다르다.
torch는 (C,H,W) 이고 numpy는 (H,W,C)의 순서를 따른다. 그렇기 때문에 전치(transpose)를 적용해야 한다.
"""
- VariationalEncoder 구현
class VariationalEncoder(Encoder):
def __init__(self, latent_dim: int=20):
super().__init__(latent_dim=latent_dim)
"""
Layer를 하나만 정의하는 이유는 encoder_class를 이미 상속을 받았다.
forward 단계에서 encoder에서 정의한 forward의 layer들을 사용할 것이기 때문에
init 단계에서는 마지막 Layer(fully_connected_Layer)만 새롭게 정의하면 된다.
"""
self.fc_log_var = nn.Linear(128, latent_dim)
def forward(self, x):
x = x.view(x.size(0), -1) #flatten
x = F.leaky_relu(self.fc1(x), negative_slope=0.2) #activation_function
x = F.leaky_relu(self.fc2(x), negative_slope=0.2)
"""
nn.Modul class가 구현될 때 __call__ 함수가 정의되어있다.
인스턴스 자체를 함수처럼 호출할 수 있다.
아니 변분 오토 인코더 실습 부분에서 질문인데.
fc_mean(x)를 넣어주면 __call_매써드가 실행되서 부모 클래스의 forward가 실행이 되잖아요.
근데 자식 클래스의 forward에서 이미 Fc1, Fc2를 통과했는데 다시 Fc1,Fc2를 통과하는 건가요?
그리고 var에 x를 넣어준 것 만으로 어떻게 log_var가 구해지는 건지도 모르겠네요..... 으윽
-> Encoder 클래스의 forward 함수가 호출되는 것이 아니라
nn.Module 클래스으 forwad 함수가 호출이 되는 것이고
그떄 해당 함수는 그 인스턴스만 forward해서 return을 한다.
결론적으로 mean = self.fc_mean(x) 이 코드는 fc_mean만 forward한 값을 return한다.
"""
mean = self.fc_mean(x)
# 오토 인코더와의 주요 차이점
log_var = self.fc_log_var(x)
return mean, log_var
- VariationalAutoencoder 구현
class VariationalAutoencoder(Autoencoder):
def __init__(self, encoder=VariationalEncoder, decoder=Decoder, latent_dim: int=16):
super().__init__(latent_dim=latent_dim)
self.encoder = encoder(latent_dim)
def reparameterize(self, mean: torch.Tensor, log_var: torch.Tensor):
# 샘플링 시에도 기울기를 전달할 수 있도록 재매개변수화 합니다.
std = torch.exp(0.5 * log_var)
# randn_like 메소드는 입력된 인자와 같은 차원의 값들을
# 표준 정규 분포에서 샘플링하여 반환합니다.
epsilon = torch.randn_like(std)
return mean + (epsilon * std)
def forward(self, x: torch.Tensor):
# 인코더를 통해 얻어지는 값이 오토 인코더와 다릅니다.
mean, log_var = self.encode(x)
# 재매개변수화를 진행합니다.
z = self.reparameterize(mean, log_var)
# 오토 인코더와 같습니다.
x_hat = self.decode(z)
return x_hat, mean, log_var
- KLDivergenceLoss 구현
def KLDivergenceLoss(mean, log_var):
return torch.sum(-0.5 * torch.sum(1 + log_var - mean.pow(2) - log_var.exp()))
- loss function 구현
def binary_cross_entropy(x, recon):
return -torch.sum(x * torch.log(recon) + (1 - x) * torch.log(1 - recon))
- 학습
init)
from torch import optim
vae = VariationalAutoencoder(latent_dim=latent_dim).to(device)
# 디코더의 손실 함수를 평균 제곱 오차를 활용할지, 이진 교차 엔트로피를 활용할지
# recon_loss = nn.MSELoss(reduction='sum')
recon_loss = binary_cross_entropy
kld_loss = KLDivergenceLoss
# KL-Divergence의 가중치를 설정합니다. 기본 값은 1입니다.
beta = 1
optimizer = optim.Adam(params=list(vae.parameters()), lr=learning_rate)
cf) beta : 손실함수에 두번째 항(KLD)에 베타라는 가중치를 둘 수 있다. 각 차원 별로 독립적인 정보를 추출하여 의미있는 잠재 표현 학습이 가능해진다. (키워드 : beta - vae)
train)
# 훈련 데이터 셋과 검증 데이터 셋에 대해 복원 손실과 KL-Divergence 손실을 기록합니다.
history = dict(
train_recon=[],
train_kld=[],
valid_recon=[],
valid_kld=[]
)
print(f"Training start with {epochs} epochs.")
for epoch in range(1, 1 + epochs):
# 훈련 정보를 기록합니다.
train_epoch_recon = 0
train_epoch_kld = 0
train_size = 0
vae.train()
for i, batch in enumerate(train_loader):
image, label = batch
image = image.to(device)
train_size += label.size(0)
# 변분 오토 인코더의 인코더가 평균과 분산을 반환합니다.
latent_mean, latent_log_var = vae.encode(image)
# 인코더가 반환한 평균과 분산으로 재매개변수화 트릭을 사용하여 잠재 변수를 얻습니다.
latent = vae.reparameterize(latent_mean, latent_log_var)
# 잠재 변수로부터 이미지를 복원합니다.
reconstruction_image = vae.decode(latent)
# 손실 함수를 계산하여 더해줍니다.
train_recon = recon_loss(image, reconstruction_image)
train_kld = kld_loss(latent_mean, latent_log_var)
train_loss = train_recon + beta*train_kld
train_epoch_recon += train_recon.item()
train_epoch_kld += train_kld.item()
optimizer.zero_grad()
train_loss.backward()
optimizer.step()
test_epoch_recon = 0
test_epoch_kld = 0
test_size = 0
vae.eval()
with torch.no_grad():
for i, batch in enumerate(test_loader):
image, label = batch
image = image.to(device)
test_size += label.size(0)
latent_mean, latent_log_var = vae.encode(image)
latent = vae.reparameterize(latent_mean, latent_log_var)
reconstruction_image = vae.decode(latent)
test_recon = recon_loss(image, reconstruction_image)
test_kld = kld_loss(latent_mean, latent_log_var)
test_loss = test_recon + test_kld
test_epoch_recon += test_recon.item()
test_epoch_kld += test_kld.item()
train_epoch_recon /= train_size
train_epoch_kld /= train_size
test_epoch_recon /= test_size
test_epoch_kld /= test_size
history["train_recon"].append(train_epoch_recon)
history["train_kld"].append(train_epoch_kld)
history["valid_recon"].append(test_epoch_recon)
history["valid_kld"].append(test_epoch_kld)
벡터 양자화 변분 오토 인코더
: 이미지나 텍스트도 '이산'적으로 표현이 가능하다. 정도를 이산화시킨 후 mapping하는 방식으로 말이다. 즉 표현 범위의 다양성을 일부 제한하는 방식으로 학습이 가능하다. -> vqvae를 사용하면 encoder의 output이 각 특징의 정도를 나타내는 벡터의 집합이 나온다.
적대적 생성 신경망(Generative Adversarial Nerworks, GANs)
확산모델(Diffusion)
잠재 확산모델(Diffusion)
미완
'Computer Science > [인공지능]' 카테고리의 다른 글
[NLP] 텍스트 전처리 (0) | 2024.05.07 |
---|---|
[NLP] 자연어처리란 + 기초 언어학 (0) | 2024.05.05 |
[Generation] 생성 모델 역사/평가지표 (0) | 2024.05.03 |
[Computer Vision] Ch 5 ~ Ch 8 (1) | 2024.04.12 |
[ComputerVision] ch 1 ~ ch 4 (0) | 2024.04.05 |