딥러닝 이야기 / Manifold Learning / 3. Autoencoder (오토인코더) 구현 및 MNIST 특징 추출

작성일: 2022.02.26
시작하기 앞서 틀린 부분이 있을 수 있으니, 틀린 부분이 있다면 지적해주시면 감사하겠습니다.
지금까지 manifold learning의 이야기를 latent variable (잠재 변수)부터 시작하여 manifold와 autoencoder (오토인코더), 그리고 t-SNE, UMAP에 대해 설명하였습니다.
오늘은 python의 PyTorch를 이용하여 vanilla autoencoder, convolutional autoencoder, denoising autoencoder를 구현하여 MNIST 데이터에 대해 적용해보겠습니다.
그리고 MNIST 데이터의 잠재 변수(latent variable)를 추출하여 t-SNE로 가시화 해보도록 하겠습니다.
그리고 manifold learning과 autoencoder에 대한 글은 여기,
t-SNE, UMAP에 관한 글은 여기를 참고하시기 바랍니다.
이렇게 구현한 autoencoder의 코드는 GitHub에 올려놓았으니 아래 링크를 참고하시기 바랍니다(본 글에서는 모델의 구현과 잠재 변수 가시화에 초점을 맞추고 있기 때문에, 데이터 전처리 및 학습을 위한 전체 코드는 아래 GitHub 링크를 참고하시기 바랍니다).
오늘의 컨텐츠입니다.
- Vanilla Autoencoder (오토인코더) 구현
- Convolutional Autoencoder (오토인코더) 구현
- Denoising Autoencoder (오토인코더) 구현
- Autoencoder (오토인코더) 결과 및 t-SNE를 통한 잠재 변수 가시화
Autoencoder (오토인코더) 구현 및 잠재 변수(latent variable) 가시화
”
먼저 가장 기본적인 vanilla autoencoder 구현 코드를 살펴보겠습니다. 코드는 PyTorch로 작성 되었으며, vanilla autoencoder의 전체적인 구조는 단순히 linear layer을 여러개 쌓은 모습입니다. 한 줄씩 자세한 설명은 코드 아래쪽에 설명을 참고하시기 바랍니다.
class AE(nn.Module):
def __init__(self, config:Config, color_channel:int):
super(AE, self).__init__()
self.height = config.height
self.width = config.width
self.hidden_dim = config.hidden_dim
self.latent_dim = config.latent_dim
self.dropout = config.dropout
self.color_channel = color_channel
self.encoder = nn.Sequential(
nn.Linear(self.height*self.width*self.color_channel, self.hidden_dim),
nn.ReLU(),
nn.Dropout(self.dropout),
nn.Linear(self.hidden_dim, self.latent_dim),
nn.ReLU()
)
self.decoder = nn.Sequential(
nn.Linear(self.latent_dim, self.hidden_dim),
nn.ReLU(),
nn.Dropout(self.dropout),
nn.Linear(self.hidden_dim, self.height*self.width*self.color_channel),
nn.Sigmoid()
)
def forward(self, x):
batch_size = x.size(0)
x = x.view(batch_size, -1)
latent_variable = self.encoder(x)
output = self.decoder(latent_variable)
output = output.view(batch_size, -1, self.height, self.width)
return output, latent_variable
모델 초기화
먼저 모델의 고정된 값을 초기화하여 hidden layer까지 초기화하는 부분입니다.
GitHub 코드에 보면 config.json이라는 파일에 존재하는 변수 값들을 모델에 적용하여 초기화 하는 것입니다.
- 4, 5번째 줄: 학습 이미지를 모두 같은 크기로 전처리 하였을 때의 세로 가로 크기.
- 6, 7번째 줄: hidden layer의 차원 및 데이터를 represenation할 잠재 변수(latent variable) 차원.
- 8번째 줄: overfitting (과적합)을 방지하기 위한 dropout 비율.
- 9번째 줄: 이미지 전처리를 하였을 때, color channel 수(흑백으로 처리를 했다면 1, 칼라로 처리 했다면 3).
- 11 ~ 17번째 줄: encoder를 정의, 첫 번째 hidden layer는 (data size * hidden dim), 두 번째 hidden layer는 (hidden dim * latent variable dim)의 크기를 가짐(데이터를 더 작게 압축).
- 18 ~ 24번째 줄: decoder를 정의, 첫 번째 hidden layer는 (latent variable dim * hidden dim), 두 번째 hidden layer는 (hidden dim * data size)의 크기를 가짐(데이터를 다시 원래 크기로 복구).
학습될 때 거치는 부분
다음은 forward 부분의 코드입니다. 이 부분은 모델을 정의 하고나서 실제로 데이터가 학습할 때 직접적으로 거치게 되는 부분입니다. Pytorch의 모델을 정의할 때 nn.Module을 상속 받기 때문에 자동으로 데이터가 forward라는 method를 거치게 됩니다. 그리고 PyTorch에서는 forward를 거치면 자동으로 데이터 backpropagation이 가능하게 구현이 되어있습니다.
- 27번째 줄: 데이터가 들어왔을 때 batch size를 저장.
- 28번째 줄: 데이터의 크기를 변화.
(e.g. 데이터가 크기가 칼라일 경우 128 * 3 * 10 * 10 (batch * channel * height * width)이고, 이를 128 * 300으로 변환). - 29번째 줄: encoder를 거쳐 latent variable 추출.
(e.g. 위 예시의 128 * 300 데이터가 인코더를 거쳐 128 * 32 크기를 갖는 잠재 변수 추출). - 30번째 줄: 잠재 변수(latent variable)를 decoder를 거쳐 원래의 크기로 복구.
(e.g. 128 * 32 크기의 잠재 변수를 원래 크기인 128 * 300으로 복구). - 31번째 줄: 복구된 데이터를 원래 raw data의 크기로 변환.
(e.g. 128 * 300 크기로 복구된 데이터를 128 * 3 * 10 * 10의 원래 데이터 크기로 변환). - 33번째 줄: 디코더의 복구 결과와 t-SNE로 가시화하기 위한 인코더의 결과인 잠재 변수(latent variable)를 내보냄.
”
이제 convolutional autoencoder 구현 코드를 살펴보겠습니다. Convolutional autoencoder와 vanilla autoencoder의 데이터를 압축하고 복구한다는 컨셉은 동일합니다. 다만 압축하고 복구하는 과정에서 vanilla autoencoder는 linear hidden layer를 사용하였다면, convolutional autoencoder는 이름에서 알 수 있듯이 convolutional layer를 사용합니다. 바로 그 유명한 CNN (Convolutional Neural Network)에서 사용하는 레이어이죠. Convolutional layer는 좀 더 복잡한 데이터에 대해 모델의 성능을 향상 시키고 싶을 때 사용할 수 있습니다. 코드는 PyTorch로 작성 되었으며, 한 줄씩 자세한 설명은 코드 아래쪽에 설명을 참고하시기 바랍니다.
class CAE(nn.Module):
def __init__(self, config:Config, color_channel:int):
super(CAE, self).__init__()
self.height = config.height
self.width = config.width
self.color_channel = color_channel
self.encoder = nn.Sequential(
nn.Conv2d(in_channel=self.color_channel, out_channel=32, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(32),
nn.ReLU()
nn.Conv2d(in_channel=32, out_channel=64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU()
nn.Conv2d(in_channel=64, out_channel=128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(128),
nn.ReLU()
nn.MaxPool2d(kernel_size=1, stride=2),
nn.Conv2d(in_channel=128, out_channel=64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU()
nn.MaxPool2d(kernel_size=1, stride=2)
)
self.decoder = nn.Sequential(
nn.ConvTranspose2d(in_channel=64, out_channel=128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(128),
nn.ReLU()
nn.ConvTranspose2d(in_channel=128, out_channel=64, kernel_size=2, stride=2, padding=0),
nn.BatchNorm2d(64),
nn.ReLU()
nn.ConvTranspose2d(in_channel=64, out_channel=32, kernel_size=2, stride=2, padding=0),
nn.BatchNorm2d(64),
nn.ReLU()
nn.ConvTranspose2d(in_channel=32, out_channel=self.color_channel, kernel_size=3, stride=1, padding=1),
nn.ReLU()
)
def forward(self, x):
latent_variable = self.encoder(x)
output = self.decoder(latent_variable)
return output, latent_variable
모델 초기화
먼저 모델의 고정된 값을 초기화하여 hidden layer까지 초기화하는 부분입니다.
이 부분은 위의 vanilla autoencoder와 유사합니다.
- 4 ~ 6번째 줄: 학습 이미지를 모두 같은 크기로 전처리 하였을 때의 세로 가로 크기 및 color channel 수(흑백으로 처리를 했다면 1, 칼라로 처리 했다면 3).
- 8 ~ 26번째 줄: convolutional layer로 구성된 encoder를 정의(데이터를 잠재 변수(latent variable)로 압축하는 과정이며 overfitting (과적합) 방지를 위해 dropout 대신 batch normalization을 사용).
- 28 ~ 43번째 줄: convolutional transposed layer로 구성된 decoder를 정의(잠재 변수(latent variable)를 복구하는 과정이며 overfitting (과적합) 방지를 위해 dropout 대신 batch normalization을 사용, 이 부분은 transposed layer 대신 데이터 자체를 interpolate 하여 convolutional layer를 사용해도 됨).
학습될 때 거치는 부분
다음은 forward 부분의 코드입니다. 이 부분은 모델을 정의 하고나서 실제로 데이터가 학습할 때 직접적으로 거치게 되는 부분입니다. 이 부분 역시 위의 vanilla autoencoder와 유사합니다.
- 47번째 줄: vanilla autoencoder와 다르게 데이터의 크기를 변화하지 않고 데이터를 encoder에 넣어서 잠재 변수를 구함(convolutional layer는 4차원의 데이터를 인자로 받기 때문 e.g. batch * color channel * height * width).
- 48번째 줄: 이렇게 압축된 잠재 변수(latent variable)를 decoder를 통해 다시 복구.
- 50번째 줄: 디코더의 복구 결과와 t-SNE로 가시화하기 위한 인코더의 결과인 잠재 변수(latent variable)를 내보냄.
”
위에서는 linear layer를 사용한 vanilla autoencoer, convolutional layer를 사용한 convolutional autoencoer에 대해 살펴보았습니다.
보통은 위에서 살펴본 모델에 사용할 데이터는 온전한 상태의 데이터가 사용됩니다.
하지만 좀 더 의미있는 특징, 잠재 변수(latent variable)를 추출하기 위해서 데이터에 노이즈를 추가하여 사용하기도 합니다.
이렇게 노이즈가 낀 데이터를 사용하면 그것이 바로 denoising autoencoder가 되는 것입니다.
따라서 위의 모델에 노이즈가 낀 데이터가 사용되어 학습을 시켰다면, 각각 denoising vanilla autoencoer, denoising convolutional autoencoder가 되는 것이지요.
이렇듯 denoising 데이터를 사용하는지 여부에 따라 모델의 종류가 바뀌므로 모델 자체의 코드를 변형할 필요는 없습니다(위의 모델 코드 그대로 사용합니다).
다만 학습을 하기 위해 dataloader를 통해 데이터를 불러오는 과정에서 데이터에 노이즈를 섞어줄지 여부를 판단하여, denoising autoencoder를 만들 것이냐 말것이냐 결정을 하게 됩니다.
자세한 코드에 대한 설명은 코드 아래를 참고하시기 바랍니다.
if self.model_type == 'AE':
self.model = AE(self.config, self.color_channel).to(self.device)
elif self.model_type == 'CAE':
self.model = CAE(self.config, self.color_channel).to(self.device)
for epoch in range(self.epochs):
start = time.time()
print(epoch+1, '/', self.epochs)
print('-'*10)
for phase in ['train', 'val']:
if phase == 'train':
self.model.train()
else:
self.model.eval()
total_loss = 0
for i, (x, _) in enumerate(self.dataloaders[phase]):
if self.denoising:
noise = torch.zeros_like(x)
noise = nn.init.normal_(noise, mean=self.config.noise_mean, std=self.config.noise_std)
x = x.to(self.device)
noise = noise.to(self.device)
noise_x = x + noise
else:
x = x.to(self.device)
self.optimizer.zero_grad()
with torch.set_grad_enabled(phase=='train'):
output, latent_variable = self.model(noise_x) if self.denoising else self.model(x)
loss = self.criterion(output, x)
if phase == 'train':
loss.backward()
self.optimizer.step()
먼저 설명하기 앞서 위에 보이는 코드들의 변수 앞에 self.라는 것이 붙어있는데 이는 실제 학습을 하기 위한 코드를 class로 제작하였기 때문에 나타난 것입니다.
이는 GitHub 코드에서 train.py를 보면 알 수 있을 것입니다.
여기서는 무시해도 무방합니다.
모델 정의
- 1 ~ 4번째 줄: 먼저 학습에 사용할 모델을 vanilla autoencoder (AE), convolutional autoencoder (CAE) 중 선택하여 정의.
Denoising Autoencoder 제작
- 18번째 줄: dataloader를 이용하여 이미지를 batch size만큼 내어주어 변수 x에 저장.
- 19 ~ 24번째 줄: denoising autoencoder를 학습하다면, 이미지 데이터 x에 가우시안 노이즈 noise를 더하여 최종적으로 noise_x 제작.
- 25 ~ 26번째 줄: denoising autoencoder를 학습하지 않는다면 그냥 이미지 데이터 x를 사용.
- 31번째 줄: denoising autoencoder라면 위에서 정의한 모델에 noise_x를 넣고, 그냥 autoencoder라면 x를 위에서 정의한 모델에 넣음.
- 32번째 줄: denoising autoencoder 여부와 상관없이 loss는 x와 복구된 output을 통해서 구함
(denoising autoencoder 모델이여도 noise_x와 loss를 계산 하지 않는다는 것에 주의).
”
Autoencoder (오토인코더) 결과
위에서 구현한 autoencoder 모델이 잘 학습이 되었다면, 그 모델은 인풋 데이터에 대해서 아주 잘 복구할 수 있는 모델이 되어있을 것입니다.
그렇다면 autoencoder를 통해 복구된 이미지의 결과와 실제 인풋으로 들어간 이미지를 비교해보겠습니다.
아래 결과에서 왼쪽은 vanilla autoencoder, 오른쪽은 denoising vanilla autoencoder의 결과를 나타냅니다.
이 결과를 내기 위해 사용된 데이터는 학습에 전혀 관여하지 않은 test 데이터입니다.
Autoencoder (오토인코더) 결과
위 그림을 보면 denoising autoencoder에 사용된 인풋 데이터가 일반 데이터에 비해 noise가 많이 있는 데이터인 것을 확인할 수 있습니다.
그리고 양쪽 두 모델 모두 MNIST 데이터를 인풋 데이터와 비교했을 때 거의 유사하게 복구하는 것을 볼 수 있습니다.
잠재 변수(latent variable) 가시화 결과
위에서 말하였듯이 구현한 autoencoder 모델이 잘 학습이 되었다면, 그 모델은 데이터의 복구 뿐 아니라 각 데이터의 특징을 잘 잡아내고 데이터의 의미 있는 잠재 변수를 추출할 수 있습니다.
그렇다면 autoencoder를 통해 구한 각 데이터의 잠재 변수 가시화 결과를 비교해보겠습니다.
아래 결과에서 왼쪽은 vanilla autoencoder, 오른쪽은 denoising vanilla autoencoder의 결과를 나타냅니다.
여기서도 역시 결과를 내기 위해 사용된 데이터는 학습에 전혀 관여하지 않은 test 데이터입니다. 즉 test 데이터에 대한 잠재 변수 가시화의 결과입니다.
t-SNE를 통한 잠재 변수(latent variable) 가시화 결과
위 그림을 보면 숫자별로 그 특징을 잘 잡아내어 의미있는 잠재 변수(latent variable)를 추출한 것을 확인할 수 있습니다.
데이터의 숫자별로 잘 군집이 된 것을 볼 수 있으며, denoising vanilla autoencoder의 군집 결과가 살짝 더 잘 군집화 된 것을 볼 수 있습니다.
이러한 denoising autoencoder의 결과에 대한 이유는 이전글을 참고하시기 바랍니다.
t-SNE를 이용한 잠재 변수(latent variable) 가시화 코드
그렇다면 이제 t-SNE를 통해 구한 데이터의 잠재 변수(latent variable)를 가시화 하는 코드에 대해 살펴보겠습니다.
t-SNE를 사용하는 코드는 sklearn 라이브러리를 이용하기 때문에 어렵지 않게 사용할 수 있습니다.
from sklearn.manifold import TSNE
np.random.seed(42)
tsne = TSNE()
total_latent_variable = total_latent_variable.view(total_latent_variable.size(0), -1)
x_test_2D = tsne.fit_transform(total_latent_variable)
x_test_2D = (x_test_2D - x_test_2D.min())/(x_test_2D.max() - x_test_2D.min())
plt.figure(figsize=(10, 10))
plt.scatter(x_test_2D[:, 0], x_test_2D[:, 1], s=10, cmap='tab10', c=total_y.numpy())
- 1번째 줄: t-SNE를 사용하기 위해서 sklearn의 라이브러리를 로드.
- 3번째 줄: t-SNE는 돌릴 때 마다 그 모양이 조금씩 변하게 되는데 그것을 방지하기 위해 random seed를 설정.
- 5번째 줄: total_latent_variable의 크기를 변환.
(e.g. total data 수 * latent dim 크기로 변환, 여기서는 10,000 * 32). - 6번째 줄: t-SNE가 적용되여 축소된 최종적인 가시화 데이터 추출.
(e.g. total data 수 * 2(3차원 가시화를 한다면 3) 크기로 변환, 여기서는 10,000 * 2). - 7번째 줄: t-SNE의 결과를 0 ~ 1 범위로 정규화.
- 9 ~ 10번째 줄: t-SNE를 통한 잠재 변수(latent variable)결과 가시화.
지금까지 vanilla autoencoder, convolutional autoencoder, denoising autoencoder 구현 방법에 대해 알아보았습니다.
그리고 autoencoder의 데이터 복구 결과 및 데이터 잠재 변수(latent variable)를 가시화한 결과를 비교하였습니다.
학습 과정에 대한 전체 코드는 GitHub에 있으니 참고하시면 될 것 같습니다.
다음에는 autoencoder와 이름이 비슷하지만 그 목적이 전혀 다른 variational autoencoder (VAE)에 대해 살펴보도록 하겠습니다.