본문 바로가기
머신러닝, 딥러닝/파이토치

[파이토치 스터디] 오토인코더

by 장찐 2022. 2. 16.

 

📚 Stacked Autoencoder (기본 오토인코더) 

 

✅오토인코더로 MNIST  손글씨 이미지 생성하기 

 

 오토인코더는 인코더와 디코더로 구성되어 있으며 정답 라벨 없이 입력된 데이터와 유사한 형태를 출력한다. 기본적인 스택 오코인코더로 MNIST 손글씨 데이터를 생성한다. 

 

📌라이브러리, 데이터 불러오기 

import torch
import torchvision
from torchvision import transforms
import torch.nn.functional as F
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt

device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
device
#데이터를 불러오고 텐서로 변경 
dataset = torchvision.datasets.MNIST('/.data/', download = True, train=True, transform=transforms.ToTensor())
trainloader = torch.utils.data.DataLoader(dataset, batch_size=50, shuffle=True)

 데이터를 불러온 후에 기본적으로 텐서 형태로 변경한다. 

 

image, label = dataset[5]
image.shape
# torch.Size([1, 28, 28])

plt.imshow(image.reshape(28,28),cmap='gist_yarg')

MNIST 데이터셋은 다음과 같이 28*28 형태로 구성되어 있다. 전체 이미지는 60000이다. 

 

 

📌모델 정의  

class Autoencoder(nn.Module):
  def __init__(self):
    super(Autoencoder, self).__init__()
    self.encoder = nn.Sequential(
        nn.Linear(784, 128),
        nn.ReLU(),
        nn.Linear(128,32),
        nn.ReLU(),
        nn.Linear(32,10), #잠재변수 10개로 줄임 
        nn.ReLU())
    
    self.decoder = nn.Sequential(
        nn.Linear(10,32),
        nn.ReLU(),
        nn.Linear(32,128),
        nn.ReLU(),
        nn.Linear(128, 28*28), #처음 입력된 이미지와 같은 크기로 나와야 한다 
        nn.Sigmoid()
    )
  
  #인코더와 디코더 연산을 차례대로 수행하도록 설정 
  def forward(self, x):
    encoded = self.encoder(x)
    decoded = self.decoder(encoded)
    return decoded

 인코더와 디코더를 각각 별개의 블록으로 묶는다. MNIST 이미지가 1x28x28 형태이기 때문에, nn.Linear에 입력하기 위해서는 이미지를 일차원으로 펼쳐서 하나의 벡터로 입력해야 한다. 따라서 첫 번째 레이어의 크기는 28x28=768로 설정한다.  

 라인9 에서 노드의 수를 10개 까지 감소시킨다. 이는 잠재변수의 수가 10개로 정의되는 것과 동일한 의미이다. 줄어든 10개의 잠재변수를 다시 디코터 블록에 넣어서 크기를 증가시킨다. 라인 17에서 처음 이미지와 같은 형태로 출력되어야 하기 때문에 28x28로 크기를 동일하게 맞춘다. 

 라인18에서 시그모이드 함수는 이미지의 픽셀 값인 0~1 사이로 빨리 수렴할 수 있도록 한다. 

 

라인22 에서 forward 함수는 인코더에서 디코더로 차례대로 연산하도록 설정한다. 

 

model =Autoencoder().to(device)
critertion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=1e-4)

 손실함수와 최적화 방법을 지정한다. 

 

📌모델 학습 

for epoch in range(51):
  running_loss = 0
  for data in trainloader:
    inputs = data[0].to(device)
    optimizer.zero_grad()
    outputs = model(inputs.view(-1, 28*28)) #이미지를 일렬로 펴서 넣기 
    outputs = outputs.view(-1, 1, 28, 28) #나온 이미지를 다시 정가각형 형태로 변환 
    loss = critertion(inputs, outputs)
    loss.backward()
    optimizer.step()
    running_loss += loss.item()
  cost = running_loss / len(trainloader)
  print('[%d] loss : %.3f' %(epoch +1, cost))

에폭을 50회로 설정해서 학습을 진행한다. 라인7에서 일렬로 펴진 이미지를 다시 정사각형 형태로 변환하는 작업을 실시한다. 

 

📌 결과 확인 

with torch.no_grad(): #requires_grad 해제
  out_img = torch.squeeze(outputs.cpu())
  #print(out_img.size())

  for i in range(50):
      plt.subplot(1,2,1)
      plt.imshow(torch.squeeze(dataset[i][0]).numpy(), cmap='gray')
      #plt.imshow(dataset[i][0].numpy(), cmap='gray')

      plt.subplot(1,2,2)
      plt.imshow(out_img[i].numpy())
      plt.show()

 오토인코더로 생성한 이미지와 원본 이미지를 확인한다. 반드시 처음에 requires_grad를 해제해야 한다. 정확하게 생성된 숫자들도 있지만, 일부 숫자들은 입력된 숫자와 다르게 나타난 것을 확인할 수 있다. 

 

 


📚 디노이징 오토인코더 (Denoising Autoencoder) 

 

오토인코더는 새로운 데이터를 만드는 것을 목적으로 한다. 따라서 입력 데이터와 완전히 동일한 데이터를 출력한다면 새로운 데이터를 만드는 의미가 없을 수 있다. 따라서 입력값에 과적합이 되지 않도록 적절하게 노이즈를 추가하거나 신경망에 dropout을 적용하는 방식을 사용한다. 이를 디노이징 오토인코더라고 한다. 

 여기서는 가우지안 노이즈를 추가하여 학습을 실시한다. 

 

📌 모델 학습 

for epoch in range(51):
  running_loss = 0

  for data in trainloader:
    inputs = data[0].to(device)
    optimizer.zero_grad()

    dirty_inputs = inputs + torch.normal(0, 0.5, size = inputs.size()).to(device) #가우시안 노이즈 추가 

    outputs = model(inputs.view(-1, 28*28)) #이미지를 일렬로 펴서 넣기 
    outputs = outputs.view(-1, 1, 28, 28) #나온 이미지를 다시 정가각형 형태로 변환 
    loss = critertion(inputs, outputs)
    loss.backward()
    optimizer.step()
    running_loss += loss.item()
  cost = running_loss / len(trainloader)
  print('[%d] loss : %.3f' %(epoch +1, cost))

 데이터 불러오기, 모델정의 부분은 일반 오토인코더와 동일하다. 디노이징 오토인코더는 중간에 input을 한번 더 수동으로 추가한다. 여기서는 가우시안 분포에 따라서 평균=0, 표준편차=0.5인 노이즈를 추가하였다. 노이즈의 텐서 크기는 이미지 사이즈와 동일해야 하기 때문에 크기를 맞춘다. 

 

 


📚 합성곱 오토인코더 (Convolutional Autoencoder) 

 합성곱 오토인코더는 nn.Linear 대신에 합성곰 레이어인 nn.Conv2d를 사용한다. 이미지를 한 줄의 벡터로 펼쳐서 전달하는 것이 아니라, 그대로 들어와서 연산이 진행된다. 일반적으로 잠재 변수 h는 일렬 벡터이기 때문에 인코더에서 나온 feature map을 일렬로 펼쳐서 h를 추출하고 다시 h를 은닉층을 거쳐서 사각형 형태의 피쳐맵으로 만든 뒤에 디코더 블록에 입력한다. 

 

📌 모델 정의

#피쳐맵을 벡터화하기 
class Flatten(torch.nn.Module):
  def forward(self, x):
    batch_size = x.shape[0]
    return x.view(batch_size, -1)

 인코더를 거친 feature map의 크기는 (배치 사이즈, 채널 수 , 이미지 가로, 이미지 세로) 형태이다. 배치 사이즈가 현재 이미지의 개수이므로, 벡터가 배치 사이즈 만큼 존재해야 한다. 여기서는 view 를 사용해서 각 피쳐 데이터를 일렬로 변환한다. 

 

#벡터를 다시 사각형 피쳐맵으로 변환하기 
class Deflatten(nn.Module):
  def __init__(self, k):
    super(Deflatten, self).__init__()
    self.k = k

  def forward(self, x):
    s = x.size()
    feature_size = int( (s[1] // self.k) ** 0.5)
    return x.view(s[0], self.k, feature_size, feature_size)

 잠재 변수 h는 (배치사이즈, 채널수 * 이미지가로 * 이미지세로) 의 형태이다. 따라서 벡터의 사이즈는 채널수*이미지가로*이미지세로) 형태이다. 이미지가 정사각형 이라면 이미지가로 = 이미지세로 가 된다. 따라서 이 식을 정리하면 라인9 에 있는 것처럼 이미지가로(또는 세로) = (벡터 사이즈/채널 수)**0.5 가 된다. 

 

마지막 줄에서 피쳐맵의 크기를 (배치사이즈, 채널수, 이미지가로, 이미지세로)로 반환한다. 

 

class Autoencoder(nn.Module):
    def __init__(self):
        super(Autoencoder, self).__init__()
        
        k = 16
        self.encoder = nn.Sequential(
                        nn.Conv2d(1, k, 3, stride=2), # nn.Conv2d(input채널, output 채널, 필터수, stride)
                        nn.ReLU(), 
                        nn.Conv2d(k, 2*k, 3, stride=2),
                        nn.ReLU(), 
                        nn.Conv2d(2*k, 4*k, 3, stride=1),
                        nn.ReLU(),
                        Flatten(),
                        nn.Linear(1024, 10), 
                        nn.ReLU()
        )
        # ConvTranspose2d
        # 입력 성분(Conv의 결과)을 출력 성분(Conv의 입력)으로 미분하여 그 값을 입력 벡터와 곱해 출력 벡터를 산출한다.
        # 출력 된 벡터는 행렬 형태로 변환한다.
        self.decoder = nn.Sequential(
                        nn.Linear(10, 1024),
                        nn.ReLU(),
                        Deflatten(4*k),
                        nn.ConvTranspose2d(4*k, 2*k, 3, stride=1), #ConvTranspose2d(입력 채널 수, 출력 채널 수, 필터 크기, stride)
                        nn.ReLU(),
                        nn.ConvTranspose2d(2*k, k, 3, stride=2),
                        nn.ReLU(),
                        nn.ConvTranspose2d(k, 1, 3, stride=2,output_padding=1),
                        nn.Sigmoid()
        )
    
    def forward(self, x):
        
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)

        return decoded

 합성곱 레이러를 사용해서 모델을 정의한다. 라인14에서 선형 은닉 레이어를 하나 거쳐서, 합성곱 층에서 나온 feature map을 일렬로 펼친다. 그리고 크기가 10인 잠재 변수를 만든다. 

 라인21에서 다시 은닉 레이어를 통해서 10개의 잠재 변수를 크기가 1024인 벡터로 ㅂ만든다. 그리고 라인 22에서 Deflatten을 이용해서 다시 사각형의 feature map으로 변환한다. 

 디코더 부분에서는 크기가 작은 input을 크기가 큰 output 으로 반환하기 위해서 nn.ConvTranspose2d를 사용한다. 이는 입력값을 출력값으로 미분하고, 그 값을 입력 벡터와 곱한 값을 반환하는 연산이다. 

 

 

 

📘nn.Conv2d 사용 방법 

 

 

📘nn.ConvTransposed2d설명 

 

nn.ConvTranspose2d(input 채널, output 채널, 필터 크기, stride) 

Transposed Convolution은 단순히 합성곱의 역 연산이 아니고, 적절하게 패딩을 추가해서 다시 차원을 늘린다. 

(추가설명 오토인코더 포스트 참고) 

 

 

📌모델 학습 

for epoch in range(51):

    running_loss = 0.0
    for data in trainloader:

        inputs = data[0].to(device)

        optimizer.zero_grad()
        outputs = model(inputs)
        
        loss = criterion(inputs, outputs) # 라벨 대신 입력 이미지와 출력 이미지를 비교

        loss.backward()
        optimizer.step()
        running_loss += loss.item()


    cost = running_loss / len(trainloader)        
    
    if epoch % 10 == 0:
        print('[%d] loss: %.3f' %(epoch + 1, cost))

앞선 오토인코더 모델과 학습 코드는 동일하지만, 이미지를 그대로 받아서 학습하기 때문에 view를 이용해서 별도의 크기 변환을 할 필요가 없다 

 

 

📌이미지 생성 결과 확인 

def normalize_output(img):
    img = (img - img.min())/(img.max()-img.min())
    return img

def check_plot():
    with torch.no_grad():
        for data in trainloader:

            inputs = data[0].to(device)
            outputs = model(inputs)
            
            input_samples = inputs.permute(0,2,3,1).cpu().numpy() # 원래 이미지
            reconstructed_samples = outputs.permute(0,2,3,1).cpu().numpy() # 생성 이미지
            break # 배치 하나만 받고 for문 종료

    #reconstructed_samples = normalize_output(reconstructed_samples) # 0~1사이로 변환
    #input_samples = normalize_output(input_samples) # 0~1사이로 변환

    columns = 10 # 시각화 전체 너비 
    rows = 5 # 시각화 전체 높이 

    fig=plt.figure(figsize=(columns, rows)) # figure 선언

    # 원래 이미지 배치 크기 만큼 보여주기
    for i in range(1, columns*rows+1):
        img = input_samples[i-1]
        fig.add_subplot(rows, columns, i)
        plt.imshow(img.squeeze()) # 1채널인 경우 2로 변환
        #plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        plt.axis('off')
    plt.show()

    # 생성 이미지 배치 크기 만큼 보여주기
    fig=plt.figure(figsize=(columns, rows))

    for i in range(1, columns*rows+1):
        img = reconstructed_samples[i-1]
        fig.add_subplot(rows, columns, i)
        plt.imshow(img.squeeze())
        #plt.imshow(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
        plt.axis('off')
    plt.show()
    
    
 check_plot()

 

 

 

 

 

 

 

 


📚  Reference

 

 딥러닝을 위한 파이토치 입문, 딥러닝호형 저, 영진닷컴

 

 Transposed Convolution 설명 

https://zir2-nam.tistory.com/entry/025-%ED%95%A9%EC%84%B1%EA%B3%B1-%EC%98%A4%ED%86%A0%EC%9D%B8%EC%BD%94%EB%8D%94-%EC%95%84%EC%8B%B8-%EC%A2%8B%EA%B5%AC%EB%82%98

 

https://velog.io/@hayaseleu/Transposed-Convolutional-Layer%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80

댓글