GDSC Sookmyung 활동/10 min Seminar

사람의 지도 없이 학습하는 오토인코더

문학개발자 2021. 2. 21. 01:33

'3분 딥러닝 파이토치맛' 을 참고하여 정리한 글입니다.

오토인코더란?

레이블 없이 특징을 추출하는 신경망을 말한다.

사람이 데이터를 직접 레이블링하여 정답을 함께 입력값에 넣어준다면 효율적인 학습이 가능하겠지만, 실생활에서 정답과 함께 존재하는 데이터셋은 많지 않다.

"정답이 있으면 오찻값을 구할 수 있다"

즉, 오토인코더는 x를 입력받아 x를 예측하고 신경망에 의미 있는 정보가 쌓일 수 있도록 설계된 신경망이다. 입력과 정답이 모두 입력 x인 신경망이라는 것!

단, 신경망은 범용근사자로서, 근사치를 출력하기 때문에 입력 x와 똑.같.은 x를 출력할 수는 없다. 정확히 말하자면 오토인코더는 입력된 x를 복원하는 신경망이다. 또 다른 특징은, 입력과 출력의 크기는 같지만 중간으로 갈수록 신경망의 차원이 줄어든다는 것이다. 이러한 구조로 인해 정보의 통로가 줄어들고 병목현상이 일어나 입력의 특징들이 '압축'되도록 학습된다.

이렇게 작은 차원이 고인 압축된 표현을 잠재 변수라 하고, 간단히 z라고 나타낸다. 인코더란 말 그대로 잠재 변수의 앞부분을, 디코더는 잠재변수의 뒷부분을 뜻한다. 인코더는 정보를 받아 압축하고, 디코더는 압축된 표현을 풀어 입력을 복원하는 역할을 한다. 오토인코더에서는 신경망이 받은 이미지를 복원하도록 학습하고 나면 잠재 변수에 이미지의 정보가 저장된다. 이 때 낮은 차원에 높은 밀도로 표현된 데이터를 압축된 데이터라고 하는데, 이 데이터는 의미의 압축이 일어난다. 여기서 의미의 압축이란, zip 압축을 뜻하는 것이 아니라 잠재 변수에 복잡한 데이터의 의미를 담는다는 점에서 특별하다고 할 수 있다. 

정보를 압축한다는 것은 결국 정보의 구성에 우선순위가 있다는 것을 의미한다. 따라서 같은 맥락으로 압축을 정보에서 덜 중요한 요소를 버리는 과정으로 정의할 수 있다. 오토인코더에서는 필연적으로 정보의 손실이 일어나는데, 이는 원본 데이터의 디테일을 잃어버린다는 뜻이기도 하지만, 중요한 정보만 남겨두는 데이터 가공이라고도 볼 수 있다. 이러한 특징들로 인해 오토인코더는 주로 복잡한 비선형 데이터의 차원을 줄이는 용도로 사용된다. 비정상 거래 검출, 데이터 시각화와 복원, 의미 추출, 이미지 검색 등 여러 가지 분야에서 응용되고 있다.

실습을 해보자!

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

        self.encoder = nn.Sequential(
            nn.Linear(28*28, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 12),
            nn.ReLU(),
            nn.Linear(12, 3),   # 입력의 특징을 3차원으로 압축합니다
        )
        self.decoder = nn.Sequential(
            nn.Linear(3, 12),
            nn.ReLU(),
            nn.Linear(12, 64),
            nn.ReLU(),
            nn.Linear(64, 128),
            nn.ReLU(),
            nn.Linear(128, 28*28),
            nn.Sigmoid(),       # 픽셀당 0과 1 사이로 값을 출력합니다
        )

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

먼저 오토인코더 모듈을 정의한다. 오토인코더에는 인코더와 디코더 두 부분이 있다. nn.Sequential은 여러 모듈을 하나의 모듈로 묶는 역할을 한다. 각 레이어를 데이터가 순차적으로 지나갈 때 사용하면 코드를 간결하게 만들 수 있다. 계층과 활성화 함수를 정의해주면 순서대로 값을 전달해 처리한다. 이 모듈을 사용하여 인코더와 디코더 두 모듈로 묶어준다. 인코더는 본류 모델과 형태가 비슷한데, 차원을 점차 줄여나가는 역할을 한다. 이 때 마지막 출력은 3차원에서 시각화할 수 있도록 특징을 3개만 남긴다. 이 출력값이 잠재 변수가 된다. 디코더는 이 3차원의 잠재 변수를 받아 다시 원래의 차원의 이미지로 복원한다. 인코더를 거꾸로 만든 것처럼 생겼으며, 출력 차원이 점점 증가하는 것을 볼 수 있다. 전체적인 오토인코더의 흐름은 먼저 인코더는 잠재 변수인 encoded를 만들고 디코더는 복원 이미지인 decoded를 만든다. 두 변수를 모두 출력하는 함수 forward를 만들었다.

autoencoder = Autoencoder().to(DEVICE)
optimizer = torch.optim.Adam(autoencoder.parameters(), lr=0.005)
criterion = nn.MSELoss()

view_data = trainset.data[:5].view(-1, 28*28)
view_data = view_data.type(torch.FloatTensor)/255.

def train(autoencoder, train_loader):
    autoencoder.train()
    for step, (x, label) in enumerate(train_loader):
        x = x.view(-1, 28*28).to(DEVICE)
        y = x.view(-1, 28*28).to(DEVICE)
        label = label.to(DEVICE)

        encoded, decoded = autoencoder(x)

        loss = criterion(decoded, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

adam함수를 사용하여 최적화를 수행한다. adam은 SGD의 변형 함수이며 학습 중인 기울기를 참고하여 학습 속도를 자동으로 변화시킨다. 디코더의 결과와 원본의 차이를 계산하기 위해 평균제곱오차 함수를 사용했다. 이 함수는 두 개의 같은 크기의 행렬을 받아 각 자리의 차이에 제곱해서 평균을 구해주는 객체를 생성한다. 학습 데이터셋에 있는 5개의 이미지를 가져와 한 epoch이 완료될 때마다 복원이 어떻게 되는지 확인할 것입니다. train함수에서 특징적인 것은 입력인 x와 대상 레이블인 y 모두 원본 이미지인 x라는 것이다.

for epoch in range(1, EPOCH+1):
    train(autoencoder, train_loader)

    # 디코더에서 나온 이미지를 시각화 하기 (두번째 열)
    test_x = view_data.to(DEVICE)
    _, decoded_data = autoencoder(test_x)

    # 원본과 디코딩 결과 비교해보기
    f, a = plt.subplots(2, 5, figsize=(5, 2))
    print("[Epoch {}]".format(epoch))
    for i in range(5):
        img = np.reshape(view_data.data.numpy()[i],(28, 28))
        a[0][i].imshow(img, cmap='gray')
        a[0][i].set_xticks(()); a[0][i].set_yticks(())

    for i in range(5):
        img = np.reshape(decoded_data.to("cpu").data.numpy()[i], (28, 28))
        a[1][i].imshow(img, cmap='gray')
        a[1][i].set_xticks(()); a[1][i].set_yticks(())
    plt.show()

epoch이 증가할 때마다 그 과정을 시각화하여 볼 수 있다. 맷플롯립은 파이토치 텐서를 지원하지 않으므로 넘파이 행렬로 변환해주어야 한다. imshow함수에 numpy함수를 불러 넘파이 행렬로 변환된 이미지를 넣고 회색을 사용하도록 cmap값을 회색으로 지정한다.

첫 번째 줄은 원본 이미지, 두 번째 줄은 오토인코더가 잠재 변수로부터 복원한 이미지이다. 이폭마다 수행된 결과를 확인할 수 있다.

실습코드는 여기서 확인할 수 있다.