AI 활용 소프트웨어 개발/AI, 머신러닝, 딥러닝

Resnet(Deep Residual Learning for Image Recognition)논문 요약 및 구현

ha2yong 2025. 8. 5. 17:52

1. Deep Residual Learning for Image Recognition(2015) 요약

[문제]

깊은 계층 모델에서의 최적화 문제

- 기존 cnn구조에서 학습계층이 깊어질수록 학습 효과가 포화하다가 급격히 감소하는 현상 발생했다.

- 동일한 출력을 하는 학습 층을 쌓기만 해도 오차가 증가하는 현상이 발생했다. 

 

[해결방안]

1. Residual Learning funtion

- Residual Learning이란, 모델이 복잡한 비선형함수 H(x)를 직접 학습하는 대신 잔차함수 F(x)를 학습하도록하여, 깊은 계층에서의 학습 난이도를 낮추는 방법이다.

- 모델은 입력값 x에 대한 작은 변화(잔차; F(x)= H(X)-x)를 학습하기 때문에 연산량이 대폭으로 감소한다. 

- 모델은 F(x) = H(X)-x 의 잔차함수에 근사하도록 w(가중치)와 b(절편)을 조정한다. 

 

2. Shortcut connection(skip connection)

- Shortcut connection이란, 입력값을 그대로 다음 계층으로 전달하는 알고리즘을 말한다.

- Resnet에서는 Residual Learning function F(x) 에 +x 를 하도록 표현된다.

- H(X) = F(x) + x 의 형태에서 잔차함수 F(x)가 0이되면 입력값 x를 그대로 출력하고, 다음층에 동일한 x를 전달하게 된다.

- Shortcut connection을 통해서, ① 입력값이 다음계층으로 그대로 전달되므로(= 역전파 시 기울기 흐름을 원활히 해줌), 학습과정에서 기울기 소실문제(vanishing gradient)를 해결할 수 있고, ② 잔차함수 F(x)에 +x를 더해줌으로써 최종적으로 목표함수 H(x)에 근사할 수 있도록 한다.

 

3. Resnet-34 블록

- Resnet-34 는 Residual Learning function F(x)와 Shortcut connection이 하나의 Block을 형성한다.

- Residual Learning function F(x)는 conv-ReLu-Conv 의 비선형 구조로 표현한다.

- Shortcut connection은 +x 로 표현된다.

- 하나의 Block은 F(x) + x로 표현되며 이는 결과적으로 목표함수 H(x)에 근사하는 효과를 지닌다.

 

4. Resnet-34 구조

Stage Feature Map 크기 채널 수(C) 블록 개수 Stride 차원 변화 내용
Conv1 112×112 64 1 stride=2 시작
MaxPool 56×56 64 0 stride=2 해상도 감소
Stage 1 56×56 64 3 stride=1 차원 동일
Stage 2 28×28 128 4 stride=2 채널 증가 + 해상도 감소
Stage 3 14×14 256 6 stride=2 채널 증가 + 해상도 감소
Stage 4 7×7 512 3 stride=2 채널 증가 + 해상도 감소
Global AvgPool 1×1 512 0 - 출력 전
FC (Softmax) - 1000 0 - 클래스 예측

 

[결론]

- Resnet모델은 Residual Learning 과 Shortcut connection 구조를 통해 복잡한 비선형함수의 근사치를 학습하여, 기존 깊은 계층CNN의 "기울기 소실 문제"와 "학습의 복잡성"을 해결하였다.

- 결론적으로 최적화된 깊은 계층을 통해 Reset은 이미지의 복잡한 특징을 학습할 수 있게 되었다.

 


 

2. Resnet 구현

[이미지 전처리]

①transform 객체 생성

# training 용 transform은 Resnet 논문 3.4. Implementation 부분을 참조하여 만들어봄.
train_transform = transforms.Compose([
    transforms.RandomCrop(32, padding=4), # resnet의 랜덤 샘플링을 RandomCrop으로 구현해봄
    transforms.ColorJitter(0.4, 0.4, 0.4, 0.1), # Alexnet기반 컬러데이터 증강 대신 ColorJitter로 구현해봄
    transforms.ToTensor(),
    transforms.RandomHorizontalFlip(0.5), # resnet 수평반전
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # resnet 정규화
])

# val, test 용 transform
val_test_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # resnet 정규화
])

 

② 데이터 셋 + 데이터로더 만들기

# 학습 데이터셋 로드
trainset = torchvision.datasets.CIFAR10(
    root='./data',
    train=True,
    download=True,
    transform=train_transform
)

# 테스트 데이터셋 로드
testset = torchvision.datasets.CIFAR10(
    root='./data',
    train=False,
    download=True,
    transform=val_test_transform
)
train_loader = DataLoader(trainset, batch_size=128, shuffle=True, num_workers=2, pin_memory=True)
test_loader = DataLoader(testset, batch_size=128, shuffle=False, num_workers=2, pin_memory=True)

# 일단 batch_size를 논문과 같이 CIFAR-10 테스트 버전: 128으로 해보고 결과를 확인해본다.

[모델]

①모델 구현

아래 구현모델 "My_Resnet_CIFAR_10"은 Resnet논문 4.2.CIFAR-10andAnalysis 에 따라 구현되었다.

대략적인 구조는 Resnet-34를 기반으로 하되, CIFAR-10(낮은해상도 + 10개 클래스)에 적합한 좀더 간단한 구조 Resnet-20으로 구현해 보았다.

#  My_Resnet_CIFAR_10: CIFAR-10 전용 간단한 Resnet 구조. 논문의 CIFAR-10 학습방식 ResNet-20에 따름
# 1. 첫 번째 레이어는 3×3 합성곱
# 2. 32→16→8 해상도의 피처맵에 대해 3×3 합성곱 레이어를 총 6n개 쌓아 올리는데, 각 해상도에서 2n개의 레이어를 사용
# 3. 서브샘플링(subsampling)은 stride=2를 가진 합성곱으로 수행
# 4. 각 해상도를 거치면서 필터맵 수는 16,32,64개로 증가
# 5. 전역 평균 풀링(global average pooling), 10-클래스 완전연결층(fully-connected layer).

class Residual_Block_CIFAR_10(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)

        # projection shortcut (채널/공간 크기 다르면 1x1로 정렬)
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1,
                          stride=stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )
        else:
            self.shortcut = nn.Identity()

    def forward(self, x):
        identity = self.shortcut(x)
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        out += identity
        out = self.relu(out)
        return out


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

        self.stem = nn.Sequential(
            nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1, bias=False),
            nn.BatchNorm2d(16),
            nn.ReLU(inplace=True),
        )

        # stage1. 해상도 32 유지 , 채널 16유지
        self.stage1 = nn.Sequential(
            Residual_Block_CIFAR_10(16, 16, stride=1),
            Residual_Block_CIFAR_10(16, 16, stride=1),
            Residual_Block_CIFAR_10(16, 16, stride=1),
        )

        # stage2 해상도 32->16, 채널 16->32
        self.stage2 = nn.Sequential(
            Residual_Block_CIFAR_10(16, 32, stride=2),
            Residual_Block_CIFAR_10(32, 32, stride=1),
            Residual_Block_CIFAR_10(32, 32, stride=1),
        )

        # stage3 해상도 16->8, 채널 32->64
        self.stage3 = nn.Sequential(
            Residual_Block_CIFAR_10(32, 64, stride=2),
            Residual_Block_CIFAR_10(64, 64, stride=1),
            Residual_Block_CIFAR_10(64, 64, stride=1),
        )

        self.head = nn.Sequential(
            nn.AdaptiveAvgPool2d(1),
            nn.Flatten(),
            nn.Linear(64, num_classes)
        )

    def forward(self, x):
        x = self.stem(x)
        x = self.stage1(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.head(x)
        return x

 

② 손실함수, Optimizer 생성

criterion = nn.CrossEntropyLoss()

optimizer = optim.SGD(
    model.parameters(),
    lr=0.1,
    momentum=0.9,
    weight_decay=1e-4
)

 

③전체 My_Resnet_CIFAR_10 흐름

 

Layer Name Type Input Channels Output Channels Stride Kernel Size Output Size
stem.conv Conv2d 3 16 1 3×3 32×32
stem.bn BatchNorm2d 16 16 - - 32×32
stem.relu ReLU - - - - 32×32
stage1.block1 Residual Block 16 16 1 3×3×2 32×32
stage1.block2 Residual Block 16 16 1 3×3×2 32×32
stage1.block3 Residual Block 16 16 1 3×3×2 32×32
stage2.block1 Residual Block (stride=2) 16 32 2 3×3×2 16×16
stage2.block2 Residual Block 32 32 1 3×3×2 16×16
stage2.block3 Residual Block 32 32 1 3×3×2 16×16
stage3.block1 Residual Block (stride=2) 32 64 2 3×3×2 8×8
stage3.block2 Residual Block 64 64 1 3×3×2 8×8
stage3.block3 Residual Block 64 64 1 3×3×2 8×8
head.pool AdaptiveAvgPool2d(1×1) 64 64 - - 1×1
head.flatten Flatten 64×1×1 64 - - 64
head.fc Linear 64 10 - - 10

 

[학습 및 테스트]

Epoch 100번에 대한 학습 결과: 

Epoch 100/100 Loss: 0.257742 Acc: 91.04%

테스트 결과: 

Test Loss: 0.4890, Test Acc: 85.42%

 

①그래프 분석

- Training Loss (왼쪽)

  • 초기 손실: 약 1.8 → 매우 높은 손실로 시작
  • 손실 감소 추세:
    • Epoch 0~20: 급격히 감소 (약 1.8 → 0.4)
    • Epoch 20~100: 점진적으로 감소하며 약 0.25에서 수렴
  • 양호함: 손실이 안정적으로 감소하고 있음 → 모델이 잘 수렴 중

- Training Accuracy (오른쪽)

  • 초기 정확도: 약 30% (무작위보단 높음. ResNet의 초기 표현력 때문으로 보임.)
  • 정확도 증가 추세:
    • Epoch 0~20: 급격한 상승 (30% → 85%)
    • Epoch 20~60: 점진적으로 상승 (85% → 90%)
    • Epoch 60~100: 거의 수평 (약 91% 부근에서 plateau 현상)

②결론

모델은 훈련 정확도 91%, 테스트 정확도 85.4%를 기록하였으며, 약 5.6%의 성능 차이는 일반적인 범위 내로, 과적합 현상은 크지 않은 것으로 판단됨.

 


 

3. VGG-19 모델 CIFAR-10 데이터 학습 및 결과

[모델 구조]

① FC 레이어 마지막 층만 num-classes=10 으로 수정 

② 손실함수 : CrossEntropyLoss

③ optimizer: Adam, 마지막층의 파라미터만 학습하도록 설정, lr=0.001

vgg19_model = vgg19(weights=None).to(device)  # 사전학습된 weight 사용 X

# 모든 파라미터 고정
for param in vgg19_model.parameters():
    param.requires_grad = False

# 마지막 레이어 교체 (클래스 10개)
vgg19_model.classifier[6] = nn.Linear(4096, 10)
vgg19_model.classifier[6].requires_grad = True
vgg19_model.classifier[6].to(device)

# 손실 함수
vgg_criterion = nn.CrossEntropyLoss()

# optimizer도 변경
vgg_optimizer = optim.Adam(vgg19_model.classifier[6].parameters(), lr=0.001)

 

[모델학습]

① CIFAR-10은 32x32 해상도이므로 VGG-19모델에 맞게 224x224 로 Resize해주어야 함. 단, 해상도가 224로 너무 커지면 학습속도가 너무 느려지는 문제가 발생. 따라서 본 모델 학습에서는 128x128 해상도로 조정해주었음. 

② DataLoader의 경우 해상도 Resize에 따라 학습량이 증가하기때문에 batch_size를 32로 조정해주었음 

vgg_train_transform=transforms.Compose([
    transforms.Resize((128,128), interpolation=InterpolationMode.BICUBIC),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
vgg_train_loader = DataLoader(vgg_trainset, batch_size=32, shuffle=True, num_workers=0, pin_memory=True)

 

[학습 결과]

 그래프분석

- Training Loss (왼쪽)

  • 초기 Loss ≈ 2.25: CrossEntropy 기준, 무작위 추측 수준과 비슷
  • 15 에폭 이후 감소세 완만 :
    •  성능 한계에 도달한것으로 보임

- Training Accuracy (오른쪽)

  • 초기 정확도 ≈ 16%  → 무작위 선택 수준
  • 정확도 증가 추세:
    • Epoch 1~5: 급격히 증가
    • 정확도 ≈ 26% 부근에서 수렴
  • 성능 한계: 정확도가 빠르게 증가했으나 26%정확도의 낮은 수준에서 학습률이 포화됨

 

② CIFAR-10 데이터셋 해상도 32x32 로 그대로 학습한다면?

해상도를 128로 증가시켜서 학습한 결과 성능이 Accuracy 26%에 그쳤음.

다른 조건을 동일하게 한 뒤, 해상도만 32x32로 변경없이 학습한다면 결과가 어떻게 나올것인가? 

 

 

결과 요약

- 정확도가 약 41% 에서 그쳤음. 

- 결과가 수렴하지 못하고 진동폭이 매우 컸음.

- 모델 자체의 성능은 128해상도로 조정한 것 보다 낫지만, 결과가 불안정함을 확인할 수 있음.

 

③ VGG-19 의 가중치를 전이학습 한 뒤 학습한다면?

vgg19_model = vgg19(weights=VGG19_Weights.IMAGENET1K_V1).to(device)

결과 요약

- 정확도가 약 75% 까지 증가함. 

- 다만 약 20 에포크부터 학습률이 완만해지는 포화상태 진입함.

- loss 및 accuracy 에 진동이 약간 존재함(약 2% 진동)

- 전이학습 결과 학습 성능은 월등히 높아졌지만 만족할만한 성능은 아니며, 불안정성도 존재하는 것으로 보임.   

 

 

4.  결론

CIFAR-10 데이터셋을 기반으로 VGG-19와 ResNet의 이미지 분류 성능을 비교한 결과, 두 모델 간의 뚜렷한 성능 차이를 확인할 수 있었다. VGG-19는 약 25%의 정확도를 기록하며 학습이 제대로 진행되지 않았고, 손실 값 또한 감소하지 않고 불안정하게 진동하는 양상을 보였다. 이는 VGG-19의 깊은 구조에서 발생할 수 있는 gradient vanishing 문제와 fully connected layer 기반의 높은 파라미터 수로 인한 과적합 위험성에서 기인한 것으로 보인다. 반면, ResNet은 91%의 높은 정확도를 달성하며 안정적으로 수렴하는 학습 곡선을 보여주었다.

ResNet의 핵심 구조인 residual connection은 역전파 시 gradient가 소실되지 않고 깊은 네트워크 전반에 걸쳐 효율적으로 전달되도록 하여 학습 안정성을 크게 향상시켰다. 또한, ResNet은 VGG보다 훨씬 적은 파라미터로도 복잡한 표현을 학습할 수 있는 구조적 효율성을 가진다. 이러한 구조적 차이는 동일한 데이터셋, 동일한 조건 하에서도 ResNet이 VGG보다 훨씬 빠르게 수렴하고 높은 정확도를 달성할 수 있었던 핵심 이유로 작용했다. 실험을 통해 ResNet이 VGG 대비 학습 효율성과 성능 면에서 모두 우수한 모델임을 명확히 확인할 수 있었다.