Joonas' Note

Joonas' Note

[딥러닝 일지] Auto Encoder (with MNIST) 본문

AI/딥러닝

[딥러닝 일지] Auto Encoder (with MNIST)

2022. 6. 3. 23:15 joonas

    이전 글 - [딥러닝 일지] MNIST Competition


    생성 모델

    이번에는 MNIST 데이터셋으로 0~9 사이의 숫자를 주면 28x28 크기의 숫자 이미지를 만들어내는 생성 모델을 연습했다.

    그 중에서도, 가장 기초적인 형태의 오토 인코더(Auto Encoder) 모델이다.

    https://www.compthree.com/blog/autoencoder/

    입력 이미지를 잠재 공간(Latent space)의 어떤 형태로 만드는 Encoder 부분과, 잠재 공간의 값을 다시 재구성하는 Decoder 부분으로 이루어진다.

    여기서 잠재 공간의 차원은 2개, 10개 등 상관없고 당연하겠지만 고차원일수록 많은 표현들을 내포할 수 있으므로 좋다.

    레이어를 분리해서 학습을 진행하는 경우도 있고 하나로 합쳐서 학습해도 되는데, 중간값을 확인할 수 있도록 분리해서 진행했다.

    encoded = Encoder(inputs)
    decoded = Decoder(encoded)

    모델

    모델은 여러가지 형태로 구현해봤는데, 이 블로그에 있는 이미지들을 만든 모델 구조는 아래와 같다.

    __________________________________________________________________________________________
    Layer                        Type                  Output Shape              Param #        
    ==========================================================================================
    autoencoder                  AutoEncoder           (-1, 1, 28, 28)           0              
    ├─encoder                    Sequential            (-1, 2)                   0              
    |    └─0                     Conv2d                (-1, 32, 28, 28)          320            
    |    └─1                     BatchNorm2d           (-1, 32, 28, 28)          129            
    |    └─2                     LeakyReLU             (-1, 32, 28, 28)          0              
    |    └─3                     Conv2d                (-1, 64, 14, 14)          18,496         
    |    └─4                     BatchNorm2d           (-1, 64, 14, 14)          257            
    |    └─5                     LeakyReLU             (-1, 64, 14, 14)          0              
    |    └─6                     Conv2d                (-1, 64, 7, 7)            36,928         
    |    └─7                     BatchNorm2d           (-1, 64, 7, 7)            257            
    |    └─8                     LeakyReLU             (-1, 64, 7, 7)            0              
    |    └─9                     Flatten               (-1, 3136)                0              
    |    └─10                    Linear                (-1, 2)                   6,274          
    |    └─11                    LeakyReLU             (-1, 2)                   0              
    ├─decoder                    Sequential            (-1, 1, 28, 28)           0              
    |    └─0                     Linear                (-1, 3136)                9,408          
    |    └─1                     Unflatten             (-1, 64, 7, 7)            0              
    |    └─2                     LeakyReLU             (-1, 64, 7, 7)            0              
    |    └─3                     ConvTranspose2d       (-1, 64, 14, 14)          36,928         
    |    └─4                     BatchNorm2d           (-1, 64, 14, 14)          257            
    |    └─5                     LeakyReLU             (-1, 64, 14, 14)          0              
    |    └─6                     ConvTranspose2d       (-1, 32, 28, 28)          18,464         
    |    └─7                     BatchNorm2d           (-1, 32, 28, 28)          129            
    |    └─8                     LeakyReLU             (-1, 32, 28, 28)          0              
    |    └─9                     ConvTranspose2d       (-1, 1, 28, 28)           289            
    |    └─10                    Tanh                  (-1, 1, 28, 28)           0              
    ==========================================================================================

    마지막에 활성화 함수로 Tanh 가 있는데, 이건 없어도 상관없다.

    초반에는 Batch Nomalization도 없이 아주 간단하게 아래와 같이 만들어서 사용했다.

    class AutoEncoder(nn.Module):
        def __init__(self, z_dim=2):
            super().__init__()
            self.encoder = nn.Sequential(
                nn.Conv2d(1, 32, 3, padding=1),
                nn.LeakyReLU(),
                nn.Conv2d(32, 64, 3, padding=1, stride=2),
                nn.LeakyReLU(),
                nn.Conv2d(64, 64, 3, padding=1, stride=2),
                nn.LeakyReLU(),
                nn.Flatten(),
                nn.Linear(64 * 7 * 7, z_dim),
                nn.LeakyReLU(),
            )
    
            self.decoder = nn.Sequential(
                nn.Linear(z_dim, 64 * 7 * 7),
                nn.Unflatten(1, (64, 7, 7)),
                nn.LeakyReLU(),
                nn.ConvTranspose2d(64, 64, 3, padding=1, output_padding=1, stride=2),
                nn.LeakyReLU(),
                nn.ConvTranspose2d(64, 32, 3, padding=1, output_padding=1, stride=2),
                nn.LeakyReLU(),
                nn.ConvTranspose2d(32, 1, 3, padding=1),
            )
    
        def forward(self, x):
            x = self.encoder(x)
            x = self.decoder(x)
            return x

     

    잠재 공간

    각 weight들이 어떤 feature를 의미하고 있을 지는 알기 어렵다. 하지만 MNIST는 매우 간단한 형태이므로 2차원의 잠재 공간으로 학습한 후에, 어떤 값이 들어있는 지 2차원 좌표 평면으로 확인할 수 있다 (라고 생각했다)

    2차원

    학습은 생각보다 잘 되는데, 2차원 잠재 공간만으로 학습하기에는 최적화 함수와 스케줄러도 여러가지 시도해보다가 SGD + CyclicLR 이 가장 잘 나와서 그대로 몇번 더 진행해봤다.
    (스케줄러 비교하기 좋은 글: https://sanghyu.tistory.com/113)

    학습하면서 확인해본 결과 이미지

    하지만 책이나 인터넷에서 본대로 예쁘게 클러스터링 되지는 않는다.

    2차원에서 만난 케이스들

    확률적으로 아래와 같은 학습 결과도 만날 수 있었다.

    나름 클러스터링이 잘 된 케이스

    이 상태에서 특정 영역에 있는 값들을 Decoder에 넣어서 이미지를 뽑아보았다.
    여기서는 2차원 좌표 선상의 (x ,y)가 입력이 되겠다.

    먼저 망한 모델부터 보자.

    망한 모델 (여기서 좌표는 관계없음)

    0과 1 외에는 착시현상이 생길것만 같은 결과들만 잔뜩 나왔다.

     

    왼쪽은 2차원 잠재 공간상의 레이블을, 오른쪽은 검은색 테두리 안의 포인트들로부터 생성한 이미지 결과이다.

    클러스터가 굉장히 겹쳐있어서 결과도 모호하게 나오지만, 이런 식으로 결과가 나오는구나 알 수 있다.

    조금 더 촘촘하게 확인해보면, 여러 숫자들의 포인트가 섞여있는 부분에서는 굉장히 난해한 이미지가 생성된 것을 확인할 수 있다.

    아래 링크한 캐글 노트북에서 Version 8 이후로 확인해보면 여러 케이스를 확인할 수 있다.

    비지도 학습의 다른 케이스

    위에서 클러스터링이 잘 된 케이스의 결과는 이러하다.

    비교적 0, 6, 7, 9 가 잘 확인되는 모습

     

    (실험) 지도 학습

    2차원 잠재공간에서 인터넷처럼 아름답게 나누어진 결과를 얻기 위해 학습에 직접 개입했다.

    모든 숫자들의 위치(잠재 공간의 값)를 직접 지정해주고 학습을 진행해보았다.

    latent_map = {
        0: (-2, 6),
        1: (-2, -6),
        2: (-6, 2),
        3: (-3, 4),
        4: (6, 2),
        5: (2, 4),
        6: (0, 2),
        7: (0, -2),
        8: (-2, 0),
        9: (2, -2),
    }

    각 숫자의 포인트를 정한 근거는 매우 주관적이다.

    전체적인 샘플링 결과
    4, 5, 9 가 있는 공간의 샘플링 결과

    생각대로 잘 나온다. 숫자 9로 보이는 빨간색 레이블들이 제대로 위치를 못 잡았지만 이정도면 만족스러운 결과이다.

     

    10차원

    학습이 잘 되는 것 같아 보이기는 한데, (5 vs 6), (3 vs 8), (4 vs 9) 에서 매우 힘겨워한다.

    flat하게 펴진 상태에서 특징을 추려내기에는 어떤 난관이 존재해보인다.

     

    Conv2D 유지

    잠재 공간을 (2, 7, 7) 크기로 유지했더니 학습도 엄청나게 빠르고 정확도도 높다.

    epoch 가 아니라 step 이다. (1 epoch만 돌렸음)

     

    마치며

    ReLU보다 LeakyReLU의 결과가 더 나았다. 잠재 벡터가 몇 안되기 때문에 하나라도 0에 가까워지면 그대로 학습이 멈춰버린다. 음수여도 의미있는 weight, bias 일 수 있기 때문이라고 생각한다.

    LR Scheduler에 따라서 학습 진행 양상이 매우 달라지는 것을 느꼈다. global minimum까지 가는 길에 그만큼 많은 local minima들이 존재했다는 의미가 아닐까.

     

    노트북

    자세한 결과는 아래의 캐글 노트북에서 확인할 수 있다.

    전체 스크립트는 kaggle에 업로드해서 실행해봤는데, 만들어지는 GIF를 실시간으로 출력하는 display 때문에 저장이 안되어서 수정했다.

     

    MNIST AutoEncoder + Visualization

    Explore and run machine learning code with Kaggle Notebooks | Using data from No attached data sources

    www.kaggle.com

     

    Comments