[CS231n] 강의6. Hardware and Software with Pytorch 리뷰
본 글은 2021년 4월에 강의한 스탠포드 대학의 “Convolutional Neural Networks for Visual Recognition” 2021년 강의를 듣고 정리한 내용입니다. 개인 공부 목적으로 작성되었으며, 설명이 맞지 않거나 글 오타가 있으면 알려주시길 바랍니다.
- 원본 링크 : cs231n.stanford.edu
- 한글 번역 링크 : aikorea.org - cs231n
-
강의 링크 : youtube - 2017 Spring (English)
- 본 강의는 CPU와 GPU의 구조적 차이 및 하드웨어 가속 원리를 소개한 후 딥러닝 소프트웨어 프레임워크를 다룬다. 여기서는 하드웨어 비교를 간략히 갈음하고 파이토치(PyTorch) 프레임워크의 동작 원리에 집중한다.1
Computational Graphs
-
먼저 계산 그래프를 numpy로 작성해본다.

import numpy as np np.random.seed(0) N, D = 3, 4 x = np.random.randn(N, D) y = np.random.randn(N, D) z = np.random.randn(N, D) a = x * y b = a + z c = np.sum(b) grad_c = 1.0 grad_b = grad_c * np.ones((N, D)) grad_a = grad_b.copy() grad_z = grad_b.copy() grad_x = grad_a * y grad_y = grad_a * x - Good
- API가 깔끔하다.
- 수치적 코드를 쓰기 편하다.
- Bad
- 고유의 그레이디언트를 계산하지 못한다.
- GPU에서 실행할 수 없다.
PyTorch
- torch.Tensor : 넘파이(numpy) 어레이(array)와 비슷하지만, GPU에서 실행할 수 있다.
- torch.autograd : 텐서로 계산 그래프(computational graph)를 만들고, 그레이디언트(gradient)를 자동으로 계산하는 패키지이다.
- torch.nn.Module : 신경망(neural network) 레이어; 상태(state) 또는 학습 가능한 가중치를 저장할 수 있다.
Tensors
- 다음은 torch.Tensor에 관한 예제이다.
- torch.randn : 평균이 0이고, 표준편차가 1인 가우시안 정규분포를 이용해 생성한다.
- torch.mm : 행렬의 행렬 곱셈을 수행한다.
- torch.clamp(min, max) : 모든 요소를 [min, max] 범위로 고정한다. 2
- torch.t(input) : input이 2차원 텐서일 때 차원 0과 1을 전치(transpose)한다.
import torch
# device 정의 = gpu가 가능하면 cuda, 아니면 cpu
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 데이터와 가중치를 위해 랜덤 텐서 생성
# H : hidden의 약어
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in, device=device)
y = torch.randn(N, D_out, device=device)
w1 = torch.randn(D_in, H, device=device)
w2 = torch.randn(H, D_out, device=device)
learning_rate = 1e-6
for t in range(500):
# 정방향 전달(forward pass) : 예측(prediction)과 손실(loss)을 계산한다.
h = x.mm(w1)
h_relu = h.clamp(min=0)
y_pred = h_relu.mm(w2)
loss = (y_pred - y).pow(2).sum()
# 역방향 전달(backward pass) : 수동으로 그레이디언트를 계산한다.
grad_y_pred = 2.0 * (y_pred - y)
grad_w2 = h_relu.t().mm(grad_y_pred)
grad_h_relu = grad_y_pred.mm(w2.t())
grad_h = grad_h_relu.clone()
grad_h[h < 0] = 0
grad_w1 = x.t().mm(grad_h)
# 가중치에 대한 경사 하강(gradient descent) 단계(step)
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2
Autograd
requires_grad=True로 텐서를 생성하면 autograd가 활성화된다.- 모든 연산 추적
requires_grad=True가 있는 텐서에 대한 연산(operation)은 pytorch가 계산 그래프(computational graph)를 빌드한다.- torch.randn(requires_grad=True) : autograd가 반환된 텐서의 연산을 기록해야 하는 경우 쓰인다.
- torch.Tensor.backward : 현재 계산 그래프에 대한(w.r.t.) 텐서의 그레이디언트를 계산한다.
- 텐서에 대한 그레이디언트는
.grad속성에 누적된다.
- 텐서에 대한 그레이디언트는
- torch.autograd.grad : 입력에 대한 출력의 그레이디언트 합계를 계산하고 반환한다.
- torch.no_grad : 이 부분에서는 계산 그래프를 만들지 말라는 의미이다.
- grad.zero_ : _로 끝나는 파이토치 메서드는 텐서를 제자리에서 수정하고, 새로운 텐서를 반환하지 않는 메서드이다.
import torch
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
w1 = torch.randn(D_in, H, requires_grad=True)
w2 = torch.randn(H, D_out, requires_grad=True)
learning_rate = 1e-6
for t in range(500):
# forward pass이지만, pytorch가 그래프에서 자동으로 추적해준다.
y_pred = x.mm(w1).clamp(min=0).mm(w2)
loss = (y_pred - y).pow(2).sum()
# w1 및 w2에 대한 손실 그레이디언트 계산
loss.backward()
# 가중치에 gradient step을 만든 다음 0으로 만든다.
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
w1.grad.zero_()
w2.grad.zero_()
New Autograd Functions
- 텐서에 대한 정방향(forward) 및 역방향(backward) 함수를 만들어 자체 autograd 함수를 정의해본다.
- torch.autograd.Function : 사용자 지정 autograd.Function를 만들 기본 클래스이다.
- 서브클래스 함수 및 forward 와 backward 메서드 구현
- forward : 연산 수행
- backward : 역방향 모드 자동 미분(reverse-mode autodiff) 연산 공식을 정의한다.
- ctx(=context object) : 컨텍스트 객체에 제공되는 메서드를 활용한다.
- ctx.save_for_backward : 나중에 backward() 호출 시 사용할 텐서를 저장한다.
import torch
class MyReLU(torch.autograd.Function):
@staticmethod
def forward(ctx, x):
ctx.save_for_backward(x)
return x.clamp(min=0)
@staticmethod
def backward(ctx, grad_y):
x, = ctx.saved_tensors
grad_input = grad_y.clone()
grad_input[x < 0] = 0
return grad_input
def my_relu(x):
return MyReLU.apply(x)
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
w1 = torch.randn(D_in, H, requires_grad=True)
w2 = torch.randn(H, D_out, requires_grad=True)
learning_rate = 1e-6
for t in range(500):
# 정방향에 새로운 autograd function 사용
y_pred = my_relu(x.mm(w1)).mm(w2)
loss = (y_pred - y).pow(2).sum()
# w1 및 w2에 대한 손실 그레이디언트 계산
loss.backward()
# 가중치에 gradient step을 만든 다음 0으로 만든다.
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
w1.grad.zero_()
w2.grad.zero_()
nn
- 신경망 작업을 위한 상위 레벨 래퍼(wrapper)이다.
- torch.nn.Linear(in_features, out_features) : 들어오는 데이터에 선형 변환 적용
- $y=xA^T+b$
- torch.nn.ReLU : rectified linear unit function을 요소별로 적용한다.
- $ReLU(x)=max(0,x)$
- torch.nn.functional.mse_loss(input, target) : 요소별 평균 제곱 오차를 측정한다.
- torch.no_grad : 그레이디언트 계산을 사용하지 않도록 설정한 컨텍스트 관리자(context-manager)
- model.parameters : 파라미터 반환(weight, bias 등 torch.tensor로 이루어진 것들을 말함) 3
import torch
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
# 모델을 일련의 데이터로 정의
model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out)
)
learning_rate = 1e-2
for t in range(500):
# forward pass
y_pred = model(x)
loss = torch.nn.functional.mse_loss(y_pred, y)
# 모든 모델 가중치에 대한 그레이디언트 계산
# requires_grad = True 를 가지고 있는 것들을
loss.backward()
# 각 모델 매개변수에 gradient step 만들기
# 이때는 그레이디언트 사용 안함
with torch.no_grad():
for param in model.parameters():
param -= learning_rate * param.grad
model.zero_grad()
optim
- optimizer.step : 파라미터 값을 업데이트하는 과정이다.
- optimizer.zero_grad : 역전파 단계 전에, optimizer 객체를 사용하여 (모델의 학습 가능한 가중치인) 갱신할 변수들에 대한 모든 그레이디언트(gradient)를 0으로 만든다. 이렇게 하는 이유는 기본적으로 .backward()를 호출할 때마다 그레이디언트가 버퍼(buffer)에 (덮어쓰지 않고) 누적되기 때문이다. 4
import torch
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
# 모델을 일련의 데이터로 정의
model = torch.nn.Sequential(
torch.nn.Linear(D_in, H),
torch.nn.ReLU(),
torch.nn.Linear(H, D_out)
)
learning_rate = 1e-4
# 다른 업데이트 규칙으로 옵티마이저(optimizer)를 적용
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for t in range(500):
# forward pass
y_pred = model(x)
loss = torch.nn.functional.mse_loss(y_pred, y)
# 모든 모델 가중치에 대한 그레이디언트 계산
# requires_grad = True 를 가지고 있는 것들을
loss.backward()
optimizer.step()
optimizer.zero_grad()
nn - Define new Modules
- PyTorch Module : 신경망 계층으로 텐서를 입력 및 출력한다.
- autograd를 사용하여 자체 모듈을 정의할 수 있다.
import torch
# 전체 모듈을 단일 모듈로 정의
class TwoLayerNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
super(TwoLayerNet, self).__init__()
self.linear1 = torch.nn.Linear(D_in, H)
self.linear2 = torch.nn.Linear(H, D_out)
# child module을 사용하여 forward pass 정의
# autograd가 알아서 처리하기 때문에 backward 정의할 필요 없음
def forward(self, x):
h_relu = self.linear1(x).clamp(min=0)
y_pred = self.linear2(h_relu)
return y_pred
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
model = TwoLayerNet(D_in, H, D_out)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4)
for t in range(500):
y_pred = model(x)
loss = torch.nn.functional.mse_loss(y_pred, y)
loss.backward()
optimizer.step()
optimizer.zero_grad()
- 사용자 지정 모듈 하위 클래스와 sequential container를 혼합하여 쓸 수도 있다.
import torch
class ParallelBlock(torch.nn.Module):
def __init__(self, D_in, D_out):
super(ParallelBlock, self).__init__()
self.linear1 = torch.nn.Linear(D_in, D_out)
self.linear2 = torch.nn.Linear(D_in, D_out)
def forward(self, x):
h1 = self.linear1(x)
h2 = self.linear2(x)
return (h1 * h2).clamp(min=0)
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
model = torch.nn.Sequential(
ParallelBlock(D_in, H),
ParallelBlock(H, H),
torch.nn.Linear(H, D_out)
)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
for t in range(500):
y_pred = model(x)
loss = torch.nn.functional.mse_loss(y_pred, y)
loss.backward()
optimizer.step()
optimizer.zero_grad()
Pretrained Models
- torchvision 패키지를 이용하면 ImageNet 등으로 사전 학습된(pre-trained) 모델 가중치를 바로 불러와 활용할 수 있다.
import torch
import torchvision
alexnet = torchvision.models.alexnet(pretrained=True)
vgg16 = torchvision.models.vgg16(pretrained=True)
resnet101 = torchvision.models.resnet101(pretrained=True)
Dynamic Computation Graphs
- 다음은 연산 단계별 동적 계산 그래프(Dynamic Computation Graph)의 생성 과정을 나타낸 도식이다.

- PyTorch는 Define-by-Run 방식을 사용하므로 매 반복(iteration)마다 정방향 연산에 따라 그래프를 새로 생성하고, 역전파가 완료되면 그래프 버퍼를 해제한다.
한계 및 트레이드오프
- 동적 그래프 생성 오버헤드: 매 반복마다 파이썬 레벨에서 연산 그래프를 새로 구성하므로, 네트워크 구조가 고정된 경우에는 사전에 전체 계산 그래프를 최적화하고 컴파일하는 정적 그래프(Define-and-Run) 프레임워크에 비해 런타임 인터프리터 오버헤드가 발생할 수 있다.
- 중간 활성화 메모리(VRAM) 점유: 자동 미분(autograd)을 수행하기 위해 정방향 연산에서 산출된 중간 텐서(intermediate activations)를 역전파 완료 시점까지 메모리에 보존해야 하므로, 모델이 깊어지거나 배치 크기가 커질수록 GPU VRAM 요구량이 증가한다.
- 그레이디언트 누적 버퍼 관리 필요: PyTorch의 autograd 엔진은 가중치 텐서의
.grad속성에 기울기를 덮어쓰지 않고 기본적으로 누적(accumulate)한다. 따라서 매 학습 스텝마다optimizer.zero_grad()또는param.grad = None을 호출하여 버퍼를 명시적으로 초기화하지 않으면 이전 배치의 기울기가 누적되어 학습이 불안정해지거나 발산할 위험이 있다.
댓글남기기