Developer's Development

3.3.22 [LLM] 자연어-이미지 멀티모달: 주요 CNN 모델 본문

LLM

3.3.22 [LLM] 자연어-이미지 멀티모달: 주요 CNN 모델

mylee 2025. 9. 28. 19:57
LeNet

 

LeNet-5는 1998년에 Yann LeCun 등이 발표한 합성곱 신경망으로, 손글씨 숫자 인식에 사용되었다.

LeNet은 딥러닝이 이미지 인식 분야에서 강력한 성능을 보일 수 있다는 것을 보여준 초기 모델이다.

 

 

AlexNet

 

AlexNet은 2012년 ILSVRC 대회에서 우승한 모델로, 딥러닝이 이미지 인식 분야에서 큰 혁신을 일으키는 계기가 되었다.

👉🏻 주요 특징

- ReLU 활성화 함수 사용: 시그모이드나 tanh 대신 ReLU를 사용하여 학습 속도를 크게 향상

- 드롭아웃(Dropout): 과적합을 방지하기 위해 드롭아웃 기법을 도입

- 데이터 증강: 데이터의 다양성을 높이기 위해 데이터 증강 기법을 적극 활용

- GPU 병렬 처리: 대규모 데이터셋과 모델을 학습하기 위해 GPU를 사용

 

 

VGGNet

 

VGGNet은 2014년 ILSVRC 대회에서 준으승한 모델로, 심층 신경망의 성능 향상을 보여준 대표적인 모델이다.

👉🏻 주요 특징

- 단순한 구조: 작은 3*3 필터를 사용하여 깊이를 깊게 쌓은 구조

- 일관성 있는 설계: 모든 합성곱 층에서 동일한 필터 크기와 패딩을 사용

- 심층 구조의 효과: 깊이를 깊게 함으로써 성능 향상을 달성

 

 

모델 비교와 선택 기준

 

1. 성능과 복잡도 비교

❗모델 복잡도

  - LeNet: 작은 규모의 모델로, 작은 데이터셋과 간단한 작업에 적합

  - AlexNet: LeNet보다 훨씬 큰 모델로, 대규모 이미지 분류 작업에 사용

  - VGGNet: AlexNet보다 깊이가 깊은 모델로, 더 나은 성능을 보임

성능 비교

  - LeNet: MNIST와 같은 간단한 데이터셋에서 높은 정확도 달성

  - AlexNet: ImageNet과 같은 대규모 데이터셋에서 우수한 성능

  - VGGNet: 더 깊은 구조로 인해 AlexNet보다 성능 향상

모델 크기와 연산량

  - 모델의 파라미터 수와 연산량이 증가할수록 더 많은 계산 자원이 필요

  - 응용 분야와 사용 가능한 자원에 따라 적절한 모델을 선택해야 함

 

2. 응용 분야에 따른 모델 선택

- 간단한 분류 작업: LeNet과 같은 작은 모델로도 충분

- 대규모 이미지 분류: AlexNet, VGGNet과 같은 대형 모델이 필요

- 실시간 처리: 연산량이 적은 경량 모델이 필요

 

 

[실습] 모델 성능 비교

 

1. LeNet, AlexNet, VGGNet의 성능 비교

목표

  - 동일한 데이터셋에 대해 세 가지 모델의 성능을 비교한다.

  - 학습 시간, 정확도, 모델 크기 등을 비교 분석한다.

실습 방법

  - 각 모델을 CIFAR-10 데이터셋에 대해 학습시킨다.

  - 모델의 학습 시간과 테스트 정확도를 기록한다.

예시 결과

  - LeNet: 학습 시간 짧음, 테스트 정확도 낮음

  - AlexNet: 학습 시간 중간, 테스트 정확도 중간

  - VGGNet: 학습시간 김, 테스트 정확도 높음

 

2. 각 모델의 장단점 분석

LeNet

  - 장점: 간단하고 빠르게 학습 가능

  - 단점: 복잡한 작업에는 성능이 부족

AlexNet

  - 장점: 대규모 데이터셋에서도 좋은 성능

  - 단점: 연산량이 많아 자원 소모 큼

VGGNet

  - 장점: 깊은 구조로 높은 성능 달성

  - 단점: 매우 큰 모델로, 학습과 추론에 많은 자원 필요

결론

  - 작업의 복잡도와 사용 가능한 자원에 따라 적절한 모델을 선택해야 한다.

  - 최신 모델은 더 나은 성능을 제공하지만, 항상 최적의 선택은 아닐 수 있다.

 

 

ResNet

 

  • 잔차 연결(Residual Connection)

Residual Connection은 깊은 신경망에서 학습이 어려운 문제를 해결하기 위해 도입된 개념이다.

입력 데이터를 몇 개의 층을 건너뛰어 출력을 더해주는 방식으로, 스킵 연결(Skip Connection)이라고도 한다.

이를 통해 기울기 소실 문제를 완화하고, 더 깊은 신경망의 학습을 가능하게 한다.

 

👉🏻 ResNet(Residual Network) 구조

2015년 마이크로소프트 연구팀이 발표한 딥러닝 모델로, 잔차 연결을 활용하여 152층까지의 깊은 신경망을 성공적으로 학습했다.

ILSVRC 2015에서 우승하며 딥러닝의 새로운 가능성을 열었다.

 

👉🏻 ResNet의 기본 블록

Identity Block: 입력과 출력의 차원이 동일한 경우 사용

Convolutional Block: 입력과 출력의 차원이 다른 경우, 1x1 합성곱으로 차원을 맞춰 사용

 

👉🏻 Residual Block의 구조 (Identity Block)

1. 입력 x를 첫 번째 합성곱 층으로 전달: 1x1 합성곱, 활성화 함수 (ReLU)

2. 두 번째 합성곱 층: 3x3 합성곱, 활성화 함수 (ReLU)

3. 세 번째 합성곱 층: 1x1 합성곱, 활성화 함수 없음

4. 입력 x와 합성곱 층을 통과한 출력 F(x)를 더함: F(x) + x

5. 합한 결과에 활성화 함수(ReLU) 적

 

 

현대 CNN 모델

 

1. InceptionNet

구글에서 개발한 모델로, 네트워크 깊이뿐만 아니라 폭을 늘려 성능을 향상시켰다.

Inception Module을 사용하여 연산 효율성을 높이고 다양한 규모의 특징을 추출한다.

 

2. DenseNet

모든 레이어가 이전 레이어의 출력에 직접 연결되는 구조이다.

 

3. MobileNet

모바일 및 임베디드 기기에서의 효율적인 딥러닝 모델을 위해 개발되었다.

경량 모델이지만, 정확도를 유지한다.

 

 

실습 (AlexNet)

 

  • 1. 모델 생성
import torch
import torch.nn as nn

IMAGE_SIZE = 227

class AlexNet(nn.Module):
    def __init__(self, num_classes=10):
        super(AlexNet, self).__init__()

        self.features = nn.Sequential(
            nn.Conv2d(3, 96, kernel_size=11, stride=4, padding=0),
            nn.BatchNorm2d(96),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),

            nn.Conv2d(96, 256, kernel_size=5, stride=1, padding=2),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2),

            nn.Conv2d(256, 384, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(384),
            nn.ReLU(inplace=True),

            nn.Conv2d(384, 384, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(384),
            nn.ReLU(inplace=True),

            nn.Conv2d(384, 256, kernel_size=3, stride=1, padding=1),
            nn.BatchNorm2d(256),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=3, stride=2)
        )

        dummy_input = torch.zeros(1, 3, IMAGE_SIZE, IMAGE_SIZE)
        dummy_output = self.features(dummy_input)
        flatten_dim = dummy_output.view(1, -1).shape[1]

        self.classifier = nn.Sequential(
            nn.Flatten(),

            nn.Linear(flatten_dim, 4096),
            nn.ReLU(inplace=True), # Corrected from RELU to ReLU
            nn.Dropout(0.5),

            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True), # Corrected from RELU to ReLU
            nn.Dropout(0.5),

            nn.Linear(4096, num_classes)
        )

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x)
        return x

 

  • 2. 커스텀 Dataset 클래스 생성
from torch.utils.data import Dataset
import numpy as np
import cv2
from sklearn.utils import shuffle

BATCH_SIZE = 64

class CIFAR100Dataset(Dataset):
    def __init__(self, images, labels=None, image_size=IMAGE_SIZE, augmentor=None,
                        preprocess_function=None, shuffle_data=False):
        self.images = images
        self.labels = labels
        self.image_size = image_size
        self.augmentor = augmentor
        self.preprocess_function = preprocess_function

        if shuffle_data and labels is not None:
            self.images, self.labels = shuffle(self.images, self.labels)

    def __len__(self):
        return len(self.images)

    def __getitem__(self, idx):
        image = self.images[idx]
        label = self.labels[idx] if self.labels is not None else None

        if self.augmentor is not None:
            image = self.augmentor(image)['image']
        
        image = cv2.resize(image, (self.image_size, self.image_size))

        if self.preprocess_function is not None:
            image = self.preprocess_function(image)
        image = np.transpose(image, (2, 0, 1)).astype(np.float32)

        if label is not None:
            return image, label
        else:
            return image

 

  • 3. 데이터 로드
from torchvision.datasets import CIFAR10
from sklearn.model_selection import train_test_split
import numpy as np
import torch
import torch.nn.functional as F

train_data = CIFAR10(root='./', train=True, download=True)
test_data = CIFAR10(root='./', train=False, download=True)

train_images = np.array(train_data.data)
train_labels = np.array(train_data.targets).reshape(-1, 1)
test_images = np.array(test_data.data)
test_labels = np.array(test_data.targets).reshape(-1, 1)

tr_images, val_images, tr_labels, val_labels = train_test_split(train_images, train_labels, test_size=0.2, random_state=42)

tr_labels = F.one_hot(torch.tensor(tr_labels.squeeze()), num_classes=10).numpy()
val_labels = F.one_hot(torch.tensor(val_labels.squeeze()), num_classes=10).numpy()
test_labels = F.one_hot(torch.tensor(test_labels.squeeze()), num_classes=10).numpy()
def pre_func(image):
    return image / 255.0
from torch.utils.data import DataLoader

tr_dataset = CIFAR100Dataset(tr_images, tr_labels, preprocess_function=pre_func, shuffle_data=True)
val_dataset = CIFAR100Dataset(val_images, val_labels, preprocess_function=pre_func)
test_dataset = CIFAR100Dataset(test_images, test_labels, preprocess_function=pre_func)

tr_loader = DataLoader(tr_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)

 

  • 4. 모델 학습
from torch.optim import Adam
from torch.optim.lr_scheduler import ReduceLROnPlateau

model = AlexNet(num_classes=10)
model = model.cuda() if torch.cuda.is_available() else model
device = next(model.parameters()).device

criterion = nn.CrossEntropyLoss()
optim = Adam(model.parameters())
scheduler = ReduceLROnPlateau(optim, mode='min', patience=5, factor=0.5)
class EarlyStopping:
    def __init__(self, patience=10, verbose=False):
        self.patience = patience
        self.verbose = verbose
        self.counter = 0
        self.best_loss = float('inf')
        self.best_model = None
        self.early_stop = False

    def __call__(self, val_loss, model):
        if val_loss < self.best_loss:
            self.best_loss = val_loss
            self.best_model = model.state_dict()
            self.counter = 0
        else:
            self.counter += 1
            if self.verbose:
                print(f"조기종료 Counter: {self.counter}/{self.patience}")
            if self.counter >= self.patience:
                self.early_stop = True

early_stopping = EarlyStopping(patience=10, verbose=True)
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 images, labels in tr_loader:
        images, labels = images.to(device), labels.to(device)
        labels = torch.argmax(labels, dim=1)

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

        train_loss += loss.detach().cpu().item()
        _, pred = torch.max(outputs, 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 images, labels in val_loader:
            images, labels = images.to(device), labels.to(device)
            labels = torch.argmax(labels, dim=1)
            outputs = model(images)
            loss = criterion(outputs, labels)

            valloss += loss.detach().cpu().item()
            , pred = torch.max(outputs, 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)

    scheduler.step(val_loss)
    early_stopping(val_loss, model)
    if early_stopping.early_stop:
        print("조기 종료!!!")
        break
import pandas as pd
import matplotlib.pyplot as plt

df = pd.DataFrame(history)
df.plot()
plt.show()

model.eval()
test_loss = 0.0
test_total = 0
test_correct = 0

with torch.no_grad():
    for images, labels in 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.8499 test_loss=0.7628

 

 

실습 (VGGNet)

 

import torch
import torch.nn as nn

class VGGNet(nn.Module):
    def __init__(self, num_classes=1000):
        super(VGGNet, self).__init__()

        self.features = nn.Sequential(
            # Block 1
            nn.Conv2d(3, 64, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            # Block 2
            nn.Conv2d(64, 128, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 128, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            # Block 3
            nn.Conv2d(128, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(256, 256, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            # Block 4
            nn.Conv2d(256, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),

            # Block 5
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(512, 512, kernel_size=3, padding=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2),
        )

        self.classifier = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),

            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(),

            nn.Linear(4096, num_classes)
        )

    def forward(self, x):
        return self.classifier(self.features(x))

 

 

실습 (ResNet)

 

import torch
import torch.nn as nn

class IdentityBlock(nn.Module):
  def __init__(self, in_channels, mid_kernel_size, filters):
    super(IdentityBlock, self).__init__()
    filter1, filter2, filter3 = filters

    self.conv1 = nn.Conv2d(in_channels, filter1, kernel_size=1, padding=0)
    self.bn1 = nn.BatchNorm2d(filter1)
    self.relu1 = nn.ReLU(inplace=True)

    self.conv2 = nn.Conv2d(filter1, filter2, kernel_size=mid_kernel_size, padding=mid_kernel_size//2)
    self.bn2 = nn.BatchNorm2d(filter2)
    self.relu2 = nn.ReLU(inplace=True)

    self.conv3 = nn.Conv2d(filter2, filter3, kernel_size=1, padding=0)
    self.bn3 = nn.BatchNorm2d(filter3)
    self.relu_out = nn.ReLU(inplace=True)

  def forward(self, x):

    identity = x

    out = self.conv1(x)
    out = self.bn1(out)
    out = self.relu1(out)

    out = self.conv2(out)
    out = self.bn2(out)
    out = self.relu2(out)

    out = self.conv3(x)
    out = self.bn3(out)

    out += identity

    out = self.relu_out(out)
    return out
import torch.nn.functional as F

class ConvolutionalBlock(nn.Module):
  def __init__(self, in_channels, mid_kernel_size, filters):
    super(ConvolutionalBlock, self).__init__()
    filter1, filter2, filter3 = filters

    self.conv1 = nn.Conv2d(in_channels, filter1, kernel_size=1, stride=2)
    self.bn1 = nn.BatchNorm2d(filter1)

    self.conv2 = nn.Conv2d(filter1, filter2, kernel_size=mid_kernel_size, padding=mid_kernel_size//2)
    self.bn2 = nn.BatchNorm2d(filter2)

    self.conv3 = nn.Conv2d(filter2, filter3, kernel_size=1, stride=2)
    self.bn3 = nn.BatchNorm2d(filter3)

    self.shortcut_conv = nn.Conv2d(in_channels, filter3, kernel_size=1, stride=2)
    self.shortcut_bn = nn.BatchNorm2d(filter3)

  def forward(self, x):
    shortcut = self.shortcut_bn(self.shortcut_conv(x))

    out = F.relu(self.bn1(self.conv1(x)))
    out = F.relu(self.bn2(self.conv2(out)))
    out = self.bn3(self.conv3(out))

    out += shortcut
    out = F.relu(out)
    return out
class ResNet(nn.Module):
  def __init__(self):
    super(ResNet, self).__init__()

    self.stage0 = nn.Sequential(
        nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),  # (224,224,3) -> (112,112,64)
        nn.BatchNorm2d(64),
        nn.ReLU(inplace=True),
        nn.MaxPool2d(kernel_size=3, stride=2, padding=1)  # (56,56,64)
    )

    self.stage1 = nn.Sequential(
        ConvolutionalBlock(64, 3, (64, 64, 256), strides=1),
        IdentityBlock(256, 3, (64, 64, 256)),
        IdentityBlock(256, 3, (64, 64, 256))
    )

    self.stage2 = nn.Sequential(
        ConvolutionalBlock(256, 3, (128, 128, 512), strides=2),
        IdentityBlock(512, 3, (128, 128, 512)),
        IdentityBlock(512, 3, (128, 128, 512)),
        IdentityBlock(512, 3, (128, 128, 512))
    )

    self.stage3 = nn.Sequential(
        ConvolutionalBlock(512, 3, (256, 256, 1024), strides=2),
        IdentityBlock(1024, 3, (256, 256, 1024)),
        IdentityBlock(1024, 3, (256, 256, 1024)),
        IdentityBlock(1024, 3, (256, 256, 1024)),
        IdentityBlock(1024, 3, (256, 256, 1024)),
        IdentityBlock(1024, 3, (256, 256, 1024))
    )

    self.stage4 = nn.Sequential(
        ConvolutionalBlock(1024, 3, (512, 512, 2048), strides=2),
        IdentityBlock(2048, 3, (512, 512, 2048)),
        IdentityBlock(2048, 3, (512, 512, 2048))
    )

    self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
    self.dropout1 = nn.Dropout(0.5)
    self.fc1 = nn.Linear(2048, 500)
    self.dropout2 = nn.Dropout(0.5)
    self.fc2 = nn.Linear(500, 1000)

  def forward(self, x):
    x = self.stage0(x)
    x = self.stage1(x)
    x = self.stage2(x)
    x = self.stage3(x)
    x = self.stage4(x)
    x = self.avgpool(x)
    x = torch.flatten(x, 1)
    x = self.dropout1(x)
    x = F.relu(self.fc1(x))
    x = self.dropout2(x)
    x = self.fc2(x)
    x = F.softmax(x, dim=1)
    return x

 

  • 사전학습 모델 로드
import torchvision.models as models

vgg_model = models.vgg16(pretrained=True)
resnet_model = models.resnet50(pretrained=True)
inception_model = models.inception_v3(pretrained=True, aux_logits=True)
mobilenet_model = models.mobilenet_v2(weights=models.MobileNet_V2_Weights.DEFAULT)