본문 바로가기
Computer Vision

ResNet 구조를 이용한 AutoEncoder 구현 & 리뷰

by Yuchulnote 2023. 8. 16.
728x90

Autoencoder(오토인코더) 란?

오토인코더는 딥러닝에서 주로 사용되는 비지도 학습 방법 중 하나입니다. 기본적인 아이디어는 입력 데이터를 압축하여 낮은 차원의 표현으로 만든 뒤, 그 압축된 표현을 사용하여 다시 원래의 입력 데이터를 재구성하는 것입니다.

이해하기 쉽게 예시를 들어보면,

시험공부를 위해 교재를 요약본을 만든다라고 생각해봅시다.

교재(원본이미지) 를 요약해서 만든 아래 중간 사진이 압축된 낮은 차원인 것이고.

이를 가지고 누가 다시 원래의 교재로 복구한다라고 생각해보시면 편하실 것 같습니다.


오토인코더는 주로 두 부분으로 나뉩니다

1. 인코더 (Encoder): 입력 데이터를 받아 압축된 표현 (latent representation 또는 코드)으로 변환합니다.

2. 디코더 (Decoder): 압축된 표현을 받아 원본 데이터로 복구합니다.

작동 원리

1. 입력: 오토인코더는 원본 데이터(예: 이미지)를 입력으로 받습니다.

2. 인코딩: 인코더는 입력 데이터를 압축된 형태로 변환합니다.(by Model Architecture(cnn, dnn...))

3. 디코딩: 이 압축된 형태는 디코더를 통해 다시 원래의 형태로 변환됩니다.

4. 재구성 오차: 오토인코더의 목표는 원본 데이터와 디코딩된 데이터 사이의 차이 (재구성 오차)를 최소화하는 것입니다.


용도

  • 차원 축소: 데이터의 차원을 줄이는 데 사용될 수 있습니다.
  • 특성 추출: 데이터에서 중요한 특성을 학습하는 데 사용될 수 있습니다.
  • 노이즈 제거: 입력 데이터의 노이즈를 제거하는 데도 사용될 수 있습니다.
  • 이상치 탐지: 학습 데이터와 크게 다른 데이터를 탐지하는 데 사용될 수 있습니다.

예시

예를 들어, 오토인코더를 통해 숫자 이미지를 학습시켰다고 가정해봅시다. 학습이 잘 이루어진 오토인코더는, 숫자 이미지를 낮은 차원으로 압축하고, 그 압축된 표현을 사용해 원본 이미지와 매우 유사한 이미지를 재구성할 수 있어야 합니다.


결론

오토인코더는 비지도 학습 방법으로서, 데이터의 압축 및 재구성을 통해 중요한 특성을 추출하고, 이를 다양한 응용 분야에 활용할 수 있습니다.


ResNet 구조를 이용한 Autoencoder

class ResNetAutoencoder(nn.Module):
    def __init__(self):
        super(ResNetAutoencoder, self).__init__()

        # 사전 훈련된 ResNet-50 모델 로드
        resnet = models.resnet50(pretrained=True)

        # ResNet-50의 레이어들을 인코더로 사용
        self.encoder = nn.Sequential(*list(resnet.children())[:-2])

        # 디코더는 간단한 업샘플링 레이어로 구성
        self.decoder = nn.Sequential(
            nn.ConvTranspose2d(2048, 1024, kernel_size=3, stride=2, padding=1, output_padding=1),
            nn.ReLU(),
            nn.ConvTranspose2d(1024, 512, kernel_size=3, stride=2, padding=1, output_padding=1),
            nn.ReLU(),
            nn.ConvTranspose2d(512, 256, kernel_size=3, stride=2, padding=1, output_padding=1),
            nn.ReLU(),
            nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, output_padding=1),
            nn.ReLU(),
            nn.ConvTranspose2d(128, 64, kernel_size=3, stride=2, padding=1, output_padding=1),
            nn.ReLU(),
            nn.ConvTranspose2d(64, 3, kernel_size=3, stride=1, padding=1),
            nn.Sigmoid()
        )


    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)
        return x

1. init(self) 함수

모델의 초기화 함수.

2. resnet = models.resnet50(pretrained=True)

사전 훈련된(ImageNet 데이터로 훈련된) ResNet-50 모델을 로드

3. self.encoder = nn.Sequential(*list(resnet.children())[:-2]

ResNet-50의 layer들 중 마지막 2개를 제외한 나머지를 인코더로 사용. 마지막 두 레이어는 Global Average Pooling, Fully Connected 레이어.

마지막 레이어가 제거된 이유

AdaptiveAvgPool2d: 이 레이어는 전역 평균 풀링을 수행하여 특성 맵의 크기를 고정된 차원 (예: 1x1)으로 줄인다.

Linear: 이는 전체 연결 레이어로, 분류 작업을 위한 최종 출력 레이어이다.ResNet-50에서는 1000개의 클래스에 대한 확률 점수를 생성하는 데 사용된다.

오토인코더를 구축할 때, 인코더는 입력 이미지를 저차원 특성 공간으로 압축하고, 디코더는 이 저차원 표현을 다시 원래의 고차원 이미지로 복원한다. 따라서 인코더의 목적은 특성을 추출하는 것이며, 분류를 위한 정보 (예: 클래스 점수)는 필요하지 않다.

따라서 AdaptiveAvgPool2d와 Linear 레이어는 오토인코더의 인코더 부분에서 불필요하며, 포함될 경우 디코더의 작업을 더 어렵게 만들 수 있기 때문에 이 두 레이어는 제외된다.

(물론 사용해도 됩니다, 추출된 특징이 조금 사라질 수도 있다는 단점이 존재합니다)

파이썬에서 * asterisk의 기능

  1. 곱셈 및 거듭제곱 연산으로 사용할 때
  2. 리스트형 컨테이너 타입의 데이터를 반복 확장하고자 할 때
  3. 가변인자(variadic parameters)를 사용하고자 할 때
  4. 컨테이너 타입의 데이터를 Unpacking 할 때 → 위 코드에 해당

resnet.children()은 ResNet 모델의 모든 하위 모듈을 반복자로 반환. list(resnet.children())를 사용하여 이 반복자를 리스트로 변환하면 각 하위 모듈이 리스트의 요소로 들어간다.

예를 들어, list(resnet.children())가 [module1, module2, module3]와 같은 리스트를 반환한다고 가정하면, *list(resnet.children())는 module1, module2, module3로 분해된다.

nn.Sequential은 모듈을 순차적으로 실행하는 컨테이너.이 컨테이너는 개별 모듈이 아닌 모듈의 목록을 인수로 받는다. 따라서, * 연산자를 사용하여 모듈의 리스트를 nn.Sequential로 전달하기 전에 unpacking한다.

즉, nn.Sequential(*list(resnet.children())[:-2]) 코드는 ResNet 모델의 모든 하위 모듈을 가져와 마지막 두 모듈을 제외한 나머지 모듈로 새로운 순차 모델을 구성한다.

self.decoder

인코더를 통해 압축된 피처 맵을 원래 이미지 크기로 복구하는 역할.

여기서는 Convolution Transpose 레이어 (또는 Deconvolution 레이어)를 사용하여 공간 정보를 upsampling 한다. 각 레이어 다음에는 ReLU 활성화 함수가 있다.

최종 출력 레이어는 3개의 채널 (RGB)을 가진 이미지를 생성하며, 활성화 함수로 Sigmoid를 사용하여 출력 픽셀 값을 [0, 1] 범위로 제한한다.

Sigmoid 사용 이유

정규화된 이미지 값: 대부분의 이미지 데이터는 픽셀 값을 [0, 255] 범위로 가진다. 이러한 이미지를 신경망에 입력하기 전에 일반적으로 [0, 1] 범위로 정규화 한다. 따라서 디코더의 출력이 이 범위를 벗어나지 않도록 하는 것이 중요하며, Sigmoid 활성화 함수를 사용하면 이를 쉽게 달성할 수 있다.

수렴 및 학습의 안정성: Sigmoid를 사용하여 출력 값을 [0, 1] 범위로 제한하면, 손실 함수 (예: MSE)를 계산할 때 예측과 목표 간의 차이가 크게 벗어나는 것을 방지하여 학습 과정을 더 안정적으로 만들 수 있다.

활성화와 비활성화: Sigmoid 활성화 함수는 "켜짐"과 "꺼짐"의 개념을 모델링하는 데 적합한데, 값이 0.5에 가까울수록 "중립" 상태, 0에 가까울수록 "꺼짐" 상태, 그리고 1에 가까울수록 "켜짐" 상태로 해석할 수 있다. 이미지 복구 작업에서 이는 특정 픽셀이 얼마나 강하게 활성화되어야 하는지에 대한 정보를 제공할 수 있다.

마지막으로, 다른 활성화 함수 (예: Tanh)가 비슷한 목적으로 사용될 수 있지만, 이미지 데이터를 다룰 때 [0, 1] 범위가 가장 자연스럽고 직관적이므로 Sigmoid가 자주 사용된다고 한다.

forward(self, x)

모델의 순전파 함수이다.

입력 이미지 x는 먼저 인코더를 통과하게 되며, 이 과정에서 고차원의 이미지 데이터가 저차원의 피처 맵으로 압축된다.

압축된 피처 맵은 다음으로 디코더를 통과하게 되며, 이 과정에서 원래의 이미지 크기로 복구된다.


resnet.children() 구조

ResNet 아키텍처의 주요 레이어나 모듈을 순차적으로 반환한다. 이러한 각각의 레이어나 모듈 내에서 Residual 구조는 주로 "bottleneck" 또는 "basic block" 구조로 사용된다. Residual 구조의 핵심 아이디어는 입력과 출력 간의 차이를 학습하는 것이다.

ResNet의 Residual 구조를 설명하기 위해 두 가지 주요 컴포넌트에 초점을 맞춘다:

  1. Residual Block: 이것은 ResNet의 기본 구성 단위이다. 각 블록은 여러 컨볼루션 레이어로 구성되며, 블록의 시작 지점에서 입력을 가져와서 블록의 끝에 다시 추가하는 "shortcut" 연결이 있다. 이것이 바로 residual 연결이다.
  2. Identity Shortcut Connection: 이것은 Residual Block 내부의 "shortcut" 연결입니다. 입력 x는 블록의 컨볼루션 레이어들을 거치지 않고 직접 출력에 더해진다. 즉, 최종 출력은 F(x)+x가 된다. 여기서 F(x)는 블록의 컨볼루션 레이어들을 거친 결과이다. 이 구조는 블록이 F(x) (즉, 입력과 출력 간의 차이)를 학습하게 한다.

이러한 Residual 구조의 주요 장점은:

  • 반대 경사 문제의 완화: 깊은 신경망에서는 경사가 소실되거나 폭발하는 문제가 발생할 수 있다. residual(잔차) 연결은 이러한 문제를 완화시키며, 따라서 모델은 훨씬 깊은 아키텍처로 확장될 수 있다.
  • 효율적인 학습: residual(잔차) 연결로 인해 모델은 필요한 경우 항등 함수를 쉽게 학습할 수 있다. 이는 실제로 아무것도 변경되지 않는 레이어를 추가하는 것과 같다.

ResNet 아키텍처에는 여러 버전이 있으며, 각 버전은 다양한 깊이의 레이어와 다양한 수의 Residual Blocks를 사용한다. 예를 들어, ResNet-50, ResNet-101, ResNet-152 등이 있다. 여기서 숫자는 모델에 포함된 총 레이어 수를 나타낸다.


ResNet에 사용되는 BasicBlock & Bottleneck

import torch.nn as nn

# BasicBlock for smaller ResNets (e.g., ResNet18, ResNet34)
class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, in_planes, planes, stride=1):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)
        self.shortcut = nn.Sequential()
        if stride != 1 or in_planes != BasicBlock.expansion*planes:  # for BasicBlock
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_planes, self.expansion*planes, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(self.expansion*planes)
            )

    def forward(self, x):
        out = nn.ReLU()(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += self.shortcut(x)
        out = nn.ReLU()(out)
        return out

# Bottleneck for larger ResNets (e.g., ResNet50, ResNet101, ResNet152)
class Bottleneck(nn.Module):
    expansion = 4

    def __init__(self, in_planes, planes, stride=1):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)
        self.conv3 = nn.Conv2d(planes, self.expansion*planes, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(self.expansion*planes)
        self.shortcut = nn.Sequential()
        if stride != 1 or in_planes != Bottleneck.expansion*planes:  # for Bottleneck
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_planes, self.expansion*planes, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(self.expansion*planes)
            )

    def forward(self, x):
        out = nn.ReLU()(self.bn1(self.conv1(x)))
        out = nn.ReLU()(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        out += self.shortcut(x)
        out = nn.ReLU()(out)
        return out

ResNet의 BasicBlock과 Bottleneck은 ResNet의 핵심 컴포넌트입니다. 그러나 두 블록의 구조와 목적은 다릅니다. 여기 두 블록의 주요 차이점과 그 작동 원리에 대해 설명드리겠습니다.

1. 구조

BasicBlock:

  • 주로 작은 깊이의 ResNet 모델 (예: ResNet18, ResNet34)에서 사용됩니다.
  • 두 개의 3x3 컨볼루션 레이어로 구성됩니다.

Bottleneck:

  • 깊은 ResNet 모델 (예: ResNet50, ResNet101, ResNet152 등)에서 주로 사용됩니다.
  • 세 개의 레이어로 구성됩니다:
    • 1x1 컨볼루션 (차원을 줄이기 위해)
    • 3x3 컨볼루션
    • 1x1 컨볼루션 (차원을 다시 늘리기 위해)
  • 이 구조는 연산 효율성을 높이기 위해 도입되었습니다.

2. 작동 원리 및 목적

BasicBlock:

  • 간단한 구조로, 주요 목적은 입력과 출력 간의 잔차(residual)를 학습하는 것입니다.

Bottleneck:

  • Bottleneck 구조의 이름에서 알 수 있듯이, 중간의 3x3 컨볼루션 레이어는 "병목"으로 작용하여 피처의 강도를 집중시킵니다.
  • 처음의 1x1 컨볼루션은 차원을 줄여 연산의 복잡성을 감소시키며, 마지막 1x1 컨볼루션은 차원을 다시 늘려 원래의 차원으로 복원합니다.
  • 이러한 구조는 깊은 네트워크에서 연산 부하를 줄이면서도 피처의 복잡성을 유지하려는 목적으로 사용됩니다.

3. 요약

BasicBlock은 간단하고 직관적인 구조로 잔차를 학습합니다.

Bottleneck은 연산 효율성을 위해 설계되었으며, 깊은 네트워크에서 복잡한 피처를 효과적으로 처리하기 위해 사용됩니다.

두 블록 모두 잔차 연결(residual connection)을 통해 입력을 출력에 직접 더하며, 이는 네트워크의 깊이가 깊어져도 그래디언트의 전파를 도와 학습을 용이하게 합니다.


더 자세한 작동 원리 및 코드 심화 버젼은 추후 작성하도록 하겠습니다~

728x90
반응형