티스토리 뷰
목차
지난 글에서는 전이 학습을 구현하고, RNN의 발전에 대해 가볍게 정리했다.
[PyTorch]전이 학습(Transfer Learning)
[Pytorch]Vanilla RNN과 확장된 기법들: LSTM, GRU, Bidirectional LSTM, Transformer
이번 글에서는 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 cv2
import matplotlib.pyplot as plt
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(f"{device} is available.")
dataset = torchvision.datasets.MNIST(
"./data/", download=True, train=True, transform=transforms.ToTensor()
)
trainloader = torch.utils.data.DataLoader(dataset, batch_size=50, shuffle=True)
class Flatten(nn.Module):
def forward(self, x):
batch_size = x.shape[0]
return x.view(batch_size, -1)
class Unflatten(nn.Module):
def __init__(self, k):
super(Unflatten, 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)
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.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(),
)
self.decoder = nn.Sequential(
nn.Linear(10, 1024),
nn.ReLU(),
Unflatten(4 * k),
nn.ConvTranspose2d(4 * k, 2 * k, 3, stride=1),
nn.ReLU(),
nn.ConvTranspose2d(2 * k, k, 3, stride=2),
nn.ReLU(),
nn.ConvTranspose2d(k, 1, 3, stride=2, output_padding=1),
)
def forward(self, x):
encoded = self.encoder(x)
decoded = self.decoder(encoded)
return decoded
model = Autoencoder().to(device)
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
columns = 10
rows = 5
fig = plt.figure(figsize=(8, 4))
for i in range(1, columns * rows + 1):
img = input_samples[i - 1]
fig.add_subplot(rows, columns, i)
plt.imshow(img.squeeze())
plt.axis("off")
plt.show()
fig = plt.figure(figsize=(8, 4))
for i in range(1, columns * rows + 1):
img = reconstructed_samples[i - 1]
fig.add_subplot(rows, columns, i)
plt.imshow(img.squeeze())
plt.axis("off")
plt.show()
criterion = nn.MSELoss()
optimizer = optim.Adam(model.parameters(), lr=1e-4)
for epoch in range(21):
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))
check_plot()
결과는 다음과 같으며,
[1] loss: 0.051
[11] loss: 0.025
[21] loss: 0.023
핵심 워크플로우는 다음과 같다.
- 데이터 준비
- MNIST 데이터셋 로드 및 정규화 (transforms.ToTensor()).
- DataLoader로 배치 단위로 나누어 학습 준비.
- 오토인코더 모델 구성
- 인코더: Conv2D와 Linear Layer로 입력 이미지를 10차원 잠재 공간으로 압축.
- 디코더: ConvTranspose2D와 Linear Layer로 잠재 공간을 원본 이미지로 복원.
- 학습 설정
- 손실 함수: 입력과 복원 이미지의 MSE.
- 옵티마이저: Adam 사용 (학습률 0.0001).
- 학습 루프
- 입력 → 인코더 → 잠재 공간(Latent Space)→ 디코더 → 복원 이미지.
- 손실 계산 → 역전파로 가중치 업데이트.
- 에포크마다 평균 손실 출력 및 결과 시각화.
- 결과 확인
- 원본 이미지와 복원 이미지를 비교하여 모델 성능 확인.
이번엔 오토인코더에 대해 먼저 간단히 정리하고 코드를 뜯어보자.
오토인코더(Autoencoder)
오토 인코더에 대한 자세한 설명은 아래 글에 있으므로,
여기서는 가볍게 정리만 한다.
오토인코더는 입력 데이터를 압축(인코딩)하고 다시 복원(디코딩)하는 과정을 통해
중요한 특징을 학습하는 비지도 학습 알고리즘이다.
주요 구성 요소는 인코더(encoder)와 디코더(decoder)이다.
- 인코더(Encoder): 입력 데이터를 점차 작은 차원의 표현(잠재 공간, Latent Space)으로 압축한다.
- 디코더(Decoder): 잠재 공간에서 데이터를 원래 차원으로 복원한다.
이 과정에서 모델은 일종의 압축된 지식을 얻고, 이를 확장해 원본 데이터와 유사한 X'를 출력하는데
이러한 점에서 오토인코더는 복잡한 데이터 내의 숨은 구조를 발견하고 요약하여 표현하는 강력한 도구라 할 수 있다.
오토인코더의 중요성
- 차원 축소: 고차원 데이터를 효율적으로 압축하여 데이터 시각화나 저장에 활용한다.
- 잡음 제거(Denoising): 잡음이 섞인 데이터를 학습하여 원래의 깨끗한 데이터를 복원할 수 있다.
- 사전 학습(Pre-training): 다른 딥러닝 모델에 입력 데이터를 효과적으로 제공할 수 있는 특성을 학습한다.
- 생성 모델: 변형 오토인코더(VAE) 등 확장된 오토인코더는 새로운 데이터를 생성하는 데도 사용된다.
이 글에서 사용된 오토 인코더
- 우리 코드에서는 컨볼루셔널 오토인코더(Convolutional Autoencoder)를 사용한다.
- 합성곱 계층을 이용해 공간적 계층 구조를 효과적으로 학습한다. 이미지와 같은 격자 구조 데이터에 적합하다.
- 입력 데이터는 MNIST 데이터셋의 손글씨 숫자 이미지이며, 2D 이미지 데이터를 다룰 수 있는 컨볼루션 레이어로 구성되어 있다.
- 잠재 공간은 10차원으로 설정되어 있어, 데이터의 중요한 특성을 압축된 형태로 학습한다.
이제 코드를 뜯어보자.
필수 라이브러리 및 데이터 설정
import torch # PyTorch 텐서 연산 및 신경망 구현
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 # Numpy 배열 기반 수학 연산
import cv2 # OpenCV: 이미지 처리 및 변환
import matplotlib.pyplot as plt # 데이터 시각화 도구
Pytorch
torch: PyTorch의 핵심 라이브러리이다. 주요 기능은 다음과 같다:
- 텐서 연산: Numpy와 유사하지만, GPU 가속을 활용할 수 있는 텐서를 제공한다.
a = torch.tensor([1, 2, 3]) # 1차원 텐서 생성
b = torch.tensor([[1, 2], [3, 4]]) # 2차원 텐서 생성
- 모델 정의: 신경망 계층을 쉽게 정의하고 학습할 수 있다.
- 자동 미분: 학습 과정에서 역전파를 자동으로 계산한다.
TorchVision
- torchvision: PyTorch와 함께 사용하는 컴퓨터 비전 라이브러리이다.
- 데이터셋 제공: 여러 인기 데이터셋(CIFAR, MNIST 등)을 쉽게 가져올 수 있다.
- 모델 제공: 사전 학습된 모델(ResNet, VGG 등)을 바로 사용할 수 있다.
- 데이터 변환: 이미지 전처리를 위한 변환(transform) 도구를 제공한다.
transforms
torchvision.transforms: 데이터 전처리를 위한 유용한 함수들을 제공한다.
- ToTensor(): 이미지를 [0, 255] 범위의 픽셀 값을 가지는 배열에서 [0, 1] 범위로 정규화된 PyTorch 텐서로 변환한다.
from PIL import Image
from torchvision import transforms
transform = transforms.ToTensor()
img = Image.open("image.jpg") # PIL 이미지 로드
tensor_img = transform(img) # 텐서로 변환
- Normalize(): 이미지를 평균과 표준편차로 정규화한다.
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize(mean=[0.5], std=[0.5]) # 평균 0.5, 표준편차 0.5로 정규화
])
PyTorch의 모듈
- torch.nn: 신경망의 계층을 정의하는 데 사용한다.
- nn.Module: PyTorch 모델의 기본 클래스로, 모든 신경망 모델은 이를 상속받아 정의한다.
- nn.Conv2d: 2D 컨볼루션 연산을 수행하는 계층이다.
- nn.ReLU: 활성화 함수로 사용되는 ReLU(Rectified Linear Unit)이다.
- nn.Linear: 완전 연결 계층(fully connected layer)이다.
fc = nn.Linear(128, 10) # 입력 128차원, 출력 10차원
- torch.nn.functional: 함수 기반으로 다양한 연산을 수행한다.
- 모델 정의 시 nn 모듈보다 함수 형태로 사용할 때 더 간결하게 작성할 수 있다
import torch.nn.functional as F
F.relu(x) # 활성화 함수
- torch.optim: 다양한 최적화 알고리즘(예: SGD, Adam 등)을 제공한다.
- Adam: 학습 속도가 빠르고 적응형 학습률을 사용하는 최적화 알고리즘이다.
optimizer = optim.Adam(model.parameters(), lr=1e-4) # Adam 최적화
NumPy
- numpy: 배열 기반의 연산을 효율적으로 처리하는 라이브러리이다.
- PyTorch는 Numpy 배열과 PyTorch 텐서 간의 변환을 지원한다.
arr = np.array([1, 2, 3])
tensor = torch.from_numpy(arr) # Numpy 배열 -> PyTorch 텐서
array = tensor.numpy() # PyTorch 텐서 -> Numpy 배열
OpenCV (cv2)
- cv2: OpenCV는 이미지 처리에 유용한 라이브러리이다.
- 이미지를 읽고 처리하며 시각화하는 데 사용한다.
import cv2
img = cv2.imread("image.jpg") # 이미지 읽기
img_resized = cv2.resize(img, (128, 128)) # 크기 조정
Matplotlib
- matplotlib.pyplot: 데이터를 시각화하는 데 사용한다.
- 이미지, 그래프 등을 화면에 출력한다.
import matplotlib.pyplot as plt
plt.imshow(img) # 이미지 출력
plt.show()
디바이스 설정
# 디바이스 설정 코드
# PyTorch는 GPU를 지원하며, GPU가 있으면 학습 속도를 대폭 향상시킬 수 있다.
# 아래 코드는 GPU가 사용 가능한지 확인한 후, 가능하면 GPU 디바이스(cuda:0)를 설정하고
# GPU를 사용할 수 없는 환경에서는 CPU를 디바이스로 설정한다.
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") # 디바이스 설정
print(f"{device} is available.") # 선택된 디바이스 출력
MNIST 데이터셋 로드
# MNIST 데이터셋을 다운로드하고 데이터로더를 생성하는 부분이다.
dataset = torchvision.datasets.MNIST(
"./data/", # 데이터를 저장할 경로를 지정한다.
download=True, # MNIST 데이터셋이 경로에 없으면 다운로드한다.
train=True, # 학습용(train) 데이터셋을 사용한다.
transform=transforms.ToTensor() # 이미지를 텐서 형태로 변환하고 [0, 1] 범위로 정규화한다.
)
# DataLoader는 데이터를 효율적으로 관리하기 위해 사용된다.
trainloader = torch.utils.data.DataLoader(
dataset, # 데이터셋을 로드한다.
batch_size=50, # 한 번에 50개의 데이터를 배치로 묶어 학습에 전달한다.
shuffle=True # 데이터를 매번 랜덤하게 섞어 학습 시 과적합을 방지한다.
)
torchvision.datasets.MNIST
- PyTorch의 torchvision 라이브러리에서 제공하는 데이터셋 클래스를 사용해 MNIST 데이터셋을 불러온다.
- MNIST 데이터셋은 손글씨 숫자 이미지로 구성된 대표적인 이미지 분류용 데이터셋이다.
- 데이터 크기: 총 60,000개의 학습 데이터와 10,000개의 테스트 데이터.
- 이미지 크기: 28x28 픽셀, 흑백(단일 채널).
- 레이블: 0부터 9까지의 정수 레이블.
./data/
- 데이터를 저장할 경로이다. 만약 해당 경로에 데이터가 없다면 download=True 옵션에 따라 데이터를 다운로드한다.
train=True
- MNIST는 학습용(train) 데이터셋과 테스트(test) 데이터셋으로 나뉜다.
- 여기서는 학습용 데이터셋만 사용한다.
transform=transforms.ToTensor()
- 데이터를 텐서(tensor) 형태로 변환한다.
- 이미지 데이터는 기본적으로 [높이, 너비, 채널] 형식이다. ToTensor()는 이를 [채널, 높이, 너비] 형식으로 바꾼다.
- 이 과정에서 픽셀 값을 [0, 1] 범위로 정규화한다. (기본적으로 픽셀 값은 [0, 255] 범위의 정수다.)
torch.utils.data.DataLoader
- 학습 과정에서 데이터를 효율적으로 제공하기 위한 도구다.
- DataLoader는 데이터를 배치 단위로 묶고, 필요할 때만 메모리에 불러오는 방식을 사용한다.
- batch_size=50: 한 번에 50개의 데이터를 묶어 하나의 배치로 전달한다.
- shuffle=True: 데이터를 랜덤 하게 섞어 학습에 사용한다. 이렇게 하면 특정 순서에 따른 학습 편향이 방지된다.
실행 흐름 요약
- MNIST 데이터셋을 지정된 경로에 다운로드한다.
- 데이터를 텐서로 변환해 [0, 1] 범위로 정규화한다.
- 데이터셋을 DataLoader를 통해 배치 단위로 관리하고 학습에서 랜덤하게 섞어 사용한다.
Flatten 클래스 정의
class Flatten(nn.Module):
def forward(self, x):
# x는 4차원 텐서로 입력됨: (배치 크기, 채널, 높이, 너비)
batch_size = x.shape[0] # 배치 크기를 가져옴
# view() 함수를 사용하여 2차원 텐서로 변환함
# 두 번째 차원은 높이, 너비, 채널을 모두 곱해 벡터 형태로 만듦
return x.view(batch_size, -1) # (배치 크기, 채널*높이*너비)
Flatten 클래스는 딥러닝 모델에서 4차원 텐서(이미지 데이터)를 2차원 텐서로 변환하는 역할을 한다.
이는 Conv2D 레이어와 Fully Connected 레이어(Fully Connected Layer)를 연결할 때 자주 사용된다.
입력 데이터의 구조
- 일반적으로 4차원 텐서 형태로 입력된다: (배치 크기, 채널, 높이, 너비)
- 배치 크기: 한 번에 학습 또는 추론에 사용하는 데이터 샘플의 수.
- 채널: 이미지의 색상 채널 (예: MNIST는 흑백 이미지이므로 1, RGB 이미지는 3).
- 높이와 너비: 이미지의 픽셀 크기.
변환의 목적
Conv2D 레이어는 입력 데이터를 여러 필터로 변환하여 특징 맵(feature map)을 생성한다.
그러나 Fully Connected 레이어는 입력 데이터를 벡터 형태(2D 텐서)로 받는다.
따라서, Conv2D에서 나온 4D 텐서를 벡터 형태로 변환해야 한다.
view() 함수
- 텐서의 차원을 재배열한다.
- batch_size는 그대로 유지하고, 나머지 차원(채널, 높이, 너비)은 곱해서 벡터로 만든다.
- 예: (50, 16, 7, 7) → (50, 16 * 7 * 7) → (50, 784)
Flatten의 동작 예시
- 입력 텐서:
- 배치 크기: 2
- 채널: 3
- 높이: 4
- 너비: 4
- 입력 텐서의 크기: (2, 3, 4, 4)
- batch_size는 2로 유지된다.
- 채널 * 높이 * 너비는 3 * 4 * 4 = 48로 계산된다.
- 최종적으로 (2, 48)로 변환된다.
왜 중요한가?
- Conv 레이어는 이미지의 지역적 특징을 추출하고,
Fully Connected 레이어는 이러한 특징을 결합해 최종 예측값(분류, 회귀 등)을 생성한다. - 두 레이어를 연결하려면 Flatten으로 데이터 형태를 맞춰야 한다.
즉, Flatten은 딥러닝 모델의 데이터 흐름에서 매우 중요한 연결 단계이다.
Unflatten 클래스 정의
class Unflatten(nn.Module):
"""
2D 텐서 (배치 크기, 벡터 크기)를 4D 텐서 (배치 크기, 채널, 높이, 너비)로 복원하는 클래스이다.
디코더에서 Fully Connected Layer의 출력을 다시 이미지 형태로 변환할 때 사용한다.
"""
def __init__(self, k):
"""
Unflatten 클래스의 생성자이다.
Args:
k (int): 복원할 4D 텐서의 채널 개수이다.
"""
super(Unflatten, self).__init__()
self.k = k # 출력 텐서의 채널 개수를 설정한다.
def forward(self, x):
"""
2D 텐서를 4D 텐서로 변환한다.
Args:
x (torch.Tensor): 입력 텐서로, 2D 형태 (배치 크기, 벡터 크기)이어야 한다.
Returns:
torch.Tensor: 4D 형태로 변환된 텐서이다.
"""
s = x.size() # 입력 텐서의 크기를 가져온다. 예: (배치 크기, 벡터 크기)
feature_size = int((s[1] // self.k) ** 0.5) # 높이와 너비를 계산한다.
# 벡터 크기 = 채널 개수 * (높이 * 너비)
# 높이와 너비는 벡터 크기를 채널 개수로 나눈 후 제곱근을 취해 구한다.
return x.view(s[0], self.k, feature_size, feature_size)
# (배치 크기, 채널 개수, 높이, 너비) 형태로 변환하여 반환한다.
Unflatten 클래스의 목적
- 입력 형태: 디코더의 Fully Connected Layer 출력은 2D 텐서로 나온다. 이 텐서는 (배치 크기, 벡터 크기) 형태이다.
- 변환 목표: 이 2D 텐서를 다시 이미지와 같은 4D 형태 (배치 크기, 채널, 높이, 너비)로 복원해야 한다.
- 채널 수(k)는 생성자에서 설정되며, 높이와 너비는 벡터 크기에서 계산한다.
Unflatten의 주요 연산
- 입력 크기 확인: x.size()를 사용해 입력 텐서의 크기를 확인한다.
- 예: (배치 크기, 벡터 크기) 형태이다.
- 높이와 너비 계산: 벡터 크기에서 채널 수(k)를 나눈 후 제곱근을 취한다.
- 이는 이미지의 높이와 너비가 동일하다고 가정한 연산이다.
- 예: feature_size = sqrt(벡터 크기 / 채널 수).
- 뷰 변환: .view 메서드로 텐서를 (배치 크기, 채널, 높이, 너비) 형태로 변환한다.
예제
다음은 입력과 출력의 구조를 예로 든 것이다.
- 입력 텐서
- 크기: (50, 1024)
- 배치 크기: 50
- 벡터 크기: 1024
- Unflatten 설정
- 채널 수(k): 16
- 변환 과정
- feature_size = sqrt(1024 / 16) = 8
- 출력 텐서의 크기: (50, 16, 8, 8) (배치 크기, 채널, 높이, 너비)
이 과정을 통해 Fully Connected Layer의 출력을 다시 이미지 형태로 복원할 수 있다.
Autoencoder 클래스 정의
class Autoencoder(nn.Module):
def __init__(self):
super(Autoencoder, self).__init__()
k = 16 # 첫 번째 레이어의 채널 수를 설정한다.
# 인코더: 입력 이미지를 점차 압축하여 잠재 공간 표현으로 변환한다.
self.encoder = nn.Sequential(
nn.Conv2d(1, k, 3, stride=2), # 입력 채널: 1 (MNIST는 흑백 이미지), 출력 채널: k, 필터 크기: 3x3, stride: 2
nn.ReLU(), # 활성화 함수로 ReLU를 사용하여 비선형성을 추가한다.
nn.Conv2d(k, 2 * k, 3, stride=2), # 채널 수를 2배로 늘리며 더 작은 특성 맵을 만든다.
nn.ReLU(),
nn.Conv2d(2 * k, 4 * k, 3, stride=1), # 채널 수를 4배로 늘리며 압축한다.
nn.ReLU(),
Flatten(), # 4D 텐서를 2D 텐서로 변환한다 (배치 크기, 전체 벡터 크기).
nn.Linear(1024, 10), # Fully Connected Layer로 압축하여 잠재 공간 크기를 10으로 설정한다.
nn.ReLU(), # 비선형성을 추가한다.
)
# 디코더: 잠재 공간 표현을 원래 이미지 크기로 복원한다.
self.decoder = nn.Sequential(
nn.Linear(10, 1024), # 잠재 공간 크기 10을 Fully Connected Layer로 확장한다.
nn.ReLU(),
Unflatten(4 * k), # 2D 텐서를 4D 텐서로 변환한다 (배치 크기, 채널, 높이, 너비).
nn.ConvTranspose2d(4 * k, 2 * k, 3, stride=1), # ConvTranspose2D로 채널 수를 줄이고 이미지를 확장한다.
nn.ReLU(),
nn.ConvTranspose2d(2 * k, k, 3, stride=2), # stride=2로 특성 맵의 크기를 2배로 키운다.
nn.ReLU(),
nn.ConvTranspose2d(k, 1, 3, stride=2, output_padding=1), # 최종적으로 입력 이미지 크기로 복원한다.
)
def forward(self, x):
encoded = self.encoder(x) # 인코더를 통해 입력 이미지를 잠재 공간으로 변환한다.
decoded = self.decoder(encoded) # 디코더를 통해 잠재 공간을 입력 이미지 크기로 복원한다.
return decoded # 복원된 이미지를 반환한다.
인코더(Encoder): 데이터 압축
- 구조:
- 3개의 Conv2D 레이어와 1개의 Linear 레이어로 구성된다.
- Conv2D는 이미지 데이터를 처리하기에 적합하며, 각 레이어에서 채널 수를 늘리고 이미지 크기를 줄인다.
- 마지막 단계에서는 Flatten과 Linear 레이어를 사용하여 잠재 공간 표현으로 변환한다.
- Conv2D 레이어의 역할:
- 입력 이미지의 공간적 특징(패턴)을 학습한다.
- 채널 수를 증가시키며 더 많은 정보를 추출한다.
- stride를 사용하여 이미지 크기를 줄인다.
- Flatten과 Linear:
- Conv2D의 출력을 1차원 벡터로 변환한 뒤 Fully Connected Layer로 잠재 공간의 크기를 10으로 압축한다.
- 여기서 잠재 공간은 데이터의 핵심 정보를 저장하며, 모델의 학습 목표는 이 공간을 통해 원본 데이터를 복원하는 것이다.
디코더(Decoder): 데이터 복원
- 구조:
- Linear 레이어와 3개의 ConvTranspose2D 레이어로 구성된다.
- ConvTranspose2D는 Conv2D의 반대 연산을 수행하여 이미지 크기를 점차 확장한다.
- ConvTranspose2D 레이어의 역할:
- 잠재 공간 데이터를 원래 이미지 크기로 복원한다.
- 각 단계에서 채널 수를 감소시키고, stride를 사용해 크기를 확장한다.
- 마지막 레이어에서 입력 이미지와 동일한 크기(28x28, 채널 1)를 만든다.
- Unflatten:
- Linear 레이어의 출력을 다시 이미지 형태(4D 텐서)로 변환한다.
Forward 메서드
- encoded = self.encoder(x): 입력 데이터를 인코더를 통해 잠재 공간으로 압축한다.
- decoded = self.decoder(encoded): 잠재 공간 데이터를 디코더를 통해 원래 이미지 크기로 복원한다.
- return decoded: 복원된 이미지를 반환한다.
추가적으로 이해해야 할 개념
잠재 공간(Latent Space)
- 원본 데이터에서 핵심적인 정보만 추출한 압축된 표현이다.
- 이 코드는 잠재 공간 크기를 10으로 설정하여 28x28 이미지를 10개의 숫자로 표현한다.
ConvTranspose2D의 역할
- Conv2D의 역연산이다. 특성 맵 크기를 확장하고 채널 수를 줄여 원래 이미지 형태로 복원한다.
- stride와 output_padding을 조합하여 원하는 크기를 정확히 만든다.
활성화 함수
- 각 레이어의 출력에 비선형성을 추가하며, 데이터 표현력을 높인다.
- ReLU(Rectified Linear Unit)를 사용해 양수는 그대로, 음수는 0으로 만든다.
잠재 공간의 크기는 클수록 좋은가?
잠재 공간의 크기는 작을수록 효율적이지만, 너무 작으면 손실이 발생한다. 적절한 크기를 선택하는 것이 중요하다.
- 잠재 공간이 클 경우:
- 데이터의 세부 정보를 많이 보존할 수 있다.
- 복원된 이미지가 더 원본에 가까울 수 있다.
- 하지만 압축 효율이 떨어지고 모델의 일반화 성능이 낮아질 위험이 있다.
- 잠재 공간이 작을 경우:
- 데이터의 핵심 정보만 압축하여 저장한다.
- 복원된 이미지가 단순해질 수 있다.
- 너무 작으면 중요한 정보가 손실되어 복원이 어려워질 수 있다.
결론적으로 잠재 공간 크기는 모델의 목적과 데이터 복잡성에 따라 조정해야 한다.
예를 들어, 우리 코드는 MNIST처럼 비교적 단순한 데이터셋에 대해 잠재 공간 크기를 10으로 설정하여
효율적인 압축과 복원을 균형 있게 달성한다.
학습 준비
# 손실 함수와 옵티마이저 설정
# 손실 함수로 MSELoss를 사용한다. 이는 입력 이미지와 복원된 이미지 간의 각 픽셀 차이 제곱의 평균을 계산한다.
# MSE는 값이 작을수록 모델이 입력 이미지를 더 잘 복원한다는 것을 의미한다.
criterion = nn.MSELoss()
# 옵티마이저로 Adam을 사용한다. Adam은 학습률을 자동으로 조정하며, 가중치 업데이트를 효율적이고 안정적으로 수행한다.
# lr=1e-4는 학습률을 0.0001로 설정하여 모델이 천천히 안정적으로 학습하도록 한다.
optimizer = optim.Adam(model.parameters(), lr=1e-4)
손실 함수 (criterion = nn.MSELoss())
- MSELoss는 Mean Squared Error를 의미하며, 두 값 간의 차이를 제곱하고 평균을 계산한다.
- 식으로 표현하면 다음과 같다:
여기서 $y_i$는 원본 이미지, $\hat{y_i}$는 복원된 이미지다. - MSE를 사용하는 이유는 픽셀 단위로 원본 이미지와 복원된 이미지의 차이를 수치화하여 모델의 성능을 평가하기 때문이다.
- MSE 값이 작아질수록 복원된 이미지가 원본 이미지와 유사하다는 것을 의미한다.
옵티마이저 (optimizer = optim.Adam(...))
- Adam 옵티마이저는 "Adaptive Moment Estimation"의 약자이다.
이는 SGD(확률적 경사 하강법)를 개선한 알고리즘으로, 학습률을 자동으로 조정한다. - Adam의 주요 특징:
- 1차(momentum)와 2차(학습률 스케일링) 변화량을 활용하여 경사를 계산한다.
- 학습 속도가 빠르면서도 안정적으로 수렴한다.
- 학습률(lr=1e-4):
- 학습률은 모델이 얼마나 빠르게 업데이트를 진행할지를 결정한다.
- 작은 학습률(0.0001)을 설정하면 모델이 점진적으로 학습해 더 안정적이지만 느리게 수렴한다.
역할
- MSELoss는 모델의 출력과 입력 이미지 간의 손실을 계산해, 복원 품질을 평가한다.
- Adam 옵티마이저는 계산된 손실을 기반으로 모델의 파라미터를 업데이트해 성능을 향상한다.
학습 루프
for epoch in range(21): # 총 21번의 에포크 동안 학습을 진행한다.
running_loss = 0.0 # 에포크 동안의 누적 손실 값을 초기화한다.
for data in trainloader: # 미니배치 단위로 데이터를 반복한다.
inputs = data[0].to(device) # 배치의 입력 데이터를 GPU 또는 CPU로 보낸다.
optimizer.zero_grad() # 이전 배치에서 계산된 기울기를 초기화한다.
outputs = model(inputs) # 모델에 입력 데이터를 전달해 복원된 출력 이미지를 생성한다.
loss = criterion(inputs, outputs) # 원본 입력과 복원된 출력의 차이를 계산한다(MSE 손실).
loss.backward() # 역전파를 통해 각 파라미터에 대한 기울기를 계산한다.
optimizer.step() # 옵티마이저를 사용해 모델의 가중치를 업데이트한다.
running_loss += loss.item() # 배치의 손실 값을 누적해 에포크 전체 손실을 계산한다.
# 에포크가 끝난 후, 평균 손실 값을 계산한다.
cost = running_loss / len(trainloader)
# 10 에포크마다 결과를 출력하고 시각화를 진행한다.
if epoch % 10 == 0:
print("[%d] loss: %.3f" % (epoch + 1, cost)) # 현재 에포크와 평균 손실 값을 출력한다.
check_plot() # 원본 이미지와 복원된 이미지를 시각화한다.
for epoch in range(21)
- 학습은 총 21번의 반복(에포크)으로 진행된다.
- 에포크란, 데이터셋 전체를 한 번 학습하는 과정을 의미한다.
running_loss = 0.0
- 에포크 동안의 손실 값을 누적하기 위한 변수를 초기화한다.
- 매 에포크 시작 시, 이전 에포크의 손실 값은 초기화된다.
for data in trainloader
- 데이터셋을 미니배치 단위로 가져온다.
- trainloader는 DataLoader 객체로, 배치 크기만큼 데이터를 제공한다.
inputs = data[0].to(device)
- 배치 데이터 중 이미지 데이터를 가져온다. MNIST는 라벨도 포함하지만, 이 코드에서는 이미지 데이터만 사용한다.
- 데이터를 device (GPU 또는 CPU)에 할당한다.
optimizer.zero_grad()
- 이전 배치에서 계산된 기울기 값을 초기화한다.
- PyTorch는 기본적으로 기울기를 누적하는 방식이므로, 초기화를 하지 않으면 이전 배치의 기울기가 남아 있다.
outputs = model(inputs)
- 모델에 입력 데이터를 전달한다.
- 인코더가 데이터를 압축하고, 디코더가 복원한 결과를 반환한다.
loss = criterion(inputs, outputs)
- 손실 함수(MSE)를 사용해 원본 입력과 복원된 출력 간의 차이를 계산한다.
- 손실 값이 작을수록 복원이 더 정확하게 이루어진 것이다.
loss.backward()
- 역전파를 통해 손실 값에 따라 모델의 각 파라미터에 대한 기울기를 계산한다.
- 이 과정은 자동 미분(autograd)을 사용해 수행된다.
optimizer.step()
- 옵티마이저가 계산된 기울기를 사용해 모델의 가중치를 업데이트한다.
- 이 과정은 학습의 핵심 단계로, 모델이 점점 더 나은 출력을 생성하도록 만든다.
running_loss += loss.item()
- 현재 배치의 손실 값을 running_loss에 더한다.
- .item()은 텐서를 파이썬 숫자로 변환하는 함수이다.
cost = running_loss / len(trainloader)
- 에포크 전체 손실 값의 평균을 계산한다.
- 평균 손실 값은 학습이 잘 진행되고 있는지 확인하는 지표로 사용된다.
if epoch % 10 == 0
- 매 10 에포크마다 결과를 출력하고 이미지를 시각화한다.
- 이 조건문은 학습 상태를 확인하기 위한 체크포인트 역할을 한다.
check_plot()
- check_plot 함수는 원본 이미지와 복원된 이미지를 시각화해 학습 상태를 시각적으로 보여준다.
시각화
def check_plot():
with torch.no_grad(): # 학습 중이 아닌 평가 모드로 설정하여 가중치 업데이트를 막는다.
for data in trainloader:
inputs = data[0].to(device) # 입력 데이터를 GPU나 CPU로 전송한다.
outputs = model(inputs) # 오토인코더를 통해 데이터를 복원한다.
# 입력 데이터와 복원된 데이터의 차원을 조정하여 NumPy 형식으로 변환한다.
# permute(0, 2, 3, 1): PyTorch의 텐서 형식(N, C, H, W)을 NumPy 형식(N, H, W, C)으로 변환한다.
# .cpu().numpy(): 데이터를 CPU로 옮기고 NumPy 배열로 변환한다.
input_samples = inputs.permute(0, 2, 3, 1).cpu().numpy()
reconstructed_samples = outputs.permute(0, 2, 3, 1).cpu().numpy()
break # 배치 하나만 처리하기 위해 반복문을 종료한다.
# 시각화를 위한 행과 열의 개수 설정
columns, rows = 10, 5 # 10열, 5행의 격자로 이미지를 시각화한다.
# 원본 이미지를 시각화한다.
fig = plt.figure(figsize=(8, 4)) # 전체 그래프 크기를 설정한다.
for i in range(1, columns * rows + 1): # 배치에서 최대 50개의 이미지를 순회한다.
img = input_samples[i - 1] # 원본 이미지를 가져온다.
fig.add_subplot(rows, columns, i) # 지정된 위치에 서브플롯을 추가한다.
plt.imshow(img.squeeze(), cmap="gray") # 이미지를 시각화하고 채널을 제거한다.
plt.axis("off") # 축과 눈금을 표시하지 않는다.
plt.show() # 원본 이미지를 화면에 표시한다.
# 복원된 이미지를 시각화한다.
fig = plt.figure(figsize=(8, 4)) # 새로운 그래프를 생성한다.
for i in range(1, columns * rows + 1): # 복원된 이미지를 시각화한다.
img = reconstructed_samples[i - 1] # 복원된 이미지를 가져온다.
fig.add_subplot(rows, columns, i) # 서브플롯을 추가한다.
plt.imshow(img.squeeze(), cmap="gray") # 이미지를 시각화한다.
plt.axis("off") # 축과 눈금을 숨긴다.
plt.show() # 복원된 이미지를 화면에 표시한다.
with torch.no_grad()
- 모델의 평가 모드를 설정한다.
- 텐서의 연산이 기록되지 않으므로 메모리 사용량이 감소하고 계산 속도가 빨라진다.
데이터 변환
input_samples = inputs.permute(0, 2, 3, 1).cpu().numpy()
reconstructed_samples = outputs.permute(0, 2, 3, 1).cpu().numpy()
- PyTorch의 텐서는 일반적으로 (배치 크기, 채널 수, 높이, 너비) 형식을 가진다.
- Matplotlib은 (높이, 너비, 채널 수) 형식을 필요로 하므로 permute를 사용해 차원을 재배치한다.
- cpu().numpy()를 사용해 GPU 텐서를 CPU로 옮기고 NumPy 배열로 변환한다.
서브플롯 구성
fig.add_subplot(rows, columns, i)
- add_subplot(rows, columns, i)는 행렬 형태의 플롯에서 특정 위치에 그래프를 추가한다.
- 예를 들어, 10열 5행의 격자에서 i는 왼쪽 위에서 오른쪽 아래로 채워진다.
이미지 출력
plt.imshow(img.squeeze(), cmap="gray")
plt.axis("off")
- squeeze()는 채널이 불필요하게 1인 차원을 제거한다.
- imshow는 이미지를 출력하며, cmap="gray"는 이미지를 흑백으로 표시한다.
- axis("off")는 좌표축을 숨겨 이미지를 깔끔하게 출력한다.
두 번의 그래프 생성
- 첫 번째 그래프는 원본 이미지를 표시한다.
- 두 번째 그래프는 복원된 이미지를 동일한 형식으로 표시한다.
결론
이렇게 해서 MNIST 데이터셋을 이용한 오토인코더 모델 코드를 뜯어보았다.
오토인코더는 데이터를 효율적으로 압축하고 복원하는 비지도 학습 모델로,
차원 축소나 잡음 제거 등 다양한 응용 가능성을 가진다.
이번 코드를 통해 오토인코더의 구조와 학습 과정을 이해하고,
원본 이미지와 복원된 이미지를 비교하며 모델의 성능을 확인했다.
이 과정을 통해 오토인코더가 데이터의 중요한 특징을 효과적으로 학습할 수 있음을 알 수 있었다.
다음은 무슨 코드일까(모름)..
일단 이 글도 끝!
'Python > PyTorch' 카테고리의 다른 글
[PyTorch]설명 가능한 AI - CAM (1) | 2024.12.16 |
---|---|
[PyTorch]비지도 학습 - 깊은 K-평균 알고리즘 (오토인코더 + K-평균 알고리즘) (0) | 2024.12.10 |
[PyTorch]생성적 적대 신경망(GAN - Generative Adversarial Network) (1) | 2024.12.09 |
[Pytorch]Vanilla RNN과 확장된 기법들: LSTM, GRU, Bidirectional LSTM, Transformer (2) | 2024.12.03 |
[PyTorch]전이 학습(Transfer Learning) (0) | 2024.11.27 |
[PyTorch]Vanilla RNN을 활용한 코스피 예측 문제 (1) | 2024.11.26 |
- Total
- Today
- Yesterday
- 파이썬
- 맛집
- 세계일주
- BOJ
- 야경
- a6000
- 자바
- spring
- Backjoon
- 스트림
- 유럽여행
- 알고리즘
- 지지
- 기술면접
- 칼이사
- RX100M5
- Python
- Algorithm
- 백준
- java
- 동적계획법
- 세계여행
- 중남미
- 세모
- 남미
- 여행
- 리스트
- 면접 준비
- 유럽
- 스프링
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |