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 대비 학습 효율성과 성능 면에서 모두 우수한 모델임을 명확히 확인할 수 있었다.
'AI 활용 소프트웨어 개발 > AI, 머신러닝, 딥러닝' 카테고리의 다른 글
| Local GPU (Vscode) 환경 설정 및 사용하기 (0) | 2025.08.13 |
|---|---|
| YOLO 하이퍼파라미터 조정하기 (yaml 파일) (3) | 2025.08.11 |
| 딥러닝, 이미지 폴더 전처리하기. (3) | 2025.08.05 |
| AlexNet 논문 핵심 내용 요약 (1) | 2025.08.04 |
| pytorch 딥러닝, CNN 학습 흐름 정리 (2) | 2025.07.30 |