Developer's Development

3.3.21 [LLM] 자연어-이미지 멀티모달: CNN 개요 및 구조 본문

LLM

3.3.21 [LLM] 자연어-이미지 멀티모달: CNN 개요 및 구조

mylee 2025. 9. 16. 16:13
합성곱 신경망 (Convolutional Neural Network, CNN)

 

  • CNN의 기본 개념

👉🏻 합성곱(Convolution)이란

이미지 처리에서 주변 필셀과의 가중합을 계산하여 특징을 추출하는 연산이다.

합성곱 신경망에서 합성곱 연산은 커널(또는 필터)을 사용하여 입력 데이터와의 내적을 수행한다.

 

합성곱 신경망의 구성 개요

- 합성곱 계층(Convolutional Layer): 필터(커널)를 이용하여 이미지의 특징을 추출

- 풀링 계층(Pooling Layer): 데이터의 크기를 줄이고 중요한 특징을 유지하여 연산량 감소

- 완전 연결 계층(Fully Connected Layer): 최종적으로 학습된 특징을 바탕으로 분류 또는 회귀 수행

- 배치 처리: 배치(Batch) 처리는 여러 개의 입력 데이터를 한 번에 연산하는 방식으로, 학습을 안정적으로 수행하고 연산 속도를 향상시킨다.

 

합성곱 신경망은 딥러닝의 한 분야로 이미지 인식과 처리에 주로 사용되는 인공신경망(ANN)의 한 종류이다.

이미지 분류, 객체 인식 및 탐지, 얼굴 인식, 의료 이미지 분석 등에 응용 가능하다.

 

[ 합성곱층의 필요성 ]

완전 연결 신경망(FCNN)은 이미지의 공간적 특성을 고려하지 않고 1차원 벡터로 변환하여 학습하므로, 지역적인 특징을 무시하고 연산량이 증가하는 문제가 발생한다. 이를 해결하기 위해 합성곱 신경망(CNN)이 도입되었다.

 

👉🏻 필터 & 특징 맵

1. 필터 (Filter)

  - 합성곱 연산에서 사용하는 커널이다.

  - 필터는 학습을 통해 최적의 값을 찾으며, 특정한 특징을 추출하는 역할을 한다.

2. 특징 맵 (Feature Map)

  - 필터를 입력 데이터에 적용하여 얻은 출력이다.

  - 입력 이미지에서 특정한 패턴이나 특징을 강조한다.

3. 필터와 특징 맵의 관계

  - 여러 개의 필터를 사용하면 여러 개의 특징 맵이 생성된다.

  - 각 필터는 다른 특징을 추출하도록 학습된다.

 

👉🏻 패딩 & 스트라이드

1. 패딩(Padding)

  - 입력 데이터의 가장자리에 특정 값(보통 0)을 추가하여 출력 크기를 조정한다.

  - 유형

    - Valid Padding: 패딩을 사용하지 않아 출력 크기가 줄어든다.

    - Same Padding: 출력 크기를 입력과 동일하게 유지하기 위해 패딩을 추가한다.

2. 스트라이드(Stride)

  - 필터를 이동시키는 간격을 의미한다.

  - 스트라이드 값이 클수록 출력 크기가 작아진다.

 

  • 풀링 레이어

👉🏻 풀링(Pooling)이란

공간적 크기를 줄여 계산량을 감소시키고, 특징의 위치 변화에 대한 강건성을 높이는 연산이다.

 

👉🏻 풀링의 종류

1. 최대 풀링 (Max Pooling)

  - 주어진 필터(영역 내)에서 가장 큰 값을 선택한다.

  - 가장 중요한 특징을 유지하면서도 크기를 줄일 수 있다. (에지와 같은 중요한 특징을 강조)

  - 경계 감지나 이미지의 강한 특징을 보존하는 데 유리하다.

2. 최소 풀링 (Min Pooling)

  - 지정된 필터 크기 내에서 가장 작은 값을 선택하여 출력한다.

  - 가장 어두운 픽셀이나 작은 값이 중요한 경우에 사용될 수 있다.

  - 하지만 일반적인 CNN 모델에서는 거의 사용되지 않는다.

3. 평균 풀링 (Average Pooling)

  - 필터(영역) 내의 모든 값을 평균 내어 출력한다.

  - 전체적인 정보 보존이 필요할 때 유용하지만, 가장 두드러진 특징이 약해질 수 있다.

4. 글로벌 풀링 (Global Average Pooling)

  - 전체 입력을 하나의 값으로 변환한다.

    - 글로벌 최대 풀링 (Global Max Pooling): 전체 특징 맵에서 가장 큰 값을 반환한다.

    - 글로벌 평균 풀링 (Global Average Pooling): 전체 값의 평균을 반환한다.

  - 주로 CNN 마지막 단계에서 완전 연결층(FC Layer)을 제거하고 특징을 요약하는 데 사용된다.

    (전체적인 흐름이나 배경 정보를 추출)

 

👉🏻 풀링의 효과

계산량 감소, 특징의 불변성 증가, 과적합 방지

 

  • CNN의 구조와 동작

👉🏻 일반적인 CNN 구조

1. 합성곱 레이어 (Convlutional Layer): 특징을 추출하는 역할을 한다.

2. 활성화 함수(Activation Function): 비선형성을 도입하여 모델의 표현력을 높인다. ex) ReLU 함수

3. 풀링 레이어 (Pooling Layer): 공간적 크기를 줄여 계산 효율성을 높인다.

4. 완전 연결 레이어 (Fully Connected Layer): 추출된 특징을 기반으로 분류나 회귀를 수행한다.

 

👉🏻 CNN의 순전파 과정

1. 입력 이미지 전달: 모델의 입력으로 이미지 데이터를 받는다.

2. 합성곱 연산: 필터를 사용하여 특징 맵을 생성한다.

3. 활성화 함수 적용: 비선형 활성화 함수를 통해 특징 맵에 비선형성을 부여한다.

4. 풀링 연산: 풀링 레이어를 통해 공간적 크기를 축소한다.

5. 완전 연결 레이어: 1차원 벡터로 변환하여 최종 출력을 계산한다.

 

 

실습 (손글씨 숫자 예측 by MLP, colab)

 

from torchvision.datasets.mnist import MNIST	# 손글씨 숫자 데이터
from torchvision.transforms import ToTensor

train_data = MNIST(root='./', train=True, download=True, transform=ToTensor())
test_data = MNIST(root='./', train=False, download=True, transform=ToTensor())
# 받아온 데이터의 크기 확인
print(len(train_data), len(test_data))
print(train_data.data.shape)
"""
60000 10000
torch.Size([60000, 28, 28])
"""
import matplotlib.pyplot as plt

plt.imshow(train_data.data[9])
plt.show()

from torch.utils.data.dataloader import DataLoader

train_loader = DataLoader(train_data, batch_size=64, shuffle=True)
test_loader = DataLoader(test_data, batch_size=64, shuffle=False)
import torch

device= 'cuda' if torch.cuda.is_available() else 'cpu'
device	# 'cuda'
# MLP 모델
import torch.nn as nn

class MLP(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(784, 128)
        self.relu1 = nn.ReLU()
        self.fc2 = nn.Linear(128, 64)
        self.relu2 = nn.ReLU()
        self.fc3 = nn.Linear(64, 10)
    
    def forward(self, x):
        x = x.view(x.size(0), -1)       # [B, 1, 28, 28] -> [B, 784]
        x = self.relu1(self.fc1(x))
        x = self.relu2(self.fc2(x))
        return self.fc3(x)

model = MLP().to(device)
# 학습
from torch.optim.adam import Adam

learning_rate = 1e-3
optim = Adam(model.parameters(), lr=learning_rate)

for epoch in range(20):
    for data, label in train_loader:
        optim.zero_grad()
        data = torch.reshape(data, (-1, 784)).to(device)
        preds = model(data)

        loss = nn.CrossEntropyLoss()(preds, label.to(device))
        loss.backward()
        optim.step()
    print(f"Epoch: {epoch + 1}, Loss: {loss.item():.4f}")	# Loss 들쭉날쭉함
# 모델의 가중치 저장
torch.save(model.state_dict(), 'MNIST.pt')

# 모델 로드 (가중치 로드)
model.load_state_dict(torch.load('MNIST.pt', map_location=device

# 모델 예측 및 평가
correct = 0

with torch.no_grad():
    for data, label in test_loader:
        data = torch.reshape(data, (-1, 784)).to(device)
        
        output = model(data)
        preds = output.data.max(1)[1]	# 최댓값을 가진 인덱스를 가져오도록
        # preds = output.argmax(dim=1)  # 위 코드와 같은 뜻

        corr = preds.eq(label.to(device).data).sum().item()
        correct += corr

print(f"Accuracy: {correct / len(test_data)}")	# Accuracy: 0.9784 -> 꽤 높음!

 

 

실습 (Conv + Pool, colab)

 

💧 CNN 구조를 PyTorch 이용해서 간단하게 만들어보기

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

input_tensor = torch.randn(1, 1, 28, 28)    # (batch, channel, height, width) -> 흑백 이미지

conv = nn.Conv2d(
    in_channels=1,          # 입력 한장
    out_channels=4,       # 4개의 채널로 출력
    kernel_size=3, 
    stride=1, 
    padding="same"     # padding의 기본값은 valid
)

pool = nn.MaxPool2d(    # 풀링 레이어
    kernel_size=2, 
    stride=2
)

output = F.relu(conv(input_tensor))
print(output.shape)

output = pool(output)
print(output.shape)
"""
torch.Size([1, 4, 28, 28])	# 4개의 특징 맵으로 4차원
torch.Size([1, 4, 14, 14])	# 반이 감소
"""

print(conv.weight.shape)
print(conv.bias.shape)
"""
torch.Size([4, 1, 3, 3])
torch.Size([4])
"""

 

  • CNN Model

1. Feature Extraction Layer

2. Fully Connected (Classification) Layer

class CNNModel(nn.Module):
    def __init__(self):
        super().__init__()

        # 특성추출층
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding='same')
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding='valid')
        self.pool = nn.MaxPool2d(2, 2)

        # 완전연결층
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(64 * 13 * 13, 100)
        self.fc2 = nn.Linear(100, 10)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = self.pool(x)
        x = self.flatten(x)
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return x
        
model = CNNModel()
model
"""
CNNModel(
  (conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=same)
  (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=valid)
  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (flatten): Flatten(start_dim=1, end_dim=-1)
  (fc1): Linear(in_features=10816, out_features=100, bias=True)
  (fc2): Linear(in_features=100, out_features=10, bias=True)
)
"""
!pip install torchsummary
from torchsummary import summary
summary(model, (1, 28, 28))
"""
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1           [-1, 32, 28, 28]             320
            Conv2d-2           [-1, 64, 26, 26]          18,496
         MaxPool2d-3           [-1, 64, 13, 13]               0
           Flatten-4                [-1, 10816]               0
            Linear-5                  [-1, 100]       1,081,700
            Linear-6                   [-1, 10]           1,010
================================================================
Total params: 1,101,526
Trainable params: 1,101,526
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.69
Params size (MB): 4.20
Estimated Total Size (MB): 4.89
----------------------------------------------------------------
"""

 

 

실습 (Fashion MNIST (grayscale))

 

CNN: 흑백 FashionMNIST 데이터셋 활용

https://github.com/zalandoresearch/fashion-mnist

 

GitHub - zalandoresearch/fashion-mnist: A MNIST-like fashion product database. Benchmark

A MNIST-like fashion product database. Benchmark :point_down: - GitHub - zalandoresearch/fashion-mnist: A MNIST-like fashion product database. Benchmark

github.com

 

  • 1. 데이터 로드
from torchvision import datasets

train_dataset = datasets.FashionMNIST(root='./', train=True, download=True)
test_dataset = datasets.FashionMNIST(root='./', train=False, download=True)

train_dataset, test_dataset
"""
(Dataset FashionMNIST
     Number of datapoints: 60000
     Root location: ./
     Split: Train,
 Dataset FashionMNIST
     Number of datapoints: 10000
     Root location: ./
     Split: Test)
"""

print(train_dataset[0])
print(train_dataset.data[0])	# 픽셀 단위로 해상도를 
print(train_dataset.data.shape)	# torch.Size([60000, 28, 28])
# 전처리 (채널차원 추가 및 정규화)
train_images = train_dataset.data.unsqueeze(1) / 255.0  # 0~1
train_images = (train_images - 0.5) / 0.5                           # -1~1
train_labels = train_dataset.targets.long()

test_images = test_dataset.data.unsqueeze(1) / 255.0
test_images = (test_images - 0.5) / 0.5
train_labels = test_dataset.targets.long()
# 학습/검증 데이터셋 분할
from sklearn.model_selection import train_test_split

train_idx, val_idx = train_test_split(range(len(train_images)), test_size=0.15, random_state=42)

tr_images = train_images[train_idx]
tr_labels = train_labels[train_idx]
val_images = train_images[val_idx]
val_labels = train_labels[val_idx]

 

데이터 시각화

import numpy as np
import matplotlib.pyplot as plt

random_idx = np.random.choice(len(tr_images), 10, replace=False)
random_images = tr_images[random_idx]
random_labels = tr_labels[random_idx]

class_name = train_dataset.classes

plt.figure(figsize=(12, 6))
for i in range(10):
    plt.subplot(2, 5, i+1)
    
    img = random_images[i].squeeze()
    plt.imshow(img, cmap='gray')
    plt.title(f"Label: {class_name[random_labels[i]]}")
    plt.axis('off')

plt.tight_layout()
plt.show()

 

  • 2. 모델 생성
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

class FashionMNISTCNN(nn.Module):
    def __init__(self):
        super().__init__()

        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding='same')
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding='valid')
        self.pool = nn.MaxPool2d(2, 2)

        self.flatten = nn.Flatten()
        self.dropout1 = nn.Dropout(0.3)
        self.fc1 = nn.Linear(64 * 13 * 13, 100)
        self.dropout2 = nn.Dropout(0.2)
        self.fc2 = nn.Linear(100, 10)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = F.relu(self.conv2(x))
        x = self.pool(x)

        x = self.flatten(x)
        x = self.dropout1(x)
        x = F.relu(self.fc1(x))
        x = self.dropout2(x)
        return self.fc2(x)

model = FashionMNISTCNN()
model
"""
FashionMNISTCNN(
  (conv1): Conv2d(1, 32, kernel_size=(3, 3), stride=(1, 1), padding=same)
  (conv2): Conv2d(32, 64, kernel_size=(3, 3), stride=(1, 1), padding=valid)
  (pool): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
  (flatten): Flatten(start_dim=1, end_dim=-1)
  (dropout1): Dropout(p=0.3, inplace=False)
  (fc1): Linear(in_features=10816, out_features=100, bias=True)
  (dropout2): Dropout(p=0.2, inplace=False)
  (fc2): Linear(in_features=100, out_features=10, bias=True)
)
"""
from torchsummary import summary

summary(model, (1, 28, 28), device="cpu")
"""
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1           [-1, 32, 28, 28]             320
            Conv2d-2           [-1, 64, 26, 26]          18,496
         MaxPool2d-3           [-1, 64, 13, 13]               0
           Flatten-4                [-1, 10816]               0
           Dropout-5                [-1, 10816]               0
            Linear-6                  [-1, 100]       1,081,700
           Dropout-7                  [-1, 100]               0
            Linear-8                   [-1, 10]           1,010
================================================================
Total params: 1,101,526
Trainable params: 1,101,526
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.00
Forward/backward pass size (MB): 0.77
Params size (MB): 4.20
Estimated Total Size (MB): 4.98
----------------------------------------------------------------
"""

 

  • 3. 모델 학습
from torch.utils.data import TensorDataset, DataLoader

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)

batch_size = 128
train_dataset = TensorDataset(tr_images, tr_labels)
val_dataset = TensorDataset(val_images, val_labels)
test_dataset = TensorDataset(test_images, test_labels)

train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)
test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)
history = {
    "train_loss": [],
    "train_acc": [],
    "val_loss": [],
    "val_acc": []
}

epochs = 30
for epoch in range(epochs):
  model.train()
  train_loss = 0.0
  train_total = 0
  train_correct = 0

  for batch_idx, (images, labels) in enumerate(train_loader):
    images, labels = images.to(device), labels.to(device)

    optimizer.zero_grad()
    outputs = model(images)
    loss = criterion(outputs, labels)
    loss.backward()
    optimizer.step()

    train_loss += loss.detach().cpu().item()
    _, pred = torch.max(outputs.data, 1)
    train_total += labels.size(0)
    train_correct += (pred == labels).sum().detach().cpu().item()

  model.eval()
  val_loss = 0.0
  val_total = 0
  val_correct = 0

  with torch.no_grad():
    for batch_idx, (images, labels) in enumerate(val_loader):
      images, labels = images.to(device), labels.to(device)
      outputs = model(images)
      loss = criterion(outputs, labels)

      val_loss += loss.detach().cpu().item()
      _, pred = torch.max(outputs.data, 1)
      val_total += labels.size(0)
      val_correct += (pred == labels).sum().detach().cpu().item()

  train_loss /= len(train_loader)
  val_loss /= len(val_loader)
  train_acc = train_correct / train_total
  val_acc = val_correct / val_total

  history["train_loss"].append(train_loss)
  history["train_acc"].append(train_acc)
  history["val_loss"].append(val_loss)
  history["val_acc"].append(val_acc)

  if (epoch + 1) % 5 == 0:
    print(f"Epoch [{epoch + 1}/{epochs}] {train_acc=:.4f} {train_loss=:.4f} {val_acc=:.4f} {val_loss=:.4f}")
# 학습 시각화
plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.plot(history['train_loss'], label='train_loss')
plt.plot(history['val_loss'], label='val_loss')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(history['train_acc'], label='train_acc')
plt.plot(history['val_acc'], label='val_acc')
plt.legend()

plt.tight_layout()
plt.show()

 

  • 4. 모델 평가
model.eval()
test_loss = 0.0
test_total = 0
test_correct = 0

with torch.no_grad():
  for batch_idx, (images, labels) in enumerate(test_loader):
    images, labels = images.to(device), labels.to(device)
    outputs = model(images)
    loss = criterion(outputs, labels)

    test_loss += loss.detach().cpu().item()
    _, pred = torch.max(outputs.data, 1)
    test_total += labels.size(0)
    test_correct += (pred == labels).sum().detach().cpu().item()

test_loss /= len(test_loader)
test_acc = test_correct / test_total

print(f"Test 결과: {test_acc=:.4f} {test_loss=:.4f}")

 

  • 5. 특성맵 시각화
sample_image = tr_images[1].squeeze()
plt.imshow(sample_image, cmap='gray')
plt.axis('off')
plt.show()

class FeatureExtractor: 
    def __init__(self, model, layer_name):
        self.model = model
        self.layer_name = layer_name
        self.features = None

        layer = dict(model.named_modules())[layer_name]
        layer.register_forward_hook(self.hook)

    def hook(self, module, input, output):
        self.features = output

    def get_features(self, x):
        self.model.eval()
        with torch.no_grad():
            _ = self.model(x)
        return self.features
        
inputs = tr_images[1:2].to(device)

conv1_extractor = FeatureExtractor(model, 'conv1')
feature_maps = conv1_extractor.get_features(inputs)
print(feature_maps.shape)	# torch.Size([1, 32, 28, 28])

feature_maps_np = feature_maps.cpu().numpy()

fig, axs = plt.subplots(4, 8, figsize=(15, 8))

for i in range(4):
    for j in range(8):
        axs[i, j].imshow(feature_maps_np[0, i * 8 + j, :, :], cmap='viridis')
        axs[i, j].axis('off')
        axs[i, j].set_title(f"Filter {i * 8 + j}", fontsize=8)

plt.suptitle("1st Conv Layer's Feature Maps")
plt.tight_layout()
plt.show()

inputs = tr_images[1:2].to(device)

conv2_extractor = FeatureExtractor(model, 'conv2')
feature_maps = conv2_extractor.get_features(inputs)
print(feature_maps.shape)	# torch.Size([1, 64, 26, 26])

feature_maps_np = feature_maps.cpu().numpy()

fig, axs = plt.subplots(8, 8, figsize=(15, 8))

for i in range(8):
    for j in range(8):
        axs[i, j].imshow(feature_maps_np[0, i * 8 + j, :, :], cmap='viridis')
        axs[i, j].axis('off')
        axs[i, j].set_title(f"Filter {i * 8 + j}", fontsize=8)

plt.suptitle("2nd Conv Layer's Feature Maps")
plt.tight_layout()
plt.show()

 

 

실습 (Color Image CNN - CIFAR10, colab)

 

CNN: 컬러 CIFAR10 데이터셋 활

💧 MNIST와 거의 동일하며, 컬러 이미지라 픽셀 수가 변한 정도

https://www.cs.toronto.edu/~kriz/cifar.html

 

CIFAR-10 and CIFAR-100 datasets

< Back to Alex Krizhevsky's home page The CIFAR-10 and CIFAR-100 datasets are labeled subsets of the 80 million tiny images dataset. CIFAR-10 and CIFAR-100 were created by Alex Krizhevsky, Vinod Nair, and Geoffrey Hinton. The CIFAR-10 dataset The CIFAR-10

www.cs.toronto.edu

 

  • 1. 데이터 로드
from torchvision.datasets import CIFAR10
from torchvision import transforms

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))  # 데이터 정규화 (RGB 평균, 표준편차)
])

trainset = CIFAR10(root='./', train=True, download=True, transform=transform)
testset = CIFAR10(root='./', train=False, download=True, transform=transform)
from torch.utils.data import DataLoader

batch_size = 64
train_loader = DataLoader(trainset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(testset, batch_size=batch_size, shuffle=False)

train_iter = iter(train_loader)
images, labels = next(train_iter)

print(images.shape)
print(labels.shape)
"""
torch.Size([64, 3, 32, 32])		# 3: 채널, 32: 가로, 32: 세로
torch.Size([64])
"""

first_image, first_label = images[0], labels[0]
print(first_image)
print(first_label)
print(trainset.classes)
"""
tensor([[[-0.6314, -0.5686, -0.6549,  ..., -0.5373, -0.5137, -0.4745],		# R
         [-0.6471, -0.6078, -0.6235,  ..., -0.5765, -0.5608, -0.5137],
         [-0.6392, -0.6000, -0.6314,  ..., -0.5686, -0.5686, -0.5608],
         ...,
         [-0.6314, -0.6157, -0.6392,  ..., -0.6863, -0.6314, -0.5765],
         [-0.6392, -0.6549, -0.6863,  ..., -0.6627, -0.6078, -0.5686],
         [-0.4902, -0.4510, -0.4275,  ..., -0.5608, -0.5451, -0.5451]],

        [[-0.6314, -0.5765, -0.6627,  ..., -0.3725, -0.3647, -0.3569],		# G
         [-0.6471, -0.6078, -0.6314,  ..., -0.4353, -0.4196, -0.3804],
         [-0.6392, -0.6078, -0.6392,  ..., -0.4510, -0.4431, -0.4196],
         ...,
         [-0.6392, -0.6471, -0.6941,  ..., -0.7176, -0.6706, -0.6314],
         [-0.6392, -0.6706, -0.7333,  ..., -0.7098, -0.6627, -0.6235],
         [-0.5059, -0.4902, -0.4902,  ..., -0.6000, -0.5922, -0.5843]],

        [[-0.5451, -0.4667, -0.5373,  ..., -0.3647, -0.3569, -0.3333],		# B
         [-0.5686, -0.5059, -0.5137,  ..., -0.4196, -0.4039, -0.3647],
         [-0.5529, -0.4980, -0.5216,  ..., -0.4275, -0.4275, -0.4039],
         ...,
         [-0.5765, -0.5686, -0.6078,  ..., -0.6000, -0.5608, -0.5529],
         [-0.6000, -0.6314, -0.6314,  ..., -0.6314, -0.5843, -0.5608],
         [-0.4667, -0.4431, -0.3804,  ..., -0.5529, -0.5373, -0.5294]]])
tensor(8)
['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
"""

 

  • 2. 데이터 시각화
import matplotlib.pyplot as plt
import torch

loader = DataLoader(trainset, batch_size=16, shuffle=True)
images, labels = next(iter(loader))

fig, axes = plt.subplots(1, 8, figsize=(22, 4))

for i in range(8):
    img = images[i].permute(1, 2, 0)
    img = (img * 0.5) + 0.5
    img = torch.clamp(img, 0, 1)	# 0과 1을 넘는 범위값들 정리
    axes[i].imshow(img.numpy())

    axes[i].set_title(trainset.classes[labels[i]])
    axes[i].axis('off')
plt.show()

 

  • 3. 모델 생성
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

class CIFAR10_CNN(nn.Module):
    def __init__(self):
        super().__init__()

        # conv 블럭 1
        self.conv1_1 = nn.Conv2d(3, 32, kernel_size=3, padding=1)
        self.conv1_2 = nn.Conv2d(32, 32, kernel_size=3, padding=1)
        self.pool1 = nn.MaxPool2d(kernel_size=2, stride=2)      # 16*16

        # conv 블럭 2
        self.conv2_1 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.conv2_2 = nn.Conv2d(64, 64, kernel_size=3, padding=1)
        self.pool2 = nn.MaxPool2d(kernel_size=2, stride=2)      # 8*8

        # conv 블럭 3
        self.conv3_1 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        self.conv3_2 = nn.Conv2d(128, 128, kernel_size=3, padding=1)
        self.pool3 = nn.MaxPool2d(kernel_size=2, stride=2)      # 4*4

        # 완전연결층
        self.flatten = nn.Flatten()
        self.dropout1 = nn.Dropout(0.5)
        self.fc1 = nn.Linear(128 * 4 * 4, 300)
        self.dropout2 = nn.Dropout(0.3)
        self.fc2 = nn.Linear(300, 10)

    def forward(self, x):
        x = F.relu(self.conv1_1(x))
        x = F.relu(self.conv1_2(x))
        x = self.pool1(x)

        x = F.relu(self.conv2_1(x))
        x = F.relu(self.conv2_2(x))
        x = self.pool2(x)

        x = F.relu(self.conv3_1(x))
        x = F.relu(self.conv3_2(x))
        x = self.pool3(x)

        x = self.flatten(x)
        x = self.dropout1(x)
        x = F.relu(self.fc1(x))
        x = self.dropout2(x)
        return self.fc2(x)
        
model = CIFAR10_CNN()
print(model)
from torchsummary import summary

summary(model, (3, 32, 32), device='cpu')
"""
----------------------------------------------------------------
        Layer (type)               Output Shape         Param #
================================================================
            Conv2d-1           [-1, 32, 32, 32]             896
            Conv2d-2           [-1, 32, 32, 32]           9,248
         MaxPool2d-3           [-1, 32, 16, 16]               0
            Conv2d-4           [-1, 64, 16, 16]          18,496
            Conv2d-5           [-1, 64, 16, 16]          36,928
         MaxPool2d-6             [-1, 64, 8, 8]               0
            Conv2d-7            [-1, 128, 8, 8]          73,856
            Conv2d-8            [-1, 128, 8, 8]         147,584
         MaxPool2d-9            [-1, 128, 4, 4]               0
          Flatten-10                 [-1, 2048]               0
          Dropout-11                 [-1, 2048]               0
           Linear-12                  [-1, 300]         614,700
          Dropout-13                  [-1, 300]               0
           Linear-14                   [-1, 10]           3,010
================================================================
Total params: 904,718
Trainable params: 904,718
Non-trainable params: 0
----------------------------------------------------------------
Input size (MB): 0.01
Forward/backward pass size (MB): 1.02
Params size (MB): 3.45
Estimated Total Size (MB): 4.48
----------------------------------------------------------------
"""

 

  • 4. 모델 학습
from torch.optim.lr_scheduler import ReduceLROnPlateau
from torch.utils.data import random_split

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=3)

train_size = int(0.85 * len(trainset))
val_size = len(trainset) - train_size

train_dataset, val_dataset = random_split(trainset, [train_size, val_size])
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False)

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
from tqdm import tqdm

history = {
    "train_loss": [],
    "train_acc": [],
    "val_loss": [],
    "val_acc": []
}

epochs = 30

best_val_loss = float("inf")
patience = 5
patience_counter = 0

for epoch in tqdm(range(epochs)):
    model.train()
    train_loss = 0.0
    train_total = 0
    train_correct = 0

    for batch_idx, (images, labels) in enumerate(train_loader):
        images, labels = images.to(device), labels.to(device)

        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        train_loss += loss.detach().cpu().item()
        _, pred = torch.max(outputs.data, 1)
        train_total += labels.size(0)
        train_correct += (pred == labels).sum().detach().cpu().item()

    model.eval()
    val_loss = 0.0
    val_total = 0
    val_correct = 0

    with torch.no_grad():
        for batch_idx, (images, labels) in enumerate(val_loader):
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            loss = criterion(outputs, labels)

            val_loss += loss.detach().cpu().item()
            _, pred = torch.max(outputs.data, 1)
            val_total += labels.size(0)
            val_correct += (pred == labels).sum().detach().cpu().item()

    train_loss /= len(train_loader)
    val_loss /= len(val_loader)
    train_acc = train_correct / train_total
    val_acc = val_correct / val_total

    history["train_loss"].append(train_loss)
    history["train_acc"].append(train_acc)
    history["val_loss"].append(val_loss)
    history["val_acc"].append(val_acc)

    if (epoch + 1) % 5 == 0:
        print(f"Epoch [{epoch + 1}/{epochs}] {train_acc=:.4f} {train_loss=:.4f} {val_acc=:.4f} {val_loss=:.4f}")

    scheduler.step(val_loss)

    if val_loss < best_val_loss:
        best_val_loss = val_loss
        patience_counter = 0
    else:
        patience_counter += 1
    
    if patience_counter >= patience:
        print(f"조기 종료 ... epoch: {epoch + 1}")
        break
# 학습 시각화
plt.figure(figsize=(12, 4))

plt.subplot(1, 2, 1)
plt.plot(history['train_loss'], label='train_loss')
plt.plot(history['val_loss'], label='val_loss')
plt.legend()

plt.subplot(1, 2, 2)
plt.plot(history['train_acc'], label='train_acc')
plt.plot(history['val_acc'], label='val_acc')
plt.legend()

plt.tight_layout()
plt.show()

 

  • 5. 모델 평가
model.eval()
test_loss = 0.0
test_total = 0
test_correct = 0

with torch.no_grad():
    for batch_idx, (images, labels) in enumerate(test_loader):
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        loss = criterion(outputs, labels)

        test_loss += loss.detach().cpu().item()
        _, pred = torch.max(outputs.data, 1)
        test_total += labels.size(0)
        test_correct += (pred == labels).sum().detach().cpu().item()

test_loss /= len(test_loader)
test_acc = test_correct / test_total

print(f"Test 결과: {test_acc=:.4f} {test_loss=:.4f}")
# Test 결과: test_acc=0.8143 test_loss=0.7483