내 풀이 github LINK: https://github.com/qkrtmdtj04/CS231n-Assignment
GitHub - qkrtmdtj04/CS231n-Assignment
Contribute to qkrtmdtj04/CS231n-Assignment development by creating an account on GitHub.
github.com
PyTorch의 필요성
Assignment 1, 2를 거치며 우리는 NumPy를 이용해 신경망의 Forward/Backward Pass, Convolution, 그리고 Optimizer까지 밑바닥부터(from scratch) 직접 구현했다. 이 과정은 신경망의 내부 원리를 이해하는 데는 최고지만, 실제 연구나 실무에서 매번 이렇게 수식을 계산하고 코드를 짜는 것은 매우 비효율적이다. PyTorch 같은 딥러닝 프레임워크는 이 문제를 두 가지 핵심 기능으로 해결해 준다.
- GPU 가속: 행렬 연산을 CPU보다 훨씬 빠르게 처리한다.
- 자동 미분 (Autograd): Forward Pass만 정의하면 Backward Pass(기울기 계산)는 프레임워크가 알아서 해준다. 정말로 이게 정말 개꿀이다. 어떤 Layer를 구성해도 내가 계산할 필요가 없다.
Q5 파트의 핵심 목표는 Barebones (기본 텐서 연산) → Module → Sequential라는 세 가지 추상화 단계를 거치며 PyTorch의 편리함을 체감하고, 최종적으로 자유롭게 모델을 설계해 CIFAR-10 이미지 분류 성능을 끌어올리는 것이다.
또한 CIFAR-10 이미지 분류 성능을 어떻게 끌어올렸는지 과정도 보여줄 예정이다.

Q5 PyTorch 풀이
Q5-1. three_layer_convnet
Convolutional Neural Network(CNN)를 Barebones PyTorch 버전으로 구현하는 파트다. PyTorch의 저수준 함수형 API(torch.nn.functional)가 내부적으로 어떻게 데이터를 흘려보내는지 감을 잡기 위한 단계다.
def three_layer_convnet(x, params):
conv_w1, conv_b1, conv_w2, conv_b2, fc_w, fc_b = params
scores = None
# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
x1 = F.conv2d(x, conv_w1, bias=conv_b1, stride=1, padding=2)
x1relu = F.relu(x1)
x2 = F.conv2d(x1relu, conv_w2, bias=conv_b2, stride=1, padding=1)
x2relu = F.relu(x2)
xflatten = flatten(x2relu)
scores = xflatten.mm(fc_w) + fc_b
# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
return scores
- 네트워크 구조: 코드 흐름을 보면 Conv2d → ReLU → Conv2d → ReLU → Flatten → Fully Connected (Affine) 순서로 이어지는 전형적인 3층 ConvNet 구조임을 명확하게 알 수 있다.
- 차원 변환 (flatten과 .mm): 합성곱 층을 통과한 데이터는 (N, C, H, W) 형태의 4차원 텐서다. 이를 Fully Connected 층(선형 분류기)에 넣기 위해서는 2차원 (N, C*H*W) 형태로 쭉 펴줘야 하는데, 이를 flatten() 함수가 수행한다. 이후 .mm()(행렬곱) 연산과 bias 덧셈을 통해 최종 클래스별 scores를 도출한다.
- NumPy로 머리 싸매고 구현했던 복잡한 Forward Pass가 직관적인 몇 줄의 텐서 연산으로 깔끔하게 정리되는 것을 보며, 딥러닝 프레임워크가 왜 필수적인지 체감할 수 있는 구간이다.
Iteration 700, loss = 1.4867 Checking accuracy on the val set Got 456 / 1000 correct (45.60%)
- 이 모델을 학습시키면 위와 같은 결과를 얻을 수 있고 저번에 conv를 구현한 모델 성능과 비슷하다는 것을 알 수 있다.
Q5-2. PyTorch Module API: Three-Layer ConvNet
이전 단계(Barebones)에서는 함수형 API(F.conv2d)를 사용하되 가중치 텐서(params)를 외부에서 일일이 리스트로 관리해야 했습니다. 하지만 신경망이 깊어질수록 파라미터를 수동으로 추적하는 것은 불가능에 가깝습니다.
이제 PyTorch의 꽃이라고 할 수 있는 nn.Module 기반의 객체 지향 API를 사용합니다. 네트워크를 하나의 클래스로 정의하고, 층(Layer)과 가중치를 객체 내부에 캡슐화(Encapsulation)하여 관리하는 본격적인 딥러닝 모델의 형태입니다.
from ast import increment_lineno
class ThreeLayerConvNet(nn.Module):
def __init__(self, in_channel, channel_1, channel_2, num_classes):
super().__init__()
# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
self.conv1 = nn.Conv2d(in_channel,channel_1,5,1,2)
nn.init.kaiming_normal_(self.conv1.weight)
self.conv2 = nn.Conv2d(channel_1,channel_2,3,1,1)
nn.init.kaiming_normal_(self.conv2.weight)
self.fc = nn.Linear(channel_2*32*32, num_classes)
nn.init.kaiming_normal_(self.fc.weight)
# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
def forward(self, x):
scores = None
# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
conv_result = F.relu(self.conv2(F.relu(self.conv1(x))))
#print(conv_result.shape)
scores = self.fc(flatten(conv_result))
# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
return scores
def test_ThreeLayerConvNet():
x = torch.zeros((64, 3, 32, 32), dtype=dtype) # minibatch size 64, image size [3, 32, 32]
model = ThreeLayerConvNet(in_channel=3, channel_1=12, channel_2=8, num_classes=10)
scores = model(x)
print(scores.size()) # you should see [64, 10]
test_ThreeLayerConvNet()
- __init__ 함수에서는 네트워크에 필요한 층(Layer)들을 정의한다. Kaiming He 초기화(kaiming_normal_)를 사용해 ReLU에 최적화된 가중치 초기화를 진행했다.
- forward 함수에서는 데이터가 층을 통과하는 로직을 짠다. Convolution -> ReLU -> Convolution -> ReLU -> Flatten -> Fully Connected 순서다.
- 가중치 파라미터는 PyTorch가 내부적으로 다 추적해 주기 때문에, 이전처럼 딕셔너리에 W1, b1을 일일이 담아둘 필요가 없다.
Iteration 700, loss = 1.5115 Checking accuracy on validation set Got 436 / 1000 correct (43.60)
- 이 모델을 학습시키면 위와 같은 결과를 얻을 수 있고 모델 구조는 같기 때문에 비슷한 결과 값이다.
Q5-3. PyTorch Sequential API: Three-Layer ConvNet
Module API도 충분히 편하지만, 단순한 Feed-forward 네트워크라면 nn.Sequential을 사용해 코드를 훨씬 더 간결하게 만들 수 있다. 레고 블록을 끼우듯 층을 순서대로 나열하기만 하면 된다.
channel_1 = 32
channel_2 = 16
learning_rate = 1e-2
model = None
optimizer = None
# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
class Flatten(nn.Module):
def forward(self, x):
return flatten(x)
model = nn.Sequential(
nn.Conv2d(3,channel_1,5,1,2),
nn.ReLU(),
nn.Conv2d(channel_1,channel_2,3,1,1),
nn.ReLU(),
Flatten(),
nn.Linear(channel_2*32*32,10),
)
optimizer = optim.SGD(model.parameters(), lr=learning_rate,
momentum=0.9, nesterov=True)
# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
################################################################################
train_part34(model, optimizer)
- forward 함수를 따로 정의할 필요조차 없다. 데이터가 선언된 리스트 순서대로 알아서 통과한다. 코드가 비약적으로 짧아진 것을 볼 수 있다.
Q5-4. CIFAR-10 Challenge (최고 모델 탐색)
자유롭게 PyTorch API를 활용해 CIFAR-10 데이터셋에서 70% 이상의 정확도를 달성하는 파트다. 이전 과제에서 배웠던 Convolution, Batch Normalization, ReLU, Max Pooling의 조합을 깊게 쌓아서 모델을 구성해 볼 거다. 또한 CS231n 강의에서 나온 CNN Architectures 중 VGG와 ResNet을 간단하게 구현해 보고 성능 비교를 해볼 예정이다. (ResNet은 자세하게 다음 게시물에서 논문 리뷰로 다룰 예정이다. 이 블로그에서는 이런 모델이 있구나 하고 넘어가면 머리가 편하다)
그냥 conv와 relu를 반복한 모델
model = None
optimizer = None
# *****START OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
from ast import increment_lineno
model = nn.Sequential(
nn.Conv2d(3,32,3,1,1),
nn.ReLU(),
nn.Conv2d(32,64,3,1,1),
nn.ReLU(),
nn.Conv2d(64,64,3,1,1),
nn.ReLU(),
nn.Conv2d(64,64,3,1,1),
nn.ReLU(),
nn.Conv2d(64,128,3,1,1),
nn.ReLU(),
nn.Conv2d(128,128,3,1,1),
nn.ReLU(),
nn.Conv2d(128,128,3,1,1),
nn.ReLU(),
nn.Flatten(),
nn.Linear(128*32*32,10),
)
learning_rate = 1e-2
optimizer = optim.SGD(model.parameters(), lr=learning_rate,
momentum=0.9, nesterov=True)
# *****END OF YOUR CODE (DO NOT DELETE/MODIFY THIS LINE)*****
train_part34(model, optimizer, epochs=10)
- Pooling(다운샘플링) 없이 Conv 층만 깊게 쌓았기 때문에, 공간 차원(32x32)이 전혀 줄어들지 않아 마지막 Fully Connected 층의 파라미터 수(128 * 32 * 32)가 기하급수적으로 폭발하는 구조다.
- 파라미터가 너무 많아 학습이 매우 느리고 메모리를 많이 차지하며, 깊이에 비해 과적합(Overfitting)에 취약해 효율이 심각하게 떨어진다.
- 128 × 32 × 32=>131072=> 131072 × 10 ( 약 131만 개)
VGG-11
단순 반복의 한계를 극복하기 위해 체계적인 다운샘플링을 도입한 모델이다.
#vgg-11
vgg11_model = nn.Sequential(
#out size 32
nn.Conv2d(3,64,3,1,1),
nn.ReLU(),
nn.MaxPool2d(2, stride=2),
#out size:16
nn.Conv2d(64,128,3,1,1),
nn.ReLU(),
nn.MaxPool2d(2, stride=2),
#out size 8
nn.Conv2d(128,256,3,1,1),
nn.ReLU(),
nn.Conv2d(256,256,3,1,1),
nn.ReLU(),
nn.MaxPool2d(2, stride=2),
#out size 4
nn.Conv2d(256,512,3,1,1),
nn.ReLU(),
nn.Conv2d(512,512,3,1,1),
nn.ReLU(),
nn.MaxPool2d(2, stride=2),
#out size 2
nn.Conv2d(512,512,3,1,1),
nn.ReLU(),
nn.Conv2d(512,512,3,1,1),
nn.ReLU(),
nn.MaxPool2d(2, stride=2),
nn.Flatten(),
nn.Linear(512,256),
nn.ReLU(),
nn.Dropout(0.5),
nn.Linear(256,10)
)
learning_rate = 1e-2
optimizer = optim.SGD(vgg11_model.parameters(), lr=learning_rate,
momentum=0.9, nesterov=True)
train_part34(vgg11_model, optimizer, epochs=10)
- 3x3 Conv 층으로 특징을 추출하고, MaxPool2d를 통해 이미지 크기를 절반씩 줄여나가며 채널 수를 늘리는 전형적인 VGG 계열의 체계적인 아키텍처다.
- 공간 차원을 효과적으로 압축하여 연산량을 줄였고, 마지막에 Dropout을 추가해 과적합을 방지함으로써 단순 Conv 모델보다 훨씬 안정적이고 높은 성능을 낸다.
Test Data 최종 성능 비교
세 가지 모델을 10 Epoch씩 학습시킨 후, 최종 Test Set에서 검증한 결과는 다음과 같다.
class ResBlock(nn.Module):
def __init__(self, in_channel,out_channel,down_sample=False):
super().__init__()
stride = 1
if down_sample:
stride = 2
self.pro_cut = False
if in_channel != out_channel or down_sample:
self.pro_cut = True
self.conv3 = nn.Conv2d(in_channel,out_channel,1,stride,0)
self.bn3 = nn.BatchNorm2d(out_channel)
self.conv1 = nn.Conv2d(in_channel,out_channel,3,stride,1)
self.conv2 = nn.Conv2d(out_channel,out_channel,3,1,1)
self.relu = nn.ReLU()
self.bn1 = nn.BatchNorm2d(out_channel)
self.bn2 = nn.BatchNorm2d(out_channel)
def forward(self, x):
#Conv->BN->ReLU->Conv->BN->Add->ReLU
x_conv1 = self.relu(self.bn1(self.conv1(x)))
x_conv2 = self.bn2(self.conv2(x_conv1))
if self.pro_cut:
pro_Identity_x = self.bn3(self.conv3(x))
scores = x_conv2 +pro_Identity_x
else:
scores = x_conv2 + x
scores = self.relu(scores)
return scores
resnet11_model = nn.Sequential(
ResBlock(3,64),
ResBlock(64,64),
ResBlock(64,128,True),
ResBlock(128,128),
ResBlock(128,256,True),
ResBlock(256,256),
ResBlock(256,512,True),
ResBlock(512,512),
nn.Flatten(),
nn.Linear(512*4*4, 10)
)
learning_rate = 1e-2
optimizer = optim.SGD(resnet11_model.parameters(), lr=learning_rate,
momentum=0.9, nesterov=True)
train_part34(resnet11_model, optimizer, epochs=10)
- 입력값을 출력값에 그대로 더해주는 지름길인 'Skip Connection (scores = x_conv2 + x)'을 도입해, 네트워크가 깊어질수록 발생하는 기울기 소실(Vanishing Gradient) 문제를 근본적으로 해결했다.
- 모든 Conv 층 뒤에 Batch Normalization이 기본으로 깔려 있어, 세 모델 중 학습 속도와 안정성이 가장 높은 정확도를 보여줘야한다.
test data 비교
-------------------------------------------------------------
Conv+Relu model
Checking accuracy on test set Got 6897 / 10000 correct (68.97)
-------------------------------------------------------------
VGG11 model
Checking accuracy on test set Got 7866 / 10000 correct (78.66)
-------------------------------------------------------------
ResNet11 model
Checking accuracy on test set Got 7896 / 10000 correct (78.96)
결과: 단순 무식하게 쌓은 모델(68%)에 비해 체계적인 아키텍처를 갖춘 VGG와 ResNet이 압도적으로 우수한 성능(약 79%)을 보여주었다. ResNet > VGG >> Plain CNN
다만, 한 가지 눈에 띄는 점은 ResNet과 VGG의 성능 차이가 0.3%p로 거의 나지 않았다는 점이다. 이는 학습을 단 10 Epoch만 진행했기 때문으로 분석된다. ResNet의 진가는 네트워크가 훨씬 더 깊어지고 학습이 길어질 때 발휘된다. 이와 관련된 깊이 있는 분석과 ResNet 논문 리뷰는 다음 포스팅에서 본격적으로 파헤쳐 볼 예정이다.
'DeepLearning > CS231n' 카테고리의 다른 글
| CS231n 2026 Assignment 3: Q1: Image Captioning with Transformers (0) | 2026.08.01 |
|---|---|
| CS231n 2024 Assignment 3: Q1: RNN (0) | 2026.07.17 |
| CS231n Assignment 2: Q4: ConvolutionalNetworks (0) | 2026.06.07 |
| CS231n Assignment 2: Q3: Dropout (0) | 2026.06.06 |
| CS231n Assignment 2: Q2: Batch Normalization (0) | 2026.05.30 |