예제 코드에서 다룰 개념 데이터셋 전처리: 이미지 데이터의 로드, 정규화 등.CNN 모델 구성: CNN 구조와 각 계층의 역할.손실 함수와 옵티마이저: 분류 모델에 필요한 손실 함수와 최적화 기법.모델 학습과 평가: 배치 학습, 모델 성능 평가.추론(예측): 학습된 모델로 새로운 이미지 분류.
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
# 1. 데이터 전처리 및 로드
transform = transforms.Compose([
transforms.ToTensor(), # 이미지를 PyTorch 텐서로 변환
transforms.Normalize((0.5,), (0.5,)) # 데이터를 표준화 (정규화)
])
# MNIST 데이터셋 로드
dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
# 데이터셋을 학습용과 검증용으로 분리
train_size = int(0.8 * len(dataset))
valid_size = len(dataset) - train_size
train_dataset, valid_dataset = random_split(dataset, [train_size, valid_size])
# 데이터 로더 설정
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
valid_loader = DataLoader(valid_dataset, batch_size=64, shuffle=False)
# 2. CNN 모델 정의
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
# 첫 번째 합성곱 층
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1)
# 두 번째 합성곱 층
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1)
self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0) # 풀링 층
self.fc1 = nn.Linear(64 * 14 * 14, 128) # 완전 연결층
self.fc2 = nn.Linear(128, 10) # 출력층, MNIST는 10개의 클래스
def forward(self, x):
x = self.pool(torch.relu(self.conv1(x))) # 첫 번째 합성곱 계층 통과
x = self.pool(torch.relu(self.conv2(x))) # 두 번째 합성곱 계층 통과
x = x.view(-1, 64 * 14 * 14) # 평탄화 (Flatten)
x = torch.relu(self.fc1(x)) # 첫 번째 완전 연결층 통과
x = self.fc2(x) # 출력층 통과
return x
# 모델 인스턴스 생성
model = SimpleCNN()
# 3. 손실 함수와 옵티마이저 정의
criterion = nn.CrossEntropyLoss() # 다중 클래스 분류용 손실 함수
optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam 옵티마이저
# 4. 모델 학습 함수 정의
def train(model, loader, criterion, optimizer):
model.train()
running_loss = 0.0
for inputs, labels in loader:
optimizer.zero_grad() # 기울기 초기화
outputs = model(inputs) # 모델에 입력
loss = criterion(outputs, labels) # 손실 계산
loss.backward() # 역전파로 기울기 계산
optimizer.step() # 가중치 업데이트
running_loss += loss.item() * inputs.size(0)
return running_loss / len(loader.dataset)
# 5. 모델 평가 함수 정의
def validate(model, loader, criterion):
model.eval()
running_loss = 0.0
correct = 0
with torch.no_grad(): # 추론 모드
for inputs, labels in loader:
outputs = model(inputs)
loss = criterion(outputs, labels)
running_loss += loss.item() * inputs.size(0)
_, predicted = torch.max(outputs, 1)
correct += (predicted == labels).sum().item()
accuracy = 100 * correct / len(loader.dataset)
return running_loss / len(loader.dataset), accuracy
# 학습 및 검증 루프
num_epochs = 5
for epoch in range(num_epochs):
train_loss = train(model, train_loader, criterion, optimizer)
valid_loss, valid_accuracy = validate(model, valid_loader, criterion)
print(f'Epoch {epoch+1}/{num_epochs}, Train Loss: {train_loss:.4f},
Valid Loss: {valid_loss:.4f}, Valid Accuracy: {valid_accuracy:.2f}%')
# 6. 예측 예시
def predict(model, image):
model.eval()
with torch.no_grad():
output = model(image.unsqueeze(0)) # 모델에 단일 이미지 입력
_, predicted = torch.max(output, 1)
return predicted.item()
# 학습된 모델로 새로운 이미지 예측
example_image, _ = valid_dataset[0]
predicted_label = predict(model, example_image)
plt.imshow(example_image.squeeze(), cmap='gray')
plt.title(f'Predicted Label: {predicted_label}')
plt.show()
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, random_split
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
필수적인 개념만 보겠습니다. from torch.utils.data import DataLoader, random_split: 데이터셋 처리와 배치 처리를 위한 모듈을 임포트합니다. DataLoader: 모델 학습 시 데이터를 배치 단위로 나누고 무작위로 섞는 기능을 제공하는 클래스입니다. 데이터 셔플링과 배치 처리가 효율적으로 이뤄지며, 학습 과정에서 오버피팅을 줄이고 일반화 성능을 높일 수 있습니다. random_split: 전체 데이터셋을 특정 비율로 나누어 학습용과 검증용 데이터셋으로 구분할 때 사용됩니다.
from torchvision import datasets, transforms: 이미지 데이터와 이미지 변환(transform)을 위한 모듈을 가져옵니다. datasets: torchvision이 제공하는 다양한 이미지 데이터셋이 포함된 모듈로, 실험에 자주 사용되는 MNIST, CIFAR-10 등 여러 데이터셋을 쉽게 불러올 수 있습니다. transforms: 이미지 전처리(예: 텐서 변환, 정규화)를 위한 기능을 제공하는 모듈입니다. 각 이미지가 모델에 입력될 수 있도록 변환하는 역할을 합니다.
# 데이터 전처리 설정
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
데이터 전처리 설정: 모델에 입력할 데이터를 미리 정의된 방식으로 변환하여 일관된 형식으로 만드는 과정입니다. 여기서는 transforms.Compose()를 통해 여러 전처리 과정을 순서대로 묶어 하나의 전처리 파이프라인을 생성하고 있습니다.
transforms.Compose([...]): Compose는 다양한 전처리 기법을 리스트로 받아 순차적으로 적용해주는 역할을 합니다. 전처리 과정을 쉽게 설정하고 관리할 수 있으며, 여기서는 두 가지 전처리 작업(ToTensor, Normalize)이 포함됩니다.
transforms.ToTensor(): 이 함수는 이미지 데이터를 텐서 형식으로 변환합니다. PyTorch 모델은 텐서를 입력으로 받기 때문에 필수적인 단계입니다.
이미지는 원래 0에서 255 범위의 픽셀값을 가지지만, ToTensor를 통해 0에서 1 범위의 부동소수점 값으로 정규화됩니다. 예를 들어, 각 픽셀 값이 128이라면 텐서로 변환된 후에는 약 0.5로 표현됩니다. transforms.Normalize((0.5,), (0.5,)): 정규화(Normalization) 함수로, 데이터의 픽셀 값을 원하는 평균과 표준편차로 변환합니다.
(0.5,)는 평균을 의미하며, 모든 픽셀 값을 0.5만큼 이동시킵니다. (0.5,)는 표준편차로, 픽셀 값의 스케일을 0.5로 줄여줍니다. 이 과정을 통해 이미지 데이터는 -1에서 1 사이의 값으로 표준화되며, 이 표준화는 모델 학습에 유리한 입력 범위를 제공하여 수렴 속도를 높이고 학습 안정성을 강화합니다.
그런데 왜 0.5일까요? 보통은 1로 설정된다고 배웠는데.... 이 이유는 바로 방금 말했듯이 입력 데이터를 [−1,1][-1, 1][−1,1] 범위로 스케일링하려는 의도입니다.
원래 이미지 데이터를 0에서 1 사이로 변환하려면 transforms.ToTensor()를 사용하여 픽셀 값을 [0,1][0, 1][0,1] 범위로 변환합니다. 이후 transforms.Normalize((0.5,), (0.5,))을 적용하면, [0,1][0, 1][0,1] 범위의 값들이 [−1,1][-1, 1][−1,1]로 변환됩니다. 구체적으로 살펴보면: 1. 데이터의 정규화 단계: Normalize(mean, std) 함수는 각 픽셀 값에서 평균(mean)을 빼고, 표준편차(std)로 나눠 정규화합니다. 여기서 mean=0.5와 std=0.5를 사용해 각 픽셀을 정규화하면 다음과 같이 계산됩니다:
2. 수식 계산: 이렇게 하면, 원래 [0,1][0, 1][0,1] 범위의 값이 다음처럼 변환됩니다:
따라서, [0,1][0, 1][0,1] 범위의 값들이 [−1,1][-1, 1][−1,1] 범위로 스케일링됩니다. 통상적인 정규화에서는 데이터의 실제 평균과 표준편차를 이용해 [−1,1]가 아닌 정규 분포를 가지도록 설정하지만, 위와 같은 방법은 주로 이미지 데이터를 신경망에 입력할 때 널리 사용됩니다. [−1,1] 범위는 특정 활성화 함수의 출력 범위와 맞추기에 유리하며, 학습을 더욱 안정적으로 만드는 데 도움을 줍니다.
# MNIST 데이터셋 로드
dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
datasets.MNIST(...): MNIST 데이터셋을 로드합니다. root는 데이터셋이 저장될 경로를 지정하고, train=True는 학습용 데이터를 요청하며, download=True는 데이터가 로컬에 없을 경우 자동으로 다운로드하도록 설정합니다. transform=transform은 위에서 정의한 변환을 적용하여 데이터셋의 모든 이미지를 변환합니다.
train_size = int(0.8 * len(dataset)): 전체 데이터셋의 80%를 학습용 데이터셋으로 할당하기 위한 크기를 계산합니다. len(dataset)은 데이터셋의 총 샘플 수를 반환합니다.
valid_size = len(dataset) - train_size: 나머지 20%는 검증용 데이터셋으로 설정합니다. 이를 통해 모델의 일반화 성능을 평가할 수 있습니다.
random_split(dataset, [train_size, valid_size]): 무작위로 데이터셋을 학습용과 검증용으로 분리합니다. 이 메소드는 지정한 크기에 따라 데이터셋을 나누고, 각 부분을 새로운 데이터셋 객체로 반환합니다.
여기서 random_split에 대해 조금 더 자세히 보겠습니다. random_split 구조: torch.utils.data.random_split(dataset, lengths) dataset: 분할하고자 하는 데이터셋. 이는 torch.utils.data.Dataset의 서브클래스여야 합니다. 데이터셋은 샘플을 가져오는 방법을 정의해야 하며, 인덱스에 따라 샘플을 반환할 수 있어야 합니다.
lengths: 리스트 또는 튜플 형태로, 각 서브셋의 크기를 지정합니다. 이 리스트의 요소 수는 반환할 서브셋의 개수와 같아야 하며, 각 요소는 서브셋의 샘플 수를 나타냅니다. 모든 요소의 합은 dataset의 길이와 같아야 합니다.
반환값: List[Dataset] : 지정된 길이만큼 나누어진 서브셋의 리스트를 반환합니다. 각 서브셋은 원래 데이터셋의 일부 샘플을 포함하는 새로운 데이터셋 객체입니다.
그런데 왜 2개나 가지고 있죠? [train_size, valid_size]의 의미: [train_size, valid_size]는 random_split 함수에 전달되는 길이(lengths) 매개변수로, 다음과 같은 의미를 가집니다: 리스트의 각 요소: 이 리스트의 각 요소는 반환할 서브셋의 크기를 지정합니다. 즉, 첫 번째 요소는 훈련 데이터셋의 샘플 수, 두 번째 요소는 검증 데이터셋의 샘플 수를 나타냅니다.
여러 서브셋 분할: 이 방식은 random_split을 통해 여러 서브셋으로 나누고자 할 때 유용합니다. 예를 들어, 세 개의 서브셋을 만들고 싶다면 [train_size, valid_size, test_size]와 같이 세 개의 요소를 가진 리스트를 전달할 수 있습니다.
train_size가 80이면, 훈련 데이터셋은 80개의 샘플을 포함하게 됩니다. valid_size가 20이면, 검증 데이터셋은 20개의 샘플을 포함하게 됩니다. 이렇게 리스트로 두 개의 크기를 전달함으로써 random_split 함수는 원본 데이터셋을 랜덤하게 80개의 샘플과 20개의 샘플로 나누어 각각 train_dataset과 valid_dataset에 할당하게 됩니다.
즉, train_size, valid_size에 있는 각 크기에 맞게 데이터를 나누어서, 그것을 다시 train_dataset, valid_dataset에 기입한다는 뜻입니다.
# 데이터 로더 설정
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
valid_loader = DataLoader(valid_dataset, batch_size=64, shuffle=False)
DataLoader(train_dataset, batch_size=64, shuffle=True): 학습용 데이터셋을 로드하는 데이터 로더를 생성합니다. batch_size=64는 한 번에 64개의 샘플을 불러오겠다는 의미입니다. shuffle=True는 매 에포크마다 데이터셋을 무작위로 섞어 모델이 특정 순서에 과적합(overfitting)되는 것을 방지합니다.
valid_loader: 검증용 데이터셋에 대한 데이터 로더입니다. shuffle=False는 검증 과정에서는 데이터 순서를 고정하여 평가의 일관성을 유지할 수 있게 합니다.
class SimpleCNN(nn.Module):
def __init__(self):
super(SimpleCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 32, kernel_size=3, stride=1, padding=1) # 첫 번째 합성곱 층
self.conv2 = nn.Conv2d(32, 64, kernel_size=3, stride=1, padding=1) # 두 번째 합성곱 층
self.pool = nn.MaxPool2d(kernel_size=2, stride=2, padding=0) # 풀링 층
self.fc1 = nn.Linear(64 * 14 * 14, 128) # 완전 연결층
self.fc2 = nn.Linear(128, 10) # 출력층, MNIST는 10개의 클래스
class SimpleCNN(nn.Module): nn.Module 클래스를 상속받아 CNN 모델을 정의하는 클래스입니다. PyTorch의 모든 신경망 모델은 이 클래스를 기반으로 만들어져야 하며, 사용자 정의 레이어를 포함할 수 있습니다.
def __init__(self): 클래스의 생성자 메소드로, 신경망의 레이어를 초기화하고 구조를 정의합니다.
super(SimpleCNN, self).__init__(): 부모 클래스인 nn.Module의 초기화 메소드를 호출하여 PyTorch의 내부 구조를 설정합니다. 이 호출이 없으면 모델이 제대로 동작하지 않습니다.
self.conv1 = nn.Conv2d(...): 첫 번째 합성곱 층을 정의합니다.
1: 입력 채널 수, MNIST 이미지는 흑백이므로 1입니다. 32: 출력 채널 수, 즉 이 레이어에서 생성할 필터(커널)의 개수입니다. kernel_size=3: 필터의 크기가 3x3임을 의미합니다. stride=1: 필터가 이동하는 간격을 1로 설정합니다. 즉, 필터가 한 픽셀씩 이동합니다. padding=1: 입력 이미지의 가장자리에 1픽셀의 패딩을 추가하여 출력 크기가 줄어드는 것을 방지합니다. self.conv2 = nn.Conv2d(...): 두 번째 합성곱 층을 정의합니다. 입력 채널 수는 32(이전 층의 출력)이고, 출력 채널 수는 64입니다.
self.pool = nn.MaxPool2d(...): 최대 풀링 층을 정의하여 이미지의 공간적 크기를 줄입니다. kernel_size=2는 2x2 영역에서 최대값을 추출하여 다운샘플링합니다. stride=2는 풀링 과정에서 두 픽셀씩 이동하게 하여 출력 크기를 절반으로 줄입니다. self.fc1 = nn.Linear(...): 첫 번째 완전 연결층을 정의합니다. 64 * 14 * 14: 이전 층의 출력 차원입니다. 64개의 채널에서 14x14 크기의 특성 맵이 생성됩니다. 128: 이 층의 출력 뉴런 수입니다. self.fc2 = nn.Linear(128, 10): 최종 완전 연결층으로, 출력층 역할을 합니다. 입력 노드 수는 128이고, 출력 노드 수는 10입니다. 이는 MNIST 데이터셋이 0에서 9까지 총 10개의 클래스(숫자)를 가지고 있기 때문입니다. 이 층은 각 클래스에 대한 확률을 제공하며, 출력된 값 중 가장 높은 확률을 가진 클래스를 예측값으로 결정하게 됩니다.
그런데 Linear는 ANN의 신경망이고. Conv2d는 CNN의 신경망인데 어떻게 같이 쓸 수 있을까요? 그 이유는 각 층이 서로 다른 기능을 가지고 있어서 특정한 방식으로 데이터를 처리하기 때문입니다. CNN의 합성곱 층(예: Conv2d)은 이미지 같은 2D 데이터의 공간적 특징을 추출하는 데 효과적입니다. 이미지에서 중요한 패턴이나 모양 등을 감지하는 데 뛰어나죠. 반면, Linear 층(완전 연결층)은 일반적인 인공신경망(ANN)에서 각 노드가 모든 입력에 연결되어 있기 때문에 입력 데이터의 특정 패턴보다는 전역적 정보에 더 집중합니다.
먼저 Conv2d와 같은 합성곱 층을 생각해보겠습니다. CNN의 기본적인 목표는,이미지의 중요한 패턴을 추출하는 것입니다. 예를 들어, 이미지를 보면가장자리나 특정 모양등이 반복적으로 나타나는 것을 볼 수 있죠. CNN은 이러한 패턴을 찾아내는 데 매우 뛰어납니다. 이를 위해 CNN은 작은 필터(커널)를 사용해 이미지 전체를 조금씩 훑으면서특정 패턴이나 특징을 감지합니다.이를 통해 모델이 특정 부분에 집중할 수 있도록 만들어주죠.
간단히 말해, CNN은: 이미지의 각 부분을 세밀하게 살피면서 패턴을 찾습니다. 패턴의 위치(예: 얼굴의 눈, 코, 입)와 같은 공간적 정보를 유지합니다. Conv2d는 이 과정에서 필터가 이미지를 탐색하는 역할을 수행합니다.
예시로 첫 번째 Conv2d 층(self.conv1)을 살펴보면,입력 채널이 1이고 출력 채널이 32입니다. 이는 입력 이미지를 1개(예를 들어 흑백 이미지)로 받고, 이 Conv2d 층을 통과한 결과로 32개의 새로운 특징 맵을 생성한다는 의미입니다. 이렇게 특징 맵을 여러 개 생성함으로써 이미지의 다양한 패턴을 잡아내죠.
Pooling 층 (MaxPool2d 층) - 중요한 정보만 남기는 과정 합성곱 층에서 나온 특징 맵에는 정보가 매우 많아질 수 있습니다. 이때 풀링 층을 사용해 이 특징 맵의크기를 줄이면서 중요한 정보만 남기고 불필요한 세부 사항을 줄입니다.예를 들어, MaxPool2d는 2x2의 영역에서 가장 큰 값을 선택해 그 영역을 대표하게 만듭니다. 이렇게 하면 계산량이 줄어들고, 중요한 특징이 더욱 부각됩니다.
ANN 층 (Linear 층) - 추출된 패턴을 바탕으로 최종 판단을 내리는 부분 합성곱과 풀링 층을 거친 데이터는 이미지의 저수준 특징(예: 가장자리, 색상)에서 고수준 특징(예: 눈, 코, 얼굴 형태)까지 단계적으로 압축됩니다. 그러나 이렇게 얻어진 데이터는 여전히 특징 맵 형태의 2차원 데이터입니다.ANN에서 사용하는 Linear 층은 일반적으로 1차원 벡터로 데이터를 다루기 때문에, 특징 맵을 평탄화(flatten)해서 1차원 형태로 변환해야 합니다.
이제 Linear 층은 변환된 데이터를 입력받아 마지막으로 클래스에 따라 구별하고 분류하는 작업을 수행합니다. 이 층은 CNN이 찾아낸 다양한 특징을 전역적으로 조합하여 가장 가능성이 높은 클래스를 예측하는 역할을 합니다. 예를 들어, fc1은 64 * 14 * 14의 특징 맵을 128개의 노드로 압축합니다. 이후 fc2 층에서 128개의 정보를 10개의 클래스(MNIST 숫자 0~9)로 분류하게 됩니다.
요약하자면, CNN만으로는 최종 분류 작업에서 부족할 수 있기 때문입니다. 1. CNN 층의 역할과 한계 아까 말한것 처럼, CNN의 합성곱 층과 풀링 층은 이미지 속에서 국소적이고 구체적인 특징을 잘 추출합니다. 예를 들어, 특정 부분에 있는 가장자리나 모서리, 질감과 같은 세부적인 패턴을 잘 감지합니다. CNN의 합성곱과 풀링 층이 쌓이면 이 정보들이 점차 압축되면서 저수준의 패턴이 모여 고수준의 패턴(예: 얼굴 형태)이 됩니다. 하지만 이러한 특징 맵만으로는 이미지 전체의 클래스(예: 고양이, 개, 숫자)를 완벽히 결정하기가 어렵습니다. 2. Linear 층을 추가하는 이유 - 전체적인 특징 조합과 분류 합성곱과 풀링을 통해 추출된 특징 맵을 최종적으로 전체적인 패턴으로 종합하고, 이를 통해 클래스 예측을 하는 데는 Linear 층(완전 연결층)이 효과적입니다. Linear 층은 CNN이 찾아낸 다양한 특징을 종합하여 이미지 전체의 의미를 파악하고, 최종 클래스에 따라 분류하는 역할을 합니다. 즉, Linear 층이 없으면 모델이 추출한 세부 특징들을 클래스에 매칭하여 결정적으로 분류하기가 어렵습니다.
사실, 기술적으로 CNN만으로도 분류가 가능할 수는 있습니다. 예를 들어, CNN 층에서 마지막에 평균 풀링을 사용해 1차원 벡터 형태로 변환하고, 이를 바로 소프트맥스(Softmax) 등으로 예측할 수도 있죠. 그러나 이렇게 하면 분류 성능이 떨어질 가능성이 큽니다. 이는 CNN 층이 전역적인 정보보다 특정 영역에 집중하기 때문에, 이미지 전체를 아우르는 정보 종합에 부족함이 있기 때문입니다.
따라서 CNN은 특정 패턴을 감지하고, ANN의 Linear 층이 감지된 패턴을 종합하여 전체적인 판단을 내리는 구조가 더 높은 성능을 보입니다. CNN과 ANN 층을 결합하여 국소적 패턴 감지 + 전역적 판단이라는 두 가지 장점을 모두 살릴 수 있는 거죠.
그런데 MNIST는 28*28이 아닌가요? 왜 14*14이죠? NIST 데이터셋은 기본적으로 28x28 픽셀 크기의 이미지입니다. 그런데 CNN 네트워크의 구조에서 풀링 층(MaxPool2d)이 28x28 이미지를 14x14로 줄이게 되는 것입니다.
왜 14x14가 되는가? 1. 초기 이미지 크기는 28x28입니다.
2. 첫 번째 Conv2d 층을 거쳐도 출력 크기는 여전히 28x28입니다. (패딩과 스트라이드 덕분에 공간 크기가 유지됨)
3. 첫 번째 풀링 층 (MaxPool2d) 이 풀링 층은 kernel_size=2, stride=2로 설정되어 있습니다. 이 설정은 2x2 영역의 가장 큰 값을 선택하면서, 출력 크기를 절반으로 줄이는 역할을 합니다. 따라서 28x28 이미지는 이 풀링을 거쳐 14x14 크기로 줄어듭니다.
MaxPool2d 층에서 stride=2와 kernel_size=2 설정으로 인해 28x28 이미지가 14x14로 절반 크기가 되는 것입니다. 이를 통해 CNN 네트워크는 점진적으로 이미지 크기를 줄이며 중요한 특징만 남기게 되죠.
그래서 64 * 14 * 14는 풀링 층을 통해 줄어든 공간적 크기 14 * 14에 Conv2d 층의 채널 수인 64를 곱해 계산된 것입니다.
def forward(self, x):
x = self.pool(torch.relu(self.conv1(x))) # 첫 번째 합성곱 계층 통과
x = self.pool(torch.relu(self.conv2(x))) # 두 번째 합성곱 계층 통과
x = x.view(-1, 64 * 14 * 14) # 평탄화 (Flatten)
x = torch.relu(self.fc1(x)) # 첫 번째 완전 연결층 통과
x = self.fc2(x) # 출력층 통과
return x
def forward(self, x): 모델의 순전파(forward) 연산을 정의합니다. 입력 x가 각 레이어를 통과하며 최종 출력을 반환하는 구조입니다. PyTorch에서는 forward 메소드 안에서 연산을 정의해야 model(input) 형태로 모델을 호출할 수 있습니다.
x = self.pool(torch.relu(self.conv1(x))): 입력 x를 첫 번째 합성곱 층(self.conv1)에 통과시키고, ReLU 활성화 함수를 적용합니다. torch.relu(self.conv1(x)): self.conv1(x)는 합성곱 연산을 수행하여 32개의 특성 맵을 생성하며, torch.relu()는 이를 비선형 활성화합니다. ReLU는 양수는 그대로, 음수는 0으로 바꿔주는 함수로, 비선형성을 추가하여 모델이 복잡한 패턴을 학습할 수 있게 해줍니다. self.pool(...): 그 후에, MaxPool2d 층을 통해 풀링을 수행하여 특성 맵의 크기를 줄입니다. 이는 중요 특징을 추출하면서 계산 비용을 줄여주고, 과적합을 방지하는 데에도 도움이 됩니다. x = self.pool(torch.relu(self.conv2(x))): 두 번째 합성곱 층에서도 동일한 방식으로 연산을 수행합니다. 이제 conv2에서 생성된 64개의 특성 맵이 최종 풀링 층을 통과하여 특성 맵의 크기가 14x14로 줄어듭니다.
x = x.view(-1, 64 * 14 * 14): x를 Flatten하여 완전 연결층에 입력할 수 있도록 만듭니다. view(-1, 64 * 14 * 14)는 텐서를 2차원으로 펼치는 연산입니다. -1은 해당 차원을 PyTorch가 자동으로 계산하도록 하는 값으로, 여기서는 배치 크기에 해당합니다. 최종 결과는 (배치 크기, 12544)의 형태가 됩니다.
x = torch.relu(self.fc1(x)): 펼친 입력 x를 첫 번째 완전 연결층 fc1에 통과시켜 128개의 노드를 생성합니다. 이후 ReLU 활성화 함수를 적용하여 비선형성을 추가합니다.
x = self.fc2(x): 출력층에 통과하여 최종 예측값을 얻습니다. fc2는 10개의 클래스에 대한 로짓 값을 출력합니다. 각 로짓은 해당 클래스일 확률이 가장 높은지를 나타냅니다.
return x: 모델의 최종 출력값을 반환합니다. 이 값은 아직 소프트맥스가 적용되지 않은 로짓(logit) 값이지만, 이후에 손실 함수로 사용될 CrossEntropyLoss에서 내부적으로 소프트맥스가 적용됩니다.
model = SimpleCNN()
model = SimpleCNN(): SimpleCNN 클래스의 인스턴스를 생성합니다. 이 단계에서 모델의 모든 레이어가 초기화되며, 학습 가능한 파라미터들이 메모리에 할당됩니다.
criterion = nn.CrossEntropyLoss() # 다중 클래스 분류용 손실 함수
optimizer = optim.Adam(model.parameters(), lr=0.001) # Adam 옵티마이저
criterion = nn.CrossEntropyLoss(): 손실 함수로 CrossEntropyLoss를 사용합니다. 이 함수는 다중 클래스 분류 문제에서 일반적으로 사용되는 손실 함수로, 모델의 출력값(로짓)을 확률로 변환하여 실제 레이블과의 오차를 계산합니다. 내부적으로 소프트맥스를 적용하여 각 클래스의 확률을 얻고, 그 확률에 로그를 적용하여 로그 손실을 계산합니다.
optimizer = optim.Adam(model.parameters(), lr=0.001): Adam 옵티마이저를 사용하여 모델의 파라미터를 업데이트합니다. model.parameters()는 모델의 모든 학습 가능한 파라미터를 가져옵니다. lr=0.001은 학습률로, 가중치를 얼마나 빠르게 업데이트할지를 결정합니다. Adam 옵티마이저는 모멘텀과 적응형 학습률을 포함하여 학습을 안정적으로 수행하도록 돕습니다.
def train(model, loader, criterion, optimizer):
model.train()
running_loss = 0.0
for inputs, labels in loader:
optimizer.zero_grad() # 기울기 초기화
outputs = model(inputs) # 모델에 입력
loss = criterion(outputs, labels) # 손실 계산
loss.backward() # 역전파로 기울기 계산
optimizer.step() # 가중치 업데이트
running_loss += loss.item() * inputs.size(0)
return running_loss / len(loader.dataset)
def train(...): 학습 단계에서 모델을 업데이트하는 함수입니다. 파라미터로 모델, 데이터 로더, 손실 함수, 옵티마이저를 받습니다.
model.train(): 모델을 학습 모드로 전환합니다. 이는 드롭아웃이나 배치 정규화와 같은 레이어가 훈련 모드로 동작하게 해줍니다.
running_loss = 0.0: 에포크 동안 손실 값을 누적하기 위한 변수입니다. 이후 각 배치에서 발생한 손실을 더해줍니다.
for inputs, labels in loader: 데이터 로더에서 inputs와 labels 배치를 가져와 학습합니다. inputs는 이미지 데이터, labels는 정답 클래스입니다.
optimizer.zero_grad(): 옵티마이저의 zero_grad()를 호출하여 모든 기울기를 초기화합니다. PyTorch에서는 기울기가 누적되기 때문에, 매번 업데이트 전에 기울기를 초기화해야 올바르게 동작합니다.
outputs = model(inputs): 현재 배치 inputs를 모델에 입력하여 예측값 outputs를 얻습니다. outputs는 각 클래스에 대한 로짓 값으로, 추후 손실 함수에서 사용됩니다.
loss = criterion(outputs, labels): 예측값 outputs와 실제 레이블 labels를 비교하여 손실을 계산합니다. criterion은 CrossEntropyLoss로 정의된 손실 함수입니다.
loss.backward(): 역전파(backpropagation)를 통해 기울기를 계산합니다. 이 과정에서 각 파라미터에 대한 손실의 변화율(기울기)이 계산되어, 이후 옵티마이저에 의해 파라미터가 업데이트됩니다.
optimizer.step(): 옵티마이저가 기울기에 따라 파라미터를 업데이트합니다. 이 단계에서 모델이 학습됩니다.
running_loss += loss.item() * inputs.size(0): 현재 배치의 손실을 배치 크기만큼 곱하여 누적 손실에 더합니다. loss.item()은 손실 값을 스칼라 형태로 반환합니다.
return running_loss / len(loader.dataset): 전체 에포크 동안의 평균 손실을 반환하여 학습이 얼마나 잘 진행되고 있는지 확인할 수 있게 합니다.
def validate(model, loader, criterion):
model.eval()
running_loss = 0.0
correct = 0
with torch.no_grad(): # 추론 모드
for inputs, labels in loader:
outputs = model(inputs) # 모델에 입력
loss = criterion(outputs, labels) # 손실 계산
running_loss += loss.item() * inputs.size(0) # 손실 누적
_, predicted = torch.max(outputs, 1) # 가장 높은 로짓을 가진 클래스 선택
correct += (predicted == labels).sum().item() # 정답 개수 누적
accuracy = 100 * correct / len(loader.dataset) # 정확도 계산
return running_loss / len(loader.dataset), accuracy # 평균 손실과 정확도 반환
def validate(...): 모델의 성능을 검증하는 함수입니다. 파라미터로 모델, 데이터 로더, 손실 함수를 받습니다. 검증 과정에서는 모델을 학습 모드에서 평가 모드로 전환하고, 기울기를 계산하지 않기 위해 torch.no_grad() 블록 안에서 수행합니다.
model.eval(): 모델을 평가 모드로 전환합니다. 이 모드에서는 드롭아웃(dropout)이나 배치 정규화(batch normalization)와 같은 훈련 시 특정 동작이 비활성화되어, 일관된 결과를 제공합니다.
running_loss = 0.0: 검증 에포크 동안의 손실을 누적할 변수를 초기화합니다.
correct = 0: 올바르게 예측한 샘플 수를 저장할 변수를 초기화합니다.
with torch.no_grad(): 이 블록 안에서는 기울기가 계산되지 않습니다. 추론 과정에서는 기울기 계산이 필요하지 않기 때문에 메모리 사용량을 줄이고 연산 속도를 높이기 위해 사용됩니다.
for inputs, labels in loader: 검증 데이터 로더에서 배치 단위로 입력과 레이블을 가져옵니다.
outputs = model(inputs): 모델에 입력을 주어 예측값을 계산합니다. outputs는 모델이 각 클래스에 대해 생성한 로짓입니다.
loss = criterion(outputs, labels): 예측값과 실제 레이블을 비교하여 손실을 계산합니다. 손실 값은 모델의 성능을 평가하는 기준이 됩니다.
running_loss += loss.item() * inputs.size(0): 현재 배치에서 계산된 손실을 누적합니다. loss.item()은 텐서를 스칼라로 변환하여 반환하고, inputs.size(0)는 현재 배치의 크기를 나타냅니다.
_, predicted = torch.max(outputs, 1): 모델의 출력 outputs에서 각 샘플에 대해 가장 높은 값을 가진 인덱스를 찾습니다. 이 인덱스는 예측한 클래스를 의미하며, _는 가장 큰 값(로짓)을 무시하기 위해 사용됩니다.
correct += (predicted == labels).sum().item(): 예측된 클래스(predicted)와 실제 레이블(labels)을 비교하여 올바른 예측의 수를 누적합니다. (predicted == labels)는 불리언 텐서를 반환하고, sum()을 통해 올바른 예측의 총 개수를 계산합니다.
accuracy = 100 * correct / len(loader.dataset): 전체 검증 데이터셋에 대한 정확도를 계산합니다. correct는 올바르게 예측한 샘플 수이고, len(loader.dataset)는 전체 샘플 수입니다.
return running_loss / len(loader.dataset), accuracy: 평균 손실과 정확도를 반환합니다. 이를 통해 모델의 성능을 평가할 수 있습니다.
num_epochs = 5
for epoch in range(num_epochs):
train_loss = train(model, train_loader, criterion, optimizer) # 모델 학습
valid_loss, valid_accuracy = validate(model, valid_loader, criterion) # 모델 검증
print(f'Epoch {epoch+1}/{num_epochs}, Train Loss:
{train_loss:.4f}, Valid Loss: {valid_loss:.4f}, Valid Accuracy: {valid_accuracy:.2f}%')
num_epochs = 5: 총 학습할 에포크 수를 정의합니다. 이 경우에는 5번의 에포크 동안 모델을 학습하고 검증합니다.
for epoch in range(num_epochs): 지정된 에포크 수만큼 반복하는 루프입니다. 각 에포크마다 모델을 학습하고 검증합니다.
train_loss = train(model, train_loader, criterion, optimizer): train 함수를 호출하여 모델을 학습합니다. 학습 손실 값을 반환받습니다.
valid_loss, valid_accuracy = validate(model, valid_loader, criterion): validate 함수를 호출하여 모델을 검증합니다. 검증 손실과 정확도를 반환받습니다.
print(f'Epoch {epoch+1}/{num_epochs}, Train Loss: {train_loss:.4f}, Valid Loss: {valid_loss:.4f}, Valid Accuracy: {valid_accuracy:.2f}%'): 각 에포크의 학습 손실, 검증 손실, 검증 정확도를 출력합니다. 이는 모델의 학습 진행 상황을 시각적으로 확인할 수 있게 도와줍니다.
def predict(model, image):
model.eval() # 평가 모드로 전환
with torch.no_grad(): # 기울기 계산 안 함
output = model(image.unsqueeze(0)) # 모델에 단일 이미지 입력
_, predicted = torch.max(output, 1) # 예측 클래스 선택
return predicted.item() # 예측된 클래스 반환
def predict(model, image): 학습된 모델을 사용하여 주어진 이미지에 대한 예측을 수행하는 함수입니다.
model.eval(): 모델을 평가 모드로 전환합니다. 이는 검증 단계에서 사용된 것과 동일합니다.
with torch.no_grad(): 기울기 계산을 하지 않도록 설정합니다. 예측 단계에서는 기울기가 필요하지 않으므로 메모리 효율성을 높일 수 있습니다.
output = model(image.unsqueeze(0)): 단일 이미지를 모델에 입력하기 위해 unsqueeze(0)를 사용하여 배치 차원을 추가합니다. 이렇게 하면 입력 형태가 (1, 1, 28, 28)로 바뀌어 모델이 정상적으로 작동합니다.
_, predicted = torch.max(output, 1): 모델의 출력에서 가장 높은 로짓을 가진 클래스를 선택합니다.
example_image, _ = valid_dataset[0] # 검증 데이터셋에서 첫 번째 이미지 가져오기
predicted_label = predict(model, example_image) # 예측 수행
plt.imshow(example_image.squeeze(), cmap='gray') # 이미지 시각화
plt.title(f'Predicted Label: {predicted_label}') # 예측 레이블 제목 추가
plt.show() # 이미지 출력
example_image, _ = valid_dataset[0]: 검증 데이터셋에서 첫 번째 이미지를 가져옵니다. example_image는 모델에 입력될 이미지이고, _는 레이블로 사용되지 않으므로 무시합니다.
predicted_label = predict(model, example_image): 가져온 이미지를 모델에 입력하여 예측을 수행합니다. 결과는 예측된 클래스 레이블입니다.
plt.imshow(example_image.squeeze(), cmap='gray'): 예측된 이미지를 시각화합니다. squeeze()는 차원 수를 줄여 이미지가 (28, 28) 형태로 되도록 합니다. cmap='gray'는 흑백 이미지를 적절하게 표시합니다.
plt.title(f'Predicted Label: {predicted_label}'): 예측된 레이블을 이미지 제목으로 추가합니다.
plt.show(): 이미지를 출력합니다. 이를 통해 모델이 어떤 숫자를 예측했는지 시각적으로 확인할 수 있습니다.
다음은 NLP에 대한 예시 코드를 보겠습니다.(오늘은 여기까지^^) (사실 아직 다 이해하지 못했지만...)